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     */
018    package org.apache.xbean.recipe;
019    
020    import java.util.ArrayList;
021    import java.util.Arrays;
022    import java.util.Collections;
023    import java.util.LinkedHashMap;
024    import java.util.LinkedList;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Iterator;
028    
029    public 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    }