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.refernceCount == 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.refernceCount--;
145 if (ref.refernceCount == 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.refernceCount++;
219 nestedNode.references.add(node);
220 }
221 } else {
222 nestedRecipes.addAll(nestedRecipe.getNestedRecipes());
223 }
224 }
225
226 return node;
227 }
228
229 private class Node {
230 String name;
231 Recipe recipe;
232 final List<Node> references = new ArrayList<Node>();
233 int refernceCount;
234 }
235
236 private static class WrapperExecutionContext extends ExecutionContext {
237 private final ExecutionContext executionContext;
238 private final Map<String, Object> constructedObject = new LinkedHashMap<String, Object>();
239
240 private WrapperExecutionContext(ExecutionContext executionContext) {
241 if (executionContext == null) throw new NullPointerException("executionContext is null");
242 this.executionContext = executionContext;
243 }
244
245 public Map<String, Object> getConstructedObject() {
246 return constructedObject;
247 }
248
249 public void push(Recipe recipe) throws CircularDependencyException {
250 executionContext.push(recipe);
251 }
252
253 public Recipe pop() {
254 return executionContext.pop();
255 }
256
257 public LinkedList<Recipe> getStack() {
258 return executionContext.getStack();
259 }
260
261 public Object getObject(String name) {
262 return executionContext.getObject(name);
263 }
264
265 public boolean containsObject(String name) {
266 return executionContext.containsObject(name);
267 }
268
269 public void addObject(String name, Object object) {
270 executionContext.addObject(name, object);
271 constructedObject.put(name, object);
272 }
273
274 public void addReference(Reference reference) {
275 executionContext.addReference(reference);
276 }
277
278 public Map<String, List<Reference>> getUnresolvedRefs() {
279 return executionContext.getUnresolvedRefs();
280 }
281
282 public ClassLoader getClassLoader() {
283 return executionContext.getClassLoader();
284 }
285 }
286 }