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
035/**
036 * @version $Rev: 6687 $ $Date: 2005-12-28T21:08:56.733437Z $
037 */
038public 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}