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