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øy
045     * @author Jö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    }