1 package de.matthias_burbach.deputy.core;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.FileOutputStream;
7 import java.io.InputStream;
8 import java.util.ArrayList;
9 import java.util.Date;
10 import java.util.Iterator;
11 import java.util.List;
12 import java.util.Properties;
13 import java.util.StringTokenizer;
14
15 import de.matthias_burbach.deputy.core.project.Project;
16 import de.matthias_burbach.deputy.core.project.ProjectChangeListener;
17 import de.matthias_burbach.deputy.core.project.ProjectComparator;
18 import de.matthias_burbach.deputy.core.repository.RepositoryConfig;
19 import de.matthias_burbach.deputy.core.rule.EnforcementRule;
20 import de.matthias_burbach.deputy.core.rule.Rule;
21 import de.matthias_burbach.deputy.core.rule.RuleSetChangeListener;
22 import de.matthias_burbach.deputy.core.util.LineBufferLog;
23 import de.matthias_burbach.deputy.core.util.Log;
24 import de.matthias_burbach.deputy.core.util.P4;
25 import de.matthias_burbach.deputy.core.util.SimpleLog;
26
27 /***
28 * Is the main class of the core application.
29 *
30 * @author Matthias Burbach
31 */
32 public class Deputy implements RuleSetChangeListener, ProjectChangeListener {
33 /***
34 * The export format 'GraphML'.
35 */
36 public static final int FORMAT_GRAPHML = 0;
37
38 /***
39 * The export format 'Deputy XML'.
40 */
41 public static final int FORMAT_DEPUTYXML = 1;
42
43 /***
44 * The path and name of the application properties file.
45 */
46 private static final String APP_PROPERTIES_FILE =
47 "deputy-application.properties";
48
49 /***
50 * The path and name of the user properties file.
51 */
52 private static final String USER_PROPERTIES_FILE =
53 System.getProperty("user.home")
54 + File.separator
55 + "deputy.properties";
56
57 /***
58 * The property key for the repository configs.
59 */
60 private static final String KEY_REPOSITORY_CONFIGS =
61 "repositoryConfigs";
62
63 /***
64 * The property key of the Maven project file the user opened the last time.
65 */
66 private static final String KEY_PROJECT_FILE =
67 "projectFile";
68
69 /***
70 * The property key of the Maven project file the user imported
71 * as enforcement rules the last time.
72 */
73 private static final String KEY_IMPORT_FILE =
74 "importFile";
75
76 /***
77 * The property key of the dependency graph file the user exported the last
78 * time.
79 */
80 private static final String KEY_DEPENDENCY_GRAPH_FILE =
81 "dependencyGraphFile";
82
83 /***
84 * The user properties plus the application properties as defaults.
85 */
86 private Properties properties;
87
88 /***
89 * The top level Maven project currently being opened.
90 */
91 private Project rootProject;
92
93 /***
94 * The registered change listeners to be notified.
95 */
96 private List changeListeners = new ArrayList();
97
98 /***
99 * The log.
100 */
101 private Log log;
102
103 /***
104 * Whether the current state of the currently opened top project is saved on
105 * disk or not.
106 */
107 private boolean isSaved = true;
108
109 /***
110 * Flags that the last change action is unknown.
111 */
112 public static final int CHANGE_ACTION_UNKNOWN = 0;
113
114 /***
115 * Flags that the last change action was 'Apply Rules'.
116 */
117 public static final int CHANGE_ACTION_APPLY_RULES = 1;
118
119 /***
120 * Flags that the last change action was 'Add Dependency' or
121 * 'Remove Dependency'.
122 */
123 public static final int CHANGE_ACTION_CHANGE_DEPENDENCIES = 2;
124
125 /***
126 * Flags that the last change action was 'Change Default Rule',
127 * 'Add Enforcement Rule', 'Add Deprecation Rule', 'Add Replacement Rule'
128 * or 'Remove Rule'.
129 */
130 public static final int CHANGE_ACTION_CHANGE_RULES = 3;
131
132 /***
133 * The last change action the user executed on the currently opened project.
134 */
135 private int lastChangeAction = CHANGE_ACTION_UNKNOWN;
136
137 /***
138 * Constructs and starts the application.
139 *
140 * @param log The log for messages.
141 * @throws Exception if anything goes unexpectedly wrong
142 */
143 public Deputy(final Log log) throws Exception {
144 this.log = log;
145 startApplication();
146 }
147
148 /***
149 * Initializes the application on start up.
150 */
151 private void startApplication() {
152
153
154
155 Properties applicationProperties = new Properties();
156
157
158
159
160 try {
161 InputStream in =
162 getClass().getResourceAsStream(APP_PROPERTIES_FILE);
163 applicationProperties.load(in);
164 in.close();
165 } catch (Exception e) {
166 e.printStackTrace();
167 }
168
169
170
171
172 properties = new Properties(applicationProperties);
173
174
175
176
177 try {
178 log.log(Log.SEVERITY_INFO, "About to load " + USER_PROPERTIES_FILE);
179 FileInputStream in = new FileInputStream(USER_PROPERTIES_FILE);
180 properties.load(in);
181 in.close();
182 } catch (Exception e) {
183 e.printStackTrace();
184 }
185 }
186
187 /***
188 * Cleans up the application on exit.
189 */
190 public void exitApplication() {
191 FileOutputStream out;
192 try {
193 out = new FileOutputStream(USER_PROPERTIES_FILE);
194 properties.store(out, "---user properties---");
195 out.close();
196 } catch (Exception e) {
197 e.printStackTrace();
198 }
199 System.exit(0);
200 }
201
202 /***
203 * @return The Deputy application version.
204 */
205 public String getVersion() {
206 return properties.getProperty("version");
207 }
208
209 /***
210 * @return The absolute file path and name of the project that is currently
211 * loaded into Deputy or which is to be loaded next into Deputy.
212 */
213 public String getProjectFile() {
214 return properties.getProperty(KEY_PROJECT_FILE);
215 }
216
217 /***
218 * @param projectFile The absolute file path and name of the project that is
219 * currently loaded into Deputy or which is to be loaded
220 * next into Deputy.
221 */
222 public void setProjectFile(final String projectFile) {
223 properties.setProperty(KEY_PROJECT_FILE, projectFile);
224 fireProjectHasChanged();
225 }
226
227 /***
228 * @return The absolute file path and name of the project that was last
229 * imported from.
230 */
231 public String getImportFile() {
232 return properties.getProperty(KEY_IMPORT_FILE);
233 }
234
235 /***
236 * @param importProjectFile The absolute file path and name of the project
237 * that was last imported from.
238 */
239 public void setImportFile(final String importProjectFile) {
240 properties.setProperty(KEY_IMPORT_FILE, importProjectFile);
241 }
242
243 /***
244 * @return Returns the virtualRepositoryActive.
245 */
246 public boolean isVirtualRepositoryActive() {
247 boolean result = false;
248 String value = getProperty("deputy.virtualRepositoryActive");
249 if (value != null) {
250 try {
251 result = Boolean.valueOf(value).booleanValue();
252 } catch (Exception e) {
253 e.printStackTrace();
254 }
255 }
256 return result;
257 }
258
259 /***
260 * @param virtualRepositoryActive The virtualRepositoryActive to set.
261 */
262 public void setVirtualRepositoryActive(
263 final boolean virtualRepositoryActive) {
264 setProperty(
265 "deputy.virtualRepositoryActive",
266 Boolean.valueOf(virtualRepositoryActive).toString());
267 }
268
269 /***
270 * Loads the currently set project file 'as is' into Deputy. 'As is' means
271 * that no rules are applied yet.
272 *
273 * @return The project opened.
274 * @throws Exception if anything goes unexpectedly wrong.
275 */
276 public Project openProjectAsIs() throws Exception {
277 ProjectRecursor parser =
278 new ProjectRecursor(getRepositoryConfigs(), log);
279 rootProject = parser.openProjectAsIs(
280 getProjectFile(),
281 isVirtualRepositoryActive());
282 rootProject.addChangeListener(this);
283 rootProject.getRuleSet().addChangeListener(this);
284 setSaved(true);
285 lastChangeAction = CHANGE_ACTION_UNKNOWN;
286 return rootProject;
287 }
288
289 /***
290 * Adds an enforcement rule for each dependency in the given project.
291 *
292 * @param aProjectFile Absolute path of the project whose dependencies to
293 * derive the enforcement rules from.
294 * @throws Exception if anything goes unexpectedly wrong.
295 */
296 public void deriveEnforcementsFromProject(
297 final String aProjectFile) throws Exception {
298 log.log(Log.SEVERITY_INFO,
299 "Removing all previously derived enforcement rules...");
300 rootProject.getRuleSet().removeAllDerivedEnforcementRules();
301
302 log.log(Log.SEVERITY_INFO,
303 "Adding an enforcement rule for each dependency of project '"
304 + aProjectFile
305 + "'...");
306 setImportFile(aProjectFile);
307 ProjectRecursor parser =
308 new ProjectRecursor(getRepositoryConfigs(), null);
309 Project aProject = parser.openProjectAsIs(
310 aProjectFile,
311 isVirtualRepositoryActive());
312
313
314
315
316 Iterator dependencies = aProject.getAllDependencies().iterator();
317 int numberOfRules = 0;
318 while (dependencies.hasNext()) {
319 Project dependency = (Project) dependencies.next();
320 EnforcementRule rule = new EnforcementRule();
321 rule.setGroupId(dependency.getGroupId());
322 rule.setArtifactId(dependency.getArtifactId());
323 rule.setVersion(dependency.getVersion());
324 rule.setDerived(true);
325 rootProject.getRuleSet().add(rule);
326 numberOfRules++;
327 }
328 String message = "Added " + numberOfRules + " derived enforcement rule";
329 if (numberOfRules == 1) {
330 message += ".";
331 } else {
332 message += "s.";
333 }
334 log.log(Log.SEVERITY_INFO, message);
335 setSaved(false);
336 lastChangeAction = CHANGE_ACTION_UNKNOWN;
337 }
338
339 /***
340 * Loads the currently set project file into Deputy and applies rules on the
341 * fly.
342 *
343 * @return The project after applying the rules to it.
344 * @throws Exception if anything goes unexpectedly wrong.
345 */
346 public Project applyRulesToProject() throws Exception {
347 ProjectRecursor parser =
348 new ProjectRecursor(getRepositoryConfigs(), log);
349 Project oldTopProject = rootProject;
350 rootProject =
351 parser.applyRulesToProject(
352 getProjectFile(),
353 oldTopProject,
354 isVirtualRepositoryActive());
355 rootProject.addChangeListener(this);
356 rootProject.getRuleSet().addChangeListener(this);
357 setSaved(false);
358 computeDiffReports(oldTopProject, rootProject);
359 lastChangeAction = CHANGE_ACTION_APPLY_RULES;
360 return rootProject;
361 }
362
363 /***
364 * Retrieves all direct and indirect SNAPSHOT dependencies of the current
365 * root project and prints them to the log in topological order such that
366 * leaves in the topological order occur first.
367 */
368 public void sortSnapshotsTopologically() {
369
370
371
372 List snapshotDependencies = new ArrayList();
373 Iterator dependencyIter = rootProject.getAllDependencies().iterator();
374 while (dependencyIter.hasNext()) {
375 Project dependency = (Project) dependencyIter.next();
376 if (dependency.getVersion().indexOf("SNAPSHOT") != -1) {
377 snapshotDependencies.add(dependency);
378 }
379 }
380
381
382
383 List sortedList = new ArrayList();
384 List cycles = new ArrayList();
385 doSortSnapshotsTopologically(sortedList, snapshotDependencies, cycles);
386
387
388
389 log.log(Log.SEVERITY_INFO, "");
390 if (sortedList.size() == 0 && cycles.size() == 0) {
391 log.log(Log.SEVERITY_INFO, "There are no SNAPSHOTs to be sorted.");
392 } else if (cycles.size() != 0) {
393 log.log(Log.SEVERITY_WARNING, "Detected one or more cycles:");
394 log.log(Log.SEVERITY_WARNING, "----------------------------");
395 for (int i = 0; i < cycles.size(); i++) {
396 List cycle = (List) cycles.get(i);
397 String cycleMessage = "";
398 for (Iterator iter = cycle.iterator(); iter.hasNext();) {
399 Project project = (Project) iter.next();
400 cycleMessage += project + " --> ";
401 }
402 cycleMessage += cycle.get(0);
403 log.log(Log.SEVERITY_WARNING,
404 "Cycle " + i + ": " + cycleMessage);
405 }
406 if (sortedList.size() > 0) {
407 log.log(Log.SEVERITY_WARNING,
408 "A topological order of unaffected SNAPSHOTs is:");
409 log.log(Log.SEVERITY_WARNING,
410 "-----------------------------------------------");
411 for (Iterator iter = sortedList.iterator(); iter.hasNext();) {
412 Project snapshot = (Project) iter.next();
413 log.log(Log.SEVERITY_WARNING, snapshot.toString());
414 }
415 }
416 } else {
417 log.log(Log.SEVERITY_INFO,
418 "A topological order of all SNAPSHOTs is:");
419 log.log(Log.SEVERITY_INFO,
420 "------------------------------------");
421 for (Iterator iter = sortedList.iterator(); iter.hasNext();) {
422 Project snapshot = (Project) iter.next();
423 log.log(Log.SEVERITY_INFO, snapshot.toString());
424 }
425 }
426 }
427
428 /***
429 * Moves all projects from <code>unsortedList</code> to
430 * <code>sortedList</code> or fails to sort completely.
431 * The <code>sortedList</code> will be sorted topologically whereas the
432 * <code>unsortedList</code> will be empty if sorting succeeds.
433 * If there are cyclic dependencies detected cycles will be moved to
434 * <code>cycles</code>.
435 *
436 * @param sortedList The list of already sorted projects.
437 * @param unsortedList The list of not yet sorted projects.
438 * @param cycles The list of lists of projects where each list of projects
439 * is a cycle.
440 */
441 private void doSortSnapshotsTopologically(
442 final List sortedList, final List unsortedList, final List cycles) {
443 if (unsortedList.size() != 0) {
444 Project nextInLine = findLeftOnlyDependee(sortedList, unsortedList);
445 if (nextInLine != null) {
446
447 sortedList.add(nextInLine);
448 unsortedList.remove(nextInLine);
449
450 doSortSnapshotsTopologically(sortedList, unsortedList, cycles);
451 } else {
452
453 List cycle = findCycle(unsortedList);
454 unsortedList.removeAll(cycle);
455 cycles.add(cycle);
456 doSortSnapshotsTopologically(sortedList, unsortedList, cycles);
457 }
458 }
459 }
460 /***
461 * Returns a project from the <code>rightDependencies</code> which has at
462 * most SNAPSHOT dependencies to projects in the
463 * <code>leftDependencies</code> but no SNAPSHOT dependencies to projects
464 * in the <code>rightDependencies</code>.
465 *
466 * @param leftDependencies The list of 'left' dependencies of type Project.
467 * @param rightDependencies The list of 'right' dependencies of type
468 * Project.
469 * @return A left dependee or <code>null</code>.
470 */
471 private Project findLeftOnlyDependee(
472 final List leftDependencies, final List rightDependencies) {
473 Project result = null;
474 for (Iterator iter = rightDependencies.iterator(); iter.hasNext();) {
475 Project candidate = (Project) iter.next();
476 Iterator prjDepIter = candidate.getDependencies();
477 boolean candidateRejected = false;
478 while (prjDepIter.hasNext()) {
479 Project prjDep = (Project) prjDepIter.next();
480 String artifactId = prjDep.getArtifactId();
481 if (getProjectForArtifactId(
482 rightDependencies, artifactId) != null) {
483 candidateRejected = true;
484 break;
485 }
486 }
487 if (!candidateRejected) {
488 result = candidate;
489 break;
490 }
491 }
492 return result;
493 }
494
495 /***
496 * Scans the list of projects for one whose artifact id matches the artifact
497 * id passed in.
498 *
499 * @param projects The projects to scan.
500 * @param artifactId The artifact id to match with.
501 * @return The project found or <code>null</code> if none was found.
502 */
503 private Project getProjectForArtifactId(
504 final List projects, final String artifactId) {
505 Project result = null;
506 for (Iterator iter = projects.iterator(); iter.hasNext();) {
507 Project candidate = (Project) iter.next();
508 if (candidate.getArtifactId().equals(artifactId)) {
509 result = candidate;
510 }
511 }
512 return result;
513 }
514
515 /***
516 * Finds a cyclic dependency chain in a list of projects that is assured to
517 * contain at least one cycle and where each project is guaranteed to be
518 * involved in one cycle at least.
519 *
520 * @param projects The projects with cyclic dependencies.
521 * Must contain at least two elements of type
522 * {@link Project}.
523 * @return A list of projects of type {@link Project}
524 * where list.get(i) depends on
525 * project list.get((i + 1) % list.size()).
526 */
527 private List findCycle(final List projects) {
528 List result = null;
529 List chain = new ArrayList();
530 chain.add(projects.get(0));
531 List successorCandidates =
532 new ArrayList(projects.subList(1, projects.size()));
533 int maxIterations = successorCandidates.size();
534 for (int i = 0; i < maxIterations; i++) {
535 Project tail = (Project) chain.get(chain.size() - 1);
536 for (Iterator iter = successorCandidates.iterator();
537 iter.hasNext();) {
538 Project candidate = (Project) iter.next();
539 if (tail.hasDependencyToSameArtifact(candidate)) {
540 chain.add(candidate);
541 successorCandidates.remove(candidate);
542 for (int from = chain.size() - 2; from >= 0; from--) {
543 if (candidate.hasDependencyToSameArtifact(
544 (Project) chain.get(from))) {
545 result = chain.subList(from, chain.size());
546 break;
547 }
548 }
549 break;
550 }
551 }
552 if (result != null) {
553 break;
554 }
555 }
556 return result;
557 }
558
559 /***
560 * Computes and logs diff reports that state the differences in their
561 * dependencies and the differences in their indirect dependencies.
562 *
563 * @param oldProject The project in the state before.
564 * @param newProject The project in the state after.
565 * @return <code>true</code> if there are differences in the direct or
566 * indirect dependencies
567 */
568 private boolean computeDiffReports(
569 final Project oldProject,
570 final Project newProject) {
571 boolean directDifferences = false;
572 boolean indirectDifferences = false;
573 log.log(Log.SEVERITY_INFO, "Applied the rules");
574 log.log(Log.SEVERITY_INFO, "Changes in the dependencies:");
575 log.log(Log.SEVERITY_INFO, "----------------------------");
576 directDifferences = computeDiffReport(
577 oldProject.getDependencies(),
578 newProject.getDependencies());
579 if (rootProject.isAssembly()) {
580 log.log(Log.SEVERITY_INFO, "Changes in the indirect dependencies:");
581 log.log(Log.SEVERITY_INFO, "-------------------------------------");
582 indirectDifferences =
583 computeDiffReport(
584 oldProject.getIndirectDependencies(),
585 newProject.getIndirectDependencies());
586 }
587 return directDifferences || indirectDifferences;
588 }
589
590 /***
591 * Computes and logs a diff report that state the differences in the two
592 * dependency iterations.
593 *
594 * @param oldDependencies The dependencies in the state before.
595 * @param newDependencies The dependencies in the state after.
596 * @return <code>true</code> if there are differences
597 */
598 private boolean computeDiffReport(
599 final Iterator oldDependencies,
600 final Iterator newDependencies) {
601
602
603
604 List unchangedDependencies = new ArrayList();
605 List changedDependencies = new ArrayList();
606 List addedDependencies = new ArrayList();
607 List removedDependencies = new ArrayList();
608 ProjectComparator comparator = new ProjectComparator();
609
610 Project oldDependency = advance(oldDependencies);
611 Project newDependency = advance(newDependencies);
612 while (oldDependency != null && newDependency != null) {
613 int comparison =
614 comparator.compare(oldDependency, newDependency);
615 if (comparison == 0) {
616 unchangedDependencies.add(getQualifier(oldDependency));
617 oldDependency = advance(oldDependencies);
618 newDependency = advance(newDependencies);
619 } else if (equalArtifacts(oldDependency, newDependency)) {
620 changedDependencies.add(
621 getQualifier(oldDependency)
622 + " to "
623 + newDependency.getVersion());
624 oldDependency = advance(oldDependencies);
625 newDependency = advance(newDependencies);
626 } else if (comparison < 0) {
627 removedDependencies.add(getQualifier(oldDependency));
628 oldDependency = advance(oldDependencies);
629 } else {
630 addedDependencies.add(getQualifier(newDependency));
631 newDependency = advance(newDependencies);
632 }
633 }
634 while (oldDependency != null) {
635 removedDependencies.add(getQualifier(oldDependency));
636 oldDependency = advance(oldDependencies);
637 }
638 while (newDependency != null) {
639 addedDependencies.add(getQualifier(newDependency));
640 newDependency = advance(newDependencies);
641 }
642
643
644
645
646 boolean noChanges = true;
647 if (changedDependencies.size() > 0) {
648 noChanges = false;
649 Iterator iter = changedDependencies.iterator();
650 while (iter.hasNext()) {
651 String changeInfo = "Changed " + (String) iter.next();
652 log.log(Log.SEVERITY_INFO, changeInfo);
653 }
654 log.log(Log.SEVERITY_INFO, "");
655 }
656 if (addedDependencies.size() > 0) {
657 noChanges = false;
658 Iterator iter = addedDependencies.iterator();
659 while (iter.hasNext()) {
660 String changeInfo = "Added " + (String) iter.next();
661 log.log(Log.SEVERITY_INFO, changeInfo);
662 }
663 log.log(Log.SEVERITY_INFO, "");
664 }
665 if (removedDependencies.size() > 0) {
666 noChanges = false;
667 Iterator iter = removedDependencies.iterator();
668 while (iter.hasNext()) {
669 String changeInfo = "Removed " + (String) iter.next();
670 log.log(Log.SEVERITY_INFO, changeInfo);
671 }
672 log.log(Log.SEVERITY_INFO, "");
673 }
674 if (noChanges) {
675 log.log(
676 Log.SEVERITY_INFO,
677 "No changes!");
678 log.log(Log.SEVERITY_INFO, "");
679 }
680 return !noChanges;
681 }
682
683 /***
684 * Convenience helper to advance the project iterator to the next project of
685 * type {@link Project}.
686 *
687 * @param projectIterator The iterator to advance.
688 * @return The next project or <code>null</code> if the iterator is
689 * exhausted.
690 */
691 private Project advance(final Iterator projectIterator) {
692 Project result = null;
693 if (projectIterator.hasNext()) {
694 result = (Project) projectIterator.next();
695 }
696 return result;
697 }
698
699 /***
700 * Determines if the two projects have equal artifact ids.
701 * @param project1 The project 1 whose artifact id to compare with the other
702 * one's.
703 * @param project2 The project 2 whose artifact id to compare with the other
704 * one's.
705 * @return <code>true</code> if both project's artifact ids are equal.
706 */
707 private boolean equalArtifacts(
708 final Project project1,
709 final Project project2) {
710 boolean result = false;
711 if (project1.getArtifactId().equals(project2.getArtifactId())) {
712 result = true;
713 }
714 return result;
715 }
716
717 /***
718 * @param project The project to create a qualifier string for.
719 * @return The fully qualified name of the project version.
720 */
721 private String getQualifier(final Project project) {
722 String result =
723 project.getGroupId()
724 + "/"
725 + project.getArtifactId()
726 + "-"
727 + project.getVersion();
728 return result;
729 }
730
731 /***
732 * @return The currently active root project loaded into Deputy.
733 * Can be <code>null.</code>
734 */
735 public Project getRootProject() {
736 return rootProject;
737 }
738
739 /***
740 * Saves the currently loaded project under the new project file name.
741 * @param newProjectFileName The absolute path and name to save the project
742 * under.
743 * @throws Exception if anything goes unexpectedly wrong
744 */
745 public void saveProjectAs(
746 final String newProjectFileName)
747 throws Exception {
748 ProjectGenerator generator =
749 new ProjectGenerator(getVersion());
750 generator.createAndSaveUpdatedDocument(
751 rootProject,
752 getProjectFile(),
753 newProjectFileName);
754 setProjectFile(newProjectFileName);
755 log.log(Log.SEVERITY_INFO, "Saved " + newProjectFileName);
756 setSaved(true);
757 }
758
759 /***
760 * @return <code>true</code> if the current state of the project is saved.
761 */
762 public boolean isSaved() {
763 return isSaved;
764 }
765
766 /***
767 * @param saved <code>true</code> if the current state of the project is
768 * saved.
769 */
770 private void setSaved(final boolean saved) {
771 if (this.isSaved != saved) {
772 this.isSaved = saved;
773 fireProjectHasChanged();
774 }
775 }
776
777 /***
778 * @return The absolute path and name of the file to export the dependency
779 * graph under.
780 */
781 public String getDependencyGraphFile() {
782 String result = properties.getProperty(KEY_DEPENDENCY_GRAPH_FILE);
783 if (result == null) {
784 result =
785 System.getProperty("user.home")
786 + File.separator
787 + "dependency-graph.xml";
788 }
789 return result;
790 }
791
792 /***
793 * @param dependencyGraphFile The absolute path and name of the file to
794 * export the dependency graph under.
795 */
796 public void setDependencyGraphFile(final String dependencyGraphFile) {
797 properties.setProperty(
798 KEY_DEPENDENCY_GRAPH_FILE,
799 dependencyGraphFile);
800 }
801
802 /***
803 * Exports the currently loaded project as a dependency graph XML that can
804 * be imported into Rational Rose or yEd for diagramming purposes.
805 * @param format The export format. Can be {@link #FORMAT_GRAPHML}
806 * or {@link #FORMAT_DEPUTYXML}.
807 * @throws Exception if anything goes unexpectedly wrong
808 */
809 public void exportDependencyGraph(final int format) throws Exception {
810 AbstractDependencyGraphGenerator generator = null;
811 if (format == FORMAT_GRAPHML) {
812 generator = new DependencyGraphMLGenerator();
813 } else {
814 generator = new DependencyGraphXmlGenerator();
815 }
816 generator.generateDependencyGraph(
817 rootProject,
818 getDependencyGraphFile());
819 }
820
821 /***
822 * @return The list of configs of type {@link RepositoryConfig}.
823 * @throws Exception if anything goes unexpectedly wrong
824 */
825 public List getRepositoryConfigs() throws Exception {
826 List result =
827 parseRepositoryConfigs(
828 properties.getProperty(KEY_REPOSITORY_CONFIGS));
829 return result;
830 }
831
832 /***
833 * @param repositoryConfigs A list of configs of type
834 * {@link RepositoryConfig}.
835 * @throws Exception if anything goes unexpectedly wrong
836 */
837 public void setRepositoryConfigs(
838 final List repositoryConfigs)
839 throws Exception {
840 String repositoryConfigsAsString =
841 convertRepositoryConfigsToString(repositoryConfigs);
842 properties.setProperty(
843 KEY_REPOSITORY_CONFIGS,
844 repositoryConfigsAsString);
845 }
846
847 /***
848 * Adds a listener to be notified on changes in the current project.
849 * @param listener The listener to be added.
850 */
851 public void addChangeListener(final DeputyChangeListener listener) {
852 if (!changeListeners.contains(listener)) {
853 changeListeners.add(listener);
854 }
855 }
856
857 /***
858 * Removes a listener to be notified on changes in the current project.
859 *
860 * @param listener The listener to be removed.
861 */
862 public void removeChangeListener(final DeputyChangeListener listener) {
863 changeListeners.remove(listener);
864 }
865
866 /***
867 * Notifies listeners that the project has changed somehow.
868 */
869 private void fireProjectHasChanged() {
870 for (Iterator iter = changeListeners.iterator(); iter.hasNext();) {
871 DeputyChangeListener listener =
872 (DeputyChangeListener) iter.next();
873 listener.projectHasChanged();
874 }
875 }
876
877 /***
878 * @param repositoryConfigsAsString A semicolon separated string of config
879 * triples each consisting of an absolute path to a
880 * Maven repository, a display name for the
881 * repository and a boolean value indicating
882 * whether to scan this repository for versions of
883 * artifacts when applying rules.
884 * @return The list of parsed configs of type {@link RepositoryConfig}.
885 * @throws Exception if the string cannot be parsed properly
886 */
887 private List parseRepositoryConfigs(
888 final String repositoryConfigsAsString)
889 throws Exception {
890 List result = new ArrayList();
891 StringTokenizer tokenizer =
892 new StringTokenizer(repositoryConfigsAsString, ";");
893 while (tokenizer.hasMoreTokens()) {
894 RepositoryConfig config =
895 new RepositoryConfig(tokenizer.nextToken());
896 if (tokenizer.hasMoreTokens()) {
897 config.setDisplayName(tokenizer.nextToken());
898 } else {
899 throw new Exception(
900 "Repository path "
901 + "'"
902 + config.getPath()
903 + "'"
904 + " is not properly followed by a semicolon separated "
905 + "display name");
906 }
907 if (tokenizer.hasMoreTokens()) {
908 config.setToBeScannedForVersions(
909 Boolean.valueOf(tokenizer.nextToken()).booleanValue());
910 } else {
911 throw new Exception(
912 "Display name "
913 + "'"
914 + config.getDisplayName()
915 + "'"
916 + " is not properly followed by a semicolon separated "
917 + "boolean value 'true' or 'false'");
918 }
919 result.add(config);
920 }
921 if (result.size() == 0) {
922 throw new FileNotFoundException("No configs specified");
923 }
924 return result;
925 }
926
927 /***
928 * @param repositoryConfigs The list of configs of type
929 * {@link RepositoryConfig} to be converted.
930 * @return The result of the conversion.
931 */
932 private String convertRepositoryConfigsToString(
933 final List repositoryConfigs) {
934 StringBuffer result = new StringBuffer("");
935 for (Iterator iter = repositoryConfigs.iterator(); iter.hasNext();) {
936 RepositoryConfig config = (RepositoryConfig) iter.next();
937 result.append(config.getPath());
938 result.append(";");
939 result.append(config.getDisplayName());
940 result.append(";");
941 result.append(config.isToBeScannedForVersions());
942 if (iter.hasNext()) {
943 result.append(";");
944 }
945 }
946 return result.toString();
947 }
948
949
950
951
952
953 /***
954 * {@inheritDoc}
955 */
956 public void changedDefaultRule() {
957 setSaved(false);
958 lastChangeAction = CHANGE_ACTION_CHANGE_RULES;
959 String defaultRule = rootProject.getRuleSet().getDefaultRule();
960 log.log(Log.SEVERITY_INFO,
961 "Changed the default rule to " + defaultRule);
962 }
963
964
965
966
967
968 /***
969 * {@inheritDoc}
970 */
971 public void addedRule(final Rule addedRule, final int index) {
972 setSaved(false);
973 lastChangeAction = CHANGE_ACTION_CHANGE_RULES;
974 log.log(Log.SEVERITY_INFO, "Added rule '" + addedRule + "'");
975 }
976
977
978
979
980
981 /***
982 * {@inheritDoc}
983 */
984 public void removedRule(final Rule removedRule) {
985 setSaved(false);
986 lastChangeAction = CHANGE_ACTION_CHANGE_RULES;
987 log.log(Log.SEVERITY_INFO, "Removed rule '" + removedRule + "'");
988 }
989
990 /***
991 * @return The log Deputy currently writes messages to.
992 * Can be <code>null</code>. Can change, don't cache a reference.
993 */
994 public Log getLog() {
995 return log;
996 }
997
998 /***
999 * @param log The log to write messages to. Can be set by the creator of
1000 * this class to define where to log to.
1001 */
1002 public void setLog(final Log log) {
1003 this.log = log;
1004 }
1005
1006 /***
1007 * @return One of {@link #CHANGE_ACTION_UNKNOWN},
1008 * {@link #CHANGE_ACTION_APPLY_RULES},
1009 * {@link #CHANGE_ACTION_CHANGE_DEPENDENCIES},
1010 */
1011 public int getLastChangeAction() {
1012 return lastChangeAction;
1013 }
1014
1015
1016
1017
1018
1019 /***
1020 * {@inheritDoc}
1021 */
1022 public void addedDependency(
1023 final Project addedDependency,
1024 final int index) {
1025 setSaved(false);
1026 lastChangeAction = CHANGE_ACTION_CHANGE_DEPENDENCIES;
1027 log.log(
1028 Log.SEVERITY_INFO,
1029 "Added dependency '" + addedDependency + "'");
1030 }
1031
1032
1033
1034
1035
1036 /***
1037 * {@inheritDoc}
1038 */
1039 public void removedDependency(final Project removedDependency) {
1040 setSaved(false);
1041 lastChangeAction = CHANGE_ACTION_CHANGE_DEPENDENCIES;
1042 log.log(
1043 Log.SEVERITY_INFO,
1044 "Removed dependency '" + removedDependency + "'");
1045 }
1046
1047
1048
1049
1050
1051
1052 /***
1053 * {@inheritDoc}
1054 */
1055 public void removedIndirectDependency(final Project removedDependency) {
1056 setSaved(false);
1057 lastChangeAction = CHANGE_ACTION_CHANGE_DEPENDENCIES;
1058 log.log(
1059 Log.SEVERITY_INFO,
1060 "Removed indirect dependency '" + removedDependency + "'");
1061 }
1062
1063 /***
1064 * @param key The property's key.
1065 * @return The property value or <code>null</code> if the property does not
1066 * exist.
1067 */
1068 public String getProperty(final String key) {
1069 String result = properties.getProperty(key);
1070 return result;
1071 }
1072
1073 /***
1074 * @param key The property's key. Must not be <code>null</code>.
1075 * @param value The property's value. Must not be <code>null</code>.
1076 */
1077 public void setProperty(final String key, final String value) {
1078 properties.setProperty(key, value);
1079 }
1080
1081 /***
1082 * Applies the rules on a POM. Optionally, saves the diff report and/or the
1083 * resulting POM.
1084 *
1085 * @param projectFile The absolute path of the POM to process.
1086 * @param diffFile The absolute path of the file to write the diff report
1087 * to. Can be <code>null</code> if no report is desired.
1088 * @param saveAsFile The absolute path of the file to save the updated
1089 * POM under. Can be <code>null</code> if results are not
1090 * to be saved.
1091 * @throws Exception if anything goes unexpectedly wrong
1092 */
1093 private void applyRulesInBatchMode(
1094 final String projectFile,
1095 final String diffFile,
1096 final String saveAsFile)
1097 throws Exception {
1098 setProjectFile(projectFile);
1099 openProjectAsIs();
1100 Project oldProject = getRootProject();
1101 applyRulesToProject();
1102 Project newProject = getRootProject();
1103 if (diffFile != null) {
1104 LineBufferLog sbLog = new LineBufferLog();
1105 setLog(sbLog);
1106 File file = new File(projectFile);
1107 Date lastModified = new Date(file.lastModified());
1108 String path = file.getAbsolutePath();
1109 int revision = -1;
1110 try {
1111 P4 p4 = new P4();
1112 revision = p4.getSynchedRevision(projectFile);
1113 } catch (Exception e) {
1114 e.printStackTrace();
1115 }
1116 sbLog.log(Log.SEVERITY_INFO, "Project: " + newProject);
1117 sbLog.log(Log.SEVERITY_INFO, "File: " + path);
1118 sbLog.log(Log.SEVERITY_INFO, "Last Modified: " + lastModified);
1119 if (revision != -1) {
1120 sbLog.log(Log.SEVERITY_INFO, "Revision: " + revision);
1121 }
1122 sbLog.log(Log.SEVERITY_INFO, "");
1123 boolean differences = computeDiffReports(oldProject, newProject);
1124 if (differences) {
1125 sbLog.writeToFile(diffFile);
1126 }
1127 }
1128 if (saveAsFile != null) {
1129 saveProjectAs(saveAsFile);
1130 }
1131 }
1132
1133 /***
1134 * Executes Deputy in batch mode. Opens the project file specified, applies
1135 * the rules on it, and optionally saves the diff report and/or the
1136 * processed project file.
1137 * <p/>
1138 * Exits with return code 0 if processing was successful,
1139 * exits with return code -1 if an error occurred.
1140 *
1141 * @param args The command line arguments:
1142 * <ul>
1143 * <li>
1144 * -projectFile=absolute path of the project file to process
1145 * <br/>
1146 * is mandatory
1147 * </li>
1148 * <li>
1149 * -diffFile=absolute path to save the diff report under
1150 * <br/>
1151 * is optional, defaults to not saving the diff report after processing
1152 * </li>
1153 * <li>
1154 * -saveAsFile=absolute path to save the processed project file under
1155 * <br/>
1156 * is optional, defaults to not saving the file after processing
1157 * </li>
1158 * </ul>
1159 */
1160 public static void main(final String[] args) {
1161 try {
1162
1163
1164
1165 String projectFile = null;
1166 String diffFile = null;
1167 String saveAsFile = null;
1168 for (int i = 0; i < args.length; i++) {
1169 if (args[i].startsWith("-projectFile=")) {
1170 projectFile =
1171 args[i].substring("-projectFile=".length());
1172 } else if (args[i].startsWith("-diffFile=")) {
1173 diffFile =
1174 args[i].substring("-diffFile=".length());
1175 } else if (args[i].startsWith("-saveAsFile=")) {
1176 saveAsFile =
1177 args[i].substring("-saveAsFile=".length());
1178 }
1179 }
1180
1181
1182
1183 if (projectFile == null) {
1184 throw new IllegalArgumentException(
1185 "project file not specified correctly");
1186 }
1187 if (!(new File(projectFile).exists())) {
1188 throw new IllegalArgumentException(
1189 "project file specified does not exist");
1190 }
1191 if (diffFile == null) {
1192 System.out.println(
1193 "diff report file not specified, will not be written");
1194 }
1195 if (saveAsFile == null) {
1196 System.out.println(
1197 "save as file not specified, will not be written");
1198 }
1199
1200
1201
1202 Deputy deputy = new Deputy(new SimpleLog());
1203 deputy.applyRulesInBatchMode(projectFile, diffFile, saveAsFile);
1204 System.exit(0);
1205 } catch (IllegalArgumentException e) {
1206 System.err.println(
1207 "Invalid arguments, usage is: "
1208 + Deputy.class.getName()
1209 + " -projectFile=<absolute path of project input file>"
1210 + " [-diffFile=<absolute path of diff report output file>]"
1211 + " [-saveAsFile=<absolute path of project output file>]");
1212 System.err.println(e.getMessage());
1213 System.exit(-1);
1214 } catch (Exception e) {
1215 e.printStackTrace();
1216 System.exit(-1);
1217 }
1218 }
1219 }