001 // Copyright 2006, 2007, 2008, 2010 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry5.ioc.internal.services; 016 017 import java.util.Collection; 018 import java.util.Collections; 019 import java.util.LinkedList; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Set; 023 import java.util.WeakHashMap; 024 025 import org.apache.tapestry5.func.F; 026 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 027 import org.apache.tapestry5.ioc.internal.util.InheritanceSearch; 028 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 029 import org.apache.tapestry5.ioc.services.ClassFabUtils; 030 import org.apache.tapestry5.ioc.services.Coercion; 031 import org.apache.tapestry5.ioc.services.CoercionTuple; 032 import org.apache.tapestry5.ioc.services.TypeCoercer; 033 import org.apache.tapestry5.ioc.util.AvailableValues; 034 import org.apache.tapestry5.ioc.util.UnknownValueException; 035 import org.apache.tapestry5.util.StringToEnumCoercion; 036 037 @SuppressWarnings("all") 038 public class TypeCoercerImpl implements TypeCoercer 039 { 040 // Constructed from the service's configuration. 041 042 private final Map<Class, List<CoercionTuple>> sourceTypeToTuple = CollectionFactory.newMap(); 043 044 /** 045 * A coercion to a specific target type. Manages a cache of coercions to specific types. 046 */ 047 private class TargetCoercion 048 { 049 private final Class type; 050 051 private final Map<Class, Coercion> cache = CollectionFactory.newConcurrentMap(); 052 053 TargetCoercion(Class type) 054 { 055 this.type = type; 056 } 057 058 void clearCache() 059 { 060 cache.clear(); 061 } 062 063 Object coerce(Object input) 064 { 065 066 Class sourceType = input != null ? input.getClass() : void.class; 067 068 if (type.isAssignableFrom(sourceType)) 069 return input; 070 071 Coercion c = getCoercion(sourceType); 072 073 try 074 { 075 return type.cast(c.coerce(input)); 076 } 077 catch (Exception ex) 078 { 079 throw new RuntimeException(ServiceMessages.failedCoercion(input, type, c, ex), ex); 080 } 081 } 082 083 String explain(Class sourceType) 084 { 085 return getCoercion(sourceType).toString(); 086 } 087 088 private Coercion getCoercion(Class sourceType) 089 { 090 Coercion c = cache.get(sourceType); 091 092 if (c == null) 093 { 094 c = findOrCreateCoercion(sourceType, type); 095 cache.put(sourceType, c); 096 } 097 return c; 098 } 099 } 100 101 /** 102 * Map from a target type to a TargetCoercion for that type. 103 */ 104 private final Map<Class, TargetCoercion> typeToTargetCoercion = new WeakHashMap<Class, TargetCoercion>(); 105 106 private static final Coercion NO_COERCION = new Coercion<Object, Object>() 107 { 108 public Object coerce(Object input) 109 { 110 return input; 111 } 112 }; 113 114 private static final Coercion COERCION_NULL_TO_OBJECT = new Coercion<Void, Object>() 115 { 116 public Object coerce(Void input) 117 { 118 return null; 119 } 120 121 @Override 122 public String toString() 123 { 124 return "null --> null"; 125 } 126 }; 127 128 public TypeCoercerImpl(Collection<CoercionTuple> tuples) 129 { 130 for (CoercionTuple tuple : tuples) 131 { 132 Class key = tuple.getSourceType(); 133 134 InternalUtils.addToMapList(sourceTypeToTuple, key, tuple); 135 } 136 } 137 138 @SuppressWarnings("unchecked") 139 public Object coerce(Object input, Class targetType) 140 { 141 assert targetType != null; 142 Class effectiveTargetType = ClassFabUtils.getWrapperType(targetType); 143 144 if (effectiveTargetType.isInstance(input)) 145 return input; 146 147 return getTargetCoercion(effectiveTargetType).coerce(input); 148 } 149 150 @SuppressWarnings("unchecked") 151 public <S, T> Coercion<S, T> getCoercion(Class<S> sourceType, Class<T> targetType) 152 { 153 assert sourceType != null; 154 assert targetType != null; 155 Class effectiveSourceType = ClassFabUtils.getWrapperType(sourceType); 156 Class effectiveTargetType = ClassFabUtils.getWrapperType(targetType); 157 158 if (effectiveTargetType.isAssignableFrom(effectiveSourceType)) 159 return NO_COERCION; 160 161 return getTargetCoercion(effectiveTargetType).getCoercion(effectiveSourceType); 162 } 163 164 @SuppressWarnings("unchecked") 165 public <S, T> String explain(Class<S> sourceType, Class<T> targetType) 166 { 167 assert sourceType != null; 168 assert targetType != null; 169 Class effectiveTargetType = ClassFabUtils.getWrapperType(targetType); 170 Class effectiveSourceType = ClassFabUtils.getWrapperType(sourceType); 171 172 // Is a coercion even necessary? Not if the target type is assignable from the 173 // input value. 174 175 if (effectiveTargetType.isAssignableFrom(effectiveSourceType)) 176 return ""; 177 178 return getTargetCoercion(targetType).explain(sourceType); 179 } 180 181 private synchronized TargetCoercion getTargetCoercion(Class targetType) 182 { 183 TargetCoercion tc = typeToTargetCoercion.get(targetType); 184 185 if (tc == null) 186 { 187 tc = new TargetCoercion(targetType); 188 typeToTargetCoercion.put(targetType, tc); 189 } 190 191 return tc; 192 } 193 194 public synchronized void clearCache() 195 { 196 // There's no need to clear the typeToTargetCoercion map, as it is a WeakHashMap and 197 // will release the keys for classes that are no longer in existence. On the other hand, 198 // there's likely all sorts of references to unloaded classes inside each TargetCoercion's 199 // individual cache, so clear all those. 200 201 for (TargetCoercion tc : typeToTargetCoercion.values()) 202 { 203 // Can tc ever be null? 204 205 tc.clearCache(); 206 } 207 } 208 209 /** 210 * Here's the real meat; we do a search of the space to find coercions, or a system of 211 * coercions, that accomplish 212 * the desired coercion. 213 * <p/> 214 * There's <strong>TREMENDOUS</strong> room to improve this algorithm. For example, inheritance lists could be 215 * cached. Further, there's probably more ways to early prune the search. However, even with dozens or perhaps 216 * hundreds of tuples, I suspect the search will still grind to a conclusion quickly. 217 * <p/> 218 * The order of operations should help ensure that the most efficient tuple chain is located. If you think about how 219 * tuples are added to the queue, there are two factors: size (the number of steps in the coercion) and 220 * "class distance" (that is, number of steps up the inheritance hiearchy). All the appropriate 1 step coercions 221 * will be considered first, in class distance order. Along the way, we'll queue up all the 2 step coercions, again 222 * in class distance order. By the time we reach some of those, we'll have begun queing up the 3 step coercions, and 223 * so forth, until we run out of input tuples we can use to fabricate multi-step compound coercions, or reach a 224 * final response. 225 * <p/> 226 * This does create a good number of short lived temporary objects (the compound tuples), but that's what the GC is 227 * really good at. 228 * 229 * @param sourceType 230 * @param targetType 231 * @return coercer from sourceType to targetType 232 */ 233 @SuppressWarnings("unchecked") 234 private Coercion findOrCreateCoercion(Class sourceType, Class targetType) 235 { 236 if (sourceType == void.class) 237 return searchForNullCoercion(targetType); 238 239 // These are instance variables because this method may be called concurrently. 240 // On a true race, we may go to the work of seeking out and/or fabricating 241 // a tuple twice, but it's more likely that different threads are looking 242 // for different source/target coercions. 243 244 Set<CoercionTuple> consideredTuples = CollectionFactory.newSet(); 245 LinkedList<CoercionTuple> queue = CollectionFactory.newLinkedList(); 246 247 seedQueue(sourceType, targetType, consideredTuples, queue); 248 249 while (!queue.isEmpty()) 250 { 251 CoercionTuple tuple = queue.removeFirst(); 252 253 // If the tuple results in a value type that is assignable to the desired target type, 254 // we're done! Later, we may add a concept of "cost" (i.e. number of steps) or 255 // "quality" (how close is the tuple target type to the desired target type). Cost 256 // is currently implicit, as compound tuples are stored deeper in the queue, 257 // so simpler coercions will be located earlier. 258 259 Class tupleTargetType = tuple.getTargetType(); 260 261 if (targetType.isAssignableFrom(tupleTargetType)) 262 return tuple.getCoercion(); 263 264 // So .. this tuple doesn't get us directly to the target type. 265 // However, it *may* get us part of the way. Each of these 266 // represents a coercion from the source type to an intermediate type. 267 // Now we're going to look for conversions from the intermediate type 268 // to some other type. 269 270 queueIntermediates(sourceType, targetType, tuple, consideredTuples, queue); 271 } 272 273 // Not found anywhere. Identify the source and target type and a (sorted) list of 274 // all the known coercions. 275 276 throw new UnknownValueException(String.format("Could not find a coercion from type %s to type %s.", 277 sourceType.getName(), targetType.getName()), buildCoercionCatalog()); 278 } 279 280 /** 281 * Coercion from null is special; we match based on the target type and its not a spanning 282 * search. In many cases, we 283 * return a pass-thru that leaves the value as null. 284 * 285 * @param targetType 286 * desired type 287 * @return the coercion 288 */ 289 private Coercion searchForNullCoercion(Class targetType) 290 { 291 List<CoercionTuple> tuples = getTuples(void.class, targetType); 292 293 for (CoercionTuple tuple : tuples) 294 { 295 Class tupleTargetType = tuple.getTargetType(); 296 297 if (targetType.equals(tupleTargetType)) 298 return tuple.getCoercion(); 299 } 300 301 // Typical case: no match, this coercion passes the null through 302 // as null. 303 304 return COERCION_NULL_TO_OBJECT; 305 } 306 307 /** 308 * Builds a string listing all the coercions configured for the type coercer, sorted 309 * alphabetically. 310 */ 311 @SuppressWarnings("unchecked") 312 private AvailableValues buildCoercionCatalog() 313 { 314 List<CoercionTuple> masterList = CollectionFactory.newList(); 315 316 for (List<CoercionTuple> list : sourceTypeToTuple.values()) 317 { 318 masterList.addAll(list); 319 } 320 321 return new AvailableValues("Configured coercions", masterList); 322 } 323 324 /** 325 * Seeds the pool with the initial set of coercions for the given type. 326 */ 327 private void seedQueue(Class sourceType, Class targetType, Set<CoercionTuple> consideredTuples, 328 LinkedList<CoercionTuple> queue) 329 { 330 // Work from the source type up looking for tuples 331 332 for (Class c : new InheritanceSearch(sourceType)) 333 { 334 List<CoercionTuple> tuples = getTuples(c, targetType); 335 336 if (tuples == null) 337 continue; 338 339 for (CoercionTuple tuple : tuples) 340 { 341 queue.addLast(tuple); 342 consideredTuples.add(tuple); 343 } 344 345 // Don't pull in Object -> type coercions when doing 346 // a search from null. 347 348 if (sourceType == void.class) 349 return; 350 } 351 } 352 353 /** 354 * Creates and adds to the pool a new set of coercions based on an intermediate tuple. Adds 355 * compound coercion tuples 356 * to the end of the queue. 357 * 358 * @param sourceType 359 * the source type of the coercion 360 * @param targetType 361 * TODO 362 * @param intermediateTuple 363 * a tuple that converts from the source type to some intermediate type (that is not 364 * assignable to the target type) 365 * @param consideredTuples 366 * set of tuples that have already been added to the pool (directly, or as a compound 367 * coercion) 368 * @param queue 369 * the work queue of tuples 370 */ 371 @SuppressWarnings("unchecked") 372 private void queueIntermediates(Class sourceType, Class targetType, CoercionTuple intermediateTuple, 373 Set<CoercionTuple> consideredTuples, LinkedList<CoercionTuple> queue) 374 { 375 Class intermediateType = intermediateTuple.getTargetType(); 376 377 for (Class c : new InheritanceSearch(intermediateType)) 378 { 379 for (CoercionTuple tuple : getTuples(c, targetType)) 380 { 381 if (consideredTuples.contains(tuple)) 382 continue; 383 384 Class newIntermediateType = tuple.getTargetType(); 385 386 // If this tuple is for coercing from an intermediate type back towards our 387 // initial source type, then ignore it. This should only be an optimization, 388 // as branches that loop back towards the source type will 389 // eventually be considered and discarded. 390 391 if (sourceType.isAssignableFrom(newIntermediateType)) 392 continue; 393 394 // The intermediateTuple coercer gets from S --> I1 (an intermediate type). 395 // The current tuple's coercer gets us from I2 --> X. where I2 is assignable 396 // from I1 (i.e., I2 is a superclass/superinterface of I1) and X is a new 397 // intermediate type, hopefully closer to our eventual target type. 398 399 Coercion compoundCoercer = new CompoundCoercion(intermediateTuple.getCoercion(), tuple.getCoercion()); 400 401 CoercionTuple compoundTuple = new CoercionTuple(sourceType, newIntermediateType, compoundCoercer, false); 402 403 // So, every tuple that is added to the queue can take as input the sourceType. 404 // The target type may be another intermediate type, or may be something 405 // assignable to the target type, which will bring the search to a successful 406 // conclusion. 407 408 queue.addLast(compoundTuple); 409 consideredTuples.add(tuple); 410 } 411 } 412 } 413 414 /** 415 * Returns a non-null list of the tuples from the source type. 416 * 417 * @param sourceType 418 * used to locate tuples 419 * @param targetType 420 * used to add synthetic tuples 421 * @return non-null list of tuples 422 */ 423 private List<CoercionTuple> getTuples(Class sourceType, Class targetType) 424 { 425 List<CoercionTuple> tuples = sourceTypeToTuple.get(sourceType); 426 427 if (tuples == null) 428 tuples = Collections.emptyList(); 429 430 // So, when we see String and an Enum type, we add an additional synthetic tuple to the end 431 // of the real list. This is the easiest way to accomplish this is a thread-safe and class-reloading 432 // safe way (i.e., what if the Enum is defined by a class loader that gets discarded? Don't want to cause 433 // memory leaks by retaining an instance). In any case, there are edge cases where we may create 434 // the tuple unnecessarily (such as when an explicit string-to-enum coercion is part of the TypeCoercer 435 // configuration), but on the whole, this is cheap at works. 436 437 if (sourceType == String.class && Enum.class.isAssignableFrom(targetType)) 438 tuples = extend(tuples, new CoercionTuple(sourceType, targetType, new StringToEnumCoercion(targetType))); 439 440 return tuples; 441 } 442 443 private static <T> List<T> extend(List<T> list, T extraValue) 444 { 445 return F.flow(list).append(extraValue).toList(); 446 } 447 }