/**
* $Id: EntityProviderMethodStoreImpl.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $
* $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/utils/src/java/org/sakaiproject/entitybroker/util/core/EntityProviderMethodStoreImpl.java $
* EntityProviderMethodStore.java - entity-broker - Jan 13, 2009 11:02:43 AM - azeckoski
**********************************************************************************
* Copyright (c) 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**********************************************************************************/
package org.sakaiproject.entitybroker.util.core;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import org.azeckoski.reflectutils.ReflectUtils;
import org.sakaiproject.entitybroker.EntityReference;
import org.sakaiproject.entitybroker.EntityView;
import org.sakaiproject.entitybroker.entityprovider.EntityProvider;
import org.sakaiproject.entitybroker.entityprovider.EntityProviderMethodStore;
import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction;
import org.sakaiproject.entitybroker.entityprovider.annotations.EntityURLRedirect;
import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsDefineable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.RedirectControllable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.RedirectDefinable;
import org.sakaiproject.entitybroker.entityprovider.extension.CustomAction;
import org.sakaiproject.entitybroker.entityprovider.extension.TemplateMap;
import org.sakaiproject.entitybroker.entityprovider.extension.URLRedirect;
import org.sakaiproject.entitybroker.entityprovider.search.Search;
import org.sakaiproject.entitybroker.util.TemplateParseUtil;
/**
* This stores the various methods used to track different methods allowed for providers
* and also tracks the various methods in the registry
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
public class EntityProviderMethodStoreImpl implements EntityProviderMethodStore {
private HashSet<String> reservedActions = null;
private Map<String, Map<String, CustomAction>> entityActions = new ConcurrentHashMap<String, Map<String,CustomAction>>();
private Map<String, List<URLRedirect>> entityRedirects = new ConcurrentHashMap<String, List<URLRedirect>>();
/**
* Full constructor
*/
public EntityProviderMethodStoreImpl() {
reservedActions = new HashSet<String>(4);
reservedActions.add("describe");
reservedActions.add("new");
reservedActions.add("edit");
reservedActions.add("delete");
}
// ACTIONS
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#findCustomActions(org.sakaiproject.entitybroker.entityprovider.EntityProvider, boolean)
*/
public CustomAction[] findCustomActions(EntityProvider entityProvider, boolean ignoreFailures) {
ArrayList<CustomAction> actions = new ArrayList<CustomAction>();
Method[] methods = entityProvider.getClass().getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(EntityCustomAction.class)) {
EntityCustomAction ecaAnnote = method.getAnnotation(EntityCustomAction.class);
String action = ecaAnnote.action();
if (null == action || "".equals(action)) {
action = method.getName();
}
String viewKey = ecaAnnote.viewKey();
if (null == viewKey || "".equals(viewKey)) {
//viewKey = EntityView.VIEW_SHOW;
viewKey = null; // allow any type of request
}
CustomAction ca = new CustomAction(action, viewKey, method.getName());
try {
ca.methodArgTypes = validateActionParamTypes(method.getParameterTypes(), method.getName());
} catch(IllegalArgumentException e) {
if (! ignoreFailures) {
throw new IllegalArgumentException(e);
}
}
ca.setMethod(method); // store the method in the ca
actions.add(ca);
} else if (method.getName().endsWith(ActionsExecutable.ACTION_METHOD_SUFFIX)) {
String action = method.getName().substring(0, method.getName().length() - ActionsExecutable.ACTION_METHOD_SUFFIX.length());
CustomAction ca = new CustomAction(action, EntityView.VIEW_SHOW, method.getName());
try {
ca.methodArgTypes = validateActionParamTypes(method.getParameterTypes(), method.getName());
} catch (IllegalArgumentException e) {
System.out.println("WARN A method ("+method.getName()+") in the entity provider for prefix ("
+ entityProvider.getEntityPrefix()+") appears to be a custom action method but"
+ "does not have a valid set of parameter types, this may be ok but should be checked on: " + e.getMessage());
continue;
}
ca.setMethod(method); // store the method in the ca
actions.add(ca);
}
}
return actions.toArray(new CustomAction[actions.size()]);
}
/**
* Set the custom actions for this prefix
* @param prefix an entity prefix
* @param actions a map of action -> {@link CustomAction}
*/
public void setCustomActions(String prefix, Map<String,CustomAction> actions) {
Map<String,CustomAction> cas = new HashMap<String, CustomAction>();
StringBuilder sb = new StringBuilder();
for (Entry<String, CustomAction> ca : actions.entrySet()) {
CustomAction action = ca.getValue();
if (action == null || ca.getKey() == null || "".equals(ca.getKey())) {
throw new IllegalArgumentException("custom action object and action key must not be null");
}
if (reservedActions.contains(ca.getKey().toLowerCase())) {
StringBuilder rsb = new StringBuilder();
for (String reserved : reservedActions) {
if (rsb.length() > 0) {
rsb.append(", ");
}
rsb.append(reserved);
}
throw new IllegalArgumentException(ca.getKey() + " is a reserved word and cannot be used as a custom action key "
+ ", reserved words include: " + rsb);
}
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(ca.getValue().toString());
cas.put(ca.getKey(), action.copy()); // make a copy to avoid holding objects from another ClassLoader
}
entityActions.put(prefix, actions);
System.out.println("INFO Registered "+actions.size()+" custom actions for entity prefix ("+prefix+"): " + sb.toString());
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#addCustomAction(java.lang.String, org.sakaiproject.entitybroker.entityprovider.extension.CustomAction)
*/
public void addCustomAction(String prefix, CustomAction customAction) {
// NOTE: we are always creating a new map here to ensure there are no collisions
Map<String,CustomAction> actions = new HashMap<String, CustomAction>();
if (entityActions.containsKey(prefix)) {
// add the existing ones first
actions.putAll(entityActions.get(prefix));
}
// add the new one to the map
actions.put(customAction.action, customAction);
// put the new map into the store
setCustomActions(prefix, actions);
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#getCustomAction(java.lang.String, java.lang.String)
*/
public CustomAction getCustomAction(String prefix, String action) {
CustomAction ca = null;
if (entityActions.containsKey(prefix)) {
ca = entityActions.get(prefix).get(action);
}
return ca;
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#removeCustomActions(java.lang.String)
*/
public void removeCustomActions(String prefix) {
entityActions.remove(prefix);
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#getCustomActions(java.lang.String)
*/
public List<CustomAction> getCustomActions(String prefix) {
List<CustomAction> actions = new ArrayList<CustomAction>();
Map<String, CustomAction> actionMap = entityActions.get(prefix);
if (actionMap != null) {
for (Entry<String, CustomAction> entry : actionMap.entrySet()) {
actions.add(entry.getValue());
}
}
return actions;
}
// REDIRECTS
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#findURLRedirectMethods(org.sakaiproject.entitybroker.entityprovider.EntityProvider)
*/
public URLRedirect[] findURLRedirectMethods(EntityProvider entityProvider) {
ArrayList<URLRedirect> redirects = new ArrayList<URLRedirect>();
Method[] methods = entityProvider.getClass().getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(EntityURLRedirect.class)) {
EntityURLRedirect eurAnnote = method.getAnnotation(EntityURLRedirect.class);
String template = eurAnnote.value();
if (null == template || "".equals(template)) {
throw new IllegalArgumentException("there is no template set for the annotation: " + EntityURLRedirect.class);
}
URLRedirect redirect = null;
try {
redirect = new URLRedirect(template, method.getName(),
validateRedirectParamTypes(method.getParameterTypes(), method.getName()));
} catch (RuntimeException e) {
throw new IllegalArgumentException("Failed to validate redirect templates from methods for prefix ("
+entityProvider.getEntityPrefix() + "): " + e.getMessage(), e);
}
redirect.setMethod(method); // cache to reduce lookup cost
redirects.add(redirect);
}
}
return redirects.toArray(new URLRedirect[redirects.size()]);
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#addURLRedirects(java.lang.String, org.sakaiproject.entitybroker.util.request.URLRedirect[])
*/
public void addURLRedirects(String prefix, URLRedirect[] redirects) {
if (redirects != null && redirects.length > 0) {
ArrayList<URLRedirect> urlRedirects = new ArrayList<URLRedirect>();
if (entityRedirects.containsKey(prefix)) {
List<URLRedirect> current = entityRedirects.get(prefix);
urlRedirects.addAll(current);
}
StringBuilder sb = new StringBuilder();
for (URLRedirect redirect : redirects) {
if (redirect == null || redirect.template == null || "".equals(redirect.template)) {
throw new IllegalArgumentException("url redirect and pattern template must not be null");
}
if (redirect.outgoingTemplate == null
&& redirect.methodName == null
&& redirect.controllable == false) {
throw new IllegalArgumentException("url redirect targetTemplate or methodName must not be null");
}
if (sb.length() > 0) {
sb.append(", ");
}
if (urlRedirects.contains(redirect)) {
throw new IllegalArgumentException("Duplicate redirect template definition: " +
"The redirect set already contains this template: " + redirect.template + ", it cannot contain 2 identical templates");
}
urlRedirects.add(redirect);
sb.append(redirect.template);
}
entityRedirects.put(prefix, urlRedirects);
System.out.println("INFO Registered "+redirects.length+" url redirects for entity prefix ("+prefix+"): " + sb.toString());
}
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#removeURLRedirects(java.lang.String)
*/
public void removeURLRedirects(String prefix) {
entityRedirects.remove(prefix);
}
/* (non-Javadoc)
* @see org.sakaiproject.entitybroker.impl.EntityProviderMethodStoreAPI#getURLRedirects(java.lang.String)
*/
public List<URLRedirect> getURLRedirects(String prefix) {
List<URLRedirect> redirects = new ArrayList<URLRedirect>();
if (entityRedirects.containsKey(prefix)) {
redirects.addAll( entityRedirects.get(prefix) );
}
return redirects;
}
// STATICS
protected static Class<?>[] validActionParamTypes = {
EntityReference.class,
EntityView.class,
Search.class,
String.class,
OutputStream.class,
Map.class
};
protected static Class<?>[] validRedirectParamTypes = {
String.class,
String[].class,
Map.class
};
/**
* Validates the parameter types on a method to make sure they are valid
* @param paramTypes an array of parameter types
* @param methodName the name of the method which is being validated (for debugging mostly)
* @return the new valid array of param types
* @throws IllegalArgumentException if the param types are invalid
*/
protected static Class<?>[] validateActionParamTypes(Class<?>[] paramTypes, String methodName) {
try {
return validateParamTypes(paramTypes, validActionParamTypes);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid custom action method ("+methodName+"): " + e.getMessage(), e);
}
}
/**
* Validates the parameter types on a method to make sure they are valid
* @param paramTypes an array of parameter types
* @param methodName the name of the method which is being validated (for debugging mostly)
* @return the new valid array of param types
* @throws IllegalArgumentException is the param types are invalid
*/
protected static Class<?>[] validateRedirectParamTypes(Class<?>[] paramTypes, String methodName) {
try {
return validateParamTypes(paramTypes, validRedirectParamTypes);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid redirect method ("+methodName+"): " + e.getMessage(), e);
}
}
/**
* Validates param types against a given list of valid types
* @param paramTypes an array of parameter types
* @param validTypes the types that are valid
* @return the new valid array of param types
* @throws IllegalArgumentException is the param types are invalid
*/
protected static Class<?>[] validateParamTypes(Class<?>[] paramTypes, Class<?>[] validTypes) {
Class<?>[] validParams = new Class<?>[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
boolean found = false;
Class<?> paramType = paramTypes[i];
if (validTypes != null) {
for (int j = 0; j < validTypes.length; j++) {
if (validTypes[j].isAssignableFrom(paramType)) {
validParams[i] = validTypes[j];
found = true;
}
}
}
if (!found) {
throw new IllegalArgumentException("Invalid method params: param type is not allowed: " + paramType.getName()
+ " : valid types include: " + ReflectUtils.arrayToString(validTypes));
}
}
return validParams;
}
/**
* Takes a set of custom actions and validates them
* @param actionsDefineable an entity provider which uses custom actions
* @param customActions
* @throws IllegalArgumentException if the custom actions are invalid
*/
public static void validateCustomActionMethods(ActionsDefineable actionsDefineable) {
CustomAction[] customActions = actionsDefineable.defineActions();
Method[] methods = actionsDefineable.getClass().getMethods();
for (int i = 0; i < customActions.length; i++) {
CustomAction ca = customActions[i];
if (ca == null) {
throw new IllegalArgumentException("Custom actions cannot be null");
}
if (ca.methodName == null || "".equals(ca.methodName)) {
throw new IllegalArgumentException("Method names must be set for all custom actions when using " + ActionsDefineable.class);
}
boolean found = false;
for (Method method : methods) {
String name = method.getName();
if (name.equals(ca.methodName)) {
ca.methodArgTypes = validateActionParamTypes(method.getParameterTypes(), ca.methodName);
ca.setMethod(method); // store the method in the ca
found = true;
break;
}
}
if (!found) {
throw new IllegalArgumentException("No public method found called ("+ca.methodName
+") in the entity provider for prefix ("+actionsDefineable.getEntityPrefix()+"), "
+ "the method was defined as a custom action by " + ActionsDefineable.class);
}
}
}
/**
* Validates the provided URL templates in an entity provider and outputs the
* URL redirect objects as an array
* @param configDefinable the entity provider
* @return the array of URL redirects
*/
public static URLRedirect[] validateDefineableTemplates(RedirectDefinable configDefinable) {
List<URLRedirect> redirects = new ArrayList<URLRedirect>();
TemplateMap[] urlMappings = configDefinable.defineURLMappings();
if (urlMappings == null || urlMappings.length == 0) {
// this is ok then, or is it?
System.out.println("WARN RedirectDefinable: no templates defined for url redirect");
} else {
for (TemplateMap templateMap : urlMappings) {
String incomingTemplate = templateMap.getIncomingTemplate();
String outgoingTemplate = templateMap.getOutgoingTemplate();
URLRedirect redirect = null;
try {
redirect = new URLRedirect(incomingTemplate, outgoingTemplate);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Failed to validate defined redirect templates for prefix ("
+configDefinable.getEntityPrefix() + "): " + e.getMessage(), e);
}
if (incomingTemplate.equals(outgoingTemplate)) {
throw new IllegalArgumentException("Invalid outgoing redirect template ("
+outgoingTemplate+") for entity prefix ("+configDefinable.getEntityPrefix()
+"), template is identical to incoming template ("+incomingTemplate+") and would cause an infinite redirect");
}
// make sure that we check the target vars match the template vars
List<String> incomingVars = new ArrayList<String>( redirect.preProcessedTemplate.variableNames );
incomingVars.add(TemplateParseUtil.PREFIX);
incomingVars.add(TemplateParseUtil.EXTENSION);
incomingVars.add(TemplateParseUtil.DOT_EXTENSION);
incomingVars.add(TemplateParseUtil.QUERY_STRING);
incomingVars.add(TemplateParseUtil.QUESTION_QUERY_STRING);
List<String> outgoingVars = redirect.outgoingPreProcessedTemplate.variableNames;
if (incomingVars.containsAll(outgoingVars)) {
// all is ok
redirects.add(redirect);
} else {
throw new IllegalArgumentException("Outgoing template ("+outgoingTemplate+") has variables which do not occur in "
+ "incoming template ("+incomingTemplate+") and " + TemplateParseUtil.PREFIX
+ ", please make sure your outgoing template only includes variables"
+ " which can be found in the incoming template and " + TemplateParseUtil.PREFIX);
}
}
}
return redirects.toArray(new URLRedirect[redirects.size()]);
}
/**
* Execute this validate and get the templates so they can be registered
* @param configControllable the entity provider
* @return the array of URL redirects
*/
public static URLRedirect[] validateControllableTemplates(RedirectControllable configControllable) {
List<URLRedirect> redirects = new ArrayList<URLRedirect>();
String[] templates = configControllable.defineHandledTemplatePatterns();
if (templates == null || templates.length == 0) {
throw new IllegalArgumentException("RedirectControllable: invalid defineHandledTemplatePatterns: " +
"this should return a non-empty array of templates or the capability should not be used");
} else {
for (String template : templates) {
URLRedirect redirect = null;
try {
redirect = new URLRedirect(template);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Failed to validate redirect templates from handled template patterns for prefix ("
+configControllable.getEntityPrefix() + "): " + e.getMessage(), e);
}
redirects.add(redirect);
}
}
return redirects.toArray(new URLRedirect[redirects.size()]);
}
}