View Javadoc

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          * Create application properties
154          */
155         Properties applicationProperties = new Properties();
156 
157         /*
158          * Now load application properties from file
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          * Create user properties with application properties as defaults
171          */
172         properties = new Properties(applicationProperties);
173 
174         /*
175          * Now load user properties from file
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          * Create an enforcement rule for each dependency in the project just
314          * opened and add this rule to the current root project.
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          * Create the list of SNAPSHOTs to be sorted
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          * Do the sorting
382          */
383         List sortedList = new ArrayList();
384         List cycles = new ArrayList();
385         doSortSnapshotsTopologically(sortedList, snapshotDependencies, cycles);
386         /*
387          * Print results to the log
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                 // move project from unsorted to sorted list
447                 sortedList.add(nextInLine);
448                 unsortedList.remove(nextInLine);
449                 // recurse
450                 doSortSnapshotsTopologically(sortedList, unsortedList, cycles);
451             } else {
452                 //there is a cycle, go find it, move it out and continue
453                 List cycle = findCycle(unsortedList);
454                 unsortedList.removeAll(cycle);
455                 cycles.add(cycle);
456                 doSortSnapshotsTopologically(sortedList, unsortedList, cycles);
457             }
458         } // else we are successfully done
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          * Compute diff report
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          * Print diff report to the log
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     /*(non-Javadoc)
950      * @see de.matthias_burbach.deputy.core.RuleSetChangeListener
951      *          #changedDefaultRule()
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     /*(non-Javadoc)
965      * @see de.matthias_burbach.deputy.core.RuleSetChangeListener
966      *          #addedRule(de.matthias_burbach.deputy.core.Rule, int)
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     /*(non-Javadoc)
978      * @see de.matthias_burbach.deputy.core.RuleSetChangeListener
979      *          #removedRule(de.matthias_burbach.deputy.core.Rule)
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     /*(non-Javadoc)
1016      * @see de.matthias_burbach.deputy.core.ProjectChangeListener
1017      *          #addedDependency(de.matthias_burbach.deputy.core.Project, int)
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     /*(non-Javadoc)
1033      * @see de.matthias_burbach.deputy.core.ProjectChangeListener
1034      *          #removedDependency(de.matthias_burbach.deputy.core.Project)
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     /*(non-Javadoc)
1048      * @see de.matthias_burbach.deputy.core.ProjectChangeListener
1049      *          #removedIndirectDependency(
1050      *              de.matthias_burbach.deputy.core.Project)
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              * Parse arguments
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              * Validate arguments
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              * Do the actual processing
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 }