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}