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