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