View Javadoc
1   package com.atlassian.plugin.util.validation;
2   
3   import org.dom4j.Node;
4   
5   import java.util.ArrayList;
6   import java.util.List;
7   
8   import static com.google.common.base.Preconditions.checkNotNull;
9   
10  /**
11   * Validates a pattern of rules against a dom4j node, patterned off of
12   * <a href="http://www.schematron.com/">Schematron</a>
13   *
14   * @since 2.2.0
15   */
16  public class ValidationPattern {
17      private final List<Rule> rules = new ArrayList<>();
18  
19      private ValidationPattern() {
20      }
21  
22      /**
23       * @return a new pattern instance
24       */
25      public static ValidationPattern createPattern() {
26          return new ValidationPattern();
27      }
28  
29      /**
30       * Adds a rule to the current pattern
31       *
32       * @param context The xpath expression to determine one or more nodes to evaluate the rules against
33       * @param tests   A series of tests
34       * @return this for chaining
35       */
36      public ValidationPattern rule(String context, RuleTest... tests) {
37          rules.add(new Rule(context, tests));
38          return this;
39      }
40  
41      /**
42       * Adds a rule to the current pattern, assuming the current context is "."
43       *
44       * @param tests A series of tests
45       * @return this for chaining
46       */
47      public ValidationPattern rule(RuleTest... tests) {
48          rules.add(new Rule(".", tests));
49          return this;
50      }
51  
52      /**
53       * Evaluates the rules against the provided node
54       *
55       * @param node The node to evaluate
56       * @throws ValidationException If a validation error occurs. If wanting to resolve i18n keys
57       *                             to messages, you can access the list of errors from the exception.
58       */
59      public void evaluate(Node node) throws ValidationException {
60          List<String> errors = new ArrayList<>();
61          for (Rule rule : rules) {
62              rule.evaluate(node, errors);
63          }
64          if (!errors.isEmpty()) {
65              if (errors.size() == 1) {
66                  throw new ValidationException(errors.get(0), errors);
67              } else {
68                  StringBuilder sb = new StringBuilder();
69                  sb.append("There were validation errors:\n");
70                  for (String msg : errors) {
71                      sb.append("\t- ").append(msg).append("\n");
72                  }
73                  throw new ValidationException(sb.toString(), errors);
74              }
75          }
76      }
77  
78      /**
79       * Creates a test using the passed xpath expression
80       *
81       * @param xpath The test expression
82       * @return The rule to mutate
83       */
84      public static RuleTest test(String xpath) {
85          return new RuleTest(xpath);
86      }
87  
88      /**
89       * A test within a rule
90       */
91      public static class RuleTest {
92          private final String xpath;
93          private String errorMessage;
94  
95          private RuleTest(String xpath) {
96              this.xpath = checkNotNull(xpath);
97          }
98  
99          /**
100          * The error message to use in the thrown exception if the test failes
101          *
102          * @param msg The message
103          * @return this for chaining
104          */
105         public RuleTest withError(String msg) {
106             this.errorMessage = msg;
107             return this;
108         }
109 
110         private void evaluate(Node ctxNode, List<String> errors) {
111             Object obj = ctxNode.selectObject(xpath);
112             if (obj == null) {
113                 errors.add(errorMessage + ": " + ctxNode.asXML());
114             } else if (obj instanceof Boolean && !((Boolean) obj)) {
115                 errors.add(errorMessage + ": " + ctxNode.asXML());
116             } else if (obj instanceof List && ((List<?>) obj).isEmpty()) {
117                 errors.add(errorMessage + ": " + ctxNode.asXML());
118             }
119         }
120     }
121 
122     /**
123      * The rule as a series of tests
124      */
125     public static class Rule {
126         private final String contextPattern;
127         private final RuleTest[] tests;
128 
129         private Rule(String contextPattern, RuleTest[] tests) {
130             this.contextPattern = checkNotNull(contextPattern);
131             this.tests = checkNotNull(tests);
132         }
133 
134         private void evaluate(Node e, List<String> errors) {
135             @SuppressWarnings("unchecked")
136             List<Node> contexts = e.selectNodes(contextPattern);
137 
138             if (contexts != null && contexts.size() > 0) {
139                 for (Node ctxNode : contexts) {
140                     for (RuleTest test : tests) {
141                         test.evaluate(ctxNode, errors);
142                     }
143                 }
144             }
145         }
146     }
147 
148 }