View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.intercept.method;
17  
18  import org.acegisecurity.ConfigAttribute;
19  import org.acegisecurity.ConfigAttributeDefinition;
20  import org.acegisecurity.SecurityConfig;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  
25  import java.lang.reflect.Method;
26  
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  
33  
34  /**
35   * Stores a {@link ConfigAttributeDefinition} for each method signature defined in a bean context.<p>For
36   * consistency with {@link MethodDefinitionAttributes} as well as support for
37   * <code>MethodDefinitionSourceAdvisor</code>, this implementation will return a
38   * <code>ConfigAttributeDefinition</code> containing all configuration attributes defined against:
39   *  <ul>
40   *      <li>The method-specific attributes defined for the intercepted method of the intercepted class.</li>
41   *      <li>The method-specific attributes defined by any explicitly implemented interface if that interface
42   *      contains a method signature matching that of the intercepted method.</li>
43   *  </ul>
44   *  </p>
45   *  <p>In general you should therefore define the <b>interface method</b>s of your secure objects, not the
46   * implementations. For example, define <code>com.company.Foo.findAll=ROLE_TEST</code> but not
47   * <code>com.company.FooImpl.findAll=ROLE_TEST</code>.</p>
48   *
49   * @author Ben Alex
50   * @version $Id: MethodDefinitionMap.java 1784 2007-02-24 21:00:24Z luke_t $
51   */
52  public class MethodDefinitionMap extends AbstractMethodDefinitionSource {
53      //~ Static fields/initializers =====================================================================================
54  
55      private static final Log logger = LogFactory.getLog(MethodDefinitionMap.class);
56  
57      //~ Instance fields ================================================================================================
58  
59      /** Map from Method to ApplicationDefinition */
60      protected Map methodMap = new HashMap();
61  
62      /** Map from Method to name pattern used for registration */
63      private Map nameMap = new HashMap();
64  
65      //~ Methods ========================================================================================================
66  
67      /**
68       * Add configuration attributes for a secure method. Method names can end or start with <code>&#42</code>
69       * for matching multiple methods.
70       *
71       * @param method the method to be secured
72       * @param attr required authorities associated with the method
73       */
74      public void addSecureMethod(Method method, ConfigAttributeDefinition attr) {
75          logger.info("Adding secure method [" + method + "] with attributes [" + attr + "]");
76          this.methodMap.put(method, attr);
77      }
78  
79      /**
80       * Add configuration attributes for a secure method. Method names can end or start with <code>&#42</code>
81       * for matching multiple methods.
82       *
83       * @param name class and method name, separated by a dot
84       * @param attr required authorities associated with the method
85       *
86       * @throws IllegalArgumentException DOCUMENT ME!
87       */
88      public void addSecureMethod(String name, ConfigAttributeDefinition attr) {
89          int lastDotIndex = name.lastIndexOf(".");
90  
91          if (lastDotIndex == -1) {
92              throw new IllegalArgumentException("'" + name + "' is not a valid method name: format is FQN.methodName");
93          }
94  
95          String className = name.substring(0, lastDotIndex);
96          String methodName = name.substring(lastDotIndex + 1);
97  
98          try {
99              Class clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader());
100             addSecureMethod(clazz, methodName, attr);
101         } catch (ClassNotFoundException ex) {
102             throw new IllegalArgumentException("Class '" + className + "' not found");
103         }
104     }
105 
106     /**
107      * Add configuration attributes for a secure method. Method names can end or start with <code>&#42</code>
108      * for matching multiple methods.
109      *
110      * @param clazz target interface or class
111      * @param mappedName mapped method name
112      * @param attr required authorities associated with the method
113      *
114      * @throws IllegalArgumentException DOCUMENT ME!
115      */
116     public void addSecureMethod(Class clazz, String mappedName, ConfigAttributeDefinition attr) {
117         String name = clazz.getName() + '.' + mappedName;
118 
119         if (logger.isDebugEnabled()) {
120             logger.debug("Adding secure method [" + name + "] with attributes [" + attr + "]");
121         }
122 
123         Method[] methods = clazz.getDeclaredMethods();
124         List matchingMethods = new ArrayList();
125 
126         for (int i = 0; i < methods.length; i++) {
127             if (methods[i].getName().equals(mappedName) || isMatch(methods[i].getName(), mappedName)) {
128                 matchingMethods.add(methods[i]);
129             }
130         }
131 
132         if (matchingMethods.isEmpty()) {
133             throw new IllegalArgumentException("Couldn't find method '" + mappedName + "' on " + clazz);
134         }
135 
136         // register all matching methods
137         for (Iterator it = matchingMethods.iterator(); it.hasNext();) {
138             Method method = (Method) it.next();
139             String regMethodName = (String) this.nameMap.get(method);
140 
141             if ((regMethodName == null) || (!regMethodName.equals(name) && (regMethodName.length() <= name.length()))) {
142                 // no already registered method name, or more specific
143                 // method name specification now -> (re-)register method
144                 if (regMethodName != null) {
145                     logger.debug("Replacing attributes for secure method [" + method + "]: current name [" + name
146                         + "] is more specific than [" + regMethodName + "]");
147                 }
148 
149                 this.nameMap.put(method, name);
150                 addSecureMethod(method, attr);
151             } else {
152                 logger.debug("Keeping attributes for secure method [" + method + "]: current name [" + name
153                     + "] is not more specific than [" + regMethodName + "]");
154             }
155         }
156     }
157 
158     /**
159      * Obtains the configuration attributes explicitly defined against this bean. This method will not return
160      * implicit configuration attributes that may be returned by {@link #lookupAttributes(Method)} as it does not have
161      * access to a method invocation at this time.
162      *
163      * @return the attributes explicitly defined against this bean
164      */
165     public Iterator getConfigAttributeDefinitions() {
166         return methodMap.values().iterator();
167     }
168 
169     /**
170      * Obtains the number of configuration attributes explicitly defined against this bean. This method will
171      * not return implicit configuration attributes that may be returned by {@link #lookupAttributes(Method)} as it
172      * does not have access to a method invocation at this time.
173      *
174      * @return the number of configuration attributes explicitly defined against this bean
175      */
176     public int getMethodMapSize() {
177         return this.methodMap.size();
178     }
179 
180     /**
181      * Return if the given method name matches the mapped name. The default implementation checks for "xxx" and
182      * "xxx" matches.
183      *
184      * @param methodName the method name of the class
185      * @param mappedName the name in the descriptor
186      *
187      * @return if the names match
188      */
189     private boolean isMatch(String methodName, String mappedName) {
190         return (mappedName.endsWith("*") && methodName.startsWith(mappedName.substring(0, mappedName.length() - 1)))
191         || (mappedName.startsWith("*") && methodName.endsWith(mappedName.substring(1, mappedName.length())));
192     }
193 
194     protected ConfigAttributeDefinition lookupAttributes(Method method) {
195         ConfigAttributeDefinition definition = new ConfigAttributeDefinition();
196 
197         // Add attributes explictly defined for this method invocation
198         ConfigAttributeDefinition directlyAssigned = (ConfigAttributeDefinition) this.methodMap.get(method);
199         merge(definition, directlyAssigned);
200 
201         // Add attributes explicitly defined for this method invocation's interfaces
202         Class[] interfaces = method.getDeclaringClass().getInterfaces();
203 
204         for (int i = 0; i < interfaces.length; i++) {
205             Class clazz = interfaces[i];
206 
207             try {
208                 // Look for the method on the current interface
209                 Method interfaceMethod = clazz.getDeclaredMethod(method.getName(), (Class[]) method.getParameterTypes());
210                 ConfigAttributeDefinition interfaceAssigned =
211                         (ConfigAttributeDefinition) this.methodMap.get(interfaceMethod);
212                 merge(definition, interfaceAssigned);
213             } catch (Exception e) {
214                 // skip this interface
215             }
216         }
217 
218         // Return null if empty, as per abstract superclass contract
219         if (definition.size() == 0) {
220             return null;
221         } else {
222             return definition;
223         }
224     }
225 
226     private void merge(ConfigAttributeDefinition definition, ConfigAttributeDefinition toMerge) {
227         if (toMerge == null) {
228             return;
229         }
230 
231         Iterator attribs = toMerge.getConfigAttributes();
232 
233         while (attribs.hasNext()) {
234             definition.addConfigAttribute((ConfigAttribute) attribs.next());
235         }
236     }
237 
238     /**
239      * Easier configuration of the instance, using {@link MethodDefinitionSourceMapping}.
240      *
241      * @param mappings {@link List} of {@link MethodDefinitionSourceMapping} objects.
242      */
243     public void setMappings(List mappings) {
244         Iterator it = mappings.iterator();
245         while (it.hasNext()) {
246             MethodDefinitionSourceMapping mapping = (MethodDefinitionSourceMapping) it.next();
247             ConfigAttributeDefinition configDefinition = new ConfigAttributeDefinition();
248 
249             Iterator configAttributesIt = mapping.getConfigAttributes().iterator();
250             while (configAttributesIt.hasNext()) {
251                 String s = (String) configAttributesIt.next();
252                 configDefinition.addConfigAttribute(new SecurityConfig(s));
253             }
254 
255             addSecureMethod(mapping.getMethodName(), configDefinition);
256         }
257     }
258 }