View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.rmi.RemoteException;
4   import java.util.LinkedHashSet;
5   import java.util.Map.Entry;
6   import java.util.Objects;
7   import java.util.Set;
8   import java.util.function.Supplier;
9   
10  import org.djutils.eval.Eval;
11  import org.djutils.event.Event;
12  import org.djutils.event.EventListener;
13  import org.djutils.event.EventType;
14  import org.djutils.event.reference.ReferenceType;
15  import org.djutils.metadata.MetaData;
16  import org.djutils.metadata.ObjectDescriptor;
17  import org.opentrafficsim.base.OtsRuntimeException;
18  import org.opentrafficsim.editor.decoration.validation.KeyValidator;
19  import org.opentrafficsim.editor.decoration.validation.KeyrefValidator;
20  import org.opentrafficsim.editor.decoration.validation.XPathValidator;
21  import org.w3c.dom.Node;
22  
23  /**
24   * Extends {@code XsdTreeNode} with event producer capabilities. In this way there is a clear central point for subscription to
25   * events on node creation and removal. Because this node itself is created, and the tree table model will expand two layers,
26   * for none of those nodes creation events can be thrown regularly after listeners have had a change to register with this root
27   * node. Therefore, this class overrides {@code addListener(...)} to throw a creation event for all existing nodes in the tree.
28   * <br>
29   * <br>
30   * This class also sets up a listener for all xsd:key, xsd:keyref and xsd:unique from the schema.
31   * <p>
32   * Copyright (c) 2023-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
33   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
34   * </p>
35   * @author Wouter Schakel
36   */
37  public class XsdTreeNodeRoot extends XsdTreeNode
38  {
39  
40      /**
41       * Event when a node is created. This event is always thrown by the root of the data structure. Listeners should register
42       * with the root.
43       */
44      public static final EventType NODE_CREATED = new EventType("NODECREATED",
45              new MetaData("Node created", "Created tree node",
46                      new ObjectDescriptor("Node created", "Created tree node", XsdTreeNode.class),
47                      new ObjectDescriptor("Parent", "Parent node", XsdTreeNode.class),
48                      new ObjectDescriptor("Index", "Index where it is in the parent", Integer.class)));
49  
50      /**
51       * Event when a node is removed. Invoked for each individual node, including all child nodes of a node that a user removes.
52       * This event is always thrown by the root of the data structure. Listeners should register with the root.
53       */
54      public static final EventType NODE_REMOVED = new EventType("NODEREMOVED",
55              new MetaData("Node removed", "Removed tree node",
56                      new ObjectDescriptor("Node removed", "Removed tree node", XsdTreeNode.class),
57                      new ObjectDescriptor("Parent", "Parent node", XsdTreeNode.class),
58                      new ObjectDescriptor("Index", "Index where it was in the parent", Integer.class)));
59  
60      /** Directory, relevant for relative paths in include nodes. */
61      private String directory;
62  
63      /** Whether validation should be ignored for the entire tree. */
64      private final Supplier<Boolean> ignoreChanges;
65  
66      /** Eval supplier. */
67      private final Supplier<Eval> evalSupplier;
68  
69      /**
70       * Constructor for root node, based on a schema.
71       * @param schema XSD Schema.
72       * @param ignoreChanges whether to ignore changes
73       * @param evalSupplier eval supplier
74       * @throws RemoteException when unable to listen for created nodes.
75       */
76      public XsdTreeNodeRoot(final Schema schema, final Supplier<Boolean> ignoreChanges, final Supplier<Eval> evalSupplier)
77              throws RemoteException
78      {
79          super(schema);
80          this.ignoreChanges = ignoreChanges;
81          this.evalSupplier = evalSupplier;
82          // pointless to fire NODE_CREATED event, no one can be listening yet
83          setupXPathListener(schema);
84      }
85  
86      /**
87       * Returns the directory.
88       * @return directory.
89       */
90      public String getDirectory()
91      {
92          return this.directory;
93      }
94  
95      /**
96       * Set the directory.
97       * @param directory directory.
98       */
99      public void setDirectory(final String directory)
100     {
101         if (Objects.equals(this.directory, directory))
102         {
103             return;
104         }
105         this.directory = directory;
106         // invalidate entire tree, as saving may trigger relative paths to includes to become ok, causing types to be found
107         invalidateAll();
108     }
109 
110     @Override
111     public XsdTreeNodeRoot getRoot()
112     {
113         return this;
114     }
115 
116     /**
117      * Returns the evaluator.
118      * @return evaluator
119      */
120     public Eval getEval()
121     {
122         return this.evalSupplier.get();
123     }
124 
125     /**
126      * {@inheritDoc} Overridden to throw events on existing nodes to the listener.
127      */
128     @Override
129     public boolean addListener(final EventListener listener, final EventType eventType, final int position,
130             final ReferenceType referenceType)
131     {
132         if (NODE_CREATED.equals(eventType))
133         {
134             try
135             {
136                 XsdTreeNodeUtil.fireCreatedEventOnExistingNodes(this, listener);
137             }
138             catch (RemoteException exception)
139             {
140                 throw new OtsRuntimeException("Unexpected remote exception in local context.", exception);
141             }
142         }
143         return super.addListener(listener, eventType, position, referenceType);
144     }
145 
146     /**
147      * Sets up the listener that reports on new and removed nodes for each xsd:key, xsd:keyref and xsd:unique. It is up to each
148      * key to determine whether the node is relevant for the key.
149      * @param schema schema.
150      * @throws RemoteException when unable to listen for created nodes.
151      */
152     private void setupXPathListener(final Schema schema) throws RemoteException
153     {
154         Set<KeyValidator> keys = new LinkedHashSet<>();
155         for (Entry<String, Node> entry : schema.keys().entrySet())
156         {
157             String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
158             keys.add(new KeyValidator(entry.getValue(), path, this.ignoreChanges));
159         }
160         Set<KeyValidator> uniques = new LinkedHashSet<>();
161         for (Entry<String, Node> entry : schema.uniques().entrySet())
162         {
163             String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
164             uniques.add(new KeyValidator(entry.getValue(), path, this.ignoreChanges));
165         }
166         Set<KeyrefValidator> keyrefs = new LinkedHashSet<>();
167         for (Entry<String, Node> entry : schema.keyrefs().entrySet())
168         {
169             String keyName = DocumentReader.getAttribute(entry.getValue(), "refer").get().replace("ots:", "");
170             for (KeyValidator key : keys)
171             {
172                 if (key.getKeyName().equals(keyName))
173                 {
174                     String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
175                     keyrefs.add(new KeyrefValidator(entry.getValue(), path, key, this.ignoreChanges));
176                     break;
177                 }
178             }
179             for (KeyValidator unique : uniques)
180             {
181                 if (unique.getKeyName().equals(keyName))
182                 {
183                     String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
184                     keyrefs.add(new KeyrefValidator(entry.getValue(), path, unique, this.ignoreChanges));
185                     break;
186                 }
187             }
188         }
189 
190         EventListener listener = new EventListener()
191         {
192             @Override
193             public void notify(final Event event)
194             {
195                 // loop keys, keyrefs and uniques to add or remove the node (key, keyref or unique determines node relevance).
196                 int iteration = 0;
197                 Set<? extends XPathValidator> keysIteration = keys;
198                 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
199                 while (iteration < 3)
200                 {
201                     for (XPathValidator key : keysIteration)
202                     {
203                         if (event.getType().equals(NODE_CREATED))
204                         {
205                             key.addNode(node);
206                         }
207                         else
208                         {
209                             key.removeNode(node);
210                         }
211                     }
212                     keysIteration = iteration == 0 ? keyrefs : uniques;
213                     iteration++;
214                 }
215             }
216         };
217 
218         addListener(listener, NODE_CREATED);
219         addListener(listener, NODE_REMOVED);
220     }
221 
222 }