    001    /*****************************************************************************
    002     * Copyright (C) PicoContainer Organization. All rights reserved.            *
    003     * ------------------------------------------------------------------------- *
    004     * The software in this package is published under the terms of the BSD      *
    005     * style license a copy of which has been included with this distribution in *
    006     * the LICENSE.txt file.                                                     *
    007     *                                                                           *
    008     * Original code by                                                          *
    009     *****************************************************************************/
    010    package org.picocontainer.parameters;
    011
    012    import org.picocontainer.ComponentAdapter;
    013    import org.picocontainer.Parameter;
    014    import org.picocontainer.NameBinding;
    015    import org.picocontainer.PicoContainer;
    016    import org.picocontainer.PicoCompositionException;
    017    import org.picocontainer.PicoVisitor;
    018
    019    import java.io.Serializable;
    020    import java.lang.reflect.Array;
    021    import java.lang.reflect.Type;
    022    import java.lang.reflect.ParameterizedType;
    023    import java.lang.annotation.Annotation;
    024    import java.util.ArrayList;
    025    import java.util.Collection;
    026    import java.util.HashMap;
    027    import java.util.HashSet;
    028    import java.util.LinkedHashMap;
    029    import java.util.List;
    030    import java.util.Map;
    031    import java.util.Set;
    032    import java.util.SortedMap;
    033    import java.util.SortedSet;
    034    import java.util.TreeMap;
    035    import java.util.TreeSet;
    036
    037
    038    /**
    039     * A CollectionComponentParameter should be used to support inject an {@link Array}, a
    040     * {@link Collection}or {@link Map}of components automatically. The collection will contain
    041     * all components of a special type and additionally the type of the key may be specified. In
    042     * case of a map, the map's keys are the one of the component adapter.
    043     *
    044     * @author Aslak Helles&oslash;y
    045     * @author J&ouml;rg Schaible
    046     */
    047    @SuppressWarnings("serial")
    048    public class CollectionComponentParameter implements Parameter, Serializable {
    049
    050        /**
    051         * Use <code>ARRAY</code> as {@link Parameter}for an Array that must have elements.
    052         */
    053        public static final CollectionComponentParameter ARRAY = new CollectionComponentParameter();
    054        /**
    055         * Use <code>ARRAY_ALLOW_EMPTY</code> as {@link Parameter}for an Array that may have no
    056         * elements.
    057         */
    058        public static final CollectionComponentParameter ARRAY_ALLOW_EMPTY = new CollectionComponentParameter(true);
    059
    060        private final boolean emptyCollection;
    061        private final Class componentKeyType;
    062        private final Class componentValueType;
    063
    064        /**
    065         * Expect an {@link Array}of an appropriate type as parameter. At least one component of
    066         * the array's component type must exist.
    067         */
    068        public CollectionComponentParameter() {
    069            this(false);
    070        }
    071
    072        /**
    073         * Expect an {@link Array}of an appropriate type as parameter.
    074         *
    075         * @param emptyCollection <code>true</code> if an empty array also is a valid dependency
    076         *                        resolution.
    077         */
    078        public CollectionComponentParameter(boolean emptyCollection) {
    079            this(Void.TYPE, emptyCollection);
    080        }
    081
    082        /**
    083         * Expect any of the collection types {@link Array},{@link Collection}or {@link Map}as
    084         * parameter.
    085         *
    086         * @param componentValueType the type of the components (ignored in case of an Array)
    087         * @param emptyCollection    <code>true</code> if an empty collection resolves the
    088         *                           dependency.
    089         */
    090        public CollectionComponentParameter(Class componentValueType, boolean emptyCollection) {
    091            this(Object.class, componentValueType, emptyCollection);
    092        }
    093
    094        /**
    095         * Expect any of the collection types {@link Array},{@link Collection}or {@link Map}as
    096         * parameter.
    097         *
    098         * @param componentKeyType   the type of the component's key
    099         * @param componentValueType the type of the components (ignored in case of an Array)
    100         * @param emptyCollection    <code>true</code> if an empty collection resolves the
    101         *                           dependency.
    102         */
    103        public CollectionComponentParameter(Class componentKeyType, Class componentValueType, boolean emptyCollection) {
    104            this.emptyCollection = emptyCollection;
    105            this.componentKeyType = componentKeyType;
    106            this.componentValueType = componentValueType;
    107        }
    108
    109        /**
    110         * Resolve the parameter for the expected type. The method will return <code>null</code>
    111         * If the expected type is not one of the collection types {@link Array},
    112         * {@link Collection}or {@link Map}. An empty collection is only a valid resolution, if
    113         * the <code>emptyCollection</code> flag was set.
    114         *
    115         * @param container           {@inheritDoc}
    116         * @param adapter             {@inheritDoc}
    117         * @param expectedType        {@inheritDoc}
    118         * @param expectedNameBinding {@inheritDoc}
    119         * @param useNames
    120         * @param binding
    121         * @return the instance of the collection type or <code>null</code>
    122         * @throws PicoCompositionException {@inheritDoc}
    123         */
    124        @SuppressWarnings({"unchecked"})
    125        public Object resolveInstance(PicoContainer container,
    126                                      ComponentAdapter<?> adapter,
    127                                      Type expectedType,  NameBinding expectedNameBinding,
    128                                      boolean useNames, Annotation binding) {
    129            // type check is done in isResolvable
    130            Object result = null;
    131            final Class collectionType = getCollectionType(expectedType);
    132            if (collectionType != null) {
    133                final Map<Object, ComponentAdapter<?>> adapterMap =
    134                        getMatchingComponentAdapters(container, adapter, componentKeyType, getValueType(expectedType));
    135                if (collectionType.isArray()) {
    136                    result = getArrayInstance(container, collectionType, adapterMap);
    137                } else if (Map.class.isAssignableFrom(collectionType)) {
    138                    result = getMapInstance(container, collectionType, adapterMap);
    139                } else if (Collection.class.isAssignableFrom(collectionType)) {
    140                    result = getCollectionInstance(container, (Class<? extends Collection>) collectionType, adapterMap);
    141                } else {
    142                    throw new PicoCompositionException(expectedType + " is not a collective type");
    143                }
    144            }
    145            return result;
    146        }
    147
    148        /**
    149         * Check for a successful dependency resolution of the parameter for the expected type. The
    150         * dependency can only be satisfied if the expected type is one of the collection types
    151         * {@link Array},{@link Collection}or {@link Map}. An empty collection is only a valid
    152         * resolution, if the <code>emptyCollection</code> flag was set.
    153         *
    154         * @param container           {@inheritDoc}
    155         * @param adapter             {@inheritDoc}
    156         * @param expectedType        {@inheritDoc}
    157         * @param expectedNameBinding {@inheritDoc}
    158         * @param useNames
    159         * @param binding
    160         * @return <code>true</code> if matching components were found or an empty collective type
    161         *         is allowed
    162         */
    163        public boolean isResolvable(PicoContainer container, ComponentAdapter<?> adapter,
    164                                    Type expectedType, NameBinding expectedNameBinding,
    165                                    boolean useNames, Annotation binding) {
    166            return getCollectionType(expectedType) != null &&
    167                    (emptyCollection || getMatchingComponentAdapters(container, adapter,
    168                            componentKeyType, getValueType(expectedType)).size() > 0);
    169        }
    170
    171        private Class getCollectionType(Type expectedType) {
    172            if (expectedType instanceof Class) {
    173                return getCollectionType((Class) expectedType);
    174            } else if (expectedType instanceof ParameterizedType) {
    175                ParameterizedType type = (ParameterizedType) expectedType;
    176
    177                return getCollectionType(type.getRawType());
    178            }
    179
    180            throw new IllegalArgumentException("Unable to get collection type from " + expectedType);
    181        }
    182
    183        /**
    184         * Verify a successful dependency resolution of the parameter for the expected type. The
    185         * method will only return if the expected type is one of the collection types {@link Array},
    186         * {@link Collection}or {@link Map}. An empty collection is only a valid resolution, if
    187         * the <code>emptyCollection</code> flag was set.
    188         *
    189         * @param container           {@inheritDoc}
    190         * @param adapter             {@inheritDoc}
    191         * @param expectedType        {@inheritDoc}
    192         * @param expectedNameBinding {@inheritDoc}
    193         * @param useNames
    194         * @param binding
    195         * @throws PicoCompositionException {@inheritDoc}
    196         */
    197        public void verify(PicoContainer container,
    198                           ComponentAdapter<?> adapter,
    199                           Type expectedType,
    200                           NameBinding expectedNameBinding, boolean useNames, Annotation binding) {
    201            final Class collectionType = getCollectionType(expectedType);
    202            if (collectionType != null) {
    203                final Class valueType = getValueType(expectedType);
    204                final Collection componentAdapters =
    205                        getMatchingComponentAdapters(container, adapter, componentKeyType, valueType).values();
    206                if (componentAdapters.isEmpty()) {
    207                    if (!emptyCollection) {
    208                        throw new PicoCompositionException(expectedType
    209                                + " not resolvable, no components of type "
    210                                + valueType.getName()
    211                                + " available");
    212                    }
    213                } else {
    214                    for (Object componentAdapter1 : componentAdapters) {
    215                        final ComponentAdapter componentAdapter = (ComponentAdapter) componentAdapter1;
    216                        componentAdapter.verify(container);
    217                    }
    218                }
    219            } else {
    220                throw new PicoCompositionException(expectedType + " is not a collective type");
    221            }
    222        }
    223
    224        /**
    225         * Visit the current {@link Parameter}.
    226         *
    227         * @see org.picocontainer.Parameter#accept(org.picocontainer.PicoVisitor)
    228         */
    229        public void accept(final PicoVisitor visitor) {
    230            visitor.visitParameter(this);
    231        }
    232
    233        /**
    234         * Evaluate whether the given component adapter will be part of the collective type.
    235         *
    236         * @param adapter a <code>ComponentAdapter</code> value
    237         * @return <code>true</code> if the adapter takes part
    238         */
    239        protected boolean evaluate(final ComponentAdapter adapter) {
    240            return adapter != null; // use parameter, prevent compiler warning
    241        }
    242
    243        /**
    244         * Collect the matching ComponentAdapter instances.
    245         *
    246         * @param container container to use for dependency resolution
    247         * @param adapter   {@link ComponentAdapter} to exclude
    248         * @param keyType   the compatible type of the key
    249         * @param valueType the compatible type of the addComponent
    250         * @return a {@link Map} with the ComponentAdapter instances and their component keys as map key.
    251         */
    252        @SuppressWarnings({"unchecked"})
    253        protected Map<Object, ComponentAdapter<?>>
    254                    getMatchingComponentAdapters(PicoContainer container, ComponentAdapter adapter,
    255                                                 Class keyType, Class valueType) {
    256            final Map<Object, ComponentAdapter<?>> adapterMap = new LinkedHashMap<Object, ComponentAdapter<?>>();
    257            final PicoContainer parent = container.getParent();
    258            if (parent != null) {
    259                adapterMap.putAll(getMatchingComponentAdapters(parent, adapter, keyType, valueType));
    260            }
    261            final Collection<ComponentAdapter<?>> allAdapters = container.getComponentAdapters();
    262            for (ComponentAdapter componentAdapter : allAdapters) {
    263                adapterMap.remove(componentAdapter.getComponentKey());
    264            }
    265            final List<ComponentAdapter> adapterList = container.getComponentAdapters(valueType);
    266            for (ComponentAdapter componentAdapter : adapterList) {
    267                final Object key = componentAdapter.getComponentKey();
    268                if (adapter != null && key.equals(adapter.getComponentKey())) {
    269                    continue;
    270                }
    271                if (keyType.isAssignableFrom(key.getClass()) && evaluate(componentAdapter)) {
    272                    adapterMap.put(key, componentAdapter);
    273                }
    274            }
    275            return adapterMap;
    276        }
    277
    278        private Class getCollectionType(final Class collectionType) {
    279            if (collectionType.isArray() ||
    280                    Map.class.isAssignableFrom(collectionType) ||
    281                    Collection.class.isAssignableFrom(collectionType)) {
    282                return collectionType;
    283            }
    284
    285            return null;
    286        }
    287
    288        private Class getValueType(Type collectionType) {
    289            if (collectionType instanceof Class) {
    290                return getValueType((Class) collectionType);
    291            } else if (collectionType instanceof ParameterizedType) {
    292                return getValueType((ParameterizedType) collectionType);        }
    293            throw new IllegalArgumentException("Unable to determine collection type from " + collectionType);
    294        }
    295
    296        private Class getValueType(final Class collectionType) {
    297            Class valueType = componentValueType;
    298            if (collectionType.isArray()) {
    299                valueType = collectionType.getComponentType();
    300            }
    301            return valueType;
    302        }
    303
    304        private Class getValueType(final ParameterizedType collectionType) {
    305            Class valueType = componentValueType;
    306            if (Collection.class.isAssignableFrom((Class<?>) collectionType.getRawType())) {
    307                Type type = collectionType.getActualTypeArguments()[0];
    308                if (type instanceof Class) {
    309                    if (((Class)type).isAssignableFrom(valueType)) {
    310                        return valueType;
    311                    }
    312                    valueType = (Class) type;
    313                }
    314            }
    315            return valueType;
    316        }
    317
    318        private Object[] getArrayInstance(final PicoContainer container,
    319                                          final Class expectedType,
    320                                          final Map<Object, ComponentAdapter<?>> adapterList) {
    321            final Object[] result = (Object[]) Array.newInstance(expectedType.getComponentType(), adapterList.size());
    322            int i = 0;
    323            for (ComponentAdapter componentAdapter : adapterList.values()) {
    324                result[i] = container.getComponent(componentAdapter.getComponentKey());
    325                i++;
    326            }
    327            return result;
    328        }
    329
    330        @SuppressWarnings({"unchecked"})
    331        private Collection getCollectionInstance(final PicoContainer container,
    332                                                 final Class<? extends Collection> expectedType,
    333                                                 final Map<Object, ComponentAdapter<?>> adapterList) {
    334            Class<? extends Collection> collectionType = expectedType;
    335            if (collectionType.isInterface()) {
    336                // The order of tests are significant. The least generic types last.
    337                if (List.class.isAssignableFrom(collectionType)) {
    338                    collectionType = ArrayList.class;
    339    //            } else if (BlockingQueue.class.isAssignableFrom(collectionType)) {
    340    //                collectionType = ArrayBlockingQueue.class;
    341    //            } else if (Queue.class.isAssignableFrom(collectionType)) {
    342    //                collectionType = LinkedList.class;
    343                } else if (SortedSet.class.isAssignableFrom(collectionType)) {
    344                    collectionType = TreeSet.class;
    345                } else if (Set.class.isAssignableFrom(collectionType)) {
    346                    collectionType = HashSet.class;
    347                } else if (Collection.class.isAssignableFrom(collectionType)) {
    348                    collectionType = ArrayList.class;
    349                }
    350            }
    351            try {
    352                Collection result = collectionType.newInstance();
    353                for (ComponentAdapter componentAdapter : adapterList.values()) {
    354                    result.add(container.getComponent(componentAdapter.getComponentKey()));
    355                }
    356                return result;
    357            } catch (InstantiationException e) {
    358                ///CLOVER:OFF
    359                throw new PicoCompositionException(e);
    360                ///CLOVER:ON
    361            } catch (IllegalAccessException e) {
    362                ///CLOVER:OFF
    363                throw new PicoCompositionException(e);
    364                ///CLOVER:ON
    365            }
    366        }
    367
    368        @SuppressWarnings({"unchecked"})
    369        private Map getMapInstance(final PicoContainer container,
    370                                   final Class<? extends Map> expectedType,
    371                                   final Map<Object, ComponentAdapter<?>> adapterList) {
    372            Class<? extends Map> collectionType = expectedType;
    373            if (collectionType.isInterface()) {
    374                // The order of tests are significant. The least generic types last.
    375                if (SortedMap.class.isAssignableFrom(collectionType)) {
    376                    collectionType = TreeMap.class;
    377    //            } else if (ConcurrentMap.class.isAssignableFrom(collectionType)) {
    378    //                collectionType = ConcurrentHashMap.class;
    379                } else if (Map.class.isAssignableFrom(collectionType)) {
    380                    collectionType = HashMap.class;
    381                }
    382            }
    383            try {
    384                Map result = collectionType.newInstance();
    385                for (Map.Entry<Object, ComponentAdapter<?>> entry : adapterList.entrySet()) {
    386                    final Object key = entry.getKey();
    387                    result.put(key, container.getComponent(key));
    388                }
    389                return result;
    390            } catch (InstantiationException e) {
    391                ///CLOVER:OFF
    392                throw new PicoCompositionException(e);
    393                ///CLOVER:ON
    394            } catch (IllegalAccessException e) {
    395                ///CLOVER:OFF
    396                throw new PicoCompositionException(e);
    397                ///CLOVER:ON
    398            }
    399        }
    400    }