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 }