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 XsdSchema; 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 String; directory.
81       */
82      public String getDirectory()
83      {
84          return this.directory;
85      }
86  
87      /**
88       * Set the directory.
89       * @param directory String; 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     /** {@inheritDoc} */
103     @Override
104     public XsdTreeNodeRoot getRoot()
105     {
106         return this;
107     }
108 
109     /**
110      * {@inheritDoc} Overridden to throw events on existing nodes to the listener.
111      */
112     @Override
113     public boolean addListener(final EventListener listener, final EventType eventType, final int position,
114             final ReferenceType referenceType)
115     {
116         if (NODE_CREATED.equals(eventType))
117         {
118             try
119             {
120                 XsdTreeNodeUtil.fireCreatedEventOnExistingNodes(this, listener);
121             }
122             catch (RemoteException exception)
123             {
124                 throw new RuntimeException("Unexpected remote exception in local context.", exception);
125             }
126         }
127         return super.addListener(listener, eventType, position, referenceType);
128     }
129 
130     /**
131      * 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
132      * key to determine whether the node is relevant for the key.
133      * @param schema XsdSchema; schema.
134      * @throws RemoteException when unable to listen for created nodes.
135      */
136     private void setupXPathListener(final Schema schema) throws RemoteException
137     {
138 
139         Set<KeyValidator> keys = new LinkedHashSet<>();
140         for (Entry<String, Node> entry : schema.keys().entrySet())
141         {
142             String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
143             keys.add(new KeyValidator(entry.getValue(), path));
144         }
145         Set<KeyValidator> uniques = new LinkedHashSet<>();
146         for (Entry<String, Node> entry : schema.uniques().entrySet())
147         {
148             String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
149             uniques.add(new KeyValidator(entry.getValue(), path));
150         }
151         Set<KeyrefValidator> keyrefs = new LinkedHashSet<>();
152         for (Entry<String, Node> entry : schema.keyrefs().entrySet())
153         {
154             String keyName = DocumentReader.getAttribute(entry.getValue(), "refer").replace("ots:", "");
155             for (KeyValidator key : keys)
156             {
157                 if (key.getKeyName().equals(keyName))
158                 {
159                     String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
160                     keyrefs.add(new KeyrefValidator(entry.getValue(), path, key));
161                     break;
162                 }
163             }
164             for (KeyValidator unique : uniques)
165             {
166                 if (unique.getKeyName().equals(keyName))
167                 {
168                     String path = entry.getKey().substring(0, entry.getKey().lastIndexOf("."));
169                     keyrefs.add(new KeyrefValidator(entry.getValue(), path, unique));
170                     break;
171                 }
172             }
173         }
174         
175 
176         EventListener listener = new EventListener()
177         {
178             /** */
179             private static final long serialVersionUID = 20230228L;
180 
181             /** {@inheritDoc} */
182             @Override
183             public void notify(final Event event) throws RemoteException
184             {
185                 int iteration = 0;
186                 Set<? extends XPathValidator> keysIteration = keys;
187                 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
188                 while (iteration < 3)
189                 {
190                     for (XPathValidator key : keysIteration)
191                     {
192                         if (event.getType().equals(NODE_CREATED))
193                         {
194                             key.addNode(node);
195                         }
196                         else
197                         {
198                             key.removeNode(node);
199                         }
200                     }
201                     keysIteration = iteration == 0 ? keyrefs : uniques;
202                     iteration++;
203                 }
204             }
205         };
206 
207         addListener(listener, NODE_CREATED);
208         addListener(listener, NODE_REMOVED);
209     }
210 
211     /**
212      * Sets the scehame location. This can't happen through the editor, but it may be set when loading a file.
213      * @param schemaLocation String; schema location.
214      */
215     public void setSchemaLocation(final String schemaLocation)
216     {
217         this.schemaLocation = schemaLocation;
218     }
219     
220     /**
221      * Returns the schema location. If it is not {@code null} it was set during file loading, and will be saved too. 
222      * @return String; schema location.
223      */
224     public String getSchemaLocation()
225     {
226         return this.schemaLocation;
227     }
228 
229 }