001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one or more
004 * contributor license agreements.  See the NOTICE file distributed with
005 * this work for additional information regarding copyright ownership.
006 * The ASF licenses this file to You under the Apache License, Version 2.0
007 * (the "License"); you may not use this file except in compliance with
008 * the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing, software
013 *  distributed under the License is distributed on an "AS IS" BASIS,
014 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *  See the License for the specific language governing permissions and
016 *  limitations under the License.
017 */
018package org.apache.xbean.recipe;
019
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.LinkedHashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import java.util.Iterator;
028
029public class ObjectGraph {
030    private Repository repository;
031
032    public ObjectGraph() {
033        this(new DefaultRepository());
034    }
035
036    public ObjectGraph(Repository repository) {
037        if (repository == null) throw new NullPointerException("repository is null");
038        this.repository = repository;
039    }
040
041    public Repository getRepository() {
042        return repository;
043    }
044
045    public void setRepository(Repository repository) {
046        if (repository == null) throw new NullPointerException("repository is null");
047        this.repository = repository;
048    }
049
050    public Object create(String name) throws ConstructionException {
051        Map<String, Object> objects = createAll(Collections.singletonList(name));
052        Object instance = objects.get(name);
053        if (instance == null) {
054            instance = repository.get(name);
055        }
056        return instance;
057    }
058
059    public Map<String,Object> createAll(String... names) throws ConstructionException {
060        return createAll(Arrays.asList(names));
061    }
062
063    public Map<String,Object> createAll(List<String> names) throws ConstructionException {
064        // setup execution context
065        boolean createNewContext = !ExecutionContext.isContextSet();
066        if (createNewContext) {
067            ExecutionContext.setContext(new DefaultExecutionContext(repository));
068        }
069        WrapperExecutionContext wrapperContext = new WrapperExecutionContext(ExecutionContext.getContext());
070        ExecutionContext.setContext(wrapperContext);
071
072        try {
073            // find recipes to create
074            LinkedHashMap<String, Recipe> recipes = getSortedRecipes(names);
075
076            // Seed the objects linked hash map with the existing objects
077            LinkedHashMap<String, Object> objects = new LinkedHashMap<String, Object>();
078            List<String> existingObjectNames = new ArrayList<String>(names);
079            existingObjectNames.removeAll(recipes.keySet());
080            for (String name : existingObjectNames) {
081                Object object = repository.get(name);
082                if (object == null) {
083                    throw new NoSuchObjectException(name);
084                }
085                objects.put(name, object);
086            }
087
088            // build each object from the recipe
089            for (Map.Entry<String, Recipe> entry : recipes.entrySet()) {
090                String name = entry.getKey();
091                Recipe recipe = entry.getValue();
092                if (!wrapperContext.containsObject(name) || wrapperContext.getObject(name) instanceof Recipe) {
093                    recipe.create(Object.class, false);
094                }
095            }
096
097            // add the constructed objects to the objects linked hash map
098            // The result map will be in construction order, with existing
099            // objects at the front
100            objects.putAll(wrapperContext.getConstructedObject());
101            return objects;
102        } finally {
103            // if we set a new execution context, remove it from the thread
104            if (createNewContext) {
105                ExecutionContext.setContext(null);
106            }
107        }
108    }
109
110    private LinkedHashMap<String, Recipe> getSortedRecipes(List<String> names) {
111        // construct the graph
112        Map<String, Node> nodes = new LinkedHashMap<String, Node>();
113        for (String name : names) {
114            Object object = repository.get(name);
115            if (object instanceof Recipe) {
116                Recipe recipe = (Recipe) object;
117                if (!recipe.getName().equals(name)) {
118                    throw new ConstructionException("Recipe '" + name + "' returned from the repository has name '" + name + "'");
119                }
120                createNode(name, recipe,  nodes);
121            }
122        }
123
124        // find all initial leaf nodes (and islands)
125        List<Node> sortedNodes = new ArrayList<Node>(nodes.size());
126        LinkedList<Node> leafNodes = new LinkedList<Node>();
127        for (Node n : nodes.values()) {
128            if (n.referenceCount == 0) {
129                // if the node is totally isolated (no in or out refs),
130                // move it directly to the finished list, so they are first
131                if (n.references.size() == 0) {
132                    sortedNodes.add(n);
133                } else {
134                    leafNodes.add(n);
135                }
136            }
137        }
138
139        // pluck the leaves until there are no leaves remaining
140        while (!leafNodes.isEmpty()) {
141            Node node = leafNodes.removeFirst();
142            sortedNodes.add(node);
143            for (Node ref : node.references) {
144                ref.referenceCount--;
145                if (ref.referenceCount == 0) {
146                    leafNodes.add(ref);
147                }
148            }
149        }
150
151        // There are no more leaves so if there are there still
152        // unprocessed nodes in the graph, we have one or more curcuits
153        if (sortedNodes.size() != nodes.size()) {
154            findCircuit(nodes.values().iterator().next(), new ArrayList<Recipe>(nodes.size()));
155            // find circuit should never fail, if it does there is a programming error
156            throw new ConstructionException("Internal Error: expected a CircularDependencyException");
157        }
158
159        // return the recipes
160        LinkedHashMap<String, Recipe> sortedRecipes = new LinkedHashMap<String, Recipe>();
161        for (Node node : sortedNodes) {
162            sortedRecipes.put(node.name, node.recipe);
163        }
164        return sortedRecipes;
165    }
166
167    private void findCircuit(Node node, ArrayList<Recipe> stack) {
168        if (stack.contains(node.recipe)) {
169            ArrayList<Recipe> circularity = new ArrayList<Recipe>(stack.subList(stack.indexOf(node.recipe), stack.size()));
170
171            // remove anonymous nodes from circularity list
172            for (Iterator<Recipe> iterator = circularity.iterator(); iterator.hasNext();) {
173                Recipe recipe = iterator.next();
174                if (recipe != node.recipe && recipe.getName() == null) {
175                    iterator.remove();
176                }
177            }
178
179            // add ending node to list so a full circuit is shown
180            circularity.add(node.recipe);
181            
182            throw new CircularDependencyException(circularity);
183        }
184
185        stack.add(node.recipe);
186        for (Node reference : node.references) {
187            findCircuit(reference, stack);
188        }
189    }
190
191    private Node createNode(String name, Recipe recipe, Map<String, Node> nodes) {
192        // if node already exists, verify that the exact same recipe instnace is used for both
193        if (nodes.containsKey(name)) {
194            Node node = nodes.get(name);
195            if (node.recipe != recipe) {
196                throw new ConstructionException("The name '" + name +"' is assigned to multiple recipies");
197            }
198            return node;
199        }
200
201        // create the node
202        Node node = new Node();
203        node.name = name;
204        node.recipe = recipe;
205        nodes.put(name, node);
206
207        // link in the references
208        LinkedList<Recipe> nestedRecipes = new LinkedList<Recipe>(recipe.getNestedRecipes());
209        LinkedList<Recipe> constructorRecipes = new LinkedList<Recipe>(recipe.getConstructorRecipes());
210        while (!nestedRecipes.isEmpty()) {
211            Recipe nestedRecipe = nestedRecipes.removeFirst();
212            String nestedName = nestedRecipe.getName();
213            if (nestedName != null) {
214                Node nestedNode = createNode(nestedName, nestedRecipe, nodes);
215
216                // if this is a constructor recipe, we need to add a reference link
217                if (constructorRecipes.contains(nestedRecipe)) {
218                    node.referenceCount++;
219                    nestedNode.references.add(node);
220                }
221            } else {
222                nestedRecipes.addAll(nestedRecipe.getNestedRecipes());
223                constructorRecipes.addAll(nestedRecipe.getConstructorRecipes());
224            }
225        }
226
227        return node;
228    }
229
230    private class Node {
231        String name;
232        Recipe recipe;
233        final List<Node> references = new ArrayList<Node>();
234        int referenceCount;
235    }
236
237    private static class WrapperExecutionContext extends ExecutionContext {
238        private final ExecutionContext executionContext;
239        private final Map<String, Object> constructedObject = new LinkedHashMap<String, Object>();
240
241        private WrapperExecutionContext(ExecutionContext executionContext) {
242            if (executionContext == null) throw new NullPointerException("executionContext is null");
243            this.executionContext = executionContext;
244        }
245
246        public Map<String, Object> getConstructedObject() {
247            return constructedObject;
248        }
249
250        public void push(Recipe recipe) throws CircularDependencyException {
251            executionContext.push(recipe);
252        }
253
254        public Recipe pop() {
255            return executionContext.pop();
256        }
257
258        public LinkedList<Recipe> getStack() {
259            return executionContext.getStack();
260        }
261
262        public Object getObject(String name) {
263            return executionContext.getObject(name);
264        }
265
266        public boolean containsObject(String name) {
267            return executionContext.containsObject(name);
268        }
269
270        public void addObject(String name, Object object) {
271            executionContext.addObject(name, object);
272            constructedObject.put(name, object);
273        }
274
275        public void addReference(Reference reference) {
276            executionContext.addReference(reference);
277        }
278
279        public Map<String, List<Reference>> getUnresolvedRefs() {
280            return executionContext.getUnresolvedRefs();
281        }
282
283        public ClassLoader getClassLoader() {
284            return executionContext.getClassLoader();
285        }
286    }
287}