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.domain;
16
17
18 import com.seventytwomiles.architecturerules.exceptions.IllegalArchitectureRuleException;
19 import junit.framework.Assert;
20 import org.apache.commons.logging.Log;
21 import org.apache.commons.logging.LogFactory;
22
23 import static java.lang.String.format;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashSet;
27
28
29
30 /***
31 * <p>Represents a <code>Rule</code> that may not be violoated.</p>
32 *
33 * @author mikenereson
34 */
35 public class Rule {
36
37
38 protected static final Log log = LogFactory.getLog(Rule.class);
39
40 /***
41 * <p>Unique id of this Rule as defined. Used to refer to this Rule in
42 * messages.</p>
43 *
44 * @parameter id String
45 */
46 private String id;
47
48 /***
49 * <p>Collection of Strings. These Strings are package names. The names of
50 * the packages that will be check against the {@link #violations}. These
51 * packages may NOT depend upon the packages listed in violations.</p>
52 *
53 * @parameter violations Collection
54 */
55 private final Collection<JPackage> packages = new HashSet<JPackage>();
56
57 /***
58 * <p>Comment about this rule that could be used in messages or just to make
59 * the configuration file more readable.</p>
60 *
61 * @parameter comment String
62 */
63 private String comment;
64
65 /***
66 * <p>Collection of Strings. These Strings are package names. The names of
67 * the packages that the {@link #packages} may NOT depend upon</p>
68 *
69 * @parameter violations Collection
70 */
71 private final Collection<JPackage> violations = new HashSet<JPackage>();
72
73
74 /***
75 * <p>Constructs a new Rule.</p>
76 */
77 public Rule() {
78
79 }
80
81
82 /***
83 * <p>Instantiates a new Rule with the given <tt>id</tt>.</p>
84 *
85 * @param id sets the {@link #id}
86 */
87 public Rule(final String id) {
88
89 this.id = id;
90 }
91
92
93 /***
94 * <p>Instantiates a new Rule with the given <tt>id</tt>.</p>
95 *
96 * <p>This constructor is to provide some sense of backwards compatibility
97 * with the releases in series 1 (1.0 and 1.1)</p>
98 *
99 * @param id sets the {@link #id}
100 * @param packageName a {@link @packages} to assert on.
101 */
102 public Rule(final String id, final String packageName) {
103
104 setId(id);
105 addPackage(packageName);
106 }
107
108
109 /***
110 * <p>Setter for property {@link #id}.</p>
111 *
112 * @param id Value to set for property <tt>id</tt>.
113 * @return Rule this <code>Rule</code> to allow for method chaining.
114 */
115 public Rule setId(final String id) {
116
117 Assert.assertNotNull("id can not be null", id);
118 Assert.assertFalse("id can not be empty", "".equals(id));
119
120 this.id = id;
121
122 return this;
123 }
124
125
126 /***
127 * <p>Adds a package to the Packages collection.</p>
128 *
129 * @param packageName String
130 * @return boolean <tt>true</tt> when the package is actually added to the
131 * Collection. <tt>false</tt> would be returned if the package was
132 * already in the Collection of packages.
133 */
134 public boolean addPackage(final String packageName) {
135
136 Assert.assertNotNull(
137 "null packageName can not be added",
138 packageName);
139
140 Assert.assertFalse(
141 "empty packageName can not be added",
142 packageName.equals(""));
143
144 final JPackage jPackage = new JPackage(packageName);
145
146 if (violations.contains(jPackage))
147 throw new IllegalArchitectureRuleException(
148 "Could not add " + packageName + " package because " +
149 "there is already a violation with the same " +
150 "package name for rule " + id);
151
152 return packages.add(jPackage);
153 }
154
155
156 /***
157 * <p>Getter for property {@link #comment}.</p>
158 *
159 * @return Value for property <tt>comment</tt>.
160 */
161 public String getComment() {
162 return this.comment;
163 }
164
165
166 /***
167 * <p>Getter for property {@link #id}.</p>
168 *
169 * @return Value for property <tt>id</tt>.
170 */
171 public String getId() {
172 return this.id;
173 }
174
175
176 /***
177 * <p>Getter for property {@link #packages}</p>
178 *
179 * @return Value for property <tt>packages</tt>.
180 */
181 public Collection<JPackage> getPackages() {
182 return this.packages;
183 }
184
185
186 /***
187 * @see Object#equals(Object)
188 */
189 @Override
190 public boolean equals(final Object object) {
191
192 if (this == object)
193 return true;
194
195 if (object == null)
196 return false;
197
198 if (!(object instanceof Rule))
199 return false;
200
201 final Rule that = (Rule) object;
202
203 if (id != null ? !id.equals(that.getId()) : that.getId() != null)
204 return false;
205
206 return true;
207 }
208
209
210 /***
211 * @see Object#hashCode()
212 */
213 @Override
214 public int hashCode() {
215 return (id != null ? id.hashCode() : 0);
216 }
217
218
219 /***
220 * <p>Add a new violation to this <code>Rule</code>.</p>
221 *
222 * @param violation String a package this this Rule's package may NOT depend
223 * upon
224 * @return Rule this <code>Rule</code> to allow for method chaining.
225 * @throws IllegalArchitectureRuleException a RuntimeException when the
226 * violation could not be added because the violation is one of the packages
227 * being checked.
228 */
229 public Rule addViolation(final JPackage violation) {
230
231 Assert.assertNotNull("null violation can not be added", violation);
232 Assert.assertFalse("empty violation can not be added",
233 violation.getPath().equals(""));
234
235 if (packages.contains(violation))
236 throw new IllegalArchitectureRuleException(
237 "Could not add architecture rule violation that creates rule " +
238 "that says a package can not use itself. Remove " +
239 "<violation>" + violation + "</violation> " +
240 "from rule " + id);
241
242 final boolean added = violations.add(violation);
243
244 if (added) {
245
246 final String debug = format("added violation %s to Rule %s",
247 violation, id);
248
249 log.debug(debug);
250
251 } else {
252
253 final String warn = format("failed to add violation %s to Rule %s",
254 violation, id);
255
256 log.warn(warn);
257 }
258
259 return this;
260 }
261
262
263 /***
264 * <p>Add a new violation to this <code>Rule</code>.</p>
265 *
266 * @param violation String a package this this Rule's package may NOT depend
267 * upon
268 * @return Rule this <code>Rule</code> to allow for method chaining.
269 * @throws IllegalArchitectureRuleException a RuntimeException when the
270 * violation could not be added because the violation is one of the packages
271 * being checked.
272 */
273 public Rule addViolation(final String violation) {
274
275 Assert.assertNotNull("null violation can not be added", violation);
276 Assert.assertFalse("empty violation can not be added",
277 violation.equals(""));
278
279 final JPackage violationPackage = new JPackage(violation);
280 return this.addViolation(violationPackage);
281 }
282
283
284 /***
285 * <p>Describes the properties of this rule in an xml-like format.</p>
286 *
287 * @return String of xml that describes this <code>Rule</code>.
288 */
289 public String describe() {
290
291 return describe(false);
292 }
293
294
295 /***
296 * <p>Describes the properties of this rule in an xml-like format.</p>
297 *
298 * @param outputToConsole boolean <tt>true</tt> to output the description to
299 * the console
300 * @return String of xml that describes this <code>Rule</code>.
301 */
302 private String describe(final boolean outputToConsole) {
303
304 final StringBuffer builder = new StringBuffer();
305
306 builder.append("<rule>").append("\r\n");
307
308 builder.append("\t")
309 .append("<id>")
310 .append(id)
311 .append("</id>")
312 .append("\r\n");
313
314 builder.append("\t")
315 .append("<packages>")
316 .append(packages)
317 .append("</packages>")
318 .append("\r\n");
319
320 builder.append("\t")
321 .append("<comment>")
322 .append(comment)
323 .append("</comment>")
324 .append("\r\n");
325
326 builder.append("\t").append("<violations>").append("\r\n");
327
328 for (final JPackage violation : violations) {
329 builder.append("\t\t").append("<violation>");
330 builder.append(violation.getPath());
331 builder.append("</violation>");
332 builder.append("\r\n");
333 }
334
335 builder.append("\t").append("</violations>").append("\r\n");
336 builder.append("</rule>").append("\r\n");
337
338 if (outputToConsole)
339 System.out.println(builder.toString());
340
341 return builder.toString();
342 }
343
344
345 /***
346 * <p>Creates a String representation of this <code>Rule</code>. Useful for
347 * debugging and logging.</p>
348 *
349 * @return String describes this rule at its current state. Such as
350 * <samp>['dao' for 'com.company.dao, com.company.dao.hibernate']</samp>
351 */
352 public String getDescriptionOfRule() {
353
354 final String ruleDescription = "['{0}' for {1}] "
355 .replaceAll("//{0}", getId())
356 .replaceAll("//{1}", describePackages());
357
358 return ruleDescription;
359 }
360
361
362 /***
363 * <p>Creates a String representation of this <tt>packages</tt>. Useful for
364 * debugging and logging.</p>
365 *
366 * @return String describes this Rule's packages. Such as
367 * <samp>com.company.dao, com.company.dao.hibernate</samp>
368 */
369 public String describePackages() {
370
371 final StringBuffer packagesDescription = new StringBuffer();
372
373 final Object[] packagesArray = packages.toArray();
374 final int totalPackages = packagesArray.length;
375
376 for (int i = 0; i < totalPackages; i++) {
377
378 final JPackage jPackage = (JPackage) packagesArray[i];
379 final String packageName = jPackage.getPath();
380
381 packagesDescription
382 .append(packageName.trim())
383 .append(" ");
384
385 if (i + 1 < totalPackages)
386 packagesDescription.append(",");
387 }
388
389 final String description = packagesDescription.toString()
390 .replaceAll("//[", "")
391 .replaceAll("//]", "");
392
393 return description;
394 }
395
396
397 /***
398 * <p>Get all of the <tt>violations</tt>.</p>
399 *
400 * <p>Note: this Collection is unmodifiable, use {@link #addViolation} and
401 * {@link #removeViolation}</p>
402 *
403 * @return Collection unmodifiable
404 * @throws UnsupportedOperationException when <code>getViolations.add(Object)</code>
405 * or <code>getViolations.remove(Object)</code> is called. Use {@link
406 * #addViolation} and {@link #removeViolation}.
407 */
408 public Collection<JPackage> getViolations() {
409
410 return Collections.unmodifiableCollection(violations);
411 }
412
413
414 /***
415 * <p>Remove a package from this Rule.</p>
416 *
417 * @param packageName String a package this this Rule's package should not
418 * test on
419 * @return Rule this <code>Rule</code> to allow for method chaining.
420 */
421 public Rule removePackage(final String packageName) {
422
423 Assert.assertNotNull(
424 "null packageName can not be removed",
425 packageName);
426
427 Assert.assertFalse(
428 "empty packageName can not be removed",
429 "".equals(packageName));
430
431 final JPackage jPackage = new JPackage(packageName);
432 final boolean removed = packages.remove(jPackage);
433
434 if (removed) {
435
436 final String debug = format(
437 "removed package %s from Rule %s",
438 packageName, this.id);
439
440 log.debug(debug);
441
442 } else {
443
444 final String warn = format(
445 "failed to remove package %s from Rule %s ",
446 packageName, this.id);
447
448 log.warn(warn);
449 }
450
451 return this;
452 }
453
454
455 /***
456 * <p>Remove a violation from this Rule.</p>
457 *
458 * @param violation String a package this this Rule's package should not
459 * test on
460 * @return Rule this <code>Rule</code> to allow for method chaining.
461 */
462 public Rule removeViolation(final String violation) {
463
464 Assert.assertFalse(
465 "empty violation can not be removed",
466 "".equals(violation));
467
468 final JPackage jPackage = new JPackage(violation);
469 return removeViolation(jPackage);
470 }
471
472
473 public Rule removeViolation(final JPackage violation) {
474
475 Assert.assertNotNull("null violation can not be removed", violation);
476
477 final boolean removed = violations.remove(violation);
478
479 if (removed) {
480
481 final String debug = format(
482 "removed violation %s from Rule %s",
483 violation, this.id);
484
485 log.debug(debug);
486
487 } else {
488
489 final String warn = format(
490 "failed to remove violation %s from Rule %s ",
491 violation, this.id);
492
493 log.warn(warn);
494 }
495
496 return this;
497 }
498
499
500 /***
501 * <p>Setter for property {@link #comment}.</p>
502 *
503 * @param comment Value to set for property <tt>comment</tt>.
504 * @return Rule this <code>Rule</code> to allow for method chaining.
505 */
506 public Rule setComment(final String comment) {
507
508 Assert.assertNotNull("comment can not be null", comment);
509
510 this.comment = comment;
511
512 return this;
513 }
514
515
516 /***
517 * <p>Same as {@link #setId(String)}. The <code>DigesterConfiguraitonFactory</code>
518 * that builds the <code>Configuration<code> class can invoke void setter.
519 * When we added method chaining at version 2.1.0, we made <tt>setId</tt>
520 * return <code>Rule</code>, and the configuration factory broke. So this
521 * method was created to allow for the ConfigurationFactory to work, and for
522 * method chaining to be supported.</p>
523 *
524 * @param id Value to set for property <tt>id</tt>.
525 */
526 public void setIdString(final String id) {
527
528 Assert.assertNotNull("id can not be null", id);
529 Assert.assertFalse("id can not be empty", "".equals(id));
530
531 this.id = id;
532 }
533 }