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}