001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id: ActionService.java 28460 2008-01-03 15:34:05Z sfermigier $
020 */
021
022package org.nuxeo.ecm.platform.actions;
023
024import java.util.ArrayList;
025import java.util.Iterator;
026import java.util.List;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
031import org.nuxeo.runtime.api.Framework;
032import org.nuxeo.runtime.metrics.MetricsService;
033import org.nuxeo.runtime.model.ComponentContext;
034import org.nuxeo.runtime.model.ComponentInstance;
035import org.nuxeo.runtime.model.ComponentName;
036import org.nuxeo.runtime.model.DefaultComponent;
037import org.nuxeo.runtime.services.config.ConfigurationService;
038
039import com.codahale.metrics.MetricRegistry;
040import com.codahale.metrics.SharedMetricRegistries;
041import com.codahale.metrics.Timer;
042
043/**
044 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
045 */
046public class ActionService extends DefaultComponent implements ActionManager {
047
048    public static final ComponentName ID = new ComponentName("org.nuxeo.ecm.platform.actions.ActionService");
049
050    private static final long serialVersionUID = -5256555810901945824L;
051
052    private static final Log log = LogFactory.getLog(ActionService.class);
053
054    private ActionContributionHandler actions;
055
056    private FilterContributionHandler filters;
057
058    protected final MetricRegistry metrics = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
059
060    private static final String LOG_MIN_DURATION_KEY = "nuxeo.actions.debug.log_min_duration_ms";
061
062    private long LOG_MIN_DURATION_NS = -1 * 1000000;
063
064    private Timer actionsTimer;
065
066    private Timer actionTimer;
067
068    private Timer filtersTimer;
069
070    private Timer filterTimer;
071
072    @Override
073    public void activate(ComponentContext context) {
074        filters = new FilterContributionHandler();
075        actions = new ActionContributionHandler(filters);
076        actionsTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "ations"));
077        actionTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "action"));
078        filtersTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "filters"));
079        filterTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "filter"));
080    }
081
082    @Override
083    public void deactivate(ComponentContext context) {
084        actions = null;
085        filters = null;
086        actionsTimer = null;
087        actionTimer = null;
088        filtersTimer = null;
089        filterTimer = null;
090    }
091
092    @Override
093    public void applicationStarted(ComponentContext context) {
094        LOG_MIN_DURATION_NS = Long.parseLong(
095                Framework.getService(ConfigurationService.class).getProperty(LOG_MIN_DURATION_KEY, "-1")) * 1000000;
096    }
097
098    /**
099     * Return the action registry
100     *
101     * @deprecated since 5.5: use interface methods on ActionManager instead of public methods on ActionService.
102     */
103    @Deprecated
104    public final ActionRegistry getActionRegistry() {
105        return actions.getRegistry();
106    }
107
108    /**
109     * Return the action filter registry
110     *
111     * @deprecated since 5.5: use interface methods on ActionManager instead of public methods on ActionService.
112     */
113    @Deprecated
114    public final ActionFilterRegistry getFilterRegistry() {
115        return filters.getRegistry();
116    }
117
118    private void applyFilters(ActionContext context, List<Action> actions) {
119        Iterator<Action> it = actions.iterator();
120        while (it.hasNext()) {
121            Action action = it.next();
122            action.setFiltered(true);
123            if (!checkFilters(context, action)) {
124                it.remove();
125            }
126        }
127    }
128
129    @Override
130    public boolean checkFilters(Action action, ActionContext context) {
131        return checkFilters(context, action);
132    }
133
134    private boolean checkFilters(ActionContext context, Action action) {
135        if (action == null) {
136            return false;
137        }
138        if (log.isTraceEnabled()) {
139            log.trace(String.format("Checking access for action '%s'...", action.getId()));
140        }
141
142        boolean granted = checkFilters(action, action.getFilterIds(), context);
143        if (granted) {
144            if (log.isTraceEnabled()) {
145                log.trace(String.format("Granting access for action '%s'", action.getId()));
146            }
147        } else {
148            if (log.isTraceEnabled()) {
149                log.trace(String.format("Denying access for action '%s'", action.getId()));
150            }
151        }
152        return granted;
153    }
154
155    @Override
156    public List<Action> getActions(String category, ActionContext context) {
157        return getActions(category, context, true);
158    }
159
160    @Override
161    public List<Action> getAllActions(String category) {
162        return getActionRegistry().getActions(category);
163    }
164
165    @Override
166    public List<Action> getActions(String category, ActionContext context, boolean hideUnavailableActions) {
167        final Timer.Context timerContext = actionsTimer.time();
168        try {
169            List<Action> actions = getActionRegistry().getActions(category);
170            if (hideUnavailableActions) {
171                applyFilters(context, actions);
172                return actions;
173            } else {
174                List<Action> allActions = new ArrayList<Action>();
175                allActions.addAll(actions);
176                applyFilters(context, actions);
177
178                for (Action a : allActions) {
179                    a.setAvailable(actions.contains(a));
180                }
181                return allActions;
182            }
183        } finally {
184            long duration = timerContext.stop();
185            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
186                log.debug(String.format("Resolving actions for category '%s' took: %.2f ms", category,
187                        duration / 1000000.0));
188            }
189        }
190    }
191
192    protected boolean isTimeTracerLogEnabled() {
193        return log.isDebugEnabled() && LOG_MIN_DURATION_NS >= 0;
194    }
195
196    @Override
197    public Action getAction(String actionId, ActionContext context, boolean hideUnavailableAction) {
198        final Timer.Context timerContext = actionTimer.time();
199        try {
200            Action action = getActionRegistry().getAction(actionId);
201            if (action != null) {
202                if (hideUnavailableAction) {
203                    if (!checkFilters(context, action)) {
204                        return null;
205                    }
206                } else {
207                    if (!checkFilters(context, action)) {
208                        action.setAvailable(false);
209                    }
210                }
211            }
212
213            action.setFiltered(true);
214            return action;
215        } finally {
216            long duration = timerContext.stop();
217            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
218                log.debug(String.format("Resolving action with id '%s' took: %.2f ms", actionId, duration / 1000000.0));
219            }
220        }
221    }
222
223    @Override
224    public Action getAction(String actionId) {
225        return getActionRegistry().getAction(actionId);
226    }
227
228    @Override
229    public boolean isRegistered(String actionId) {
230        return getActionRegistry().getAction(actionId) != null;
231    }
232
233    @Override
234    public boolean isEnabled(String actionId, ActionContext context) {
235        Action action = getActionRegistry().getAction(actionId);
236        if (action != null) {
237            return isEnabled(action, context);
238        }
239        return false;
240    }
241
242    public boolean isEnabled(Action action, ActionContext context) {
243        ActionFilterRegistry filterReg = getFilterRegistry();
244        for (String filterId : action.getFilterIds()) {
245            ActionFilter filter = filterReg.getFilter(filterId);
246            if (filter != null && !filter.accept(action, context)) {
247                return false;
248            }
249        }
250        return true;
251    }
252
253    @Override
254    public ActionFilter[] getFilters(String actionId) {
255        Action action = getActionRegistry().getAction(actionId);
256        if (action == null) {
257            return null;
258        }
259        ActionFilterRegistry filterReg = getFilterRegistry();
260        List<String> filterIds = action.getFilterIds();
261        if (filterIds != null && !filterIds.isEmpty()) {
262            ActionFilter[] filters = new ActionFilter[filterIds.size()];
263            for (int i = 0; i < filters.length; i++) {
264                String filterId = filterIds.get(i);
265                filters[i] = filterReg.getFilter(filterId);
266            }
267            return filters;
268        }
269        return null;
270    }
271
272    @Override
273    public boolean checkFilter(String filterId, ActionContext context) {
274        final Timer.Context timerContext = filterTimer.time();
275        try {
276            ActionFilterRegistry filterReg = getFilterRegistry();
277            ActionFilter filter = filterReg.getFilter(filterId);
278            if (filter == null) {
279                return false;
280            }
281            return filter.accept(null, context);
282        } finally {
283            long duration = timerContext.stop();
284            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
285                log.debug(String.format("Resolving filter with id '%s' took: %.2f ms", filterId, duration / 1000000.0));
286            }
287        }
288    }
289
290    @Override
291    public boolean checkFilters(List<String> filterIds, ActionContext context) {
292        return checkFilters(null, filterIds, context);
293    }
294
295    protected boolean checkFilters(Action action, List<String> filterIds, ActionContext context) {
296        final Timer.Context timerContext = filtersTimer.time();
297        try {
298            ActionFilterRegistry filterReg = getFilterRegistry();
299            for (String filterId : filterIds) {
300                ActionFilter filter = filterReg.getFilter(filterId);
301                if (filter == null) {
302                    continue;
303                }
304                if (!filter.accept(action, context)) {
305                    // denying filter found => ignore following filters
306                    if (log.isTraceEnabled()) {
307                        log.trace(String.format("Filter '%s' denied access", filterId));
308                    }
309                    return false;
310                }
311                if (log.isTraceEnabled()) {
312                    log.trace(String.format("Filter '%s' granted access", filterId));
313                }
314            }
315            return true;
316        } finally {
317            long duration = timerContext.stop();
318            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
319                log.debug(String.format("Resolving filters %s took: %.2f ms", filterIds, duration / 1000000.0));
320            }
321        }
322    }
323
324    @Override
325    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
326        if ("actions".equals(extensionPoint)) {
327            actions.addContribution((Action) contribution);
328        } else if ("filters".equals(extensionPoint)) {
329            if (contribution.getClass() == FilterFactory.class) {
330                registerFilterFactory((FilterFactory) contribution);
331            } else {
332                filters.addContribution((DefaultActionFilter) contribution);
333            }
334        } else if ("typeCompatibility".equals(extensionPoint)) {
335            actions.getRegistry().getTypeCategoryRelations().add((TypeCompatibility) contribution);
336        }
337    }
338
339    @Override
340    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
341        if ("actions".equals(extensionPoint)) {
342            actions.removeContribution((Action) contribution);
343        } else if ("filters".equals(extensionPoint)) {
344            if (contribution.getClass() == FilterFactory.class) {
345                unregisterFilterFactory((FilterFactory) contribution);
346            } else {
347                filters.removeContribution((DefaultActionFilter) contribution);
348            }
349        }
350    }
351
352    /**
353     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
354     * @param ff
355     */
356    @Deprecated
357    protected void registerFilterFactory(FilterFactory ff) {
358        getFilterRegistry().removeFilter(ff.id);
359        try {
360            ActionFilter filter = (ActionFilter) Thread.currentThread()
361                                                       .getContextClassLoader()
362                                                       .loadClass(ff.className)
363                                                       .newInstance();
364            filter.setId(ff.id);
365            getFilterRegistry().addFilter(filter);
366        } catch (ReflectiveOperationException e) {
367            log.error("Failed to create action filter", e);
368        }
369    }
370
371    /**
372     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
373     * @param ff
374     */
375    @Deprecated
376    public void unregisterFilterFactory(FilterFactory ff) {
377        getFilterRegistry().removeFilter(ff.id);
378    }
379
380    @Override
381    public void remove() {
382    }
383
384}