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}