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