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