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