    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.InternalCommonsUtils;
    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            InternalCommonsUtils.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}