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