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