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