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}