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 static final long LOG_MIN_DURATION_NS = Long.parseLong(
063            Framework.getService(ConfigurationService.class).getProperty(LOG_MIN_DURATION_KEY, "-1")) * 1000000;
064
065    private Timer actionsTimer;
066
067    private Timer actionTimer;
068
069    private Timer filtersTimer;
070
071    private Timer filterTimer;
072
073    @Override
074    public void activate(ComponentContext context) {
075        filters = new FilterContributionHandler();
076        actions = new ActionContributionHandler(filters);
077        actionsTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "ations"));
078        actionTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "action"));
079        filtersTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "filters"));
080        filterTimer = metrics.timer(MetricRegistry.name("nuxeo", "ActionService", "filter"));
081    }
082
083    @Override
084    public void deactivate(ComponentContext context) {
085        actions = null;
086        filters = null;
087        actionsTimer = null;
088        actionTimer = null;
089        filtersTimer = null;
090        filterTimer = null;
091    }
092
093    /**
094     * Return the action registry
095     *
096     * @deprecated since 5.5: use interface methods on ActionManager instead of public methods on ActionService.
097     */
098    @Deprecated
099    public final ActionRegistry getActionRegistry() {
100        return actions.getRegistry();
101    }
102
103    /**
104     * Return the action filter registry
105     *
106     * @deprecated since 5.5: use interface methods on ActionManager instead of public methods on ActionService.
107     */
108    @Deprecated
109    public final ActionFilterRegistry getFilterRegistry() {
110        return filters.getRegistry();
111    }
112
113    private void applyFilters(ActionContext context, List<Action> actions) {
114        Iterator<Action> it = actions.iterator();
115        while (it.hasNext()) {
116            Action action = it.next();
117            action.setFiltered(true);
118            if (!checkFilters(context, action)) {
119                it.remove();
120            }
121        }
122    }
123
124    @Override
125    public boolean checkFilters(Action action, ActionContext context) {
126        return checkFilters(context, action);
127    }
128
129    private boolean checkFilters(ActionContext context, Action action) {
130        if (action == null) {
131            return false;
132        }
133        if (log.isDebugEnabled()) {
134            log.debug(String.format("Checking access for action '%s'...", action.getId()));
135        }
136
137        boolean granted = checkFilters(action, action.getFilterIds(), context);
138        if (granted) {
139            if (log.isDebugEnabled()) {
140                log.debug(String.format("Granting access for action '%s'", action.getId()));
141            }
142        } else {
143            if (log.isDebugEnabled()) {
144                log.debug(String.format("Denying access for action '%s'", action.getId()));
145            }
146        }
147        return granted;
148    }
149
150    @Override
151    public List<Action> getActions(String category, ActionContext context) {
152        return getActions(category, context, true);
153    }
154
155    @Override
156    public List<Action> getAllActions(String category) {
157        return getActionRegistry().getActions(category);
158    }
159
160    @Override
161    public List<Action> getActions(String category, ActionContext context, boolean hideUnavailableActions) {
162        final Timer.Context timerContext = actionsTimer.time();
163        try {
164            List<Action> actions = getActionRegistry().getActions(category);
165            if (hideUnavailableActions) {
166                applyFilters(context, actions);
167                return actions;
168            } else {
169                List<Action> allActions = new ArrayList<Action>();
170                allActions.addAll(actions);
171                applyFilters(context, actions);
172
173                for (Action a : allActions) {
174                    a.setAvailable(actions.contains(a));
175                }
176                return allActions;
177            }
178        } finally {
179            long duration = timerContext.stop();
180            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
181                log.info(String.format("Resolving actions for category '%s' took: %.2f ms", category,
182                        duration / 1000000.0));
183            }
184        }
185    }
186
187    @Override
188    public Action getAction(String actionId, ActionContext context, boolean hideUnavailableAction) {
189        final Timer.Context timerContext = actionTimer.time();
190        try {
191            Action action = getActionRegistry().getAction(actionId);
192            if (action != null) {
193                if (hideUnavailableAction) {
194                    if (!checkFilters(context, action)) {
195                        return null;
196                    }
197                } else {
198                    if (!checkFilters(context, action)) {
199                        action.setAvailable(false);
200                    }
201                }
202            }
203
204            action.setFiltered(true);
205            return action;
206        } finally {
207            long duration = timerContext.stop();
208            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
209                log.info(String.format("Resolving action with id '%s' took: %.2f ms", actionId, duration / 1000000.0));
210            }
211        }
212    }
213
214    @Override
215    public Action getAction(String actionId) {
216        return getActionRegistry().getAction(actionId);
217    }
218
219    @Override
220    public boolean isRegistered(String actionId) {
221        return getActionRegistry().getAction(actionId) != null;
222    }
223
224    @Override
225    public boolean isEnabled(String actionId, ActionContext context) {
226        Action action = getActionRegistry().getAction(actionId);
227        if (action != null) {
228            return isEnabled(action, context);
229        }
230        return false;
231    }
232
233    public boolean isEnabled(Action action, ActionContext context) {
234        ActionFilterRegistry filterReg = getFilterRegistry();
235        for (String filterId : action.getFilterIds()) {
236            ActionFilter filter = filterReg.getFilter(filterId);
237            if (filter != null && !filter.accept(action, context)) {
238                return false;
239            }
240        }
241        return true;
242    }
243
244    @Override
245    public ActionFilter[] getFilters(String actionId) {
246        Action action = getActionRegistry().getAction(actionId);
247        if (action == null) {
248            return null;
249        }
250        ActionFilterRegistry filterReg = getFilterRegistry();
251        List<String> filterIds = action.getFilterIds();
252        if (filterIds != null && !filterIds.isEmpty()) {
253            ActionFilter[] filters = new ActionFilter[filterIds.size()];
254            for (int i = 0; i < filters.length; i++) {
255                String filterId = filterIds.get(i);
256                filters[i] = filterReg.getFilter(filterId);
257            }
258            return filters;
259        }
260        return null;
261    }
262
263    @Override
264    public boolean checkFilter(String filterId, ActionContext context) {
265        final Timer.Context timerContext = filterTimer.time();
266        try {
267            ActionFilterRegistry filterReg = getFilterRegistry();
268            ActionFilter filter = filterReg.getFilter(filterId);
269            if (filter == null) {
270                return false;
271            }
272            return filter.accept(null, context);
273        } finally {
274            long duration = timerContext.stop();
275            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
276                log.info(String.format("Resolving filter with id '%s' took: %.2f ms", filterId, duration / 1000000.0));
277            }
278        }
279    }
280
281    @Override
282    public boolean checkFilters(List<String> filterIds, ActionContext context) {
283        return checkFilters(null, filterIds, context);
284    }
285
286    protected boolean checkFilters(Action action, List<String> filterIds, ActionContext context) {
287        final Timer.Context timerContext = filtersTimer.time();
288        try {
289            ActionFilterRegistry filterReg = getFilterRegistry();
290            for (String filterId : filterIds) {
291                ActionFilter filter = filterReg.getFilter(filterId);
292                if (filter == null) {
293                    continue;
294                }
295                if (!filter.accept(action, context)) {
296                    // denying filter found => ignore following filters
297                    if (log.isDebugEnabled()) {
298                        log.debug(String.format("Filter '%s' denied access", filterId));
299                    }
300                    return false;
301                }
302                if (log.isDebugEnabled()) {
303                    log.debug(String.format("Filter '%s' granted access", filterId));
304                }
305            }
306            return true;
307        } finally {
308            long duration = timerContext.stop();
309            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
310                log.info(String.format("Resolving filters %s took: %.2f ms", filterIds, duration / 1000000.0));
311            }
312        }
313    }
314
315    @Override
316    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
317        if ("actions".equals(extensionPoint)) {
318            actions.addContribution((Action) contribution);
319        } else if ("filters".equals(extensionPoint)) {
320            if (contribution.getClass() == FilterFactory.class) {
321                registerFilterFactory((FilterFactory) contribution);
322            } else {
323                filters.addContribution((DefaultActionFilter) contribution);
324            }
325        } else if ("typeCompatibility".equals(extensionPoint)) {
326            actions.getRegistry().getTypeCategoryRelations().add((TypeCompatibility) contribution);
327        }
328    }
329
330    @Override
331    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
332        if ("actions".equals(extensionPoint)) {
333            actions.removeContribution((Action) contribution);
334        } else if ("filters".equals(extensionPoint)) {
335            if (contribution.getClass() == FilterFactory.class) {
336                unregisterFilterFactory((FilterFactory) contribution);
337            } else {
338                filters.removeContribution((DefaultActionFilter) contribution);
339            }
340        }
341    }
342
343    /**
344     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
345     * @param ff
346     */
347    @Deprecated
348    protected void registerFilterFactory(FilterFactory ff) {
349        getFilterRegistry().removeFilter(ff.id);
350        try {
351            ActionFilter filter = (ActionFilter) Thread.currentThread()
352                                                       .getContextClassLoader()
353                                                       .loadClass(ff.className)
354                                                       .newInstance();
355            filter.setId(ff.id);
356            getFilterRegistry().addFilter(filter);
357        } catch (ReflectiveOperationException e) {
358            log.error("Failed to create action filter", e);
359        }
360    }
361
362    /**
363     * @deprecated seems not used in Nuxeo - should be removed - and anyway the merge is not done
364     * @param ff
365     */
366    @Deprecated
367    public void unregisterFilterFactory(FilterFactory ff) {
368        getFilterRegistry().removeFilter(ff.id);
369    }
370
371    @Override
372    public void remove() {
373    }
374
375}