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 */
019package org.apache.shiro.crypto.hash.format;
020
021import org.apache.shiro.util.ClassUtils;
022import org.apache.shiro.util.StringUtils;
023import org.apache.shiro.util.UnknownClassException;
024
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Map;
028import 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 */
037public 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}