View Javadoc

1   package de.matthias_burbach.deputy.core;
2   
3   import java.io.File;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.HashMap;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Map;
10  
11  import org.jdom.Attribute;
12  import org.jdom.Document;
13  import org.jdom.Element;
14  import org.jdom.input.SAXBuilder;
15  import org.jdom.xpath.XPath;
16  
17  import de.matthias_burbach.deputy.core.project.Project;
18  import de.matthias_burbach.deputy.core.project.ProjectQualifier;
19  import de.matthias_burbach.deputy.core.project.ProjectQualifierImpl;
20  import de.matthias_burbach.deputy.core.project.VersionComparator;
21  import de.matthias_burbach.deputy.core.repository.RepositoryConfig;
22  import de.matthias_burbach.deputy.core.repository.VersionScanner;
23  import de.matthias_burbach.deputy.core.rule.DeprecationRule;
24  import de.matthias_burbach.deputy.core.rule.EnforcementRule;
25  import de.matthias_burbach.deputy.core.rule.RemovalRule;
26  import de.matthias_burbach.deputy.core.rule.ReplacementRule;
27  import de.matthias_burbach.deputy.core.rule.RetentionRule;
28  import de.matthias_burbach.deputy.core.rule.RuleSet;
29  import de.matthias_burbach.deputy.core.util.Log;
30  
31  /***
32   * Parses a project and its dependencies recursively. Applies rules to decide
33   * which versions of the dependencies to follow while parsing.
34   *
35   * @author Matthias Burbach
36   */
37  public class ProjectRecursor {
38      /***
39       * Bundles properties that occur in a dependency declaration.
40       *
41       * @author Matthias Burbach
42       */
43      private class ProjectReference {
44          /***
45           * The referenced project's group id.
46           */
47          private String groupId;
48  
49          /***
50           * The referenced project's artifact id.
51           */
52          private String artifactId;
53  
54          /***
55           * The referenced project's version.
56           */
57          private String version;
58  
59          /***
60           * The referenced project's jar file name.
61           */
62          private String jar;
63  
64          /***
65           * The referenced project's type.
66           */
67          private String type;
68  
69          /***
70           * The referenced project's hompage URL.
71           */
72          private String url;
73  
74          /***
75           * @param groupId The referenced project's group id.
76           * @param artifactId The referenced project's artifact id.
77           * @param version The referenced project's version.
78           * @param jar The referenced project's jar file name.
79           * @param type The referenced project's type.
80           * @param url The referenced project's hompage URL.
81           */
82          public ProjectReference(
83                  final String groupId,
84                  final String artifactId,
85                  final String version,
86                  final String jar,
87                  final String type,
88                  final String url) {
89              this.groupId = groupId;
90              this.artifactId = artifactId;
91              this.version = version;
92              this.jar = jar;
93              this.type = type;
94              this.url = url;
95          }
96  
97          /***
98           * @return Returns the artifactId.
99           */
100         public String getArtifactId() {
101             return artifactId;
102         }
103 
104         /***
105          * @return Returns the groupId.
106          */
107         public String getGroupId() {
108             return groupId;
109         }
110 
111         /***
112          * @return Returns the type.
113          */
114         public String getType() {
115             return type;
116         }
117 
118         /***
119          * @return Returns the jar.
120          */
121         public String getJar() {
122             return jar;
123         }
124 
125         /***
126          * @return Returns the url.
127          */
128         public String getUrl() {
129             return url;
130         }
131 
132         /***
133          * @return Returns the version.
134          */
135         public String getVersion() {
136             return version;
137         }
138     }
139 
140     /***
141      * Parses the graph of dependencies recursively as is,
142      * i. e. as it is literally defined by the dependencies in the project.xml
143      * files.
144      */
145     private static final int STRATEGY_AS_IS = 0;
146 
147     /***
148      * Parses the graph of dependencies recursively, but usually follows
149      * versions according to rules instead of what is defined in the
150      * dependencies of the project.xml in terms of dependency versions.
151      * <p/>
152      * Does not parse dependencies which contain the top property set to false.
153      */
154     private static final int STRATEGY_APPLY_RULES = 1;
155 
156     /***
157      * Parses the project.xml including its dependencies but does not follow
158      * dependencies recursively. This is used to parse versions of artifacts
159      * which have been overruled by some other version. They still need to be
160      * parsed - though only non-recursively - to have them for later conflict
161      * reporting.
162      */
163     private static final int STRATEGY_NON_RECURSIVELY = 2;
164 
165     /***
166      * The list of repository configs pointing to the repositories to be used.
167      */
168     private List repositoryConfigs = new ArrayList();
169 
170     /***
171      * Maps virtual relative repository paths of POMs to physical paths of POMs
172      * that are not in a true repository but which are scattered around anywhere
173      * in the file system. These take precedence over their correspondences in
174      * any of the configured repositories. This allows to process POMs which
175      * have not yet been released to a repository.
176      */
177     private Map virtualRepositoryPoms = new HashMap();
178 
179     /***
180      * Scans directories for all versions of a given artifact.
181      */
182     private VersionScanner versionScanner = new VersionScanner();
183 
184     /***
185      * Used to sort project versions.
186      */
187     private VersionComparator versionComparator = new VersionComparator();
188 
189     /***
190      * The project generator used to generate a transient XML document from
191      * a top project potentially updated by the user before applying the rules.
192      */
193     private ProjectGenerator projectGenerator = new ProjectGenerator("");
194 
195     /***
196      * The document representing the top project file if the strategy is
197      * STRATEGY_APPLY_RULES and the document must not be loaded but used in the
198      * state currently available in main memory (with potentially added and/or
199      * removed dependencies).
200      */
201     private Document currentTopDocument = null;
202 
203     /***
204      * The file name of the {@link #currentTopDocument}.
205      */
206     private String currentTopProjectFile = null;
207 
208     /***
209      * The optional log that may be supplied by the caller.
210      */
211     private Log userLog;
212 
213     /***
214      * <code>true</code> if messages are to be written to the user's log, too.
215      */
216     private boolean userLogEnabled = true;
217 
218     /***
219      * Maps 'group id + "/" + artifact id' to the version that is presently
220      * chosen in the current top project.
221      * <p/>
222      * Supports the default rule 'PRESENT_RELEASE'. What is mapped here is the
223      * default version for the artifact.
224      */
225     private Map presentVersions = null;
226 
227     /***
228      * @param repositoryConfigs The list of configs of type
229      *                          {@link RepositoryConfig} pointing to the
230      *                          repositories of Maven project
231      *                          object model files (POMs) to be used.
232      * @param userLog The user's log to write messages to while recursing.
233      */
234     public ProjectRecursor(final List repositoryConfigs, final Log userLog) {
235         this.repositoryConfigs = repositoryConfigs;
236         this.userLog = userLog;
237     }
238 
239     /***
240      * @param projectFile The absolute path and file name of the Maven project
241      *                    to open. Must not be <code>null</code>.
242      * @param virtualRepositoryActive Whether to use the virtual repository
243      *                                in the dependency recursion.
244      * @return The top Maven project of the dependency graph of recursively
245      *         parsed Maven projects.
246      * @throws Exception if anything goes unexpectedly wrong
247      */
248     public Project openProjectAsIs(
249             final String projectFile,
250             final boolean virtualRepositoryActive) throws Exception {
251         return parseProject(
252                 projectFile,
253                 null,
254                 STRATEGY_AS_IS,
255                 virtualRepositoryActive);
256     }
257 
258     /***
259      * @param projectFile The absolute path and file name of the Maven project
260      *                    to parse recursively. Must not be <code>null</code>.
261      * @param project The project to apply the rules to. Must not be
262      *                <code>null</code>.
263      * @param virtualRepositoryActive Whether to use the virtual repository
264      *                                in the dependency recursion.
265      * @return The top Maven project of the dependency tree of recursively
266      *         parsed Maven projects after applying the rules.
267      * @throws Exception if anything goes unexpectedly wrong
268      */
269     public Project applyRulesToProject(
270             final String projectFile,
271             final Project project,
272             final boolean virtualRepositoryActive)
273             throws Exception {
274         return parseProject(
275                 projectFile,
276                 project,
277                 STRATEGY_APPLY_RULES,
278                 virtualRepositoryActive);
279     }
280 
281     /***
282      * @param projectFile The absolute path and file name of the Maven project
283      *                    to parse recursively. Must not be <code>null</code>.
284      * @param project The project to apply the rules to if the strategy is
285      *                {@link #STRATEGY_APPLY_RULES}. Must not be
286      *                <code>null</code> then. Must be null if the strategy is
287      *                {@link #STRATEGY_AS_IS}.
288      * @param strategy Either {@link #STRATEGY_AS_IS} or
289      *                 {@link #STRATEGY_APPLY_RULES}.
290      * @param virtualRepositoryActive Whether to use the virtual repository
291      *                                in the dependency recursion.
292      * @return The top Maven project of the dependency tree of recursively
293      *         parsed Maven projects.
294      * @throws Exception if anything goes unexpectedly wrong
295      */
296     private Project parseProject(
297             final String projectFile,
298             final Project project,
299             final int strategy,
300             final boolean virtualRepositoryActive)
301             throws Exception {
302         if (virtualRepositoryActive) {
303             loadVirtualRepository();
304         } else {
305             unloadVirtualRepository();
306         }
307         RuleSet ruleSet = null;
308         Boolean isAssembly = null;
309         if (project != null) {
310             ruleSet = project.getRuleSet();
311             isAssembly = new Boolean(project.isAssembly());
312             currentTopProjectFile = projectFile;
313             currentTopDocument =
314                 projectGenerator.createUpdatedDocument(project, projectFile);
315             presentVersions = createPresentVersionsMap(project);
316         } else {
317             currentTopDocument = null;
318             currentTopProjectFile = null;
319             presentVersions = null;
320         }
321         if (ruleSet == null) {
322             ruleSet = new RuleSet();
323         }
324         if (strategy != STRATEGY_APPLY_RULES) {
325             //only one parsing pass, so log now
326             userLogEnabled = true;
327         } else {
328             //only log later during the second parsing pass
329             userLogEnabled = false;
330         }
331 
332         Project topProject =
333             parseProject(
334                 projectFile,
335                 new ProjectReference(
336                     null,
337                     null,
338                     null,
339                     null,
340                     null,
341                     null),
342                 new HashMap(),
343                 ruleSet,
344                 strategy);
345 
346         if (strategy == ProjectRecursor.STRATEGY_APPLY_RULES) {
347             //about to parse in the second pass, only log now
348             userLogEnabled = true;
349             /*
350              * missing POMs or dynamic SNAPSHOT enforcements for some but not
351              * all paths to an artifact may have lead to duplicate artifacts
352              * with different versions in the dependency graph
353              */
354             RuleSet extendedRuleSet =
355                 createExtendedRuleSet(topProject, ruleSet);
356 
357             /*
358              * Do the whole thing again, this time with additional enforcements
359              * that will avoid duplicates
360              */
361             topProject =
362                 parseProject(
363                     projectFile,
364                     new ProjectReference(
365                         null,
366                         null,
367                         null,
368                         null,
369                         null,
370                         null),
371                     new HashMap(),
372                     extendedRuleSet,
373                     strategy);
374         }
375 
376         if (strategy == ProjectRecursor.STRATEGY_AS_IS) {
377             Document projectDocument = loadDocument(projectFile);
378             ruleSet = parseRuleSet(projectDocument);
379             isAssembly = parseIsAssembly(projectDocument);
380             if (isAssembly == null) {
381                 //guess the right answer from the project name or the file name
382                 if (topProject.getArtifactId().startsWith("vrp-assembly")
383                         || projectFile.endsWith("test.xml")) {
384                     isAssembly = Boolean.TRUE;
385                 } else {
386                     isAssembly = Boolean.FALSE;
387                 }
388             }
389         } // else keep old rule set and old assembly decision
390         topProject.setRuleSet(ruleSet);
391         topProject.setAssembly(isAssembly.booleanValue());
392         if (topProject.isAssembly()) {
393             info("The project is considered to be an assembly. "
394                  + "Indirect dependencies will be written to the file "
395                  + "when you save it.");
396         } else {
397             info("The project is considered to be a component. "
398                  + "Indirect dependencies will **NOT** be written to the file "
399                  + "when you save it.");
400         }
401 
402         return topProject;
403     }
404 
405     /***
406      * @param project The current top project to create the map for.
407      * @return A map that maps each artifactId to the versions presently chosen
408      *         for the artifact in the current top project.
409      */
410     private Map createPresentVersionsMap(final Project project) {
411         Map result = new HashMap();
412         Iterator iter = project.getAllDependencies().iterator();
413         while (iter.hasNext()) {
414             Project dependency = (Project) iter.next();
415             result.put(
416                 dependency.getGroupId() + "/" + dependency.getArtifactId(),
417                 dependency.getVersion());
418         }
419         return result;
420     }
421 
422     /***
423      * @param projectFile The Maven project object model file to parse.
424      * @param projectRef The properties of the project to parse.
425      * @param parsedProjects The map of projects already (being) parsed.
426      * @param ruleSet The set of rules that help to determine which version to
427      *                choose of which artifact.
428      * @param strategy One of the STRATEGY constants defined by this class.
429      *
430      * @return The top Maven project of the dependency tree of recursively
431      *         parsed Maven projects.
432      * @throws Exception if anything goes unexpectedly wrong
433      */
434     private Project parseProject(
435             final String projectFile,
436             final ProjectReference projectRef,
437             final Map parsedProjects,
438             final RuleSet ruleSet,
439             final int strategy)
440             throws Exception {
441         /*
442          * Create Maven project and set basic attributes
443          */
444         Document xmlDocument = loadDocument(projectFile);
445         Project project =
446             parseProjectWithBasicAttributes(
447                 xmlDocument,
448                 projectFile,
449                 projectRef);
450         if (parsedProjects.isEmpty()) {
451             /*
452              * No other projects parsed yet, so this must be the root project
453              */
454             project.setRootProject(true);
455         }
456         /*
457          * Keep what's already being parsed in a map to avoid multiple
458          * parsings of the same file
459          */
460         parsedProjects.put(
461             getProjectHashKey(project.getArtifactId(), project.getVersion()),
462             project);
463 
464         if (strategy == STRATEGY_NON_RECURSIVELY) {
465             /*
466              * Return early and don't recurse over any kind of dependency
467              */
468             return project;
469         }
470         /*
471          * Parse dependencies and recurse
472          */
473         if (strategy == STRATEGY_AS_IS) {
474             /*
475              * For this strategy it must be avoided that indirect dependencies
476              * will be computed if none are present in the file,
477              * so initialize with empty list to suppress potential computation.
478              */
479             project.setIndirectDependencies(new ArrayList());
480         }
481         Iterator dependencyElementIter =
482                     XPath.newInstance("descendant::dependency")
483                          .selectNodes(xmlDocument).iterator();
484         while (dependencyElementIter.hasNext()) {
485             Element dependencyElement = (Element) dependencyElementIter.next();
486             String dependencyGroupId =
487                 parseElementText(dependencyElement, "groupId", "id");
488             String dependencyArtifactId =
489                 parseElementText(dependencyElement, "artifactId", "id");
490             String dependencyVersion =
491                 parseElementText(dependencyElement, "version");
492             String dependencyJar =
493                 parseElementText(dependencyElement, "jar");
494             String dependencyType =
495                 parseElementText(dependencyElement, "type");
496             String dependencyUrl =
497                 parseElementText(dependencyElement, "url");
498             boolean isTopLevelDependency =
499                 !hasPropertyTopWithValueFalse(dependencyElement);
500             parseDependencyPropertiesToPreserve(project, dependencyElement);
501 
502             boolean isRemoved =
503                 ruleSet.isRemoved(
504                     dependencyArtifactId,
505                     dependencyVersion);
506             if (strategy == STRATEGY_APPLY_RULES
507                     && (!isTopLevelDependency || isRemoved)) {
508                 /*
509                  * Ignore this dependency, it's a derived indirect dependency
510                  * or there is a removal rule that kicks it out of the game!
511                  */
512                 continue;
513             }
514             ProjectQualifierImpl literalQualifier = new ProjectQualifierImpl();
515             literalQualifier.setGroupId(dependencyGroupId);
516             literalQualifier.setArtifactId(dependencyArtifactId);
517             literalQualifier.setVersion(dependencyVersion);
518 
519             ProjectQualifierImpl recursiveQualifier =
520                 getNextToParseRecursively(
521                     dependencyGroupId,
522                     dependencyArtifactId,
523                     dependencyVersion,
524                     strategy,
525                     ruleSet,
526                     project.isRootProject());
527             /*
528              * Parse the literal dependency recursively if it is still valid
529              * or non-recursively if it is overruled by a different one
530              */
531             int dependencyStrategy = strategy;
532             if (strategy == STRATEGY_APPLY_RULES
533                     && !literalQualifier.equals(recursiveQualifier)) {
534                 dependencyStrategy = STRATEGY_NON_RECURSIVELY;
535             }
536             Project dependencyProject =
537                 parseDependency(
538                     parsedProjects,
539                     new ProjectReference(
540                         dependencyGroupId,
541                         dependencyArtifactId,
542                         dependencyVersion,
543                         dependencyJar,
544                         dependencyType,
545                         dependencyUrl),
546                     dependencyStrategy,
547                     ruleSet);
548             /*
549              * Finally, link the literal dependency with the parent project
550              */
551             if (strategy == STRATEGY_APPLY_RULES
552                     && !literalQualifier.equals(recursiveQualifier)) {
553                 project.addDeprecatedDependency(dependencyProject);
554             } else if (strategy == STRATEGY_AS_IS && !isTopLevelDependency) {
555                 project.addIndirectDependency(dependencyProject);
556             } else {
557                 project.addDependency(dependencyProject);
558             }
559             dependencyProject.getClients().add(project);
560             /*
561              * Do we need to parse another dependency additionally
562              * because the literal dependency was overruled?
563              */
564             if (strategy == STRATEGY_APPLY_RULES
565                     && !literalQualifier.equals(recursiveQualifier)) {
566                 /*
567                  * Yes!
568                  */
569                 Project enforcedProject =
570                     parseDependency(
571                         parsedProjects,
572                         new ProjectReference(
573                             recursiveQualifier.getGroupId(),
574                             recursiveQualifier.getArtifactId(),
575                             recursiveQualifier.getVersion(),
576                             dependencyJar,
577                             dependencyType,
578                             dependencyUrl),
579                         STRATEGY_APPLY_RULES,
580                         ruleSet);
581                 linkProjectWithEnforcedDependency(
582                         project, enforcedProject, literalQualifier);
583             }
584         }
585         return project;
586     }
587 
588     /***
589      * @param project The dependee.
590      * @param dependency The project the dependee is computed to depend on.
591      * @param literalQualifier The qualifer of the dependency stated literally
592      *                         in the project's project.xml that caused the
593      *                         overriding dependency.
594      */
595     private void linkProjectWithEnforcedDependency(
596             final Project project,
597             final Project dependency,
598             final ProjectQualifier literalQualifier) {
599         project.addDependency(dependency);
600         dependency.getClients().add(project);
601         project.getDependencyCauses().put(
602                 dependency, literalQualifier);
603         dependency.getClientCauses().put(
604                 project, literalQualifier);
605     }
606 
607     /***
608      * Evaluates the rule set to determine which group id, artifact id and
609      * version to follow next in the recursion.
610      *
611      * @param groupId The literal group id to follow if not overruled.
612      * @param artifactId The literal artifact id to follow if not overruled.
613      * @param version The literal version to follow if not overruled.
614      * @param strategy The current recursion strategy. Must be one of
615      *                 {@link #STRATEGY_APPLY_RULES},
616      *                 {@link #STRATEGY_AS_IS},
617      *                 {@link #STRATEGY_NON_RECURSIVELY}.
618      * @param ruleSet The rule set of the root project to evaluate.
619      * @param dependeeIsRootProject <code>true</code> if the project that
620      *                              depends on the next dependency to parse is
621      *                              the root in the dependency graph.
622      * @return The (group id, artifact id, version) triple to follow.
623      */
624     private ProjectQualifierImpl getNextToParseRecursively(
625             final String groupId,
626             final String artifactId,
627             final String version,
628             final int strategy,
629             final RuleSet ruleSet,
630             final boolean dependeeIsRootProject) {
631         ProjectQualifierImpl result = new ProjectQualifierImpl();
632         result.setGroupId(groupId);
633         result.setArtifactId(artifactId);
634         result.setVersion(version);
635         if (strategy == STRATEGY_APPLY_RULES) {
636             ReplacementRule replacementRule =
637                 ruleSet.getReplacementRule(artifactId, version);
638             if (replacementRule != null) {
639                 /*
640                  * Apply replacement rule to change the
641                  * groupId, artifactId, and the version.
642                  */
643                 if (replacementRule.getGroupId() != null) {
644                     result.setGroupId(
645                         replacementRule.getGroupId());
646                 }
647                 if (replacementRule.getArtifactId() != null) {
648                     result.setArtifactId(
649                         replacementRule.getArtifactId());
650                 }
651                 if (replacementRule.getVersion() != null) {
652                     result.setVersion(replacementRule.getVersion());
653                 }
654             }
655 
656             /*
657              * Apply rules to determine the version to select
658              */
659 
660             /*
661              * Rule #0: Is there a retention?
662              */
663             if (ruleSet.isRetained(artifactId, version)) {
664                 /*
665                  * Yes, so return the retained result
666                  */
667                 return result;
668             }
669 
670             /*
671              * Rule #1: Is there an enforcement?
672              */
673             String enforcedVersion =
674                 ruleSet.getEnforcedVersion(result.getArtifactId());
675             if (enforcedVersion != null) {
676                 /*
677                  * Yes, so return the enforcement
678                  */
679                 result.setVersion(enforcedVersion);
680                 return result;
681             }
682 
683             /*
684              * Determine all versions to select one from
685              */
686             List selectableVersions =
687                 getVersionsForArtifact(
688                     result.getGroupId(),
689                     result.getArtifactId(),
690                     ruleSet.getDefaultRule());
691             if (!selectableVersions.contains(result.getVersion())) {
692                 /*
693                  * The version suggested by the caller must still be added
694                  */
695                 selectableVersions.add(result.getVersion());
696             }
697             Collections.sort(selectableVersions, versionComparator);
698 
699             if (!dependeeIsRootProject) {
700                 /*
701                  * Remove all versions less than the one suggested by the
702                  * caller, they are too low to be selected at any rate
703                  */
704                 int lastGoodIndex =
705                     selectableVersions.indexOf(result.getVersion());
706                 selectableVersions =
707                     selectableVersions.subList(0, lastGoodIndex + 1);
708             } // else, the root level must not dictate the minimum version!
709 
710             /*
711              * Rule #2: Select the SNAPSHOT or the highest released version
712              *          depending on the default rule,
713              *          reject deprecated versions as long as there is an
714              *          alternative.
715              */
716             String selectedVersion = (String) selectableVersions.get(0);
717             int index = 1;
718             while (selectableVersions.size() > index) {
719                 /*
720                  * There is at least one more alternative version to take
721                  * if the current selection is not exactly what we want
722                  */
723                 if (selectedVersion.endsWith("SNAPSHOT")
724                         && !ruleSet.getDefaultRule().equals(
725                                 RuleSet.DEFAULT_RULE_SNAPSHOT)) {
726                     /*
727                      * We don't want SNAPSHOTS if possible
728                      */
729                     selectedVersion = (String) selectableVersions.get(index);
730                 } else if (ruleSet.isDeprecated(
731                                 result.getArtifactId(),
732                                 selectedVersion)) {
733                     /*
734                      * We don't want deprecated versions if possible
735                      */
736                     selectedVersion = (String) selectableVersions.get(index);
737                 } else {
738                     /*
739                      * The selected version is already the 'best' choice
740                      */
741                     break;
742                 }
743                 index++;
744             }
745             result.setVersion(selectedVersion);
746         }
747         return result;
748     }
749 
750     /***
751      * @param groupId The group id of the artifact to find all possible versions
752      *                for.
753      * @param artifactId The artifact id of the artifact to find all possible
754      *                   versions for.
755      * @param defaultRule The default rule of the POM being updated.
756      * @return The list of versions of type {@link String}.
757      *         Is never <code>null</code>.
758      */
759     private List getVersionsForArtifact(
760             final String groupId,
761             final String artifactId,
762             final String defaultRule) {
763         List versions = new ArrayList();
764         if (!defaultRule.equals(RuleSet.DEFAULT_RULE_LATEST_RELEASE_NO_SCAN)
765                 && !defaultRule.equals(
766                         RuleSet.DEFAULT_RULE_PRESENT_RELEASE)) {
767             versions = scanRepositoriesForAllVersions(groupId, artifactId);
768         } else if (defaultRule.equals(RuleSet.DEFAULT_RULE_PRESENT_RELEASE)) {
769             String presentVersion = null;
770             if (presentVersions != null) {
771                 presentVersion =
772                     (String) presentVersions.get(groupId + "/" + artifactId);
773             }
774             if (presentVersion != null
775                     && presentVersion.indexOf("SNAPSHOT") == -1) {
776                 versions.add(presentVersion);
777             } else {
778                 versions = scanRepositoriesForAllVersions(groupId, artifactId);
779             }
780         }
781         return versions;
782     }
783 
784     /***
785      * @param groupId The group id of the artifact to find all possible versions
786      *                for.
787      * @param artifactId The artifact id of the artifact to find all possible
788      *                   versions for.
789      * @return The list of versions of type {@link String}.
790      *         Is never <code>null</code>.
791      */
792     private List scanRepositoriesForAllVersions(
793             final String groupId,
794             final String artifactId) {
795         List versions = new ArrayList();
796         /*
797          * Construct relative path of poms directory
798          */
799         String relativePath =
800             File.separator
801             + groupId
802             + File.separator
803             + "poms"
804             + File.separator;
805         /*
806          * Try repository paths one after another until one contains the group.
807          * Exclude repositories which must not be scanned for versions.
808          */
809 
810         for (Iterator iter = repositoryConfigs.iterator(); iter.hasNext();) {
811             RepositoryConfig config = (RepositoryConfig) iter.next();
812             if (config.isToBeScannedForVersions()) {
813                 String repositoryPath = config.getPath();
814                 String fullPath = repositoryPath + relativePath;
815                 File dir = new File(fullPath);
816                 if (dir.exists()) {
817                     // found the correct path
818                     versions =
819                         versionScanner.getAllVersions(
820                             dir.getAbsolutePath(),
821                             artifactId);
822                     break;
823                 }
824             }
825         }
826         return versions;
827     }
828 
829     /***
830      * Creates a rule set which contains all the rules of <code>ruleSet</code>
831      * plus further enforcement rules that allow unique choices where there are
832      * still artifact duplicates in the direct and indirect dependencies of the
833      * <code>topProject</code> after the first recursion pass.<br/>
834      * These additional rules will enforce the highest version in each of those
835      * artifact duplication lists when being applied in the second recursion
836      * pass.
837      *
838      * @param topProject The top project to parse.
839      * @param ruleSet The original rule set associated with the top project.
840      * @return The extended rule set.
841      */
842     private RuleSet createExtendedRuleSet(
843             final Project topProject,
844             final RuleSet ruleSet) {
845         RuleSet result = new RuleSet();
846         result.setDefaultRule(ruleSet.getDefaultRule());
847         /*
848          * - Get all direct and indirect dependencies
849          *   (ordered by artifact id and version),
850          *
851          * - group dependencies by artifact id
852          *
853          * - create an enforcement rule for each group having more than one
854          *   element. The first element in each such group is the highest
855          *   version which must be enforced in the upcoming second recursion
856          *   pass.
857          *
858          * Note, that for each such artifact group with more than one element
859          * the following is true:
860          *
861          * - All replacement rules that may apply have been applied in the first
862          *   recursion pass already and thus need not be considered here
863          *   explicitly in the choice of the version to enforce.
864          *
865          * - There is no enforcement rule that applies for such an artifact
866          *   group because otherwise the group would not have more than one
867          *   element.
868          *
869          * - All deprecation rules that may apply have been applied in the first
870          *   recursion pass already and thus need not be considered here
871          *   explicitly in the choice of the version to enforce.
872          */
873         List allDependencies = topProject.getAllDependencies();
874         String currentArtifactId = null;
875         List currentGroup = new ArrayList();
876         for (Iterator iter = allDependencies.iterator(); iter.hasNext();) {
877             Project project = (Project) iter.next();
878             if (currentArtifactId == null
879                     || currentArtifactId.equals(project.getArtifactId())) {
880                 /*
881                  * Add to the existing group (which is empty or non-empty)
882                  */
883                 currentArtifactId = project.getArtifactId();
884                 currentGroup.add(project);
885             } else {
886                 /*
887                  * Perform a change of groups. Create an enforcement rule for
888                  * the old group if it has more than one element.
889                  */
890                 if (currentGroup.size() > 1) {
891                     Project firstOfGroup = (Project) currentGroup.get(0);
892                     EnforcementRule rule = createEnforcementRule(firstOfGroup);
893                     result.add(rule);
894                 }
895                 currentArtifactId = project.getArtifactId();
896                 currentGroup = new ArrayList();
897                 currentGroup.add(project);
898             }
899         }
900         /*
901          * Handle the last group
902          */
903         if (currentGroup.size() > 1) {
904             Project firstOfGroup = (Project) currentGroup.get(0);
905             EnforcementRule rule = createEnforcementRule(firstOfGroup);
906             result.add(rule);
907         }
908         /*
909          * Merge the new rule set with the existing one to get the extended one.
910          */
911         result.add(ruleSet);
912         return result;
913     }
914 
915     /***
916      * Creates an enforcement rule for the project. Is used for the extended
917      * rule set that eliminates duplicates in the second pass of the recursion.
918      *
919      * @param project The project to create the rule for.
920      * @return The rule created.
921      */
922     private EnforcementRule createEnforcementRule(final Project project) {
923         EnforcementRule rule = new EnforcementRule();
924         rule.setGroupId(project.getGroupId());
925         rule.setArtifactId(project.getArtifactId());
926         rule.setVersion(project.getVersion());
927         return rule;
928     }
929 
930     /***
931      * Parses the 'isAssembly' attribute of the deputy element in the project's
932      * properties.
933      *
934      * @param xmlDocument The project object model document to parse.
935      * @return <code>true</code> if the project is flagged to be an assembly.
936      * @throws Exception if anything goes unexpectedly wrong
937      */
938     private Boolean parseIsAssembly(
939             final Document xmlDocument) throws Exception {
940         Boolean result = null;
941         String path = "project/properties/deputy";
942 
943         /*
944          * Parse the deputy element
945          */
946         Element deputyElement =
947             (Element) XPath.newInstance(path).selectSingleNode(xmlDocument);
948         if (deputyElement != null) {
949             Attribute attribute = deputyElement.getAttribute("isAssembly");
950             if (attribute != null) {
951                 String value = attribute.getValue();
952                 if ("true".equalsIgnoreCase(value)) {
953                     result = Boolean.TRUE;
954                 } else if ("false".equalsIgnoreCase(value)) {
955                     result = Boolean.FALSE;
956                 }
957             }
958         }
959         return result;
960     }
961 
962     /***
963      * Parses the rule set properties of the <code>projectFile</code>.
964      *
965      * @param xmlDocument The project object model document to parse.
966      * @return The rule set defined in the <code>projectFile</code> or a default
967      *         rule set.
968      * @throws Exception if anything goes unexpectedly wrong
969      */
970     private RuleSet parseRuleSet(
971             final Document xmlDocument) throws Exception {
972         RuleSet result = new RuleSet();
973         String rootPath = "project/properties/deputy/rules/";
974 
975         String path = rootPath + "default";
976         /*
977          * Parse the default rule
978          */
979         Element defaultRuleElement =
980             (Element) XPath.newInstance(path).selectSingleNode(xmlDocument);
981         if (defaultRuleElement != null) {
982             String value = defaultRuleElement.getAttribute("value").getValue();
983             if (RuleSet.DEFAULT_RULE_SNAPSHOT.equals(value)) {
984                 result.setDefaultRule(RuleSet.DEFAULT_RULE_SNAPSHOT);
985             } else if (RuleSet.DEFAULT_RULE_LATEST_RELEASE_NO_SCAN.equals(
986                             value)) {
987                 result.setDefaultRule(
988                         RuleSet.DEFAULT_RULE_LATEST_RELEASE_NO_SCAN);
989             } else if (RuleSet.DEFAULT_RULE_PRESENT_RELEASE.equals(value)) {
990                 result.setDefaultRule(RuleSet.DEFAULT_RULE_PRESENT_RELEASE);
991             }
992         }
993 
994         /*
995          * Parse the enforcement rules
996          */
997         path = rootPath + "enforcements/enforcement";
998         Iterator elementIter =
999             XPath.newInstance(path).selectNodes(xmlDocument).iterator();
1000         while (elementIter.hasNext()) {
1001             Element element = (Element) elementIter.next();
1002             String groupId = parseElementText(element, "groupId");
1003             String artifactId = parseElementText(element, "artifactId");
1004             String version = parseElementText(element, "version");
1005             String derivedAsString = parseElementText(element, "derived");
1006 
1007             EnforcementRule rule = new EnforcementRule();
1008             rule.setGroupId(groupId);
1009             rule.setArtifactId(artifactId);
1010             rule.setVersion(version);
1011             if (derivedAsString != null) {
1012                 boolean derived = derivedAsString.equalsIgnoreCase("true");
1013                 rule.setDerived(derived);
1014             }
1015             result.add(rule);
1016         }
1017 
1018         /*
1019          * Parse the deprecation rules
1020          */
1021         path = rootPath + "deprecations/deprecation";
1022         elementIter =
1023             XPath.newInstance(path).selectNodes(xmlDocument).iterator();
1024         while (elementIter.hasNext()) {
1025             Element element = (Element) elementIter.next();
1026             String groupId = parseElementText(element, "groupId");
1027             String artifactId = parseElementText(element, "artifactId");
1028             String version = parseElementText(element, "version");
1029 
1030             DeprecationRule rule = new DeprecationRule();
1031             rule.setGroupId(groupId);
1032             rule.setArtifactId(artifactId);
1033             rule.setVersion(version);
1034             result.add(rule);
1035         }
1036 
1037         /*
1038          * Parse the replacement rules
1039          */
1040         path = rootPath + "replacements/replacement";
1041         elementIter =
1042             XPath.newInstance(path).selectNodes(xmlDocument).iterator();
1043         while (elementIter.hasNext()) {
1044             Element element = (Element) elementIter.next();
1045             Element displacementElement =
1046                 (Element) XPath.newInstance("displacement").selectSingleNode(
1047                     element);
1048             String displacedGroupId =
1049                 parseElementText(displacementElement, "groupId");
1050             String displacedArtifactId =
1051                 parseElementText(displacementElement, "artifactId");
1052             String displacedVersion =
1053                 parseElementText(displacementElement, "version");
1054 
1055             Element placementElement =
1056                 (Element) XPath.newInstance("placement").selectSingleNode(
1057                     element);
1058             String placedGroupId =
1059                 parseElementText(placementElement, "groupId");
1060             String placedArtifactId =
1061                 parseElementText(placementElement, "artifactId");
1062             String placedVersion =
1063                 parseElementText(placementElement, "version");
1064 
1065             ReplacementRule rule = new ReplacementRule();
1066             rule.setGroupId(placedGroupId);
1067             rule.setArtifactId(placedArtifactId);
1068             rule.setVersion(placedVersion);
1069 
1070             rule.setDisplacedGroupId(displacedGroupId);
1071             rule.setDisplacedArtifactId(displacedArtifactId);
1072             rule.setDisplacedVersion(displacedVersion);
1073 
1074             result.add(rule);
1075         }
1076 
1077         /*
1078          * Parse the removal rules
1079          */
1080         path = rootPath + "removals/removal";
1081         elementIter =
1082             XPath.newInstance(path).selectNodes(xmlDocument).iterator();
1083         while (elementIter.hasNext()) {
1084             Element element = (Element) elementIter.next();
1085             String groupId = parseElementText(element, "groupId");
1086             String artifactId = parseElementText(element, "artifactId");
1087             String version = parseElementText(element, "version");
1088 
1089             RemovalRule rule = new RemovalRule();
1090             rule.setGroupId(groupId);
1091             rule.setArtifactId(artifactId);
1092             rule.setVersion(version);
1093             result.add(rule);
1094         }
1095 
1096         /*
1097          * Parse the retention rules
1098          */
1099         path = rootPath + "retentions/retention";
1100         elementIter =
1101             XPath.newInstance(path).selectNodes(xmlDocument).iterator();
1102         while (elementIter.hasNext()) {
1103             Element element = (Element) elementIter.next();
1104             String groupId = parseElementText(element, "groupId");
1105             String artifactId = parseElementText(element, "artifactId");
1106             String version = parseElementText(element, "version");
1107 
1108             RetentionRule rule = new RetentionRule();
1109             rule.setGroupId(groupId);
1110             rule.setArtifactId(artifactId);
1111             rule.setVersion(version);
1112             result.add(rule);
1113         }
1114 
1115         return result;
1116     }
1117 
1118     /***
1119      * @param project The project defining the dependency. The parsed properties
1120      *                will be stored in the project for later reconstruction.
1121      * @param dependencyElement The dependency element to parse the child
1122      *                          properties from
1123      * @throws Exception if anything goes unexpectedly wrong
1124      */
1125     private void parseDependencyPropertiesToPreserve(
1126             final Project project,
1127             final Element dependencyElement)
1128             throws Exception {
1129         Iterator propertyElementIter =
1130                     XPath.newInstance("properties/child::*")
1131                          .selectNodes(dependencyElement).iterator();
1132         List result = new ArrayList();
1133         while (propertyElementIter.hasNext()) {
1134             Element propertyElement = (Element) propertyElementIter.next();
1135             if (!propertyElement.getName().equals("top")) {
1136                 result.add(propertyElement);
1137             }
1138         }
1139         if (result.size() > 0) {
1140             String artifactId =
1141                 parseElementText(dependencyElement, "artifactId", "id");
1142             project.addDependencyPropertyList(artifactId, result);
1143         }
1144     }
1145 
1146     /***
1147      * Parses all the needed basic elements describing a project.
1148      *
1149      * @param xmlDocument The document to parse.
1150      * @param projectFile The file path and name of the document to parse.
1151      * @param projectRef The properties of the project to parse.
1152      * @return The project created from the parsed information.
1153      * @throws Exception if anything goes unexpectedly wrong
1154      */
1155     private Project parseProjectWithBasicAttributes(
1156             final Document xmlDocument,
1157             final String projectFile,
1158             final ProjectReference projectRef)
1159             throws Exception {
1160         Project project = new Project();
1161 
1162         project.setGroupId(
1163             parseElementText(xmlDocument, "project/groupId", "project/id"));
1164         if (projectRef.getGroupId() != null
1165                 && !projectRef.getGroupId().equals(project.getGroupId())) {
1166             //group id is inconsistent with directory or
1167             //missing in the project file
1168             project.setGroupId(projectRef.getGroupId());
1169         }
1170 
1171         project.setArtifactId(
1172             parseElementText(xmlDocument, "project/artifactId", "project/id"));
1173 
1174         if (project.getGroupId() == null
1175                 && project.getArtifactId() != null
1176                 && (project.getArtifactId().startsWith("vrp-")
1177                         || project.getArtifactId().startsWith("test-vrp-"))) {
1178             project.setGroupId("vrp");
1179             info(
1180                 "Automatically set missing group id of "
1181                     + project.getArtifactId()
1182                     + " to '"
1183                     + project.getGroupId()
1184                     + "'");
1185         }
1186 
1187         project.setName(
1188             parseElementText(xmlDocument, "project/name", "project/name"));
1189 
1190         project.setVersion(
1191             parseElementText(xmlDocument, "project/currentVersion"));
1192         if (projectRef.getVersion() != null
1193                 && !projectRef.getVersion().equals(project.getVersion())) {
1194             //version part of file name is inconsistent
1195             //with value of field 'currentVersion',
1196             //this may occur for SNAPSHOT files
1197             project.setVersion(projectRef.getVersion());
1198         }
1199 
1200         project.setJar(
1201             parseElementText(xmlDocument, "project/jar"));
1202         if (project.getJar() == null) {
1203             //jar is not defined in the project file,
1204             //use the jar possibly learnt from a dependency
1205             project.setJar(projectRef.getJar());
1206         }
1207 
1208         project.setType(
1209             parseElementText(xmlDocument, "project/type"));
1210         if (project.getType() == null) {
1211             //type is not defined in the project file,
1212             //use the type possibly learnt from a dependency
1213             project.setType(projectRef.getType());
1214         }
1215 
1216         project.setUrl(parseElementText(xmlDocument, "project/url"));
1217         if (project.getUrl() == null) {
1218             //url is not defined in the project file,
1219             //use the url possibly learnt from a dependency
1220             project.setUrl(projectRef.getUrl());
1221         }
1222 
1223         /*
1224          * Do a sanity check and report missing attributes
1225          */
1226         if (project.getGroupId() == null) {
1227              error("No group id defined in file " + projectFile);
1228         }
1229         if (project.getArtifactId() == null) {
1230              error("No artifact id defined in file " + projectFile);
1231         }
1232         if (project.getVersion() == null) {
1233             error("No current version defined in file " + projectFile);
1234         }
1235 
1236         return project;
1237     }
1238 
1239     /***
1240      * Parses a dependency and creates a project representing it.
1241      *
1242      * @param parsedProjects The map of projects parsed already. Keys are
1243      *                       created by
1244      *                       {@link #getProjectHashKey(String, String)}.
1245      * @param dependencyRef The properties of the dependency to parse.
1246      * @param dependencyStrategy Must be one of
1247      *                           {@link #STRATEGY_APPLY_RULES},
1248      *                           {@link #STRATEGY_AS_IS},
1249      *                           {@link #STRATEGY_NON_RECURSIVELY}.
1250      * @param ruleSet The rule set of the root project.
1251      * @return The dependency project parsed.
1252      * @throws Exception if anything goes unexpectedly wrong
1253      */
1254     private Project parseDependency(
1255             final Map parsedProjects,
1256             final ProjectReference dependencyRef,
1257             final int dependencyStrategy,
1258             final RuleSet ruleSet) throws Exception {
1259         /*
1260          * Check if the dependency project is already (being) parsed
1261          */
1262         Project dependencyProject =
1263             (Project) parsedProjects.get(
1264                     getProjectHashKey(
1265                         dependencyRef.getArtifactId(),
1266                         dependencyRef.getVersion()));
1267         if (dependencyProject == null) {
1268             /*
1269              * Not yet parsed, so start it now
1270              */
1271             String dependencyProjectFile =
1272                 getAbsolutePathAndFileNameForPom(
1273                     dependencyRef.getGroupId(),
1274                     dependencyRef.getArtifactId(),
1275                     dependencyRef.getVersion());
1276             if (dependencyProjectFile != null) {
1277                 dependencyProject =
1278                     parseProject(
1279                         dependencyProjectFile,
1280                         dependencyRef,
1281                         parsedProjects,
1282                         ruleSet,
1283                         dependencyStrategy);
1284             } else {
1285                 /*
1286                  *  There is no POM for the dependency.
1287                  *  Inform and assume its a leaf in the tree.
1288                  */
1289                 info(
1290                     "Did not find POM file for "
1291                     + "groupId = '" + dependencyRef.getGroupId() + "' "
1292                     + "artifactId = '" + dependencyRef.getArtifactId() + "' "
1293                     + "version = '" + dependencyRef.getVersion() + "'. "
1294                     + "Assuming the artifact has no dependencies.");
1295                 dependencyProject = new Project();
1296                 dependencyProject.setGroupId(dependencyRef.getGroupId());
1297                 dependencyProject.setArtifactId(dependencyRef.getArtifactId());
1298                 dependencyProject.setVersion(dependencyRef.getVersion());
1299                 dependencyProject.setUrl(dependencyRef.getUrl());
1300                 dependencyProject.setJar(dependencyRef.getJar());
1301                 parsedProjects.put(
1302                     getProjectHashKey(
1303                         dependencyRef.getArtifactId(),
1304                         dependencyRef.getVersion()),
1305                     dependencyProject);
1306             }
1307         }
1308         if (dependencyProject.getUrl() == null
1309                 && dependencyRef.getUrl() != null) {
1310             /*
1311              * The dependency project may have been found in the parsedProjects
1312              * map because it was parsed earlier already. The URL, however,
1313              * may still be null, so set it now (again) to the (newly) learnt
1314              * value learnt from the current dependency reference.
1315              */
1316             dependencyProject.setUrl(dependencyRef.getUrl());
1317         }
1318         return dependencyProject;
1319     }
1320 
1321     /***
1322      * @param dependencyElement The dependency element to check the property
1323      *                          for.
1324      * @return <code>true</code> if the dependency owns a property 'top'
1325      *         indicating that it is a direct (or: top level) dependency of the
1326      *         defining project.
1327      * @throws Exception if anything goes unexpectedly wrong
1328      */
1329     private boolean hasPropertyTopWithValueFalse(
1330             final Element dependencyElement) throws Exception {
1331         boolean result = false;
1332         Element topElement =
1333             (Element) XPath.newInstance("properties/top")
1334                  .selectSingleNode(dependencyElement);
1335         if (topElement != null
1336                 && "false".equalsIgnoreCase(topElement.getText())) {
1337             result = true;
1338         }
1339         return result;
1340     }
1341 
1342     /***
1343      * Tries to parse the element named <code>elementName.</code>. If such an
1344      * element does not exist, it tries to parse an element with name
1345      * <code>fallbackElementName</code>.
1346      *
1347      * @param xmlNode The XML node to that contains the element to parse.
1348      * @param elementName The name of the element to try first.
1349      * @param fallbackElementName The name of the element to try as a fallback.
1350      * @return The contents of the parsed element or of the fallback element or
1351      *         <code>null</code>.
1352      * @throws Exception if anything goes unexpectedly wrong
1353      */
1354     private String parseElementText(
1355             final Object xmlNode,
1356             final String elementName,
1357             final String fallbackElementName)
1358             throws Exception {
1359         String result = parseElementText(xmlNode, elementName);
1360         if (result == null) {
1361             result = parseElementText(xmlNode, fallbackElementName);
1362         }
1363         return result;
1364     }
1365 
1366     /***
1367      * Tries to parse the element named <code>elementName.</code>.
1368      *
1369      * @param xmlNode The XML node to that contains the element to parse.
1370      * @param elementName The name of the element to parse.
1371      * @return The contents of the element to parse or <code>null</code>.
1372      * @throws Exception if anything goes unexpectedly wrong
1373      */
1374     private String parseElementText(
1375             final Object xmlNode,
1376             final String elementName)
1377             throws Exception {
1378         Element element =
1379             (Element) XPath.newInstance(elementName).selectSingleNode(xmlNode);
1380         String result = null;
1381         if (element != null) {
1382             result = element.getText();
1383         }
1384         return result;
1385     }
1386 
1387     /***
1388      * @param artifactId The artifact id of the project to create a hash key
1389      *                   for.
1390      * @param version The version of the project to create a hash key for.
1391      * @return The key of the project to be uses for hashing.
1392      */
1393     private String getProjectHashKey(
1394             final String artifactId,
1395             final String version) {
1396         return artifactId + "-" + version;
1397     }
1398 
1399     /***
1400      * Constructs a valid absolute path and file name of the POM specified by
1401      * the parameters passed in and the known repository paths.
1402      *
1403      * @param groupId The group id of the POM.
1404      * @param artifactId The artifact id of the POM.
1405      * @param version The version of the POM.
1406      * @return The absolute path and name of the POM file or null if it does not
1407      *         exist under any of the known repository paths.
1408      */
1409     private String getAbsolutePathAndFileNameForPom(
1410             final String groupId,
1411             final String artifactId,
1412             final String version) {
1413         /*
1414          * Try to find the POM in the virtual repository
1415          */
1416         String key =
1417             createVirtualRepositoryPomKey(groupId, artifactId, version);
1418         String result = (String) virtualRepositoryPoms.get(key);
1419         if (result == null) {
1420             /*
1421              * It is not in the virtual repository,
1422              * so try the real ones
1423              */
1424             String relativePath =
1425                 getRelativePathAndFileNameForPom(groupId, artifactId, version);
1426             /*
1427              * Try repository paths one after another until one yields the file
1428              */
1429             for (Iterator iter = repositoryConfigs.iterator();
1430                     iter.hasNext();) {
1431                 RepositoryConfig config = (RepositoryConfig) iter.next();
1432                 String repositoryPath = config.getPath();
1433                 String candidate = repositoryPath + relativePath;
1434                 if ((new File(candidate)).exists()) {
1435                     // found the correct path
1436                     result = candidate;
1437                     break;
1438                 }
1439             }
1440         }
1441         return result;
1442     }
1443 
1444     /***
1445      * Constructs a relative path and file name of the POM specified by
1446      * the parameters passed.
1447      *
1448      * @param groupId The group id of the POM.
1449      * @param artifactId The artifact id of the POM.
1450      * @param version The version of the POM.
1451      * @return The relative path and name of the POM file.
1452      */
1453     private String getRelativePathAndFileNameForPom(
1454             final String groupId,
1455             final String artifactId,
1456             final String version) {
1457         String relativePath =
1458             File.separator
1459             + groupId
1460             + File.separator
1461             + "poms"
1462             + File.separator
1463             + artifactId
1464             + "-"
1465             + version
1466             + ".pom";
1467         return relativePath;
1468     }
1469 
1470     /***
1471      * @param projectFile The file name and path of the XML document to load.
1472      * @return The document loaded.
1473      * @throws Exception if anything goes unexpectedly wrong
1474      */
1475     private Document loadDocument(final String projectFile) throws Exception {
1476         Document result = null;
1477         if (currentTopDocument != null
1478                 && projectFile.equals(currentTopProjectFile)) {
1479             result = currentTopDocument;
1480         } else {
1481             SAXBuilder builder = new SAXBuilder();
1482             result = builder.build(projectFile);
1483         }
1484         return result;
1485     }
1486 
1487     /***
1488      * Loads the list of POMs which are not in a true repository but which are
1489      * scattered around in the file system and puts them into the map
1490      * {@link #virtualRepositoryPoms}.
1491      */
1492     private void loadVirtualRepository() {
1493         try {
1494             final String path =
1495                 System.getProperty("user.home")
1496                 + File.separator
1497                 + "deputy-virtual-repository.xml";
1498             if ((new File(path)).exists()) {
1499                 Document virtualRepositoryDocument = loadDocument(path);
1500                 virtualRepositoryPoms =
1501                     createVirtualRepositoryPomsMap(virtualRepositoryDocument);
1502             } else {
1503                 warn("Did not find the optional file " + path);
1504                 warn("So POMs of dependencies will only be read "
1505                         + "from the configured repositories.");
1506             }
1507         } catch (Exception e) {
1508             error("Failed to read the virtual repository: " + e.getMessage());
1509         }
1510     }
1511 
1512     /***
1513      * Unloads, i. e. clears the the map {@link #virtualRepositoryPoms},
1514      * so that the virtual repository will not be used in effect.
1515      */
1516     private void unloadVirtualRepository() {
1517         virtualRepositoryPoms = new HashMap();
1518     }
1519 
1520     /***
1521      * @param document The document loaded from /deputy-virtual-repository.xml
1522      * @return The map mapping a POM key to the absolute path of the POM.
1523      * @throws Exception if anything goes unexpectedly wrong
1524      */
1525     private Map createVirtualRepositoryPomsMap(
1526             final Document document)
1527             throws Exception {
1528         Map result = new HashMap();
1529         Iterator pomElementIter =
1530             XPath.newInstance("poms/pom")
1531                  .selectNodes(document).iterator();
1532         while (pomElementIter.hasNext()) {
1533             Element pomElement = (Element) pomElementIter.next();
1534             String path = pomElement.getAttributeValue("path");
1535             String key = createVirtualRepositoryPomKey(path);
1536             result.put(key, path);
1537             info("The virtual repository contains the file " + path);
1538             info("It overrides the repository file " + key + ".");
1539         }
1540         return result;
1541     }
1542 
1543     /***
1544      * @param path The absolute path of a POM file anywhere in the file system.
1545      * @return They key to use for the hash map {@link #virtualRepositoryPoms}.
1546      * @throws Exception if anything goes unexpectedly wrong
1547      */
1548     private String createVirtualRepositoryPomKey(
1549             final String path) throws Exception {
1550         Document xmlDocument = loadDocument(path);
1551         String groupId =
1552             parseElementText(xmlDocument, "project/groupId", "project/id");
1553         String artifactId =
1554             parseElementText(xmlDocument, "project/artifactId", "project/id");
1555 
1556         if (groupId == null
1557                 && artifactId != null
1558                 && (artifactId.startsWith("vrp-")
1559                         || artifactId.startsWith("test-vrp-"))) {
1560             groupId = "vrp";
1561             info(
1562                 "Automatically set missing group id of "
1563                     + path
1564                     + " to '"
1565                     + groupId
1566                     + "'");
1567         }
1568         String version =
1569             parseElementText(xmlDocument, "project/currentVersion");
1570         String result =
1571             createVirtualRepositoryPomKey(groupId, artifactId, version);
1572         return result;
1573     }
1574 
1575     /***
1576      * @param groupId The group id of the pom.
1577      * @param artifactId The artifact id of the pom.
1578      * @param version The version of the pom.
1579      * @return They key to use for the hash map {@link #virtualRepositoryPoms}.
1580      */
1581     private String createVirtualRepositoryPomKey(
1582             final String groupId,
1583             final String artifactId,
1584             final String version) {
1585         String result =
1586             getRelativePathAndFileNameForPom(groupId, artifactId, version);
1587         return result;
1588     }
1589 
1590     /***
1591      * @param message The info message to log.
1592      */
1593     private void info(final String message) {
1594         log(Log.SEVERITY_INFO, message);
1595     }
1596 
1597     /***
1598      * @param message The warning message to log.
1599      */
1600     private void warn(final String message) {
1601         log(Log.SEVERITY_WARNING, message);
1602     }
1603 
1604     /***
1605      * @param message The error message to log.
1606      */
1607     private void error(final String message) {
1608         log(Log.SEVERITY_ERROR, message);
1609     }
1610 
1611     /***
1612      * @param severity The severity of the message. Must be one of
1613      *                 {@link Log#SEVERITY_INFO},
1614      *                 {@link Log#SEVERITY_WARNING},
1615      *                 {@link Log#SEVERITY_ERROR}.
1616      * @param message The message to log.
1617      */
1618     private void log(final String severity, final String message) {
1619         if (severity == Log.SEVERITY_ERROR) {
1620             System.err.println(message);
1621         } else {
1622             System.out.println(message);
1623         }
1624         if (userLog != null && userLogEnabled) {
1625             userLog.log(severity, message);
1626         }
1627     }
1628 }