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}