1 /***
2 * Copyright 2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * For more information visit
11 * http://72miles.com and
12 * http://architecturerules.googlecode.com/svn/docs/index.html
13 */
14
15 package com.seventytwomiles.architecturerules.services;
16
17
18 import com.seventytwomiles.architecturerules.configuration.Configuration;
19 import com.seventytwomiles.architecturerules.exceptions.CyclicRedundancyException;
20 import com.seventytwomiles.architecturerules.exceptions.NoPackagesFoundException;
21 import com.seventytwomiles.architecturerules.exceptions.SourceNotFoundException;
22 import javassist.ClassPool;
23 import javassist.NotFoundException;
24 import jdepend.framework.JavaClass;
25 import jdepend.framework.JavaPackage;
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28
29 import java.util.*;
30
31
32
33 /***
34 * <p>Checks for cyclic redundancy among application packages in the source
35 * folders.</p>
36 *
37 * @author mikenereson
38 * @see AbstractArchitecturalRules
39 */
40 public class CyclicRedundancyServiceImpl extends AbstractArchitecturalRules
41 implements CyclicRedundancyService {
42
43
44 /***
45 * <p>Log to log with</p>
46 *
47 * @parameter log Log
48 */
49 protected static final Log log = LogFactory.getLog(
50 CyclicRedundancyServiceImpl.class);
51
52
53 /***
54 * <p>Constructor instantiates a new <code>CyclicRedundancyService</code></p>
55 *
56 * @param configuration Configuration which contains the source directories
57 * to inspect
58 * @throws SourceNotFoundException when an required source directory does
59 * not exist and when <tt>exception</tt>=<tt>"true"</tt> in the source
60 * configuration
61 * @throws NoPackagesFoundException when none of the source directories
62 * exist and <tt>no-packages</tt>="<tt>ignore</tt>" in the sources
63 * configuration
64 */
65 public CyclicRedundancyServiceImpl(final Configuration configuration) {
66
67 super(configuration);
68
69 log.info("instantiating new CyclicRedundancyService");
70 }
71
72
73 /***
74 * <p>Check all the packages in all of the source directories and search for
75 * any cyclic redundancy</p>
76 */
77 public void performCyclicRedundancyCheck() {
78
79 log.info("cyclic redundancy check requested");
80
81 final Collection<JavaPackage> packages = getPackages();
82
83 /***
84 * TODO: report classes involved
85 * Report on which class is causing the cyclic redundancy.
86 * The information is all available from within the JavaPackage
87 * class.
88 */
89
90 /***
91 * Holds any cycles that are found. The structure of this Map is
92 * <JavaPackage, Set> where the first String is the name of the package,
93 * and the Set contains JavaPackge of packages that are in a cycle with
94 * the named package.
95 */
96 final Map<JavaPackage, Set<JavaPackage>> cycles
97 = new HashMap<JavaPackage, Set<JavaPackage>>();
98
99 for (final JavaPackage javaPackage : packages) {
100
101 final Collection<JavaPackage> afferents
102 = javaPackage.getAfferents();
103 final Collection<JavaPackage> efferents
104 = javaPackage.getEfferents();
105
106 /***
107 * afferents Collection is no longer a reference to the afferents,
108 * but now a Collection of packages involved in a cyclic dependency
109 * with the javaPackage. By calling retainAll the afferents
110 * Collection now contains only those javaPackages that were in both
111 * the efferents and afferents Collections. And by definition, when
112 * a package is both uses the given package, and used by the given
113 * package, a cyclic dependency exists.
114 */
115 afferents.retainAll(efferents);
116
117 final boolean cyclesFound = afferents.size() > 0;
118
119 if (cyclesFound)
120 addCycle(cycles, javaPackage, afferents);
121 }
122
123 if (cycles.isEmpty()) {
124
125 log.info("found no cyclic redundancies");
126
127 } else {
128
129 log.warn("found " + cycles.size() + " cyclic redundancies");
130
131 final CyclicRedundancyException cyclicRedundancyException
132 = buildCyclicRedundancyException(cycles);
133
134 throw cyclicRedundancyException;
135 }
136
137 log.info("cyclic redundancy test completed");
138 }
139
140
141 private CyclicRedundancyException buildCyclicRedundancyException(
142 final Map<JavaPackage, Set<JavaPackage>> cycles) {
143
144 final String message = buildCyclicRedundancyMessage(cycles);
145
146 final CyclicRedundancyException exception
147 = new CyclicRedundancyException(message);
148
149 final Map<String, Set<String>> cycleStrings = exception.getCycles();
150
151 for (final Map.Entry<JavaPackage, Set<JavaPackage>> cycle : cycles.entrySet()) {
152
153 final String packageName = cycle.getKey().getName();
154 final Set<JavaPackage> packageCycles = cycle.getValue();
155
156 final Set<String> cyclicPackages;
157
158 if (cycleStrings.containsKey(packageName)) {
159
160 cyclicPackages = cycleStrings.get(packageName);
161
162 } else {
163
164 cyclicPackages = new HashSet<String>();
165 }
166
167 for (final JavaPackage packageCycle : packageCycles) {
168
169 final String cycleName = packageCycle.getName();
170 cyclicPackages.add(cycleName);
171 }
172
173 cycleStrings.put(packageName, cyclicPackages);
174 }
175
176 exception.getCycles().putAll(cycleStrings);
177
178 return exception;
179 }
180
181
182 /***
183 * <p>Updates a Map, or puts a new record into a Map of a JavaPackage and
184 * its cyclic dependency packages.</p>
185 *
186 * @param cycles Map of cycles already discovered.
187 * @param javaPackage JavaPackage involved in a cyclic dependency
188 * @param dependencies Collection of JavaPackages involved in a cyclic
189 * dependency with the given javaPackage argument.
190 */
191 private void addCycle(final Map cycles, final JavaPackage javaPackage,
192 final Collection dependencies) {
193
194 final boolean exists = cycles.containsKey(javaPackage);
195 final Set cyclicPackages;
196
197 if (exists) {
198 /***
199 * Get existing Set, this JavaPackage has already been
200 * discovered for being in a cycle
201 */
202 final Object value = cycles.get(javaPackage);
203 cyclicPackages = (HashSet) value;
204
205 } else {
206
207 /***
208 * Build a new Set
209 */
210 cyclicPackages = new HashSet();
211 }
212
213 cyclicPackages.addAll(dependencies);
214 cycles.put(javaPackage, cyclicPackages);
215 }
216
217
218 /***
219 * <p>Builds a message detailing all of the java packages that are involved
220 * in a cyclic dependency and the packages involved with.</p>
221 *
222 * @param cycles Map of cycles discovered.
223 * @return String a complete message detailing all of the cyclic
224 * dependencies found.
225 */
226 private String buildCyclicRedundancyMessage(
227 final Map<JavaPackage, Set<JavaPackage>> cycles) {
228
229 final StringBuffer message = new StringBuffer();
230
231 message.append(cycles.size())
232 .append(" cyclic dependencies found:")
233 .append("\r\n")
234 .append("\r\n\t");
235
236 for (final Map.Entry<JavaPackage, Set<JavaPackage>> entry : cycles.entrySet()) {
237
238 final JavaPackage javaPackage = entry.getKey();
239 final Set<JavaPackage> cyclicDependencies = entry.getValue();
240
241 message.append("-- ")
242 .append(javaPackage.getName())
243 .append("\r\n\t");
244
245 message.append("| |")
246 .append(" (depends on one or more of)")
247 .append("\r\n\t");
248
249 for (Iterator dependencyIterator = cyclicDependencies.iterator();
250 dependencyIterator.hasNext();) {
251
252 final JavaPackage dependency
253 = (JavaPackage) dependencyIterator.next();
254
255 final String listOfClasses
256 = buildListOfClasses(javaPackage, dependency);
257
258 message.append("| |");
259 message.append("\r\n\t");
260 message.append("| |-- ");
261 message.append(dependency.getName());
262 message.append("\n").append(listOfClasses);
263 message.append("\r\n\t");
264
265 if (!dependencyIterator.hasNext())
266 message.append("|").append("\r\n\t");
267 }
268 }
269
270 return message.toString();
271 }
272
273
274 /***
275 * <p></p>
276 *
277 * @param javaPackage <code>JavaPackage</code> package to describe
278 * @param dependency <code>JavaPackage</code> that the javaPackage argument
279 * depends on
280 * @return String that can be output to the console that describes the given
281 * javaPackages's dependency.
282 */
283 private String buildListOfClasses(final JavaPackage javaPackage,
284 final JavaPackage dependency) {
285
286 final StringBuffer listOfClasses = new StringBuffer();
287 final Collection<JavaClass> classes = dependency.getClasses();
288
289 for (final JavaClass javaClass : classes) {
290
291 final Collection<JavaPackage> importedPackages
292 = javaClass.getImportedPackages();
293
294 final Collection<JavaClass> referencedClassNames
295 = buildListOfImports(javaPackage, javaClass);
296
297 if (importedPackages.contains(javaPackage)) {
298
299 final String referencesToPrint;
300
301 if (referencedClassNames.isEmpty()) {
302
303 referencesToPrint = "referenced classes not found";
304
305 } else {
306
307 referencesToPrint = referencedClassNames.toString();
308 }
309
310 listOfClasses.append("\t|\t")
311 .append(javaClass.getName())
312 .append(": ")
313 .append(referencesToPrint)
314 .append("\n");
315 }
316 }
317
318 return listOfClasses.toString();
319 }
320
321
322 /***
323 * <p>Builds a List of class names that the given classWithImports argument
324 * are involved with cycle with</p>
325 *
326 * @param packageInCycle JavaPackage involved in cyclic dependency
327 * @param classWithImports JavaClass that is involved in the cyclic
328 * dependency
329 * @return List of class names which this class imports from the package
330 * involved in the cycle
331 */
332 private List buildListOfImports(final JavaPackage packageInCycle,
333 final JavaClass classWithImports) {
334
335 final List<String> referencedClassNames = new LinkedList<String>();
336
337 try {
338
339 final ClassPool classPool = ClassPool.getDefault();
340 final String name = classWithImports.getName();
341 final Collection<String> refClasses
342 = classPool.get(name).getRefClasses();
343
344 for (final String importedClass : refClasses) {
345
346 final boolean notSelfClass = !name.equals(importedClass);
347
348 try {
349
350 final String packageName
351 = classPool.get(importedClass).getPackageName();
352
353 final boolean importFromCycle
354 = packageName.equals(packageInCycle.getName());
355
356 if (notSelfClass && importFromCycle)
357 referencedClassNames.add(importedClass);
358
359 } catch (NotFoundException e) {
360
361 log.error("could not load reference class", e);
362 }
363 }
364
365 } catch (NotFoundException e) {
366
367 log.error("could not load reference class", e);
368 }
369
370 return referencedClassNames;
371 }
372 }