View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.crypto.hash.format;
20  
21  import org.apache.shiro.util.ClassUtils;
22  import org.apache.shiro.util.StringUtils;
23  import org.apache.shiro.util.UnknownClassException;
24  
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.Map;
28  import java.util.Set;
29  
30  /**
31   * This default {@code HashFormatFactory} implementation heuristically determines a {@code HashFormat} class to
32   * instantiate based on the input argument and returns a new instance of the discovered class.  The heuristics are
33   * detailed in the {@link #getInstance(String) getInstance} method documentation.
34   *
35   * @since 1.2
36   */
37  public class DefaultHashFormatFactory implements HashFormatFactory {
38  
39      private Map<String, String> formatClassNames; //id - to - fully qualified class name
40  
41      private Set<String> searchPackages; //packages to search for HashFormat implementations
42  
43      public DefaultHashFormatFactory() {
44          this.searchPackages = new HashSet<String>();
45          this.formatClassNames = new HashMap<String, String>();
46      }
47  
48      /**
49       * Returns a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
50       * <p/>
51       * This map will be used by the {@link #getInstance(String) getInstance} implementation:  that method's argument
52       * will be used as a lookup key to this map.  If the map returns a value, that value will be used to instantiate
53       * and return a new {@code HashFormat} instance.
54       * <h3>Defaults</h3>
55       * Shiro's default HashFormat implementations (as listed by the {@link ProvidedHashFormat} enum) will
56       * be searched automatically independently of this map.  You only need to populate this map with custom
57       * {@code HashFormat} implementations that are <em>not</em> already represented by a {@code ProvidedHashFormat}.
58       * <h3>Efficiency</h3>
59       * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
60       * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
61       * need to be supported by this factory.
62       *
63       * @return a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
64       */
65      public Map<String, String> getFormatClassNames() {
66          return formatClassNames;
67      }
68  
69      /**
70       * Sets the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation} map to be used in
71       * the {@link #getInstance(String)} implementation.  See the {@link #getFormatClassNames()} JavaDoc for more
72       * information.
73       * <h3>Efficiency</h3>
74       * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
75       * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
76       * need to be supported by this factory.
77       *
78       * @param formatClassNames the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation}
79       *                         map to be used in the {@link #getInstance(String)} implementation.
80       */
81      public void setFormatClassNames(Map<String, String> formatClassNames) {
82          this.formatClassNames = formatClassNames;
83      }
84  
85      /**
86       * Returns a set of package names that can be searched for {@link HashFormat} implementations according to
87       * heuristics defined in the {@link #getHashFormatClass(String, String) getHashFormat(packageName, token)} JavaDoc.
88       * <h3>Efficiency</h3>
89       * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
90       * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
91       * need to be supported by this factory.
92       *
93       * @return a set of package names that can be searched for {@link HashFormat} implementations
94       * @see #getHashFormatClass(String, String)
95       */
96      public Set<String> getSearchPackages() {
97          return searchPackages;
98      }
99  
100     /**
101      * Sets a set of package names that can be searched for {@link HashFormat} implementations according to
102      * heuristics defined in the {@link #getHashFormatClass(String, String) getHashFormat(packageName, token)} JavaDoc.
103      * <h3>Efficiency</h3>
104      * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
105      * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
106      * need to be supported by this factory.
107      *
108      * @param searchPackages a set of package names that can be searched for {@link HashFormat} implementations
109      */
110     public void setSearchPackages(Set<String> searchPackages) {
111         this.searchPackages = searchPackages;
112     }
113 
114     public HashFormat getInstance(String in) {
115         if (in == null) {
116             return null;
117         }
118 
119         HashFormat hashFormat = null;
120         Class clazz = null;
121 
122         //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance
123         //optimization.  If the input arg is an MCF-formatted string, there will be many unnecessary ClassLoader
124         //misses which can be slow.  By checking the MCF-formatted option, we can significantly improve performance
125         if (in.startsWith(ModularCryptFormat.TOKEN_DELIMITER)) {
126             //odds are high that the input argument is not a fully qualified class name or a format key (e.g. 'hex',
127             //base64' or 'shiro1').  Try to find the key and lookup via that:
128             String test = in.substring(ModularCryptFormat.TOKEN_DELIMITER.length());
129             String[] tokens = test.split("\\" + ModularCryptFormat.TOKEN_DELIMITER);
130             //the MCF ID is always the first token in the delimited string:
131             String possibleMcfId = (tokens != null && tokens.length > 0) ? tokens[0] : null;
132             if (possibleMcfId != null) {
133                 //found a possible MCF ID - test it using our heuristics to see if we can find a corresponding class:
134                 clazz = getHashFormatClass(possibleMcfId);
135             }
136         }
137 
138         if (clazz == null) {
139             //not an MCF-formatted string - use the unaltered input arg and go through our heuristics:
140             clazz = getHashFormatClass(in);
141         }
142 
143         if (clazz != null) {
144             //we found a HashFormat class - instantiate it:
145             hashFormat = newHashFormatInstance(clazz);
146         }
147 
148         return hashFormat;
149     }
150 
151     /**
152      * Heuristically determine the fully qualified HashFormat implementation class name based on the specified
153      * token.
154      * <p/>
155      * This implementation functions as follows (in order):
156      * <ol>
157      * <li>See if the argument can be used as a lookup key in the {@link #getFormatClassNames() formatClassNames}
158      * map.  If a value (a fully qualified class name {@link HashFormat HashFormat} implementation) is found,
159      * {@link ClassUtils#forName(String) lookup} the class and return it.</li>
160      * <li>
161      * Check to see if the token argument is a
162      * {@link ProvidedHashFormat} enum value.  If so, acquire the corresponding {@code HashFormat} class and
163      * return it.
164      * </li>
165      * <li>
166      * Check to see if the token argument is itself a fully qualified class name.  If so, try to load the class
167      * and return it.
168      * </li>
169      * <li>If the above options do not result in a discovered class, search all all configured
170      * {@link #getSearchPackages() searchPackages} using heuristics defined in the
171      * {@link #getHashFormatClass(String, String) getHashFormatClass(packageName, token)} method documentation
172      * (relaying the {@code token} argument to that method for each configured package).
173      * </li>
174      * </ol>
175      * <p/>
176      * If a class is not discovered via any of the above means, {@code null} is returned to indicate the class
177      * could not be found.
178      *
179      * @param token the string token from which a class name will be heuristically determined.
180      * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined.
181      */
182     protected Class getHashFormatClass(String token) {
183 
184         Class clazz = null;
185 
186         //check to see if the token is a configured FQCN alias.  This is faster than searching packages,
187         //so we try this first:
188         if (this.formatClassNames != null) {
189             String value = this.formatClassNames.get(token);
190             if (value != null) {
191                 //found an alias - see if the value is a class:
192                 clazz = lookupHashFormatClass(value);
193             }
194         }
195 
196         //check to see if the token is one of Shiro's provided FQCN aliases (again, faster than searching):
197         if (clazz == null) {
198             ProvidedHashFormat provided = ProvidedHashFormat.byId(token);
199             if (provided != null) {
200                 clazz = provided.getHashFormatClass();
201             }
202         }
203 
204         if (clazz == null) {
205             //check to see if 'token' was a FQCN itself:
206             clazz = lookupHashFormatClass(token);
207         }
208 
209         if (clazz == null) {
210             //token wasn't a FQCN or a FQCN alias - try searching in configured packages:
211             if (this.searchPackages != null) {
212                 for (String packageName : this.searchPackages) {
213                     clazz = getHashFormatClass(packageName, token);
214                     if (clazz != null) {
215                         //found it:
216                         break;
217                     }
218                 }
219             }
220         }
221 
222         if (clazz != null) {
223             assertHashFormatImpl(clazz);
224         }
225 
226         return clazz;
227     }
228 
229     /**
230      * Heuristically determine the fully qualified {@code HashFormat} implementation class name in the specified
231      * package based on the provided token.
232      * <p/>
233      * The token is expected to be a relevant fragment of an unqualified class name in the specified package.
234      * A 'relevant fragment' can be one of the following:
235      * <ul>
236      * <li>The {@code HashFormat} implementation unqualified class name</li>
237      * <li>The prefix of an unqualified class name ending with the text {@code Format}.  The first character of
238      * this prefix can be upper or lower case and both options will be tried.</li>
239      * <li>The prefix of an unqualified class name ending with the text {@code HashFormat}.  The first character of
240      * this prefix can be upper or lower case and both options will be tried.</li>
241      * <li>The prefix of an unqualified class name ending with the text {@code CryptoFormat}.  The first character
242      * of this prefix can be upper or lower case and both options will be tried.</li>
243      * </ul>
244      * <p/>
245      * Some examples:
246      * <table>
247      * <tr>
248      * <th>Package Name</th>
249      * <th>Token</th>
250      * <th>Expected Output Class</th>
251      * <th>Notes</th>
252      * </tr>
253      * <tr>
254      * <td>{@code com.foo.whatever}</td>
255      * <td>{@code MyBarFormat}</td>
256      * <td>{@code com.foo.whatever.MyBarFormat}</td>
257      * <td>Token is a complete unqualified class name</td>
258      * </tr>
259      * <tr>
260      * <td>{@code com.foo.whatever}</td>
261      * <td>{@code Bar}</td>
262      * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
263      * {@code com.foo.whatever.BarCryptFormat}</td>
264      * <td>The token is only part of the unqualified class name - i.e. all characters in front of the {@code *Format}
265      * {@code *HashFormat} or {@code *CryptFormat} suffix.  Note that the {@code *Format} variant will be tried before
266      * {@code *HashFormat} and then finally {@code *CryptFormat}</td>
267      * </tr>
268      * <tr>
269      * <td>{@code com.foo.whatever}</td>
270      * <td>{@code bar}</td>
271      * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
272      * {@code com.foo.whatever.BarCryptFormat}</td>
273      * <td>Exact same output as the above {@code Bar} input example. (The token differs only by the first character)</td>
274      * </tr>
275      * </table>
276      *
277      * @param packageName the package to search for matching {@code HashFormat} implementations.
278      * @param token       the string token from which a class name will be heuristically determined.
279      * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined.
280      */
281     protected Class getHashFormatClass(String packageName, String token) {
282         String test = token;
283         Class clazz = null;
284         String pkg = packageName == null ? "" : packageName;
285 
286         //1. Assume the arg is a fully qualified class name in the classpath:
287         clazz = lookupHashFormatClass(test);
288 
289         if (clazz == null) {
290             test = pkg + "." + token;
291             clazz = lookupHashFormatClass(test);
292         }
293 
294         if (clazz == null) {
295             test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "Format";
296             clazz = lookupHashFormatClass(test);
297         }
298 
299         if (clazz == null) {
300             test = pkg + "." + token + "Format";
301             clazz = lookupHashFormatClass(test);
302         }
303 
304         if (clazz == null) {
305             test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "HashFormat";
306             clazz = lookupHashFormatClass(test);
307         }
308 
309         if (clazz == null) {
310             test = pkg + "." + token + "HashFormat";
311             clazz = lookupHashFormatClass(test);
312         }
313 
314         if (clazz == null) {
315             test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "CryptFormat";
316             clazz = lookupHashFormatClass(test);
317         }
318 
319         if (clazz == null) {
320             test = pkg + "." + token + "CryptFormat";
321             clazz = lookupHashFormatClass(test);
322         }
323 
324         if (clazz == null) {
325             return null; //ran out of options
326         }
327 
328         assertHashFormatImpl(clazz);
329 
330         return clazz;
331     }
332 
333     protected Class lookupHashFormatClass(String name) {
334         try {
335             return ClassUtils.forName(name);
336         } catch (UnknownClassException ignored) {
337         }
338 
339         return null;
340     }
341 
342     protected final void assertHashFormatImpl(Class clazz) {
343         if (!HashFormat.class.isAssignableFrom(clazz) || clazz.isInterface()) {
344             throw new IllegalArgumentException("Discovered class [" + clazz.getName() + "] is not a " +
345                     HashFormat.class.getName() + " implementation.");
346         }
347 
348     }
349 
350     protected final HashFormat newHashFormatInstance(Class clazz) {
351         assertHashFormatImpl(clazz);
352         return (HashFormat) ClassUtils.newInstance(clazz);
353     }
354 }