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
326 userLogEnabled = true;
327 } else {
328
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
348 userLogEnabled = true;
349
350
351
352
353
354 RuleSet extendedRuleSet =
355 createExtendedRuleSet(topProject, ruleSet);
356
357
358
359
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
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 }
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
443
444 Document xmlDocument = loadDocument(projectFile);
445 Project project =
446 parseProjectWithBasicAttributes(
447 xmlDocument,
448 projectFile,
449 projectRef);
450 if (parsedProjects.isEmpty()) {
451
452
453
454 project.setRootProject(true);
455 }
456
457
458
459
460 parsedProjects.put(
461 getProjectHashKey(project.getArtifactId(), project.getVersion()),
462 project);
463
464 if (strategy == STRATEGY_NON_RECURSIVELY) {
465
466
467
468 return project;
469 }
470
471
472
473 if (strategy == STRATEGY_AS_IS) {
474
475
476
477
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
510
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
529
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
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
562
563
564 if (strategy == STRATEGY_APPLY_RULES
565 && !literalQualifier.equals(recursiveQualifier)) {
566
567
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
641
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
658
659
660
661
662
663 if (ruleSet.isRetained(artifactId, version)) {
664
665
666
667 return result;
668 }
669
670
671
672
673 String enforcedVersion =
674 ruleSet.getEnforcedVersion(result.getArtifactId());
675 if (enforcedVersion != null) {
676
677
678
679 result.setVersion(enforcedVersion);
680 return result;
681 }
682
683
684
685
686 List selectableVersions =
687 getVersionsForArtifact(
688 result.getGroupId(),
689 result.getArtifactId(),
690 ruleSet.getDefaultRule());
691 if (!selectableVersions.contains(result.getVersion())) {
692
693
694
695 selectableVersions.add(result.getVersion());
696 }
697 Collections.sort(selectableVersions, versionComparator);
698
699 if (!dependeeIsRootProject) {
700
701
702
703
704 int lastGoodIndex =
705 selectableVersions.indexOf(result.getVersion());
706 selectableVersions =
707 selectableVersions.subList(0, lastGoodIndex + 1);
708 }
709
710
711
712
713
714
715
716 String selectedVersion = (String) selectableVersions.get(0);
717 int index = 1;
718 while (selectableVersions.size() > index) {
719
720
721
722
723 if (selectedVersion.endsWith("SNAPSHOT")
724 && !ruleSet.getDefaultRule().equals(
725 RuleSet.DEFAULT_RULE_SNAPSHOT)) {
726
727
728
729 selectedVersion = (String) selectableVersions.get(index);
730 } else if (ruleSet.isDeprecated(
731 result.getArtifactId(),
732 selectedVersion)) {
733
734
735
736 selectedVersion = (String) selectableVersions.get(index);
737 } else {
738
739
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
798
799 String relativePath =
800 File.separator
801 + groupId
802 + File.separator
803 + "poms"
804 + File.separator;
805
806
807
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
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
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
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
882
883 currentArtifactId = project.getArtifactId();
884 currentGroup.add(project);
885 } else {
886
887
888
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
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
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
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
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
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
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
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
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
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
1167
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
1195
1196
1197 project.setVersion(projectRef.getVersion());
1198 }
1199
1200 project.setJar(
1201 parseElementText(xmlDocument, "project/jar"));
1202 if (project.getJar() == null) {
1203
1204
1205 project.setJar(projectRef.getJar());
1206 }
1207
1208 project.setType(
1209 parseElementText(xmlDocument, "project/type"));
1210 if (project.getType() == null) {
1211
1212
1213 project.setType(projectRef.getType());
1214 }
1215
1216 project.setUrl(parseElementText(xmlDocument, "project/url"));
1217 if (project.getUrl() == null) {
1218
1219
1220 project.setUrl(projectRef.getUrl());
1221 }
1222
1223
1224
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
1261
1262 Project dependencyProject =
1263 (Project) parsedProjects.get(
1264 getProjectHashKey(
1265 dependencyRef.getArtifactId(),
1266 dependencyRef.getVersion()));
1267 if (dependencyProject == null) {
1268
1269
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
1287
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
1312
1313
1314
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
1415
1416 String key =
1417 createVirtualRepositoryPomKey(groupId, artifactId, version);
1418 String result = (String) virtualRepositoryPoms.get(key);
1419 if (result == null) {
1420
1421
1422
1423
1424 String relativePath =
1425 getRelativePathAndFileNameForPom(groupId, artifactId, version);
1426
1427
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
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 }