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}