/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache 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.apache.org/licenses/LICENSE-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.apache.wicket.security.swarm.actions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.wicket.authorization.Action; import org.apache.wicket.security.actions.*; import org.apache.wicket.util.string.AppendingStringBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Default implementation of an action factory. It handles access, inherit, render and * enable actions. Because actions are immutable and in order to improve performance, * generated actions are cached. * * @author marrink */ public class SwarmActionFactory implements WaspActionFactory { private static final Logger log = LoggerFactory.getLogger(SwarmActionFactory.class); /** * Maximum power of 2 that can be used to assign to an action. */ protected static final int maxAssingableAction = (int) Math.pow(2, 30); /** * maps int's to Strings. */ private Map<Integer, String> stringValues = new HashMap<Integer, String>(10); /** * cache that maps int's to actions and Strings to actions */ private Map<String, SwarmAction> cachedStringActions = new HashMap<String, SwarmAction>(); private Map<Integer, SwarmAction> cachedIntActions = new HashMap<Integer, SwarmAction>(); /** * Maps int's to actions. and classes to actions. */ private Map<Integer, SwarmAction> registeredIntActions = new HashMap<Integer, SwarmAction>(); private Map<Class< ? extends WaspAction>, SwarmAction> registeredClassActions = new HashMap<Class< ? extends WaspAction>, SwarmAction>(); private int power = -1; private int maxAction = 0; private final Object factoryKey; /** * Registers the default actions: access, inherit, render and enable. * * @param key * using this key the factory registers itself to the {@link Actions} * object. */ public SwarmActionFactory(Object key) { super(); this.factoryKey = key; Actions.registerActionFactory(key, this); try { register(Access.class, "access"); register(Inherit.class, "inherit"); register(Render.class, "render"); register(Enable.class, new ImpliesOtherAction("enable", this, Render.class)); } catch (RegistrationException e) { log.error(e.getMessage(), e); } } /** * * @see org.apache.wicket.security.actions.WaspActionFactory#getAction(org.apache.wicket.authorization.Action) */ public WaspAction getAction(Action action) { if (action != null) try { return getAction(action.getName()); } catch (IllegalArgumentException e) { // according to the spec we return null if the action does not // exist } return null; } /** * @param actions * empty string means Access * @see ActionFactory#getAction(String) */ public WaspAction getAction(String actions) { String saveActions = convertWicket2Wasp(actions); SwarmAction sa = getCachedAction(saveActions); if (sa == null) { int actionValues = parseActions(saveActions); // rebuild action name String nameValues = buildActionString(actionValues); sa = new SwarmAction(actionValues, nameValues, getFactoryKey()); cacheAction(saveActions, sa); } return sa; } /** * Caches an action under its string form. * * @param name * @param action */ protected synchronized final void cacheAction(String name, SwarmAction action) { cachedStringActions.put(name, action); } /** * Returns a cached action. * * @param name * @return the cached action or null. */ protected synchronized final SwarmAction getCachedAction(String name) { return cachedStringActions.get(name); } /** * Returns an action based on its int value. * * @param actions * @return the action * @throws IllegalArgumentException * if no action can be formed based on the input */ public SwarmAction getAction(int actions) { SwarmAction ja = getCachedAction(actions); if (ja == null) { if (actions > maxAction) throw new IllegalArgumentException("Max value for actions = " + maxAction + ", you used " + actions); if (actions < 0) throw new IllegalArgumentException("Min value for actions = 0, you used " + actions); ja = new SwarmAction(actions, buildActionString(actions), getFactoryKey()); cacheAction(new Integer(actions), ja); } return ja; } /** * Caches an action under its int form. * * @param actions * @param ja */ protected synchronized final void cacheAction(Integer actions, SwarmAction ja) { cachedIntActions.put(actions, ja); } /** * Returns a cached action. * * @param actions * @return the cached action or null. */ protected synchronized final SwarmAction getCachedAction(int actions) { return cachedIntActions.get(actions); } /** * Returns the registered string value of the given action. * * @param action * the internal value of the action * @return the registered string or null if no string value was registered for this * action. */ protected final String valueOf(Integer action) { return stringValues.get(action); } /** * Builds a logically ordered comma separated string of all the actions this * permission has. Based on the logical and of the supplied actions. Subclasses should * always return the same string (action order) for the same action. * * @param actions * the internal action value * @return string containing all the actions. * */ protected String buildActionString(int actions) { AppendingStringBuffer buff = new AppendingStringBuffer(power > 0 ? 10 * power : 10); // estimate 10 chars per name for (int i = -1; i < power; i++) { appendActionString(buff, actions, registeredIntActions.get(i).actions()); } if (buff.length() > 0) // should always be the case buff.delete(buff.length() - 2, buff.length()); return buff.toString(); } /** * Appends the string value of the action only if the actions imply the waspAction * * @param buff * where the string will be appended to. * @param actions * the available actions * @param waspAction * the action it should imply in order to append the string */ protected final void appendActionString(AppendingStringBuffer buff, int actions, int waspAction) { if (implies(actions, waspAction)) buff.append(valueOf(new Integer(waspAction))).append(", "); } /** * Check if the action is available in the actions. This performs a bitwise check. * * @param actions * the actions that might contain action * @param action * the action we check for in actions * @return true if actions contains action */ protected final boolean implies(int actions, int action) { return ((actions & action) == action); } /** * Parses a comma separated String containing actions. Access is the default and will * also be substituted for any empty or null String. using a string like 'render, * render' is pointless but does not brake anything. Order of the actions is also not * important. * * @param actions * @return a logical and of the actions. * @throws IllegalArgumentException * if (one of) the action(s) is not recognized. */ protected int parseActions(String actions) { int sum = 0; // no actions or empty actions equal access if (actions != null) { String[] actionz = actions.split(","); String action = null; Set<Integer> keys = stringValues.keySet(); for (int i = 0; i < actionz.length; i++) { action = actionz[i].trim(); if (action.equals("")) break; // Access if (action.equals("all")) return getAction(AllActions.class).actions(); boolean found = false; for (Integer key : keys) { if (action.equalsIgnoreCase(valueOf(key))) { sum = sum | key.intValue(); found = true; break; } } if (!found) throw new IllegalArgumentException("Invalid action: " + action + " in: " + actions); } } return sum; } /** * * @see org.apache.wicket.security.actions.ActionFactory#getAction(java.lang.Class) */ public synchronized SwarmAction getAction(Class< ? extends WaspAction> waspActionClass) { if (AllActions.class.isAssignableFrom(waspActionClass)) { SwarmAction all = registeredClassActions.get(Access.class); for (WaspAction action : registeredClassActions.values()) { all = all.add(action); } return all; } SwarmAction action = registeredClassActions.get(waspActionClass); if (action == null) throw new IllegalArgumentException("" + waspActionClass + " is not registered"); return action; } /** * * @see org.apache.wicket.security.actions.ActionFactory#register(java.lang.Class, * java.lang.String) */ public synchronized SwarmAction register(Class< ? extends WaspAction> waspActionClass, String name) throws RegistrationException { if (AllActions.class.isAssignableFrom(waspActionClass)) throw new RegistrationException("Can not register 'all' actions"); SwarmAction temp = registeredClassActions.get(waspActionClass); if (temp != null) return temp; if (WaspAction.class.isAssignableFrom(waspActionClass)) { if (power > 30) throw new RegistrationException("Can not register more then 32 different actions."); // 32 since we start at 0 :) int action = nextPowerOf2(); return register(waspActionClass, new SwarmAction(action, name, getFactoryKey())); } throw new RegistrationException(waspActionClass + " is not a " + WaspAction.class.getName()); } /** * Returns the number of registered classes. By default there are 4 classes * registered. * * @return the number of registered classes. */ public final int getNumberOfRegisteredClasses() { return power + 1; } /** * The next action value. Note that you can only register classes while this is < * {@link Integer#MAX_VALUE} * * @return the int value of 2^(numberOfRegisteredClasses()-1) */ protected final int nextPowerOf2() { return (int) Math.pow(2, power); } /** * Renames build in wicket actions to there wasp counterpart. more specifically it * converts the string to lowercase. * * @param name * the name of the action * @return the converted name of the action. */ protected final String convertWicket2Wasp(String name) { if (name == null) return ""; return name.toLowerCase(); } /** * Registers a new action. Use this in combination with * {@link SwarmAction#SwarmAction(int, String, ActionFactory)}. Example:<br> * * <pre> * <code> * register(Enable.class, new ImpliesReadAction(nextPowerOf2(), "enable", this)); * class ImpliesReadAction extends SwarmAction * { * public ImpliesReadAction(int actions, String name, ActionFactory factory) * { * super(actions * | ((SwarmAction)factory.getAction(org.apache.wicket.security.actions.Render.class)) * .actions(), name); * } * } * </code> * </pre> * * Note all actions registered in this way must use nextPowerOf2() and then * immediately register the action to preserve consistency. * * @param waspActionClass * the class under which to register the action * @param action * the actual implementation (note that it does not need to implement the * supplied waspActionClass) * @return the action * @throws RegistrationException * if the action can not be registered. * @see #nextPowerOf2() * @see SwarmAction#SwarmAction(int, String, ActionFactory) * */ protected final synchronized SwarmAction register(Class< ? extends WaspAction> waspActionClass, SwarmAction action) throws RegistrationException { // sanity checks if (AllActions.class.isAssignableFrom(waspActionClass)) throw new RegistrationException("Can not register 'all' actions"); if (power > 30) throw new RegistrationException("Can not register more then 32 different actions."); int assignedPowerOf2 = nextPowerOf2(); if (assignedPowerOf2 > maxAssingableAction) throw new RegistrationException( "Unable to register an action with a base value greater then " + maxAssingableAction); if (assignedPowerOf2 < 0) throw new RegistrationException(assignedPowerOf2 + " is not a positive value"); // includes any implied actions Integer powerOf2 = new Integer(action.actions()); if (!implies(powerOf2.intValue(), assignedPowerOf2)) throw new RegistrationException("Unable to register action '" + action.getName() + "' with value " + powerOf2 + " expected " + assignedPowerOf2 + " or more."); // end of checks stringValues.put(powerOf2, action.getName()); registeredIntActions.put(power, action); registeredClassActions.put(waspActionClass, action); maxAction += assignedPowerOf2; power++; return action; } /** * * @see org.apache.wicket.security.actions.ActionFactory#getRegisteredActions() */ public List<WaspAction> getRegisteredActions() { return new ArrayList<WaspAction>(registeredClassActions.values()); } /** * Clears registration and cached values. After you destroy this factory you must not * use it again. * * @see org.apache.wicket.security.actions.ActionFactory#destroy() */ public void destroy() { power = 31; // prevents new registrations maxAction = 0; // prevents lookups // protected against multiple destroy calls if (registeredClassActions != null) registeredClassActions.clear(); if (registeredIntActions != null) registeredIntActions.clear(); if (cachedStringActions != null) cachedStringActions.clear(); if (cachedIntActions != null) cachedIntActions.clear(); if (stringValues != null) stringValues.clear(); registeredClassActions = null; registeredIntActions = null; cachedStringActions = null; cachedIntActions = null; stringValues = null; Actions.unregisterActionFactory(getFactoryKey()); } /** * Any class that implies another action. * * @author marrink */ protected static class ImpliesOtherAction extends SwarmAction { private static final long serialVersionUID = 1L; /** * the base action value * * @param name * name of the new action * @param factory * factory where this class will be registered * @param otherAction * a single action class to imply, not null */ public ImpliesOtherAction(String name, SwarmActionFactory factory, Class< ? extends WaspAction> otherAction) { super(factory.nextPowerOf2() | (factory.getAction(otherAction)).actions(), name, factory.getFactoryKey()); } /** * the base action value * * @param name * name of the new action * @param factory * factory where this class will be registered * @param otherActions * any number of action classes to imply */ public ImpliesOtherAction(String name, SwarmActionFactory factory, Class< ? extends WaspAction>... otherActions) { super(factory.nextPowerOf2() | bitwiseOr(factory, otherActions), name, factory .getFactoryKey()); } /** * Creates a bitwise or of all the actions supplied. Note that all these classes * must already be registered. * * @param factory * @param otherActions * any number of action classes to imply * @return */ private static final int bitwiseOr(SwarmActionFactory factory, Class< ? extends WaspAction>[] otherActions) { int result = 0; if (otherActions != null) { for (Class< ? extends WaspAction> action : otherActions) { result = result | factory.getAction(action).actions(); } } return result; } } /** * Gets key. * * @return key */ protected final Object getFactoryKey() { return factoryKey; } }