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 java.util.Iterator;
022import java.util.stream.Stream;
023
024import org.apache.shiro.lang.util.ClassUtils;
025import org.apache.shiro.lang.util.StringUtils;
026import org.apache.shiro.lang.util.UnknownClassException;
027
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Map;
031import java.util.Set;
032
033/**
034 * This default {@code HashFormatFactory} implementation heuristically determines a {@code HashFormat} class to
035 * instantiate based on the input argument and returns a new instance of the discovered class.  The heuristics are
036 * detailed in the {@link #getInstance(String) getInstance} method documentation.
037 *
038 * @since 1.2
039 */
040public class DefaultHashFormatFactory implements HashFormatFactory {
041
042    //id - to - fully qualified class name
043    private Map<String, String> formatClassNames;
044
045    //packages to search for HashFormat implementations
046    private Set<String> searchPackages;
047
048    public DefaultHashFormatFactory() {
049        this.searchPackages = new HashSet<String>();
050        this.formatClassNames = new HashMap<String, String>();
051    }
052
053    /**
054     * Returns a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
055     * <p/>
056     * This map will be used by the {@link #getInstance(String) getInstance} implementation:  that method's argument
057     * will be used as a lookup key to this map.  If the map returns a value, that value will be used to instantiate
058     * and return a new {@code HashFormat} instance.
059     * <h3>Defaults</h3>
060     * Shiro's default HashFormat implementations (as listed by the {@link ProvidedHashFormat} enum) will
061     * be searched automatically independently of this map.  You only need to populate this map with custom
062     * {@code HashFormat} implementations that are <em>not</em> already represented by a {@code ProvidedHashFormat}.
063     * <h3>Efficiency</h3>
064     * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
065     * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
066     * need to be supported by this factory.
067     *
068     * @return a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
069     */
070    public Map<String, String> getFormatClassNames() {
071        return formatClassNames;
072    }
073
074    /**
075     * Sets the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation} map to be used in
076     * the {@link #getInstance(String)} implementation.  See the {@link #getFormatClassNames()} JavaDoc for more
077     * information.
078     * <h3>Efficiency</h3>
079     * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
080     * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
081     * need to be supported by this factory.
082     *
083     * @param formatClassNames the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation}
084     *                         map to be used in the {@link #getInstance(String)} implementation.
085     */
086    public void setFormatClassNames(Map<String, String> formatClassNames) {
087        this.formatClassNames = formatClassNames;
088    }
089
090    /**
091     * Returns a set of package names that can be searched for {@link HashFormat} implementations according to
092     * heuristics defined in the {@link #getHashFormatClass(String, String) getHashFormat(packageName, token)} JavaDoc.
093     * <h3>Efficiency</h3>
094     * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
095     * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
096     * need to be supported by this factory.
097     *
098     * @return a set of package names that can be searched for {@link HashFormat} implementations
099     * @see #getHashFormatClass(String, String)
100     */
101    public Set<String> getSearchPackages() {
102        return searchPackages;
103    }
104
105    /**
106     * Sets a set of package names that can be searched for {@link HashFormat} implementations according to
107     * heuristics defined in the {@link #getHashFormatClass(String, String) getHashFormat(packageName, token)} JavaDoc.
108     * <h3>Efficiency</h3>
109     * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
110     * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
111     * need to be supported by this factory.
112     *
113     * @param searchPackages a set of package names that can be searched for {@link HashFormat} implementations
114     */
115    public void setSearchPackages(Set<String> searchPackages) {
116        this.searchPackages = searchPackages;
117    }
118
119    @Override
120    public HashFormat getInstance(String in) {
121        if (in == null) {
122            return null;
123        }
124
125        HashFormat hashFormat = null;
126        Class<?> clazz = null;
127
128        //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance
129        //optimization.  If the input arg is an MCF-formatted string, there will be many unnecessary ClassLoader
130        //misses which can be slow.  By checking the MCF-formatted option, we can significantly improve performance
131        if (in.startsWith(ModularCryptFormat.TOKEN_DELIMITER)) {
132            //odds are high that the input argument is not a fully qualified class name or a format key (e.g. 'hex',
133            //base64' or 'shiro1').  Try to find the key and lookup via that:
134            String test = in.substring(ModularCryptFormat.TOKEN_DELIMITER.length());
135            String[] tokens = test.split("\\" + ModularCryptFormat.TOKEN_DELIMITER);
136            //the MCF ID is always the first token in the delimited string:
137            String possibleMcfId = tokens.length > 0 ? tokens[0] : null;
138            if (possibleMcfId != null) {
139                //found a possible MCF ID - test it using our heuristics to see if we can find a corresponding class:
140                clazz = getHashFormatClass(possibleMcfId);
141            }
142        }
143
144        if (clazz == null) {
145            //not an MCF-formatted string - use the unaltered input arg and go through our heuristics:
146            clazz = getHashFormatClass(in);
147        }
148
149        if (clazz != null) {
150            //we found a HashFormat class - instantiate it:
151            hashFormat = newHashFormatInstance(clazz);
152        }
153
154        return hashFormat;
155    }
156
157    /**
158     * Heuristically determine the fully qualified HashFormat implementation class name based on the specified
159     * token.
160     * <p/>
161     * This implementation functions as follows (in order):
162     * <ol>
163     * <li>See if the argument can be used as a lookup key in the {@link #getFormatClassNames() formatClassNames}
164     * map.  If a value (a fully qualified class name {@link HashFormat HashFormat} implementation) is found,
165     * {@link ClassUtils#forName(String) lookup} the class and return it.</li>
166     * <li>
167     * Check to see if the token argument is a
168     * {@link ProvidedHashFormat} enum value.  If so, acquire the corresponding {@code HashFormat} class and
169     * return it.
170     * </li>
171     * <li>
172     * Check to see if the token argument is itself a fully qualified class name.  If so, try to load the class
173     * and return it.
174     * </li>
175     * <li>If the above options do not result in a discovered class, search all all configured
176     * {@link #getSearchPackages() searchPackages} using heuristics defined in the
177     * {@link #getHashFormatClass(String, String) getHashFormatClass(packageName, token)} method documentation
178     * (relaying the {@code token} argument to that method for each configured package).
179     * </li>
180     * </ol>
181     * <p/>
182     * If a class is not discovered via any of the above means, {@code null} is returned to indicate the class
183     * could not be found.
184     *
185     * @param token the string token from which a class name will be heuristically determined.
186     * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined.
187     */
188    protected Class getHashFormatClass(String token) {
189
190        Class clazz = null;
191
192        //check to see if the token is a configured FQCN alias.  This is faster than searching packages,
193        //so we try this first:
194        if (this.formatClassNames != null) {
195            String value = this.formatClassNames.get(token);
196            if (value != null) {
197                //found an alias - see if the value is a class:
198                clazz = lookupHashFormatClass(value);
199            }
200        }
201
202        //check to see if the token is one of Shiro's provided FQCN aliases (again, faster than searching):
203        if (clazz == null) {
204            ProvidedHashFormat provided = ProvidedHashFormat.byId(token);
205            if (provided != null) {
206                clazz = provided.getHashFormatClass();
207            }
208        }
209
210        if (clazz == null) {
211            //check to see if 'token' was a FQCN itself:
212            clazz = lookupHashFormatClass(token);
213        }
214
215        if (clazz == null) {
216            //token wasn't a FQCN or a FQCN alias - try searching in configured packages:
217            if (this.searchPackages != null) {
218                for (String packageName : this.searchPackages) {
219                    clazz = getHashFormatClass(packageName, token);
220                    if (clazz != null) {
221                        //found it:
222                        break;
223                    }
224                }
225            }
226        }
227
228        if (clazz != null) {
229            assertHashFormatImpl(clazz);
230        }
231
232        return clazz;
233    }
234
235    /**
236     * Heuristically determine the fully qualified {@code HashFormat} implementation class name in the specified
237     * package based on the provided token.
238     * <p/>
239     * The token is expected to be a relevant fragment of an unqualified class name in the specified package.
240     * A 'relevant fragment' can be one of the following:
241     * <ul>
242     * <li>The {@code HashFormat} implementation unqualified class name</li>
243     * <li>The prefix of an unqualified class name ending with the text {@code Format}.  The first character of
244     * this prefix can be upper or lower case and both options will be tried.</li>
245     * <li>The prefix of an unqualified class name ending with the text {@code HashFormat}.  The first character of
246     * this prefix can be upper or lower case and both options will be tried.</li>
247     * <li>The prefix of an unqualified class name ending with the text {@code CryptoFormat}.  The first character
248     * of this prefix can be upper or lower case and both options will be tried.</li>
249     * </ul>
250     * <p/>
251     * Some examples:
252     * <table>
253     * <tr>
254     * <th>Package Name</th>
255     * <th>Token</th>
256     * <th>Expected Output Class</th>
257     * <th>Notes</th>
258     * </tr>
259     * <tr>
260     * <td>{@code com.foo.whatever}</td>
261     * <td>{@code MyBarFormat}</td>
262     * <td>{@code com.foo.whatever.MyBarFormat}</td>
263     * <td>Token is a complete unqualified class name</td>
264     * </tr>
265     * <tr>
266     * <td>{@code com.foo.whatever}</td>
267     * <td>{@code Bar}</td>
268     * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
269     * {@code com.foo.whatever.BarCryptFormat}</td>
270     * <td>The token is only part of the unqualified class name - i.e. all characters in front of the {@code *Format}
271     * {@code *HashFormat} or {@code *CryptFormat} suffix.  Note that the {@code *Format} variant will be tried before
272     * {@code *HashFormat} and then finally {@code *CryptFormat}</td>
273     * </tr>
274     * <tr>
275     * <td>{@code com.foo.whatever}</td>
276     * <td>{@code bar}</td>
277     * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
278     * {@code com.foo.whatever.BarCryptFormat}</td>
279     * <td>Exact same output as the above {@code Bar} input example. (The token differs only by the first character)</td>
280     * </tr>
281     * </table>
282     *
283     * @param packageName the package to search for matching {@code HashFormat} implementations.
284     * @param token       the string token from which a class name will be heuristically determined.
285     * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined.
286     */
287    protected Class<?> getHashFormatClass(String packageName, String token) {
288        String test = token;
289        Class<?> clazz;
290        String pkg = packageName == null ? "" : packageName;
291
292        //1. Assume the arg is a fully qualified class name in the classpath:
293        clazz = lookupHashFormatClass(test);
294
295        final Iterator<String> iterator = Stream.of(
296                pkg + "." + token,
297                pkg + "." + StringUtils.uppercaseFirstChar(token) + "Format",
298                pkg + "." + token + "Format",
299                pkg + "." + StringUtils.uppercaseFirstChar(token) + "HashFormat",
300                pkg + "." + token + "HashFormat",
301                pkg + "." + StringUtils.uppercaseFirstChar(token) + "CryptFormat",
302                pkg + "." + token + "CryptFormat").iterator();
303
304        while (clazz == null && iterator.hasNext()) {
305            clazz = lookupHashFormatClass(iterator.next());
306        }
307
308        if (clazz == null) {
309            //ran out of options
310            return null;
311        }
312
313        assertHashFormatImpl(clazz);
314
315        return clazz;
316    }
317
318    protected Class<?> lookupHashFormatClass(String name) {
319        try {
320            return ClassUtils.forName(name);
321        } catch (UnknownClassException ignored) {
322        }
323
324        return null;
325    }
326
327    protected final void assertHashFormatImpl(Class clazz) {
328        if (!HashFormat.class.isAssignableFrom(clazz) || clazz.isInterface()) {
329            throw new IllegalArgumentException("Discovered class [" + clazz.getName() + "] is not a "
330                    + HashFormat.class.getName() + " implementation.");
331        }
332
333    }
334
335    protected final HashFormat newHashFormatInstance(Class clazz) {
336        assertHashFormatImpl(clazz);
337        return (HashFormat) ClassUtils.newInstance(clazz);
338    }
339}