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 module hunt.shiro.crypto.hash.format.DefaultHashFormatFactory;
20 
21 import hunt.shiro.crypto.hash.format.ModularCryptFormat;
22 // import hunt.shiro.util.ClassUtils;
23 // import hunt.shiro.util.StringUtils;
24 // import hunt.shiro.util.UnknownClassException;
25 import hunt.shiro.crypto.hash.format.HashFormat;
26 import hunt.shiro.crypto.hash.format.HashFormatFactory;
27 
28 import hunt.collection.HashMap;
29 import hunt.collection.HashSet;
30 import hunt.collection.Map;
31 import hunt.collection.Set;
32 import hunt.Exceptions;
33 
34 import std.string;
35 
36 /**
37  * This default {@code HashFormatFactory} implementation heuristically determines a {@code HashFormat} class to
38  * instantiate based on the input argument and returns a new instance of the discovered class.  The heuristics are
39  * detailed in the {@link #getInstance(string) getInstance} method documentation.
40  *
41  */
42 class DefaultHashFormatFactory : HashFormatFactory {
43 
44     private Map!(string, string) formatClassNames; //id - to - fully qualified class name
45 
46     private Set!(string) searchPackages; //packages to search for HashFormat implementations
47 
48     this() {
49         this.searchPackages = new HashSet!(string)();
50         this.formatClassNames = new HashMap!(string, string)();
51     }
52 
53     /**
54      * Returns a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
55      * <p/>
56      * This map will be used by the {@link #getInstance(string) getInstance} implementation:  that method's argument
57      * will be used as a lookup key to this map.  If the map returns a value, that value will be used to instantiate
58      * and return a new {@code HashFormat} instance.
59      * <h3>Defaults</h3>
60      * Shiro's default HashFormat implementations (as listed by the {@link ProvidedHashFormat} enum) will
61      * be searched automatically independently of this map.  You only need to populate this map with custom
62      * {@code HashFormat} implementations that are <em>not</em> already represented by a {@code ProvidedHashFormat}.
63      * <h3>Efficiency</h3>
64      * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
65      * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
66      * need to be supported by this factory.
67      *
68      * @return a {@code hashFormatAlias}-to-<code>fullyQualifiedHashFormatClassNameImplementation</code> map.
69      */
70     Map!(string, string) getFormatClassNames() {
71         return formatClassNames;
72     }
73 
74     /**
75      * Sets the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation} map to be used in
76      * the {@link #getInstance(string)} implementation.  See the {@link #getFormatClassNames()} JavaDoc for more
77      * information.
78      * <h3>Efficiency</h3>
79      * Populating this map will be more efficient than configuring {@link #getSearchPackages() searchPackages},
80      * but search packages may be more convenient depending on the number of {@code HashFormat} implementations that
81      * need to be supported by this factory.
82      *
83      * @param formatClassNames the {@code hash-format-alias}-to-{@code fullyQualifiedHashFormatClassNameImplementation}
84      *                         map to be used in the {@link #getInstance(string)} implementation.
85      */
86     void setFormatClassNames(Map!(string, string) formatClassNames) {
87         this.formatClassNames = formatClassNames;
88     }
89 
90     /**
91      * Returns a set of package names that can be searched for {@link HashFormat} implementations according to
92      * heuristics defined in the {@link #getHashFormatClass(string, string) getHashFormat(packageName, token)} JavaDoc.
93      * <h3>Efficiency</h3>
94      * Configuring this property is not as efficient as configuring a {@link #getFormatClassNames() formatClassNames}
95      * map, but it may be more convenient depending on the number of {@code HashFormat} implementations that
96      * need to be supported by this factory.
97      *
98      * @return a set of package names that can be searched for {@link HashFormat} implementations
99      * @see #getHashFormatClass(string, string)
100      */
101     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     void setSearchPackages(Set!(string) searchPackages) {
116         this.searchPackages = searchPackages;
117     }
118 
119     HashFormat getInstance(string in1) {
120         if (in1  is null) {
121             return null;
122         }
123 
124         HashFormat hashFormat = null;
125         TypeInfo_Class clazz = null;
126         enum delimiter = ModularCryptFormat.TOKEN_DELIMITER;
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 (in1.startsWith(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 = in1[delimiter.length .. $];
135             string[] tokens = test.split("\\" ~ delimiter);
136             //the MCF ID is always the first token in the delimited string:
137             string possibleMcfId = (tokens !is null && tokens.length > 0) ? tokens[0] : null;
138             if (possibleMcfId !is 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  is null) {
145             //not an MCF-formatted string - use the unaltered input arg and go through our heuristics:
146             clazz = getHashFormatClass(in1);
147         }
148 
149         if (clazz !is 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 TypeInfo_Class getHashFormatClass(string token) {
189 
190         implementationMissing(false);
191         return null;
192     //     Class clazz = null;
193 
194     //     //check to see if the token is a configured FQCN alias.  This is faster than searching packages,
195     //     //so we try this first:
196     //     if (this.formatClassNames !is null) {
197     //         string value = this.formatClassNames.get(token);
198     //         if (value !is null) {
199     //             //found an alias - see if the value is a class:
200     //             clazz = lookupHashFormatClass(value);
201     //         }
202     //     }
203 
204     //     //check to see if the token is one of Shiro's provided FQCN aliases (again, faster than searching):
205     //     if (clazz  is null) {
206     //         ProvidedHashFormat provided = ProvidedHashFormat.byId(token);
207     //         if (provided !is null) {
208     //             clazz = provided.getHashFormatClass();
209     //         }
210     //     }
211 
212     //     if (clazz  is null) {
213     //         //check to see if 'token' was a FQCN itself:
214     //         clazz = lookupHashFormatClass(token);
215     //     }
216 
217     //     if (clazz  is null) {
218     //         //token wasn't a FQCN or a FQCN alias - try searching in configured packages:
219     //         if (this.searchPackages !is null) {
220     //              foreach(string packageName ; this.searchPackages) {
221     //                 clazz = getHashFormatClass(packageName, token);
222     //                 if (clazz !is null) {
223     //                     //found it:
224     //                     break;
225     //                 }
226     //             }
227     //         }
228     //     }
229 
230     //     if (clazz !is null) {
231     //         assertHashFormatImpl(clazz);
232     //     }
233 
234     //     return clazz;
235     }
236 
237     /**
238      * Heuristically determine the fully qualified {@code HashFormat} implementation class name in the specified
239      * package based on the provided token.
240      * <p/>
241      * The token is expected to be a relevant fragment of an unqualified class name in the specified package.
242      * A 'relevant fragment' can be one of the following:
243      * <ul>
244      * <li>The {@code HashFormat} implementation unqualified class name</li>
245      * <li>The prefix of an unqualified class name ending with the text {@code Format}.  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 HashFormat}.  The first character of
248      * this prefix can be upper or lower case and both options will be tried.</li>
249      * <li>The prefix of an unqualified class name ending with the text {@code CryptoFormat}.  The first character
250      * of this prefix can be upper or lower case and both options will be tried.</li>
251      * </ul>
252      * <p/>
253      * Some examples:
254      * <table>
255      * <tr>
256      * <th>Package Name</th>
257      * <th>Token</th>
258      * <th>Expected Output Class</th>
259      * <th>Notes</th>
260      * </tr>
261      * <tr>
262      * <td>{@code com.foo.whatever}</td>
263      * <td>{@code MyBarFormat}</td>
264      * <td>{@code com.foo.whatever.MyBarFormat}</td>
265      * <td>Token is a complete unqualified class name</td>
266      * </tr>
267      * <tr>
268      * <td>{@code com.foo.whatever}</td>
269      * <td>{@code Bar}</td>
270      * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
271      * {@code com.foo.whatever.BarCryptFormat}</td>
272      * <td>The token is only part of the unqualified class name - i.e. all characters in front of the {@code *Format}
273      * {@code *HashFormat} or {@code *CryptFormat} suffix.  Note that the {@code *Format} variant will be tried before
274      * {@code *HashFormat} and then finally {@code *CryptFormat}</td>
275      * </tr>
276      * <tr>
277      * <td>{@code com.foo.whatever}</td>
278      * <td>{@code bar}</td>
279      * <td>{@code com.foo.whatever.BarFormat} <em>or</em> {@code com.foo.whatever.BarHashFormat} <em>or</em>
280      * {@code com.foo.whatever.BarCryptFormat}</td>
281      * <td>Exact same output as the above {@code Bar} input example. (The token differs only by the first character)</td>
282      * </tr>
283      * </table>
284      *
285      * @param packageName the package to search for matching {@code HashFormat} implementations.
286      * @param token       the string token from which a class name will be heuristically determined.
287      * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined.
288      */
289     // protected Class getHashFormatClass(string packageName, string token) {
290     //     string test = token;
291     //     Class clazz = null;
292     //     string pkg = packageName  is null ? "" : packageName;
293 
294     //     //1. Assume the arg is a fully qualified class name in the classpath:
295     //     clazz = lookupHashFormatClass(test);
296 
297     //     if (clazz  is null) {
298     //         test = pkg ~ "." ~ token;
299     //         clazz = lookupHashFormatClass(test);
300     //     }
301 
302     //     if (clazz  is null) {
303     //         test = pkg ~ "." ~ StringUtils.uppercaseFirstChar(token) ~ "Format";
304     //         clazz = lookupHashFormatClass(test);
305     //     }
306 
307     //     if (clazz  is null) {
308     //         test = pkg ~ "." ~ token ~ "Format";
309     //         clazz = lookupHashFormatClass(test);
310     //     }
311 
312     //     if (clazz  is null) {
313     //         test = pkg ~ "." ~ StringUtils.uppercaseFirstChar(token) ~ "HashFormat";
314     //         clazz = lookupHashFormatClass(test);
315     //     }
316 
317     //     if (clazz  is null) {
318     //         test = pkg ~ "." ~ token ~ "HashFormat";
319     //         clazz = lookupHashFormatClass(test);
320     //     }
321 
322     //     if (clazz  is null) {
323     //         test = pkg ~ "." ~ StringUtils.uppercaseFirstChar(token) ~ "CryptFormat";
324     //         clazz = lookupHashFormatClass(test);
325     //     }
326 
327     //     if (clazz  is null) {
328     //         test = pkg ~ "." ~ token ~ "CryptFormat";
329     //         clazz = lookupHashFormatClass(test);
330     //     }
331 
332     //     if (clazz  is null) {
333     //         return null; //ran out of options
334     //     }
335 
336     //     assertHashFormatImpl(clazz);
337 
338     //     return clazz;
339     // }
340 
341     // protected Class lookupHashFormatClass(string name) {
342     //     try {
343     //         return ClassUtils.forName(name);
344     //     } catch (UnknownClassException ignored) {
345     //     }
346 
347     //     return null;
348     // }
349 
350     protected final void assertHashFormatImpl(TypeInfo_Class clazz) {
351         // if (!HashFormat.class.isAssignableFrom(clazz) || clazz.isInterface()) {
352         //     throw new IllegalArgumentException("Discovered class [" ~ clazz.getName() ~ "] is not a " ~
353         //             "HashFormat implementation.");
354         // }
355     }
356 
357     protected final HashFormat newHashFormatInstance(TypeInfo_Class clazz) {
358         assertHashFormatImpl(clazz);
359         return cast(HashFormat) clazz.create();
360     }
361 }