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