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