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