001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.xbean.recipe;
018    
019    import java.lang.reflect.Type;
020    import java.util.ArrayList;
021    import java.util.Collection;
022    import java.util.Collections;
023    import java.util.EnumSet;
024    import java.util.LinkedHashMap;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.SortedMap;
028    import java.util.TreeMap;
029    import java.util.Dictionary;
030    import java.util.AbstractMap;
031    import java.util.Set;
032    import java.util.concurrent.ConcurrentHashMap;
033    import java.util.concurrent.ConcurrentMap;
034    
035    /**
036     * @version $Rev: 6687 $ $Date: 2005-12-28T21:08:56.733437Z $
037     */
038    public class MapRecipe extends AbstractRecipe {
039        private final List<Object[]> entries;
040        private String typeName;
041        private Class typeClass;
042        private final EnumSet<Option> options = EnumSet.noneOf(Option.class);
043    
044        public MapRecipe() {
045            entries = new ArrayList<Object[]>();
046        }
047    
048        public MapRecipe(String type) {
049            this.typeName = type;
050            entries = new ArrayList<Object[]>();
051        }
052    
053        public MapRecipe(Class type) {
054            if (type == null) throw new NullPointerException("type is null");
055            this.typeClass = type;
056            entries = new ArrayList<Object[]>();
057        }
058    
059        public MapRecipe(Map<?,?> map) {
060            if (map == null) throw new NullPointerException("map is null");
061    
062            entries = new ArrayList<Object[]>(map.size());
063    
064            // If the specified set has a default constructor we will recreate the set, otherwise we use a LinkedHashMap or TreeMap
065            if (RecipeHelper.hasDefaultConstructor(map.getClass())) {
066                this.typeClass = map.getClass();
067            } else if (map instanceof SortedMap) {
068                this.typeClass = TreeMap.class;
069            } else if (map instanceof ConcurrentMap) {
070                this.typeClass = ConcurrentHashMap.class;
071            } else {
072                this.typeClass = LinkedHashMap.class;
073            }
074            putAll(map);
075        }
076    
077        public MapRecipe(MapRecipe mapRecipe) {
078            if (mapRecipe == null) throw new NullPointerException("mapRecipe is null");
079            this.typeName = mapRecipe.typeName;
080            this.typeClass = mapRecipe.typeClass;
081            entries = new ArrayList<Object[]>(mapRecipe.entries);
082        }
083    
084        public void allow(Option option){
085            options.add(option);
086        }
087    
088        public void disallow(Option option){
089            options.remove(option);
090        }
091    
092        public List<Recipe> getNestedRecipes() {
093            List<Recipe> nestedRecipes = new ArrayList<Recipe>(entries.size() * 2);
094            for (Object[] entry : entries) {
095                Object key = entry[0];
096                if (key instanceof Recipe) {
097                    Recipe recipe = (Recipe) key;
098                    nestedRecipes.add(recipe);
099                }
100    
101                Object value = entry[1];
102                if (value instanceof Recipe) {
103                    Recipe recipe = (Recipe) value;
104                    nestedRecipes.add(recipe);
105                }
106            }
107            return nestedRecipes;
108        }
109    
110        public List<Recipe> getConstructorRecipes() {
111            if (!options.contains(Option.LAZY_ASSIGNMENT)) {
112                return getNestedRecipes();
113            }
114            return Collections.emptyList();
115        }
116    
117        public boolean canCreate(Type type) {
118            Class myType = getType(type);
119            return RecipeHelper.isAssignable(type, myType);
120        }
121    
122        protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException {
123            Class mapType = getType(expectedType);
124    
125            if (!RecipeHelper.hasDefaultConstructor(mapType)) {
126                throw new ConstructionException("Type does not have a default constructor " + mapType.getName());
127            }
128    
129            Object o;
130            try {
131                o = mapType.newInstance();
132            } catch (Exception e) {
133                throw new ConstructionException("Error while creating set instance: " + mapType.getName());
134            }
135    
136            Map instance;
137            if (o instanceof Map) {
138                instance = (Map) o;
139            } else if (o instanceof Dictionary) {
140                instance = new DummyDictionaryAsMap((Dictionary) o);
141            } else {
142                throw new ConstructionException("Specified map type does not implement the Map interface: " + mapType.getName());
143            }
144    
145            // get component type
146            Type keyType = Object.class;
147            Type valueType = Object.class;
148            Type[] typeParameters = RecipeHelper.getTypeParameters(Map.class, expectedType);
149            if (typeParameters != null && typeParameters.length == 2) {
150                if (typeParameters[0] instanceof Class) {
151                    keyType = typeParameters[0];
152                }
153                if (typeParameters[1] instanceof Class) {
154                    valueType = typeParameters[1];
155                }
156            }
157    
158            // add to execution context if name is specified
159            if (getName() != null) {
160                ExecutionContext.getContext().addObject(getName(), instance);
161            }
162    
163            // add map entries
164            boolean refAllowed = options.contains(Option.LAZY_ASSIGNMENT);
165            for (Object[] entry : entries) {
166                Object key = RecipeHelper.convert(keyType, entry[0], refAllowed);
167                Object value = RecipeHelper.convert(valueType, entry[1], refAllowed);
168    
169                if (key instanceof Reference) {
170                    // when the key reference and optional value reference are both resolved
171                    // the key/value pair will be added to the map
172                    Reference.Action action = new UpdateMap(instance, key, value);
173                    ((Reference) key).setAction(action);
174                    if (value instanceof Reference) {
175                        ((Reference) value).setAction(action);
176                    }
177                } else if (value instanceof Reference) {
178                    // add a null place holder assigned to the key
179                    //noinspection unchecked
180                    instance.put(key, null);
181                    // when value is resolved we will replace the null value with they real value
182                    Reference.Action action = new UpdateValue(instance, key);
183                    ((Reference) value).setAction(action);
184                } else {
185                    //noinspection unchecked
186                    instance.put(key, value);
187                }
188            }
189            return instance;
190        }
191    
192        private Class getType(Type expectedType) {
193            Class expectedClass = RecipeHelper.toClass(expectedType);
194            if (typeClass != null || typeName != null) {
195                Class type = typeClass;
196                if (type == null) {
197                    try {
198                        type = RecipeHelper.loadClass(typeName);
199                    } catch (ClassNotFoundException e) {
200                        throw new ConstructionException("Type class could not be found: " + typeName);
201                    }
202                }
203    
204                // if expectedType is a subclass of the assigned type,
205                // we use it assuming it has a default constructor
206                if (type.isAssignableFrom(expectedClass)) {
207                    return getMap(expectedClass);                
208                } else {
209                    return getMap(type);
210                }
211            }
212    
213            // no type explicitly set
214            return getMap(expectedClass);
215        }
216        
217        private Class getMap(Class type) {
218            if (RecipeHelper.hasDefaultConstructor(type)) {
219                return type;
220            } else if (SortedMap.class.isAssignableFrom(type)) {
221                return TreeMap.class;
222            } else if (ConcurrentMap.class.isAssignableFrom(type)) {
223                return ConcurrentHashMap.class;
224            } else {
225                return LinkedHashMap.class;
226            }
227        }
228    
229        public void put(Object key, Object value) {
230            if (key == null) throw new NullPointerException("key is null");
231            entries.add(new Object[] { key, value});
232        }
233    
234        public void putAll(Map<?,?> map) {
235            if (map == null) throw new NullPointerException("map is null");
236            for (Map.Entry<?,?> entry : map.entrySet()) {
237                Object key = entry.getKey();
238                Object value = entry.getValue();
239                put(key, value);
240            }
241        }
242    
243        private static class UpdateValue implements Reference.Action {
244            private final Map map;
245            private final Object key;
246    
247            public UpdateValue(Map map, Object key) {
248                this.map = map;
249                this.key = key;
250            }
251    
252            @SuppressWarnings({"unchecked"})
253            public void onSet(Reference ref) {
254                map.put(key, ref.get());
255            }
256        }
257    
258    
259        private static class UpdateMap implements Reference.Action {
260            private final Map map;
261            private final Object key;
262            private final Object value;
263    
264            public UpdateMap(Map map, Object key, Object value) {
265                this.map = map;
266                this.key = key;
267                this.value = value;
268            }
269    
270            @SuppressWarnings({"unchecked"})
271            public void onSet(Reference ignored) {
272                Object key = this.key;
273                if (key instanceof Reference) {
274                    Reference reference = (Reference) key;
275                    if (!reference.isResolved()) {
276                        return;
277                    }
278                    key = reference.get();
279                }
280                Object value = this.value;
281                if (value instanceof Reference) {
282                    Reference reference = (Reference) value;
283                    if (!reference.isResolved()) {
284                        return;
285                    }
286                    value = reference.get();
287                }
288                map.put(key, value);
289            }
290        }
291    
292        public static class DummyDictionaryAsMap extends AbstractMap {
293    
294            private final Dictionary dictionary;
295    
296            public DummyDictionaryAsMap(Dictionary dictionary) {
297                this.dictionary = dictionary;
298            }
299    
300            @Override
301            public Object put(Object key, Object value) {
302                return dictionary.put(key, value);
303            }
304    
305            public Set entrySet() {
306                throw new UnsupportedOperationException();
307            }
308        }
309    
310    }