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