View Javadoc
1   /**
2    * Copyright (C) 2014-2017 Philip Helger (www.helger.com)
3    * philip[at]helger[dot]com
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *         http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.helger.schematron.pure.model;
18  
19  import java.util.Map;
20  
21  import javax.annotation.Nonnegative;
22  import javax.annotation.Nonnull;
23  import javax.annotation.Nullable;
24  import javax.annotation.concurrent.NotThreadSafe;
25  
26  import com.helger.commons.ValueEnforcer;
27  import com.helger.commons.annotation.ReturnsMutableCopy;
28  import com.helger.commons.collection.CollectionHelper;
29  import com.helger.commons.collection.ext.CommonsArrayList;
30  import com.helger.commons.collection.ext.CommonsLinkedHashMap;
31  import com.helger.commons.collection.ext.ICommonsList;
32  import com.helger.commons.collection.ext.ICommonsOrderedMap;
33  import com.helger.commons.string.StringHelper;
34  import com.helger.commons.string.ToStringGenerator;
35  import com.helger.schematron.CSchematron;
36  import com.helger.schematron.CSchematronXML;
37  import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
38  import com.helger.xml.microdom.IMicroElement;
39  import com.helger.xml.microdom.MicroElement;
40  
41  /**
42   * A single Schematron rule-element.<br>
43   * A list of assertions tested within the context specified by the required
44   * context attribute. The context attribute specifies the rule context
45   * expression.<br>
46   * NOTE: It is not an error if a rule never fires in a document. In order to
47   * test that a document always has some context, a new pattern should be created
48   * from the context of the document, with an assertion requiring the element or
49   * attribute.<br>
50   * The icon, see and fpi attributes allow rich interfaces and documentation.<br>
51   * The flag attribute allows more detailed outcomes.<br>
52   * The role and subject attributes allow explicit identification of some part of
53   * a pattern as part of the validation outcome.<br>
54   * When the rule element has the attribute abstract with a value true, then the
55   * rule is an abstract rule. An abstract rule shall not have a context
56   * attribute. An abstract rule is a list of assertions that will be invoked by
57   * other rules belonging to the same pattern using the extends element. Abstract
58   * rules provide a mechanism for reducing schema size.
59   *
60   * @author Philip Helger
61   */
62  @NotThreadSafe
63  public class PSRule implements
64                      IPSElement,
65                      IPSHasID,
66                      IPSHasFlag,
67                      IPSHasForeignElements,
68                      IPSHasIncludes,
69                      IPSHasLets,
70                      IPSHasRichGroup,
71                      IPSHasLinkableGroup
72  {
73    public static final boolean DEFAULT_ABSTRACT = false;
74  
75    private String m_sFlag;
76    private PSRichGroup m_aRich;
77    private PSLinkableGroup m_aLinkable;
78    private boolean m_bAbstract = DEFAULT_ABSTRACT;
79    private String m_sContext;
80    private String m_sID;
81    private final ICommonsList <PSInclude> m_aIncludes = new CommonsArrayList <> ();
82    private final ICommonsList <PSLet> m_aLets = new CommonsArrayList <> ();
83    private final ICommonsList <IPSElement> m_aContent = new CommonsArrayList <> ();
84    private ICommonsOrderedMap <String, String> m_aForeignAttrs;
85    private ICommonsList <IMicroElement> m_aForeignElements;
86  
87    public PSRule ()
88    {}
89  
90    public boolean isValid (@Nonnull final IPSErrorHandler aErrorHandler)
91    {
92      // abstract rules need an ID
93      if (m_bAbstract && StringHelper.hasNoText (m_sID))
94      {
95        aErrorHandler.error (this, "abstract <rule> has no 'id'");
96        return false;
97      }
98      // abstract rules may not have a context
99      if (m_bAbstract && StringHelper.hasText (m_sContext))
100     {
101       aErrorHandler.error (this, "abstract <rule> may not have a 'context'");
102       return false;
103     }
104     // Non-abstract rules need a context
105     if (!m_bAbstract && StringHelper.hasNoText (m_sContext))
106     {
107       aErrorHandler.error (this, "<rule> must have a 'context'");
108       return false;
109     }
110     // At least one assert, report or extends must be present
111     if (m_aContent.isEmpty ())
112     {
113       aErrorHandler.error (this, "<rule> has no content");
114       return false;
115     }
116     for (final PSInclude aInclude : m_aIncludes)
117       if (!aInclude.isValid (aErrorHandler))
118         return false;
119     for (final PSLet aLet : m_aLets)
120       if (!aLet.isValid (aErrorHandler))
121         return false;
122     for (final IPSElement aContent : m_aContent)
123       if (!aContent.isValid (aErrorHandler))
124         return false;
125     return true;
126   }
127 
128   public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
129   {
130     // abstract rules need an ID
131     if (m_bAbstract && StringHelper.hasNoText (m_sID))
132       aErrorHandler.error (this, "abstract <rule> has no 'id'");
133     // abstract rules may not have a context
134     if (m_bAbstract && StringHelper.hasText (m_sContext))
135       aErrorHandler.error (this, "abstract <rule> may not have a 'context'");
136     // Non-abstract rules need a context
137     if (!m_bAbstract && StringHelper.hasNoText (m_sContext))
138       aErrorHandler.error (this, "<rule> must have a 'context'");
139     // At least one assert, report or extends must be present
140     if (m_aContent.isEmpty ())
141       aErrorHandler.error (this, "<rule> has no content");
142     for (final PSInclude aInclude : m_aIncludes)
143       aInclude.validateCompletely (aErrorHandler);
144     for (final PSLet aLet : m_aLets)
145       aLet.validateCompletely (aErrorHandler);
146     for (final IPSElement aContent : m_aContent)
147       aContent.validateCompletely (aErrorHandler);
148   }
149 
150   public boolean isMinimal ()
151   {
152     for (final PSInclude aInclude : m_aIncludes)
153       if (!aInclude.isMinimal ())
154         return false;
155     for (final PSLet aLet : m_aLets)
156       if (!aLet.isMinimal ())
157         return false;
158     for (final IPSElement aContent : m_aContent)
159       if (!aContent.isMinimal ())
160         return false;
161     return true;
162   }
163 
164   public void addForeignElement (@Nonnull final IMicroElement aForeignElement)
165   {
166     ValueEnforcer.notNull (aForeignElement, "ForeignElement");
167     if (aForeignElement.hasParent ())
168       throw new IllegalArgumentException ("ForeignElement already has a parent!");
169     if (m_aForeignElements == null)
170       m_aForeignElements = new CommonsArrayList <> ();
171     m_aForeignElements.add (aForeignElement);
172   }
173 
174   public boolean hasForeignElements ()
175   {
176     return m_aForeignElements != null && m_aForeignElements.isNotEmpty ();
177   }
178 
179   @Nonnull
180   @ReturnsMutableCopy
181   public ICommonsList <IMicroElement> getAllForeignElements ()
182   {
183     return new CommonsArrayList <> (m_aForeignElements);
184   }
185 
186   public void addForeignAttribute (@Nonnull final String sAttrName, @Nonnull final String sAttrValue)
187   {
188     ValueEnforcer.notNull (sAttrName, "AttrName");
189     ValueEnforcer.notNull (sAttrValue, "AttrValue");
190     if (m_aForeignAttrs == null)
191       m_aForeignAttrs = new CommonsLinkedHashMap <> ();
192     m_aForeignAttrs.put (sAttrName, sAttrValue);
193   }
194 
195   public boolean hasForeignAttributes ()
196   {
197     return m_aForeignAttrs != null && m_aForeignAttrs.isNotEmpty ();
198   }
199 
200   @Nonnull
201   @ReturnsMutableCopy
202   public ICommonsOrderedMap <String, String> getAllForeignAttributes ()
203   {
204     return new CommonsLinkedHashMap <> (m_aForeignAttrs);
205   }
206 
207   public void setFlag (@Nullable final String sFlag)
208   {
209     m_sFlag = sFlag;
210   }
211 
212   @Nullable
213   public String getFlag ()
214   {
215     return m_sFlag;
216   }
217 
218   public void setRich (@Nullable final PSRichGroup aRich)
219   {
220     m_aRich = aRich;
221   }
222 
223   @Nullable
224   public PSRichGroup getRich ()
225   {
226     return m_aRich;
227   }
228 
229   public void setLinkable (@Nullable final PSLinkableGroup aLinkable)
230   {
231     m_aLinkable = aLinkable;
232   }
233 
234   @Nullable
235   public PSLinkableGroup getLinkable ()
236   {
237     return m_aLinkable;
238   }
239 
240   /**
241    * @param bAbstract
242    *        The abstract state of this rule.
243    */
244   public void setAbstract (final boolean bAbstract)
245   {
246     m_bAbstract = bAbstract;
247   }
248 
249   /**
250    * @return <code>true</code> if this rule is abstract, <code>false</code>
251    *         otherwise. Default is {@value #DEFAULT_ABSTRACT}.
252    */
253   public boolean isAbstract ()
254   {
255     return m_bAbstract;
256   }
257 
258   public void setContext (@Nullable final String sContext)
259   {
260     m_sContext = sContext;
261   }
262 
263   @Nullable
264   public String getContext ()
265   {
266     return m_sContext;
267   }
268 
269   public void setID (@Nullable final String sID)
270   {
271     m_sID = sID;
272   }
273 
274   @Nullable
275   public String getID ()
276   {
277     return m_sID;
278   }
279 
280   public void addInclude (@Nonnull final PSInclude aInclude)
281   {
282     ValueEnforcer.notNull (aInclude, "Include");
283     m_aIncludes.add (aInclude);
284   }
285 
286   public boolean hasAnyInclude ()
287   {
288     return m_aIncludes.isNotEmpty ();
289   }
290 
291   @Nonnull
292   @ReturnsMutableCopy
293   public ICommonsList <PSInclude> getAllIncludes ()
294   {
295     return m_aIncludes.getClone ();
296   }
297 
298   public void addLet (@Nonnull final PSLet aLet)
299   {
300     ValueEnforcer.notNull (aLet, "Let");
301     m_aLets.add (aLet);
302   }
303 
304   public boolean hasAnyLet ()
305   {
306     return m_aLets.isNotEmpty ();
307   }
308 
309   @Nonnull
310   @ReturnsMutableCopy
311   public ICommonsList <PSLet> getAllLets ()
312   {
313     return m_aLets.getClone ();
314   }
315 
316   @Nonnull
317   @ReturnsMutableCopy
318   public ICommonsOrderedMap <String, String> getAllLetsAsMap ()
319   {
320     final ICommonsOrderedMap <String, String> ret = new CommonsLinkedHashMap <> ();
321     for (final PSLet aLet : m_aLets)
322       ret.put (aLet.getName (), aLet.getValue ());
323     return ret;
324   }
325 
326   public void addAssertReport (@Nonnull final PSAssertReport aAssertReport)
327   {
328     ValueEnforcer.notNull (aAssertReport, "AssertReport");
329     m_aContent.add (aAssertReport);
330   }
331 
332   @Nonnull
333   @ReturnsMutableCopy
334   public ICommonsList <PSAssertReport> getAllAssertReports ()
335   {
336     return m_aContent.getAllInstanceOf (PSAssertReport.class);
337   }
338 
339   public void addExtends (@Nonnull final PSExtends aExtends)
340   {
341     ValueEnforcer.notNull (aExtends, "Extends");
342     m_aContent.add (aExtends);
343   }
344 
345   @Nonnull
346   @ReturnsMutableCopy
347   public ICommonsList <PSExtends> getAllExtends ()
348   {
349     return m_aContent.getAllInstanceOf (PSExtends.class);
350   }
351 
352   @Nonnegative
353   public int getExtendsCount ()
354   {
355     return m_aContent.getCount (e -> e instanceof PSExtends);
356   }
357 
358   public boolean hasAnyExtends ()
359   {
360     return m_aContent.containsAny (e -> e instanceof PSExtends);
361   }
362 
363   /**
364    * @return A list consisting of {@link PSAssertReport} and {@link PSExtends}
365    *         parameters
366    */
367   @Nonnull
368   @ReturnsMutableCopy
369   public ICommonsList <IPSElement> getAllContentElements ()
370   {
371     return m_aContent.getClone ();
372   }
373 
374   @Nonnull
375   public IMicroElement getAsMicroElement ()
376   {
377     final IMicroElement ret = new MicroElement (CSchematron.NAMESPACE_SCHEMATRON, CSchematronXML.ELEMENT_RULE);
378     ret.setAttribute (CSchematronXML.ATTR_FLAG, m_sFlag);
379     if (m_bAbstract)
380       ret.setAttribute (CSchematronXML.ATTR_ABSTRACT, "true");
381     ret.setAttribute (CSchematronXML.ATTR_CONTEXT, m_sContext);
382     ret.setAttribute (CSchematronXML.ATTR_ID, m_sID);
383     if (m_aRich != null)
384       m_aRich.fillMicroElement (ret);
385     if (m_aLinkable != null)
386       m_aLinkable.fillMicroElement (ret);
387     if (m_aForeignElements != null)
388       for (final IMicroElement aForeignElement : m_aForeignElements)
389         ret.appendChild (aForeignElement.getClone ());
390     for (final PSInclude aInclude : m_aIncludes)
391       ret.appendChild (aInclude.getAsMicroElement ());
392     for (final PSLet aLet : m_aLets)
393       ret.appendChild (aLet.getAsMicroElement ());
394     for (final IPSElement aContent : m_aContent)
395       ret.appendChild (aContent.getAsMicroElement ());
396     if (m_aForeignAttrs != null)
397       for (final Map.Entry <String, String> aEntry : m_aForeignAttrs.entrySet ())
398         ret.setAttribute (aEntry.getKey (), aEntry.getValue ());
399     return ret;
400   }
401 
402   @Override
403   public String toString ()
404   {
405     return new ToStringGenerator (this).appendIfNotNull ("flag", m_sFlag)
406                                        .appendIfNotNull ("rich", m_aRich)
407                                        .appendIfNotNull ("linkable", m_aLinkable)
408                                        .append ("abstract", m_bAbstract)
409                                        .appendIfNotNull ("context", m_sContext)
410                                        .appendIfNotNull ("id", m_sID)
411                                        .appendIf ("includes", m_aIncludes, CollectionHelper::isNotEmpty)
412                                        .appendIf ("lets", m_aLets, CollectionHelper::isNotEmpty)
413                                        .appendIf ("content", m_aContent, CollectionHelper::isNotEmpty)
414                                        .appendIf ("foreignAttrs", m_aForeignAttrs, CollectionHelper::isNotEmpty)
415                                        .appendIf ("foreignElements", m_aForeignElements, CollectionHelper::isNotEmpty)
416                                        .getToString ();
417   }
418 }