    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        // These are instance variables because this method may be called concurrently.
    285        // On a true race, we may go to the work of seeking out and/or fabricating
    286        // a tuple twice, but it's more likely that different threads are looking
    287        // for different source/target coercions.
    288
    289        Set<CoercionTuple> consideredTuples = CollectionFactory.newSet();
    290        LinkedList<CoercionTuple> queue = CollectionFactory.newLinkedList();
    291
    292        seedQueue(sourceType, targetType, consideredTuples, queue);
    293
    294        while (!queue.isEmpty())
    295        {
    296            CoercionTuple tuple = queue.removeFirst();
    297
    298            // If the tuple results in a value type that is assignable to the desired target type,
    299            // we're done! Later, we may add a concept of "cost" (i.e. number of steps) or
    300            // "quality" (how close is the tuple target type to the desired target type). Cost
    301            // is currently implicit, as compound tuples are stored deeper in the queue,
    302            // so simpler coercions will be located earlier.
    303
    304            Class tupleTargetType = tuple.getTargetType();
    305
    306            if (targetType.isAssignableFrom(tupleTargetType))
    307            {
    308                return tuple.getCoercion();
    309            }
    310
    311            // So .. this tuple doesn't get us directly to the target type.
    312            // However, it *may* get us part of the way. Each of these
    313            // represents a coercion from the source type to an intermediate type.
    314            // Now we're going to look for conversions from the intermediate type
    315            // to some other type.
    316
    317            queueIntermediates(sourceType, targetType, tuple, consideredTuples, queue);
    318        }
    319
    320        // Not found anywhere. Identify the source and target type and a (sorted) list of
    321        // all the known coercions.
    322
    323        throw new UnknownValueException(String.format("Could not find a coercion from type %s to type %s.",
    324                sourceType.getName(), targetType.getName()), buildCoercionCatalog());
    325    }
    326
    327    /**
    328     * Coercion from null is special; we match based on the target type and its not a spanning
    329     * search. In many cases, we
    330     * return a pass-thru that leaves the value as null.
    331     *
    332     * @param targetType
    333     *         desired type
    334     * @return the coercion
    335     */
    336    private Coercion searchForNullCoercion(Class targetType)
    337    {
    338        List<CoercionTuple> tuples = getTuples(Void.class, targetType);
    339
    340        for (CoercionTuple tuple : tuples)
    341        {
    342            Class tupleTargetType = tuple.getTargetType();
    343
    344            if (targetType.equals(tupleTargetType))
    345                return tuple.getCoercion();
    346        }
    347
    348        // Typical case: no match, this coercion passes the null through
    349        // as null.
    350
    351        return COERCION_NULL_TO_OBJECT;
    352    }
    353
    354    /**
    355     * Builds a string listing all the coercions configured for the type coercer, sorted
    356     * alphabetically.
    357     */
    358    @SuppressWarnings("unchecked")
    359    private AvailableValues buildCoercionCatalog()
    360    {
    361        List<CoercionTuple> masterList = CollectionFactory.newList();
    362
    363        for (List<CoercionTuple> list : sourceTypeToTuple.values())
    364        {
    365            masterList.addAll(list);
    366        }
    367
    368        return new AvailableValues("Configured coercions", masterList);
    369    }
    370
    371    /**
    372     * Seeds the pool with the initial set of coercions for the given type.
    373     */
    374    private void seedQueue(Class sourceType, Class targetType, Set<CoercionTuple> consideredTuples,
    375                           LinkedList<CoercionTuple> queue)
    376    {
    377        // Work from the source type up looking for tuples
    378
    379        for (Class c : new InheritanceSearch(sourceType))
    380        {
    381            List<CoercionTuple> tuples = getTuples(c, targetType);
    382
    383            if (tuples == null)
    384            {
    385                continue;
    386            }
    387
    388            for (CoercionTuple tuple : tuples)
    389            {
    390                queue.addLast(tuple);
    391                consideredTuples.add(tuple);
    392            }
    393
    394            // Don't pull in Object -> type coercions when doing
    395            // a search from null.
    396
    397            if (sourceType == Void.class)
    398            {
    399                return;
    400            }
    401        }
    402    }
    403
    404    /**
    405     * Creates and adds to the pool a new set of coercions based on an intermediate tuple. Adds
    406     * compound coercion tuples
    407     * to the end of the queue.
    408     *
    409     * @param sourceType
    410     *         the source type of the coercion
    411     * @param targetType
    412     *         TODO
    413     * @param intermediateTuple
    414     *         a tuple that converts from the source type to some intermediate type (that is not
    415     *         assignable to the target type)
    416     * @param consideredTuples
    417     *         set of tuples that have already been added to the pool (directly, or as a compound
    418     *         coercion)
    419     * @param queue
    420     *         the work queue of tuples
    421     */
    422    @SuppressWarnings("unchecked")
    423    private void queueIntermediates(Class sourceType, Class targetType, CoercionTuple intermediateTuple,
    424                                    Set<CoercionTuple> consideredTuples, LinkedList<CoercionTuple> queue)
    425    {
    426        Class intermediateType = intermediateTuple.getTargetType();
    427
    428        for (Class c : new InheritanceSearch(intermediateType))
    429        {
    430            for (CoercionTuple tuple : getTuples(c, targetType))
    431            {
    432                if (consideredTuples.contains(tuple))
    433                {
    434                    continue;
    435                }
    436
    437                Class newIntermediateType = tuple.getTargetType();
    438
    439                // If this tuple is for coercing from an intermediate type back towards our
    440                // initial source type, then ignore it. This should only be an optimization,
    441                // as branches that loop back towards the source type will
    442                // eventually be considered and discarded.
    443
    444                if (sourceType.isAssignableFrom(newIntermediateType))
    445                {
    446                    continue;
    447                }
    448
    449                // The intermediateTuple coercer gets from S --> I1 (an intermediate type).
    450                // The current tuple's coercer gets us from I2 --> X. where I2 is assignable
    451                // from I1 (i.e., I2 is a superclass/superinterface of I1) and X is a new
    452                // intermediate type, hopefully closer to our eventual target type.
    453
    454                Coercion compoundCoercer = new CompoundCoercion(intermediateTuple.getCoercion(), tuple.getCoercion());
    455
    456                CoercionTuple compoundTuple = new CoercionTuple(sourceType, newIntermediateType, compoundCoercer, false);
    457
    458                // So, every tuple that is added to the queue can take as input the sourceType.
    459                // The target type may be another intermediate type, or may be something
    460                // assignable to the target type, which will bring the search to a successful
    461                // conclusion.
    462
    463                queue.addLast(compoundTuple);
    464                consideredTuples.add(tuple);
    465            }
    466        }
    467    }
    468
    469    /**
    470     * Returns a non-null list of the tuples from the source type.
    471     *
    472     * @param sourceType
    473     *         used to locate tuples
    474     * @param targetType
    475     *         used to add synthetic tuples
    476     * @return non-null list of tuples
    477     */
    478    private List<CoercionTuple> getTuples(Class sourceType, Class targetType)
    479    {
    480        List<CoercionTuple> tuples = sourceTypeToTuple.get(sourceType);
    481
    482        if (tuples == null)
    483        {
    484            tuples = Collections.emptyList();
    485        }
    486
    487        // So, when we see String and an Enum type, we add an additional synthetic tuple to the end
    488        // of the real list. This is the easiest way to accomplish this is a thread-safe and class-reloading
    489        // safe way (i.e., what if the Enum is defined by a class loader that gets discarded?  Don't want to cause
    490        // memory leaks by retaining an instance). In any case, there are edge cases where we may create
    491        // the tuple unnecessarily (such as when an explicit string-to-enum coercion is part of the TypeCoercer
    492        // configuration), but on the whole, this is cheap and works.
    493
    494        if (sourceType == String.class && Enum.class.isAssignableFrom(targetType))
    495        {
    496            tuples = extend(tuples, new CoercionTuple(sourceType, targetType, new StringToEnumCoercion(targetType)));
    497        }
    498        else if (Enum.class.isAssignableFrom(sourceType) && targetType == String.class)
    499        {
    500            // TAP5-2565
    501            tuples = extend(tuples, new CoercionTuple(sourceType, targetType, (value)->((Enum) value).name()));
    502        }
    503
    504        return tuples;
    505    }
    506
    507    private static <T> List<T> extend(List<T> list, T extraValue)
    508    {
    509        return F.flow(list).append(extraValue).toList();
    510    }
    511}