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