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.concurrent.ConcurrentHashMap;
030    import java.util.concurrent.ConcurrentMap;
031    
032    /**
033     * @version $Rev: 6687 $ $Date: 2005-12-28T21:08:56.733437Z $
034     */
035    public class MapRecipe extends AbstractRecipe {
036        private final List<Object[]> entries;
037        private String typeName;
038        private Class typeClass;
039        private final EnumSet<Option> options = EnumSet.noneOf(Option.class);
040    
041        public MapRecipe() {
042            entries = new ArrayList<Object[]>();
043        }
044    
045        public MapRecipe(String type) {
046            this.typeName = type;
047            entries = new ArrayList<Object[]>();
048        }
049    
050        public MapRecipe(Class type) {
051            this.typeClass = type;
052            if (!RecipeHelper.hasDefaultConstructor(type)) throw new IllegalArgumentException("Type does not have a default constructor " + type);
053            entries = new ArrayList<Object[]>();
054        }
055    
056        public MapRecipe(Map<?,?> map) {
057            if (map == null) throw new NullPointerException("map is null");
058    
059            entries = new ArrayList<Object[]>(map.size());
060    
061            // If the specified set has a default constructor we will recreate the set, otherwise we use a LinkedHashMap or TreeMap
062            if (RecipeHelper.hasDefaultConstructor(map.getClass())) {
063                this.typeClass = map.getClass();
064            } else if (map instanceof SortedMap) {
065                this.typeClass = TreeMap.class;
066            } else if (map instanceof ConcurrentMap) {
067                this.typeClass = ConcurrentHashMap.class;
068            } else {
069                this.typeClass = LinkedHashMap.class;
070            }
071            putAll(map);
072        }
073    
074        public MapRecipe(MapRecipe mapRecipe) {
075            if (mapRecipe == null) throw new NullPointerException("mapRecipe is null");
076            this.typeName = mapRecipe.typeName;
077            this.typeClass = mapRecipe.typeClass;
078            entries = new ArrayList<Object[]>(mapRecipe.entries);
079        }
080    
081        public void allow(Option option){
082            options.add(option);
083        }
084    
085        public void disallow(Option option){
086            options.remove(option);
087        }
088    
089        public List<Recipe> getNestedRecipes() {
090            List<Recipe> nestedRecipes = new ArrayList<Recipe>(entries.size() * 2);
091            for (Object[] entry : entries) {
092                Object key = entry[0];
093                if (key instanceof Recipe) {
094                    Recipe recipe = (Recipe) key;
095                    nestedRecipes.add(recipe);
096                }
097    
098                Object value = entry[1];
099                if (value instanceof Recipe) {
100                    Recipe recipe = (Recipe) value;
101                    nestedRecipes.add(recipe);
102                }
103            }
104            return nestedRecipes;
105        }
106    
107        public List<Recipe> getConstructorRecipes() {
108            if (!options.contains(Option.LAZY_ASSIGNMENT)) {
109                return getNestedRecipes();
110            }
111            return Collections.emptyList();
112        }
113    
114        public boolean canCreate(Type type) {
115            Class myType = getType(type);
116            return RecipeHelper.isAssignable(type, myType);
117        }
118    
119        protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException {
120            Class mapType = getType(expectedType);
121    
122            if (!RecipeHelper.hasDefaultConstructor(mapType)) {
123                throw new ConstructionException("Type does not have a default constructor " + mapType.getName());
124            }
125    
126            Object o;
127            try {
128                o = mapType.newInstance();
129            } catch (Exception e) {
130                throw new ConstructionException("Error while creating set instance: " + mapType.getName());
131            }
132    
133            if(!(o instanceof Map)) {
134                throw new ConstructionException("Specified map type does not implement the Map interface: " + mapType.getName());
135            }
136            Map instance = (Map) o;
137    
138            // get component type
139            Type keyType = Object.class;
140            Type valueType = Object.class;
141            Type[] typeParameters = RecipeHelper.getTypeParameters(Collection.class, expectedType);
142            if (typeParameters != null && typeParameters.length == 2) {
143                if (typeParameters[0] instanceof Class) {
144                    keyType = typeParameters[0];
145                }
146                if (typeParameters[1] instanceof Class) {
147                    valueType = typeParameters[1];
148                }
149            }
150    
151            // add to execution context if name is specified
152            if (getName() != null) {
153                ExecutionContext.getContext().addObject(getName(), instance);
154            }
155    
156            // add map entries
157            boolean refAllowed = options.contains(Option.LAZY_ASSIGNMENT);
158            for (Object[] entry : entries) {
159                Object key = RecipeHelper.convert(keyType, entry[0], refAllowed);
160                Object value = RecipeHelper.convert(valueType, entry[1], refAllowed);
161    
162                if (key instanceof Reference) {
163                    // when the key reference and optional value reference are both resolved
164                    // the key/value pair will be added to the map
165                    Reference.Action action = new UpdateMap(instance, key, value);
166                    ((Reference) key).setAction(action);
167                    if (value instanceof Reference) {
168                        ((Reference) value).setAction(action);
169                    }
170                } else if (value instanceof Reference) {
171                    // add a null place holder assigned to the key
172                    //noinspection unchecked
173                    instance.put(key, null);
174                    // when value is resolved we will replace the null value with they real value
175                    Reference.Action action = new UpdateValue(instance, key);
176                    ((Reference) value).setAction(action);
177                } else {
178                    //noinspection unchecked
179                    instance.put(key, value);
180                }
181            }
182            return instance;
183        }
184    
185        private Class getType(Type expectedType) {
186            Class expectedClass = RecipeHelper.toClass(expectedType);
187            if (typeClass != null || typeName != null) {
188                Class type = typeClass;
189                if (type == null) {
190                    try {
191                        type = RecipeHelper.loadClass(typeName);
192                    } catch (ClassNotFoundException e) {
193                        throw new ConstructionException("Type class could not be found: " + typeName);
194                    }
195                }
196    
197                // if expectedType is a subclass of the assigned type,
198                // we use it assuming it has a default constructor
199                if (type.isAssignableFrom(expectedClass) && RecipeHelper.hasDefaultConstructor(expectedClass)) {
200                    return expectedClass;
201                }
202            }
203    
204            // no type explicitly set
205            if (RecipeHelper.hasDefaultConstructor(expectedClass)) {
206                return expectedClass;
207            } else if (expectedClass.isAssignableFrom(SortedMap.class)) {
208                return TreeMap.class;
209            } else if (expectedClass.isAssignableFrom(ConcurrentMap.class)) {
210                return ConcurrentHashMap.class;
211            } else {
212                return LinkedHashMap.class;
213            }
214        }
215    
216    
217        public void put(Object key, Object value) {
218            if (key == null) throw new NullPointerException("key is null");
219            entries.add(new Object[] { key, value});
220        }
221    
222        public void putAll(Map<?,?> map) {
223            if (map == null) throw new NullPointerException("map is null");
224            for (Map.Entry<?,?> entry : map.entrySet()) {
225                Object key = entry.getKey();
226                Object value = entry.getValue();
227                put(key, value);
228            }
229        }
230    
231        private static class UpdateValue implements Reference.Action {
232            private final Map map;
233            private final Object key;
234    
235            public UpdateValue(Map map, Object key) {
236                this.map = map;
237                this.key = key;
238            }
239    
240            @SuppressWarnings({"unchecked"})
241            public void onSet(Reference ref) {
242                map.put(key, ref.get());
243            }
244        }
245    
246    
247        private static class UpdateMap implements Reference.Action {
248            private final Map map;
249            private final Object key;
250            private final Object value;
251    
252            public UpdateMap(Map map, Object key, Object value) {
253                this.map = map;
254                this.key = key;
255                this.value = value;
256            }
257    
258            @SuppressWarnings({"unchecked"})
259            public void onSet(Reference ignored) {
260                Object key = this.key;
261                if (key instanceof Reference) {
262                    Reference reference = (Reference) key;
263                    if (!reference.isResolved()) {
264                        return;
265                    }
266                    key = reference.get();
267                }
268                Object value = this.value;
269                if (value instanceof Reference) {
270                    Reference reference = (Reference) value;
271                    if (!reference.isResolved()) {
272                        return;
273                    }
274                    value = reference.get();
275                }
276                map.put(key, value);
277            }
278        }
279    
280    }