View Javadoc
1   package org.opentrafficsim.editor.decoration.validation;
2   
3   import java.util.ArrayList;
4   import java.util.Arrays;
5   import java.util.LinkedHashMap;
6   import java.util.LinkedHashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Map.Entry;
10  import java.util.Set;
11  
12  import org.djutils.exceptions.Throw;
13  import org.opentrafficsim.editor.DocumentReader;
14  import org.opentrafficsim.editor.XsdTreeNode;
15  import org.w3c.dom.Node;
16  
17  /**
18   * Validator for xsd:keyref, which allows to define multiple fields. This class will maintain a list of nodes (fed by an
19   * external listener) and validate that the field values are, as a set, within the given {@code KeyValidator}.
20   * <p>
21   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
22   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
23   * </p>
24   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25   */
26  public class KeyrefValidator extends XPathValidator implements CoupledValidator
27  {
28  
29      /** Key that is referred to by an xsd:keyref. */
30      private final KeyValidator refer;
31  
32      /** Nodes who's value has to match some field in this validator. */
33      private final Set<XsdTreeNode> valueValidating = new LinkedHashSet<>();
34  
35      /** Nodes who's attribute has to match some field in this validator, grouped per attribute name. */
36      private final Map<String, Set<XsdTreeNode>> attributeValidating = new LinkedHashMap<>();
37  
38      /** Mapping of node coupled by this keyref validator, to the key node its coupled with in some other key validator. */
39      private final Map<XsdTreeNode, XsdTreeNode> coupledKeyrefNodes = new LinkedHashMap<>();
40  
41      /**
42       * Constructor.
43       * @param keyNode Node; node defining the xsd:keyref.
44       * @param keyPath String; path where the keyref was defined.
45       * @param refer KeyValidator; key that is referred to by this xsd:keyref.
46       */
47      public KeyrefValidator(final Node keyNode, final String keyPath, final KeyValidator refer)
48      {
49          super(keyNode, keyPath);
50          Throw.when(!keyNode.getNodeName().equals("xsd:keyref"), IllegalArgumentException.class,
51                  "The given node is not an xsd:keyref node.");
52          Throw.whenNull(refer, "Refer validator may not be null.");
53          String referName = DocumentReader.getAttribute(keyNode, "refer").replace("ots:", "");
54          Throw.when(!referName.equals(refer.getKeyName()), IllegalArgumentException.class,
55                  "The key node refers to key/unique %s, but the provided refer validator has name %s.", referName,
56                  refer.getKeyName());
57          this.refer = refer;
58          refer.addListeningKeyrefValidator(this);
59      }
60  
61      /** {@inheritDoc} */
62      @Override
63      public void addNode(final XsdTreeNode node)
64      {
65          for (int fieldIndex = 0; fieldIndex < this.fields.size(); fieldIndex++)
66          {
67              Field field = this.fields.get(fieldIndex);
68              int pathIndex = field.getValidPathIndex(node);
69              if (pathIndex >= 0)
70              {
71                  String path = field.getFieldPath(pathIndex);
72                  int attr = path.indexOf("@");
73                  if (attr < 0)
74                  {
75                      node.addValueValidator(this, field);
76                      this.valueValidating.add(node);
77                  }
78                  else
79                  {
80                      String attribute = path.substring(attr + 1);
81                      node.addAttributeValidator(attribute, this, field);
82                      this.attributeValidating.computeIfAbsent(attribute, (n) -> new LinkedHashSet<>()).add(node);
83                  }
84              }
85          }
86      }
87  
88      /** {@inheritDoc} */
89      @Override
90      public void removeNode(final XsdTreeNode node)
91      {
92          this.valueValidating.remove(node);
93          this.attributeValidating.values().forEach((s) -> s.remove(node));
94          this.coupledKeyrefNodes.remove(node);
95      }
96  
97      /** {@inheritDoc} */
98      @Override
99      public String validate(final XsdTreeNode node)
100     {
101         if (node.getParent() == null)
102         {
103             return null; // Node was deleted, but is still visible in the GUI tree for a moment
104         }
105         List<String> values = gatherFieldValues(node);
106         if (values.stream().allMatch((v) -> v == null))
107         {
108             return null;
109         }
110         // xsd:keyref referred value is present ?
111         Map<XsdTreeNode, List<String>> valueMap = this.refer.getAllValueSets(node);
112         XsdTreeNode matched = null;
113         for (Entry<XsdTreeNode, List<String>> entry : valueMap.entrySet())
114         {
115             if (matchingKeyref(entry.getValue(), values))
116             {
117                 if (matched != null)
118                 {
119                     // duplicate match based on subset of values (there are null's), do not couple but also do not invalidate
120                     this.coupledKeyrefNodes.remove(node);
121                     return null;
122                 }
123                 matched = entry.getKey();
124             }
125         }
126         if (matched != null)
127         {
128             this.coupledKeyrefNodes.put(node, matched);
129             return null;
130         }
131         // not matched
132         this.coupledKeyrefNodes.remove(node);
133         String[] types = this.refer.getSelectorTypeString();
134         String typeString = user(types.length == 1 ? types[0] : Arrays.asList(types).toString());
135         if (values.size() == 1)
136         {
137             String value = values.get(0);
138             String name = user(this.fields.get(0).getFullFieldName());
139             return "Value " + value + " for " + name + " does not refer to a known and unique " + typeString + " within "
140                     + this.keyPath + ".";
141         }
142         values.removeIf((value) -> value != null && value.startsWith("{") && value.endsWith("}")); // expressions
143         return "Values " + values + " do not refer to a known and unique " + typeString + " within " + this.keyPath + ".";
144     }
145 
146     /**
147      * Checks that a set of values in a key, matches the values in a keyref node. Note, in dealing with null values the two sets
148      * should <b>not</b> be given in the wrong order. It does not matter whether the keyref refers to a key or unique. In both
149      * cases the values from a keyref are a match if all its non-null values match respective values in the key.
150      * @param keyValues List&lt;String&gt;; set of values from a key node.
151      * @param keyrefValues List&lt;String&gt;; set of values from a keyref node.
152      * @return boolean; whether the key values match the keyref values.
153      */
154     private boolean matchingKeyref(final List<String> keyValues, final List<String> keyrefValues)
155     {
156         for (int i = 0; i < keyValues.size(); i++)
157         {
158             if (keyrefValues.get(i) != null && !keyrefValues.get(i).equals(keyValues.get(i)))
159             {
160                 return false;
161             }
162         }
163         return true;
164     }
165 
166     /** {@inheritDoc} */
167     @Override
168     public List<String> getOptions(final XsdTreeNode node, final Object field)
169     {
170         /*
171          * We gather values from the referred xsd:key, drawing the appropriate context from the node relevant somewhere in the
172          * xsd:keyref context. The xsd:keyref may not have a more specific context than the xsd:key. If the xsd:keyref has a
173          * bigger context, there may be only one instance of a more specific context of the xsd:key. If both contexts are the
174          * same, this is trivially ok.
175          */
176         XsdTreeNode contextKeyref = getContext(node);
177         XsdTreeNode contextKey = this.refer.getContext(node); // can be null when out of context
178         boolean uniqueScope = contextKeyref.equals(contextKey);
179         if (!uniqueScope)
180         {
181             List<XsdTreeNode> contextKeyPath = contextKey.getPath();
182             if (contextKeyPath.contains(contextKeyref)) // xsd:key more specific, i.e. longer path
183             {
184                 contextKeyPath.removeAll(contextKeyref.getPath());
185                 Set<XsdTreeNode> containedKeyScopes = new LinkedHashSet<>();
186                 gatherScopes(contextKeyref, contextKeyPath, containedKeyScopes);
187                 uniqueScope = containedKeyScopes.size() == 1;
188             }
189         }
190         if (!uniqueScope)
191         {
192             return null;
193         }
194         Map<XsdTreeNode, List<String>> values = this.refer.getAllValueSets(node);
195         List<String> result = new ArrayList<>(values.size());
196         int index = this.fields.indexOf(field);
197         values.forEach((n, list) -> result.add(list.get(index)));
198         result.removeIf((v) -> v == null || v.isEmpty());
199         return result;
200     }
201 
202     /**
203      * Gathers xsd:key-level contexts from an xsd:keyref context that is larger than the key's.
204      * @param node XsdTreeNode; current node to browse the children of, or return in the set.
205      * @param remainingPath List&lt;XsdTreeNode&gt;; remaining intermediate levels, starting with the sub-level of the keyref.
206      * @param set Set&lt;XsdTreeNode&gt;; set to gather contexts in.
207      */
208     private void gatherScopes(final XsdTreeNode node, final List<XsdTreeNode> remainingPath, final Set<XsdTreeNode> set)
209     {
210         String path = node.getPathString() + "." + remainingPath.get(0);
211         for (XsdTreeNode child : node.getChildren())
212         {
213             if (child.getPathString().equals(path))
214             {
215                 if (remainingPath.size() == 1)
216                 {
217                     set.add(child);
218                 }
219                 else
220                 {
221                     gatherScopes(child, remainingPath.subList(1, remainingPath.size()), set);
222                 }
223             }
224         }
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public XsdTreeNode getCoupledKeyrefNode(final XsdTreeNode node)
230     {
231         return this.coupledKeyrefNodes.get(node);
232     }
233 
234     /**
235      * Update value as it was changed at the key.
236      * @param node XsdTreeNode; node on which the value was changed.
237      * @param fieldIndex int; index of field that was changed.
238      * @param newValue String; new value.
239      */
240     public void updateFieldValue(final XsdTreeNode node, final int fieldIndex, final String newValue)
241     {
242         for (Entry<XsdTreeNode, XsdTreeNode> entry : this.coupledKeyrefNodes.entrySet())
243         {
244             if (entry.getValue().equals(node))
245             {
246                 if (this.valueValidating.contains(entry.getKey()))
247                 {
248                     CoupledValidator.setValueIfNotNull(entry.getKey(), newValue);
249                 }
250                 else
251                 {
252                     for (Entry<String, Set<XsdTreeNode>> attrEntry : this.attributeValidating.entrySet())
253                     {
254                         if (attrEntry.getValue().contains(entry.getKey()))
255                         {
256                             int index = this.fields.get(fieldIndex).getValidPathIndex(entry.getKey());
257                             if (index >= 0)
258                             {
259                                 String field = this.fields.get(fieldIndex).getFieldPath(index);
260                                 int attr = field.indexOf("@");
261                                 String attribute = field.substring(attr + 1);
262                                 CoupledValidator.setAttributeIfNotNull(entry.getKey(), attribute, newValue);
263                             }
264                         }
265                     }
266                 }
267             }
268         }
269     }
270 
271     /**
272      * Invalidates all nodes that are validating through this keyref. Used by a key validator to notify when nodes are added or
273      * removed, activated or deactivated, or an attribute or value is changed, possibly making any keyref valid or invalid.
274      */
275     public void invalidateNodes()
276     {
277         for (XsdTreeNode node : this.valueValidating)
278         {
279             node.invalidate();
280         }
281         for (Set<XsdTreeNode> set : this.attributeValidating.values())
282         {
283             for (XsdTreeNode node : set)
284             {
285                 node.invalidate();
286             }
287         }
288     }
289 
290 }