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