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", "actions"));
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                action.setFiltered(true);
212            }
213            return action;
214        } finally {
215            long duration = timerContext.stop();
216            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
217                log.debug(String.format("Resolving action with id '%s' took: %.2f ms", actionId, duration / 1000000.0));
218            }
219        }
220    }
221
222    @Override
223    public Action getAction(String actionId) {
224        return getActionRegistry().getAction(actionId);
225    }
226
227    @Override
228    public boolean isRegistered(String actionId) {
229        return getActionRegistry().getAction(actionId) != null;
230    }
231
232    @Override
233    public boolean isEnabled(String actionId, ActionContext context) {
234        Action action = getActionRegistry().getAction(actionId);
235        if (action != null) {
236            return isEnabled(action, context);
237        }
238        return false;
239    }
240
241    public boolean isEnabled(Action action, ActionContext context) {
242        ActionFilterRegistry filterReg = getFilterRegistry();
243        for (String filterId : action.getFilterIds()) {
244            ActionFilter filter = filterReg.getFilter(filterId);
245            if (filter != null && !filter.accept(action, context)) {
246                return false;
247            }
248        }
249        return true;
250    }
251
252    @Override
253    public ActionFilter[] getFilters(String actionId) {
254        Action action = getActionRegistry().getAction(actionId);
255        if (action == null) {
256            return null;
257        }
258        ActionFilterRegistry filterReg = getFilterRegistry();
259        List<String> filterIds = action.getFilterIds();
260        if (filterIds != null && !filterIds.isEmpty()) {
261            ActionFilter[] filters = new ActionFilter[filterIds.size()];
262            for (int i = 0; i < filters.length; i++) {
263                String filterId = filterIds.get(i);
264                filters[i] = filterReg.getFilter(filterId);
265            }
266            return filters;
267        }
268        return null;
269    }
270
271    @Override
272    public boolean checkFilter(String filterId, ActionContext context) {
273        final Timer.Context timerContext = filterTimer.time();
274        try {
275            ActionFilterRegistry filterReg = getFilterRegistry();
276            ActionFilter filter = filterReg.getFilter(filterId);
277            if (filter == null) {
278                return false;
279            }
280            return filter.accept(null, context);
281        } finally {
282            long duration = timerContext.stop();
283            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
284                log.debug(String.format("Resolving filter with id '%s' took: %.2f ms", filterId, duration / 1000000.0));
285            }
286        }
287    }
288
289    @Override
290    public boolean checkFilters(List<String> filterIds, ActionContext context) {
291        return checkFilters(null, filterIds, context);
292    }
293
294    protected boolean checkFilters(Action action, List<String> filterIds, ActionContext context) {
295        if (filterIds == null || filterIds.isEmpty()) {
296            return true;
297        }
298        final Timer.Context timerContext = filtersTimer.time();
299        try {
300            ActionFilterRegistry filterReg = getFilterRegistry();
301            for (String filterId : filterIds) {
302                ActionFilter filter = filterReg.getFilter(filterId);
303                if (filter == null) {
304                    continue;
305                }
306                if (!filter.accept(action, context)) {
307                    // denying filter found => ignore following filters
308                    if (log.isTraceEnabled()) {
309                        log.trace(String.format("Filter '%s' denied access", filterId));
310                    }
311                    return false;
312                }
313                if (log.isTraceEnabled()) {
314                    log.trace(String.format("Filter '%s' granted access", filterId));
315                }
316            }
317            return true;
318        } finally {
319            long duration = timerContext.stop();
320            if (isTimeTracerLogEnabled() && (duration > LOG_MIN_DURATION_NS)) {
321                log.debug(String.format("Resolving filters %s took: %.2f ms", filterIds, duration / 1000000.0));
322            }
323        }
324    }
325
326    @Override
327    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
328        if ("actions".equals(extensionPoint)) {
329            actions.addContribution((Action) contribution);
330        } else if ("filters".equals(extensionPoint)) {
331            if (contribution.getClass() == FilterFactory.class) {
332                registerFilterFactory((FilterFactory) contribution);
333            } else {
334                filters.addContribution((DefaultActionFilter) contribution);
335            }
336        } else if ("typeCompatibility".equals(extensionPoint)) {
337            actions.getRegistry().getTypeCategoryRelations().add((TypeCompatibility) contribution);
338        }
339    }
340
341    @Override
342    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
343        if ("actions".equals(extensionPoint)) {
344            actions.removeContribution((Action) contribution);
345        } else if ("filters".equals(extensionPoint)) {
346            if (contribution.getClass() == FilterFactory.class) {
347                unregisterFilterFactory((FilterFactory) contribution);
348            } else {
349                filters.removeContribution((DefaultActionFilter) contribution);
350            }
351        }
352    }
353
354    /**
355     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
356     * @param ff
357     */
358    @Deprecated
359    protected void registerFilterFactory(FilterFactory ff) {
360        getFilterRegistry().removeFilter(ff.id);
361        try {
362            ActionFilter filter = (ActionFilter) Thread.currentThread()
363                                                       .getContextClassLoader()
364                                                       .loadClass(ff.className)
365                                                       .newInstance();
366            filter.setId(ff.id);
367            getFilterRegistry().addFilter(filter);
368        } catch (ReflectiveOperationException e) {
369            log.error("Failed to create action filter", e);
370        }
371    }
372
373    /**
374     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
375     * @param ff
376     */
377    @Deprecated
378    public void unregisterFilterFactory(FilterFactory ff) {
379        getFilterRegistry().removeFilter(ff.id);
380    }
381
382    @Override
383    public void remove() {
384    }
385
386}