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