001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.crypto.hash.format;
020    
021    import org.apache.shiro.util.ClassUtils;
022    import org.apache.shiro.util.StringUtils;
023    import org.apache.shiro.util.UnknownClassException;
024    
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.Map;
028    import java.util.Set;
029    
030    /**
031     * This default {@code HashFormatFactory} implementation heuristically determines a {@code HashFormat} class to
032     * instantiate based on the input argument and returns a new instance of the discovered class.  The heuristics are
033     * detailed in the {@link #getInstance(String) getInstance} method documentation.
034     *
035     * @since 1.2
036     */
037    public class DefaultHashFormatFactory implements HashFormatFactory {
038    
039        private Map<String, String> formatClassNames; //id - to - fully qualified class name
040    
041        private Set<String> searchPackages; //packages to search for HashFormat implementations
042    
043        public DefaultHashFormatFactory() {
044            this.searchPackages = new HashSet<String>();
045            this.formatClassNames = new HashMap<String, String>();
046        }
047    
048        /**
049         * Returns a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
050         * <p/>
051         * This map will be used by the {@link #getInstance(String) getInstance} implementation:  that method's argument
052         * will be used as a lookup key to this map.  If the map returns a value, that value will be used to instantiate
053         * and return a new {@code HashFormat} instance.
054         * <h3>Defaults</h3>
055         * Shiro's default HashFormat implementations (as listed by the {@link ProvidedHashFormat} enum) will
056         * be searched automatically independently of this map.  You only need to populate this map with custom
057         * {@code HashFormat} implementations that are <em>not</em> already represented by a {@code ProvidedHashFormat}.
058         * <h3>Efficiency</h3>
059         * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
060         * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
061         * need to be supported by this factory.
062         *
063         * @return a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
064         */
065        public Map<String, String> getFormatClassNames() {
066            return formatClassNames;
067        }
068    
069        /**
070         * Sets the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation} map to be used in
071         * the {@link #getInstance(String)} implementation.  See the {@link #getFormatClassNames()} JavaDoc for more
072         * information.
073         * <h3>Efficiency</h3>
074         * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
075         * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
076         * need to be supported by this factory.
077         *
078         * @param formatClassNames the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation}
079         *                         map to be used in the {@link #getInstance(String)} implementation.
080         */
081        public void setFormatClassNames(Map<String, String> formatClassNames) {
082            this.formatClassNames = formatClassNames;
083        }
084    
085        /**
086         * Returns a set of package names that can be searched for {@link HashFormat} implementations according to
087         * heuristics defined in the {@link #getHashFormatClass(String, String) getHashFormat(packageName, token)} JavaDoc.
088         * <h3>Efficiency</h3>
089         * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
090         * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
091         * need to be supported by this factory.
092         *
093         * @return a set of package names that can be searched for {@link HashFormat} implementations
094         * @see #getHashFormatClass(String, String)
095         */
096        public Set<String> getSearchPackages() {
097            return searchPackages;
098        }
099    
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    }