View Javadoc

1   /*
2    * $Id: ClasspathPackageProvider.java 684566 2008-08-10 18:19:28Z davenewton $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.config;
23  
24  import com.opensymphony.xwork2.Action;
25  import com.opensymphony.xwork2.config.Configuration;
26  import com.opensymphony.xwork2.config.ConfigurationException;
27  import com.opensymphony.xwork2.config.PackageProvider;
28  import com.opensymphony.xwork2.config.entities.ActionConfig;
29  import com.opensymphony.xwork2.config.entities.PackageConfig;
30  import com.opensymphony.xwork2.config.entities.ResultConfig;
31  import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
32  import com.opensymphony.xwork2.inject.Inject;
33  import com.opensymphony.xwork2.util.ClassLoaderUtil;
34  import com.opensymphony.xwork2.util.ResolverUtil;
35  import com.opensymphony.xwork2.util.ResolverUtil.ClassTest;
36  import com.opensymphony.xwork2.util.TextUtils;
37  import com.opensymphony.xwork2.util.logging.Logger;
38  import com.opensymphony.xwork2.util.logging.LoggerFactory;
39  
40  import javax.servlet.ServletContext;
41  import java.lang.annotation.Annotation;
42  import java.lang.reflect.Modifier;
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  import java.util.*;
46  
47  /***
48   * ClasspathPackageProvider loads the configuration
49   * by scanning the classpath or selected packages for Action classes.
50   * <p>
51   * This provider is only invoked if one or more action packages are passed to the dispatcher,
52   * usually from the web.xml.
53   * Configurations are created for objects that either implement Action or have classnames that end with "Action".
54   */
55  public class ClasspathPackageProvider implements PackageProvider {
56  
57      /***
58       * The default page prefix (or "path").
59       * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
60       */
61      protected static final String DEFAULT_PAGE_PREFIX = "struts.configuration.classpath.defaultPagePrefix";
62  
63      /***
64       * The default page prefix (none).
65       */
66      private String defaultPagePrefix = "";
67  
68      /***
69       * The default page extension,  to use in place of ".jsp".
70       */
71      protected static final String DEFAULT_PAGE_EXTENSION = "struts.configuration.classpath.defaultPageExtension";
72  
73      /***
74       * The defacto default page extension, usually associated with JavaServer Pages.
75       */
76      private String defaultPageExtension = ".jsp";
77  
78      /***
79       * A setting to indicate a custom default parent package,
80       * to use in place of "struts-default".
81       */
82      protected static final String DEFAULT_PARENT_PACKAGE = "struts.configuration.classpath.defaultParentPackage";
83  
84      /***
85       * A setting to disable action scanning.
86       */
87      protected static final String DISABLE_ACTION_SCANNING = "struts.configuration.classpath.disableActionScanning";
88  
89      /***
90       * Name of the framework's default configuration package,
91       * that application configuration packages automatically inherit.
92       */
93      private String defaultParentPackage = "struts-default";
94  
95      /***
96       * The default page prefix (or "path").
97       * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
98       */
99      protected static final String FORCE_LOWER_CASE = "struts.configuration.classpath.forceLowerCase";
100 
101     /***
102      * Whether to use a lowercase letter as the initial letter of an action.
103      * If false, actions will retain the initial uppercase letter from the Action class.
104      * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
105      */
106     private boolean forceLowerCase = true;
107 
108     protected static final String CLASS_SUFFIX = "struts.codebehind.classSuffix";
109     /***
110      * Default suffix that can be used to indicate POJO "Action" classes.
111      */
112     protected String classSuffix = "Action";
113 
114     protected static final String CHECK_IMPLEMENTS_ACTION = "struts.codebehind.checkImplementsAction";
115 
116     /***
117      * When testing a class, check that it implements Action
118      */
119     protected boolean checkImplementsAction = true;
120 
121     protected static final String CHECK_ANNOTATION = "struts.codebehind.checkAnnotation";
122 
123     /***
124      * When testing a class, check that it has an @Action annotation
125      */
126     protected boolean checkAnnotation = true;
127 
128     /***
129      * Helper class to scan class path for server pages.
130      */
131     private PageLocator pageLocator = new ClasspathPageLocator();
132 
133     /***
134      * Flag to indicate the packages have been loaded.
135      *
136      * @see #loadPackages
137      * @see #needsReload
138      */
139     private boolean initialized = false;
140 
141     private boolean disableActionScanning = false;
142 
143     private PackageLoader packageLoader;
144 
145     /***
146      * Logging instance for this class.
147      */
148     private static final Logger LOG = LoggerFactory.getLogger(ClasspathPackageProvider.class);
149 
150     /***
151      * The XWork Configuration for this application.
152      *
153      * @see #init
154      */
155     private Configuration configuration;
156 
157     private String actionPackages;
158 
159     private ServletContext servletContext;
160 
161     public ClasspathPackageProvider() {
162     }
163 
164     /***
165      * PageLocator defines a locate method that can be used to discover server pages.
166      */
167     public static interface PageLocator {
168         public URL locate(String path);
169     }
170 
171     /***
172      * ClasspathPathLocator searches the classpath for server pages.
173      */
174     public static class ClasspathPageLocator implements PageLocator {
175         public URL locate(String path) {
176             return ClassLoaderUtil.getResource(path, getClass());
177         }
178     }
179 
180     @Inject("actionPackages")
181     public void setActionPackages(String packages) {
182         this.actionPackages = packages;
183     }
184 
185     public void setServletContext(ServletContext ctx) {
186         this.servletContext = ctx;
187     }
188 
189     /***
190      * Disables action scanning.
191      *
192      * @param disableActionScanning True to disable
193      */
194     @Inject(value=DISABLE_ACTION_SCANNING, required=false)
195     public void setDisableActionScanning(String disableActionScanning) {
196         this.disableActionScanning = "true".equals(disableActionScanning);
197     }
198 
199     /***
200      * Check that the class implements Action
201      *
202      * @param checkImplementsAction True to check
203      */
204     @Inject(value=CHECK_IMPLEMENTS_ACTION, required=false)
205     public void setCheckImplementsAction(String checkImplementsAction) {
206         this.checkImplementsAction = "true".equals(checkImplementsAction);
207     }
208 
209     /***
210      * Check that the class has an @Action annotation
211      *
212      * @param checkImplementsAction True to check
213      */
214     @Inject(value=CHECK_ANNOTATION, required=false)
215     public void setCheckAnnotation(String checkAnnotation) {
216         this.checkAnnotation = "true".equals(checkAnnotation);
217     }
218 
219     /***
220      * Register a default parent package for the actions.
221      *
222      * @param defaultParentPackage the new defaultParentPackage
223      */
224     @Inject(value=DEFAULT_PARENT_PACKAGE, required=false)
225     public void setDefaultParentPackage(String defaultParentPackage) {
226         this.defaultParentPackage = defaultParentPackage;
227     }
228 
229     /***
230      * Register a default page extension to use when locating pages.
231      *
232      * @param defaultPageExtension the new defaultPageExtension
233      */
234     @Inject(value=DEFAULT_PAGE_EXTENSION, required=false)
235     public void setDefaultPageExtension(String defaultPageExtension) {
236         this.defaultPageExtension = defaultPageExtension;
237     }
238 
239     /***
240      * Reigster a default page prefix to use when locating pages.
241      *
242      * @param defaultPagePrefix the defaultPagePrefix to set
243      */
244     @Inject(value=DEFAULT_PAGE_PREFIX, required=false)
245     public void setDefaultPagePrefix(String defaultPagePrefix) {
246         this.defaultPagePrefix = defaultPagePrefix;
247     }
248 
249     /***
250      * Default suffix that can be used to indicate POJO "Action" classes.
251      *
252      * @param classSuffix the classSuffix to set
253      */
254     @Inject(value=CLASS_SUFFIX, required=false)
255     public void setClassSuffix(String classSuffix) {
256         this.classSuffix = classSuffix;
257     }
258 
259     /***
260      * Whether to use a lowercase letter as the initial letter of an action.
261      *
262      * @param force If false, actions will retain the initial uppercase letter from the Action class.
263      * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
264      */
265     @Inject(value=FORCE_LOWER_CASE, required=false)
266     public void setForceLowerCase(String force) {
267         this.forceLowerCase = "true".equals(force);
268     }
269 
270     /***
271      * Register a PageLocation to use to scan for server pages.
272      *
273      * @param locator
274      */
275     public void setPageLocator(PageLocator locator) {
276         this.pageLocator = locator;
277     }
278 
279     /***
280      * Scan a list of packages for Action classes.
281      *
282      * This method loads classes that implement the Action interface
283      * or have a class name that ends with the letters "Action".
284      *
285      * @param pkgs A list of packages to load
286      * @see #processActionClass
287      */
288     protected void loadPackages(String[] pkgs) {
289 
290         packageLoader = new PackageLoader();
291         ResolverUtil<Class> resolver = new ResolverUtil<Class>();
292         resolver.find(createActionClassTest(), pkgs);
293 
294         Set<? extends Class<? extends Class>> actionClasses = resolver.getClasses();
295         for (Object obj : actionClasses) {
296            Class cls = (Class) obj;
297            if (!Modifier.isAbstract(cls.getModifiers())) {
298                processActionClass(cls, pkgs);
299            }
300         }
301 
302         for (PackageConfig config : packageLoader.createPackageConfigs()) {
303             configuration.addPackageConfig(config.getName(), config);
304         }
305     }
306 
307     protected ClassTest createActionClassTest() {
308         return new ClassTest() {
309             // Match Action implementations and classes ending with "Action"
310             public boolean matches(Class type) {
311                 // TODO: should also find annotated classes
312                 return ((checkImplementsAction && Action.class.isAssignableFrom(type)) ||
313                         type.getSimpleName().endsWith(getClassSuffix()) ||
314                         (checkAnnotation && type.getAnnotation(org.apache.struts2.config.Action.class) != null));
315             }
316 
317         };
318     }
319 
320     protected String getClassSuffix() {
321         return classSuffix;
322     }
323 
324     /***
325      * Create a default action mapping for a class instance.
326      *
327      * The namespace annotation is honored, if found, otherwise
328      * the Java package is converted into the namespace
329      * by changing the dots (".") to slashes ("/").
330      *
331      * @param cls Action or POJO instance to process
332      * @param pkgs List of packages that were scanned for Actions
333      */
334     protected void processActionClass(Class<?> cls, String[] pkgs) {
335         String name = cls.getName();
336         String actionPackage = cls.getPackage().getName();
337         String actionNamespace = null;
338         String actionName = null;
339 
340         org.apache.struts2.config.Action actionAnn =
341             (org.apache.struts2.config.Action) cls.getAnnotation(org.apache.struts2.config.Action.class);
342         if (actionAnn != null) {
343             actionName = actionAnn.name();
344             if (actionAnn.namespace().equals(org.apache.struts2.config.Action.DEFAULT_NAMESPACE)) {
345                 actionNamespace = "";
346             } else {
347                 actionNamespace = actionAnn.namespace();
348             }
349         } else {
350             for (String pkg : pkgs) {
351                 if (name.startsWith(pkg)) {
352                     if (LOG.isDebugEnabled()) {
353                         LOG.debug("ClasspathPackageProvider: Processing class "+name);
354                     }
355                     name = name.substring(pkg.length() + 1);
356 
357                     actionNamespace = "";
358                     actionName = name;
359                     int pos = name.lastIndexOf('.');
360                     if (pos > -1) {
361                         actionNamespace = "/" + name.substring(0, pos).replace('.','/');
362                         actionName = name.substring(pos+1);
363                     }
364                     break;
365                 }
366             }
367             // Truncate Action suffix if found
368             if (actionName.endsWith(getClassSuffix())) {
369                 actionName = actionName.substring(0, actionName.length() - getClassSuffix().length());
370             }
371 
372             // Force initial letter of action to lowercase, if desired
373             if ((forceLowerCase) && (actionName.length() > 1)) {
374                 int lowerPos = actionName.lastIndexOf('/') + 1;
375                 StringBuilder sb = new StringBuilder();
376                 sb.append(actionName.substring(0, lowerPos));
377                 sb.append(Character.toLowerCase(actionName.charAt(lowerPos)));
378                 sb.append(actionName.substring(lowerPos + 1));
379                 actionName = sb.toString();
380             }
381         }
382 
383         PackageConfig.Builder pkgConfig = loadPackageConfig(actionNamespace, actionPackage, cls);
384 
385         // In case the package changed due to namespace annotation processing
386         if (!actionPackage.equals(pkgConfig.getName())) {
387             actionPackage = pkgConfig.getName();
388         }
389 
390         List<PackageConfig> parents = findAllParentPackages(cls);
391         if (parents.size() > 0) {
392             pkgConfig.addParents(parents);
393 
394             // Try to guess the namespace from the first package
395             PackageConfig firstParent = parents.get(0);
396             if (!TextUtils.stringSet(pkgConfig.getNamespace()) && TextUtils.stringSet(firstParent.getNamespace())) {
397                 pkgConfig.namespace(firstParent.getNamespace());
398             }
399         }
400 
401 
402         ResultTypeConfig defaultResultType = packageLoader.getDefaultResultType(pkgConfig);
403         ActionConfig actionConfig = new ActionConfig.Builder(actionPackage, actionName, cls.getName())
404                 .addResultConfigs(new ResultMap<String,ResultConfig>(cls, actionName, defaultResultType))
405                 .build();
406         pkgConfig.addActionConfig(actionName, actionConfig);
407     }
408 
409     /***
410      * Finds all parent packages by first looking at the ParentPackage annotation on the package, then the class
411      * @param cls The action class
412      * @return A list of unique packages to add
413      */
414     private List<PackageConfig> findAllParentPackages(Class<?> cls) {
415 
416         List<PackageConfig> parents = new ArrayList<PackageConfig>();
417         // Favor parent package annotations from the package
418         Set<String> parentNames = new LinkedHashSet<String>();
419         ParentPackage annotation = cls.getPackage().getAnnotation(ParentPackage.class);
420         if (annotation != null) {
421             parentNames.addAll(Arrays.asList(annotation.value()));
422         }
423         annotation = cls.getAnnotation(ParentPackage.class);
424         if (annotation != null) {
425             parentNames.addAll(Arrays.asList(annotation.value()));
426         }
427         if (parentNames.size() > 0) {
428             for (String parent : parentNames) {
429                 PackageConfig parentPkg = configuration.getPackageConfig(parent);
430                 if (parentPkg == null) {
431                     throw new ConfigurationException("ClasspathPackageProvider: Unable to locate parent package "+parent, annotation);
432                 }
433                 parents.add(parentPkg);
434             }
435         }
436         return parents;
437     }
438 
439     /***
440      * Finds or creates the package configuration for an Action class.
441      *
442      * The namespace annotation is honored, if found,
443      * and the namespace is checked for a parent configuration.
444      *
445      * @param actionNamespace The configuration namespace
446      * @param actionPackage The Java package containing our Action classes
447      * @param actionClass The Action class instance
448      * @return PackageConfig object for the Action class
449      */
450     protected PackageConfig.Builder loadPackageConfig(String actionNamespace, String actionPackage, Class actionClass) {
451         PackageConfig.Builder parent = null;
452 
453         // Check for the @Namespace annotation
454         if (actionClass != null) {
455             Namespace ns = (Namespace) actionClass.getAnnotation(Namespace.class);
456             if (ns != null) {
457                 parent = loadPackageConfig(actionNamespace, actionPackage, null);
458                 actionNamespace = ns.value();
459                 actionPackage = actionClass.getName();
460 
461             // See if the namespace has been overridden by the @Action annotation
462             } else {
463                 org.apache.struts2.config.Action actionAnn =
464                     (org.apache.struts2.config.Action) actionClass.getAnnotation(org.apache.struts2.config.Action.class);
465                 if (actionAnn != null && !actionAnn.DEFAULT_NAMESPACE.equals(actionAnn.namespace())) {
466                     // we pass null as the namespace in case the parent package hasn't been loaded yet
467                     parent = loadPackageConfig(null, actionPackage, null);
468                     actionPackage = actionClass.getName();
469                 }
470             }
471         }
472 
473 
474         PackageConfig.Builder pkgConfig = packageLoader.getPackage(actionPackage);
475         if (pkgConfig == null) {
476             pkgConfig = new PackageConfig.Builder(actionPackage);
477 
478             pkgConfig.namespace(actionNamespace);
479             if (parent == null) {
480                 PackageConfig cfg = configuration.getPackageConfig(defaultParentPackage);
481                 if (cfg != null) {
482                     pkgConfig.addParent(cfg);
483                 } else {
484                     throw new ConfigurationException("ClasspathPackageProvider: Unable to locate default parent package: " +
485                         defaultParentPackage);
486                 }
487             }
488 
489             packageLoader.registerPackage(pkgConfig);
490 
491         // if the parent package was first created by a child, ensure the namespace is correct
492         } else if (pkgConfig.getNamespace() == null) {
493             pkgConfig.namespace(actionNamespace);
494         }
495 
496         if (parent != null) {
497             packageLoader.registerChildToParent(pkgConfig, parent);
498         }
499 
500         if (LOG.isDebugEnabled()) {
501             LOG.debug("class:"+actionClass+" parent:"+parent+" current:"+(pkgConfig != null ? pkgConfig.getName() : ""));
502         }
503 
504         return pkgConfig;
505     }
506 
507     /***
508      * Default destructor. Override to provide behavior.
509      */
510     public void destroy() {
511 
512     }
513 
514     /***
515      * Register this application's configuration.
516      *
517      * @param config The configuration for this application.
518      */
519     public void init(Configuration config) {
520         this.configuration = config;
521     }
522 
523     /***
524      * Clears and loads the list of packages registered at construction.
525      *
526      * @throws ConfigurationException
527      */
528     public void loadPackages() throws ConfigurationException {
529         if (actionPackages != null && !disableActionScanning) {
530             String[] names = actionPackages.split("//s*[,]//s*");
531             // Initialize the classloader scanner with the configured packages
532             if (names.length > 0) {
533                 setPageLocator(new ServletContextPageLocator(servletContext));
534             }
535             loadPackages(names);
536         }
537         initialized = true;
538     }
539 
540     /***
541      * Indicates whether the packages have been initialized.
542      *
543      * @return True if the packages have been initialized
544      */
545     public boolean needsReload() {
546         return !initialized;
547     }
548 
549     /***
550      * Creates ResultConfig objects from result annotations,
551      * and if a result isn't found, creates it on the fly.
552      */
553     class ResultMap<K,V> extends HashMap<K,V> {
554         private Class actionClass;
555         private String actionName;
556         private ResultTypeConfig defaultResultType;
557 
558         public ResultMap(Class actionClass, String actionName, ResultTypeConfig defaultResultType) {
559             this.actionClass = actionClass;
560             this.actionName = actionName;
561             this.defaultResultType = defaultResultType;
562 
563             // check if any annotations are around
564             while (!actionClass.getName().equals(Object.class.getName())) {
565                 //noinspection unchecked
566                 Results results = (Results) actionClass.getAnnotation(Results.class);
567                 if (results != null) {
568                     // first check here...
569                     for (int i = 0; i < results.value().length; i++) {
570                         Result result = results.value()[i];
571                         ResultConfig config = createResultConfig(result);
572 						if (!containsKey((K)config.getName())) {
573                             put((K)config.getName(), (V)config);
574                         }
575                     }
576                 }
577 
578                 // what about a single Result annotation?
579                 Result result = (Result) actionClass.getAnnotation(Result.class);
580                 if (result != null) {
581                     ResultConfig config = createResultConfig(result);
582                     if (!containsKey((K)config.getName())) {
583                         put((K)config.getName(), (V)config);
584                     }
585                 }
586 
587                 actionClass = actionClass.getSuperclass();
588             }
589         }
590 
591         /***
592          * Extracts result name and value and calls {@link #createResultConfig}.
593          *
594          * @param result Result annotation reference representing result type to create
595          * @return New or cached ResultConfig object for result
596          */
597         protected ResultConfig createResultConfig(Result result) {
598             Class<? extends Object> cls = result.type();
599             if (cls == NullResult.class) {
600                 cls = null;
601             }
602             return createResultConfig(result.name(), cls, result.value(), createParameterMap(result.params()));
603         }
604 
605         protected Map<String, String> createParameterMap(String[] parms) {
606             Map<String, String> map = new HashMap<String, String>();
607             int subtract = parms.length % 2;
608             if(subtract != 0) {
609                 LOG.warn("Odd number of result parameters key/values specified.  The final one will be ignored.");
610             }
611             for (int i = 0; i < parms.length - subtract; i++) {
612                 String key = parms[i++];
613                 String value = parms[i];
614                 map.put(key, value);
615                 if(LOG.isDebugEnabled()) {
616                     LOG.debug("Adding parmeter["+key+":"+value+"] to result.");
617                 }
618             }
619             return map;
620         }
621 
622         /***
623          * Creates a default ResultConfig,
624          * using either the resultClass or the default ResultType for configuration package
625          * associated this ResultMap class.
626          *
627          * @param key The result type name
628          * @param resultClass The class for the result type
629          * @param location Path to the resource represented by this type
630          * @return A ResultConfig for key mapped to location
631          */
632         private ResultConfig createResultConfig(Object key, Class<? extends Object> resultClass,
633                                                 String location,
634                                                 Map<? extends Object,? extends Object > configParams) {
635             if (resultClass == null) {
636                 configParams = defaultResultType.getParams();
637                 String className = defaultResultType.getClassName();
638                 try {
639                     resultClass = ClassLoaderUtil.loadClass(className, getClass());
640                 } catch (ClassNotFoundException ex) {
641                     throw new ConfigurationException("ClasspathPackageProvider: Unable to locate result class "+className, actionClass);
642                 }
643             }
644 
645             String defaultParam;
646             try {
647                 defaultParam = (String) resultClass.getField("DEFAULT_PARAM").get(null);
648             } catch (Exception e) {
649                 // not sure why this happened, but let's just use a sensible choice
650                 defaultParam = "location";
651             }
652 
653             HashMap params = new HashMap();
654             if (configParams != null) {
655                 params.putAll(configParams);
656             }
657 
658             params.put(defaultParam, location);
659             return new ResultConfig.Builder((String) key, resultClass.getName()).addParams(params).build();
660         }
661     }
662 
663     /***
664      * Search classpath for a page.
665      */
666     private final class ServletContextPageLocator implements PageLocator {
667         private final ServletContext context;
668         private ClasspathPageLocator classpathPageLocator = new ClasspathPageLocator();
669 
670         private ServletContextPageLocator(ServletContext context) {
671             this.context = context;
672         }
673 
674         public URL locate(String path) {
675             URL url = null;
676             try {
677                 url = context.getResource(path);
678                 if (url == null) {
679                     url = classpathPageLocator.locate(path);
680                 }
681             } catch (MalformedURLException e) {
682                 if (LOG.isDebugEnabled()) {
683                     LOG.debug("Unable to resolve path "+path+" against the servlet context");
684                 }
685             }
686             return url;
687         }
688     }
689 
690     private static class PackageLoader {
691 
692         /***
693          * The package configurations for scanned Actions.
694          */
695         private Map<String,PackageConfig.Builder> packageConfigBuilders = new HashMap<String,PackageConfig.Builder>();
696 
697         private Map<PackageConfig.Builder,PackageConfig.Builder> childToParent = new HashMap<PackageConfig.Builder,PackageConfig.Builder>();
698 
699         public PackageConfig.Builder getPackage(String name) {
700             return packageConfigBuilders.get(name);
701         }
702 
703         public void registerChildToParent(PackageConfig.Builder child, PackageConfig.Builder parent) {
704             childToParent.put(child, parent);
705         }
706 
707         public void registerPackage(PackageConfig.Builder builder) {
708             packageConfigBuilders.put(builder.getName(), builder);
709         }
710 
711         public Collection<PackageConfig> createPackageConfigs() {
712             Map<String, PackageConfig> configs = new HashMap<String, PackageConfig>();
713 
714             Set<PackageConfig.Builder> builders;
715             while ((builders = findPackagesWithNoParents()).size() > 0) {
716                 for (PackageConfig.Builder parent : builders) {
717                     PackageConfig config = parent.build();
718                     configs.put(config.getName(), config);
719                     packageConfigBuilders.remove(config.getName());
720 
721                     for (Iterator<Map.Entry<PackageConfig.Builder,PackageConfig.Builder>> i = childToParent.entrySet().iterator(); i.hasNext(); ) {
722                         Map.Entry<PackageConfig.Builder,PackageConfig.Builder> entry = i.next();
723                         if (entry.getValue() == parent) {
724                             entry.getKey().addParent(config);
725                             i.remove();
726                         }
727                     }
728                 }
729             }
730             return configs.values();
731         }
732 
733         Set<PackageConfig.Builder> findPackagesWithNoParents() {
734             Set<PackageConfig.Builder> builders = new HashSet<PackageConfig.Builder>();
735             for (PackageConfig.Builder child : packageConfigBuilders.values()) {
736                 if (!childToParent.containsKey(child)) {
737                     builders.add(child);
738                 }
739             }
740             return builders;
741         }
742 
743         public ResultTypeConfig getDefaultResultType(PackageConfig.Builder pkgConfig) {
744             PackageConfig.Builder parent;
745             PackageConfig.Builder current = pkgConfig;
746 
747             while ((parent = childToParent.get(current)) != null) {
748                 current = parent;
749             }
750             return current.getResultType(current.getFullDefaultResultType());
751         }
752     }
753 }