View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.repository.sequencer;
25  
26  import java.io.ByteArrayInputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.util.Collection;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.concurrent.atomic.AtomicInteger;
37  import org.modeshape.common.collection.Collections;
38  import org.modeshape.common.collection.Problems;
39  import org.modeshape.common.util.Logger;
40  import org.modeshape.common.util.Reflection;
41  import org.modeshape.graph.Graph;
42  import org.modeshape.graph.JcrLexicon;
43  import org.modeshape.graph.JcrNtLexicon;
44  import org.modeshape.graph.Location;
45  import org.modeshape.graph.Node;
46  import org.modeshape.graph.Results;
47  import org.modeshape.graph.io.Destination;
48  import org.modeshape.graph.observe.NetChangeObserver.NetChange;
49  import org.modeshape.graph.property.Binary;
50  import org.modeshape.graph.property.Name;
51  import org.modeshape.graph.property.Path;
52  import org.modeshape.graph.property.PathFactory;
53  import org.modeshape.graph.property.PathNotFoundException;
54  import org.modeshape.graph.property.Property;
55  import org.modeshape.graph.property.PropertyFactory;
56  import org.modeshape.graph.property.ValueFactories;
57  import org.modeshape.graph.property.ValueFormatException;
58  import org.modeshape.graph.property.Path.Segment;
59  import org.modeshape.graph.sequencer.StreamSequencer;
60  import org.modeshape.graph.sequencer.StreamSequencerContext;
61  import org.modeshape.repository.ModeShapeLexicon;
62  import org.modeshape.repository.RepositoryI18n;
63  import org.modeshape.repository.util.RepositoryNodePath;
64  
65  /**
66   * An adapter class that wraps a {@link StreamSequencer} instance to be a {@link Sequencer}.
67   */
68  public class StreamSequencerAdapter implements Sequencer {
69  
70      public static final boolean DEFAULT_ADD_DEFAULT_MIXIN = true;
71  
72      private static final Logger LOGGER = Logger.getLogger(StreamSequencerAdapter.class);
73  
74      private SequencerConfig configuration;
75      private final StreamSequencer streamSequencer;
76      private final boolean addDerivedMixin;
77  
78      public StreamSequencerAdapter( StreamSequencer streamSequencer ) {
79          this(streamSequencer, DEFAULT_ADD_DEFAULT_MIXIN);
80      }
81  
82      public StreamSequencerAdapter( StreamSequencer streamSequencer,
83                                     boolean addDerivedMixin ) {
84          this.streamSequencer = streamSequencer;
85          this.addDerivedMixin = addDerivedMixin;
86      }
87  
88      /**
89       * {@inheritDoc}
90       */
91      public SequencerConfig getConfiguration() {
92          return this.configuration;
93      }
94  
95      /**
96       * {@inheritDoc}
97       */
98      public void setConfiguration( SequencerConfig configuration ) {
99          this.configuration = configuration;
100 
101         /*
102          * Try to pass the configured properties through to the stream sequencer
103          */
104         if (configuration.getProperties() != null) {
105             final Class<?> streamSequencerClass = streamSequencer.getClass();
106             Reflection reflection = new Reflection(streamSequencerClass);
107             // Try to set the 'classpath' property first ...
108             try {
109                 reflection.invokeSetterMethodOnTarget("classpath", streamSequencer, configuration.getComponentClasspathArray());
110             } catch (Exception e) {
111                 // Ignore, but try the list form ...
112                 try {
113                     reflection.invokeSetterMethodOnTarget("classpath", streamSequencer, configuration.getComponentClasspath());
114                 } catch (Exception e2) {
115                     // Ignore ...
116                 }
117             }
118             // Now set all the other properties ...
119             for (Map.Entry<String, Object> entry : configuration.getProperties().entrySet()) {
120                 // Set the JavaBean-style property on the RepositorySource instance ...
121                 final String propertyName = entry.getKey();
122                 try {
123                     reflection.invokeSetterMethodOnTarget(propertyName, streamSequencer, entry.getValue());
124                     LOGGER.trace("Set '{0}' property from sequencer configuration on '{1}' stream sequencer implementation to {2}",
125                                  propertyName,
126                                  streamSequencerClass.getName(),
127                                  entry.getValue());
128                 } catch (NoSuchMethodException e) {
129                     // If the value is an Object[], see if the values are compatible with String[] and try again
130                 } catch (Exception ignore) {
131                     LOGGER.debug("Unable to set '{0}' property from sequencer configuration on '{1}' stream sequencer implementation",
132                                  propertyName,
133                                  streamSequencerClass.getName());
134                     // It's possible that these properties weren't intended for the stream sequencer anyway
135                 }
136             }
137         }
138     }
139 
140     /**
141      * {@inheritDoc}
142      */
143     public void execute( Node input,
144                          String sequencedPropertyName,
145                          NetChange changes,
146                          Set<RepositoryNodePath> outputPaths,
147                          SequencerContext context,
148                          Problems problems ) throws SequencerException {
149         // 'sequencedPropertyName' contains the name of the modified property on 'input' that resulted in the call to this
150         // sequencer.
151         // 'changes' contains all of the changes to this node that occurred in the transaction.
152         // 'outputPaths' contains the paths of the node(s) where this sequencer is to save it's data.
153 
154         // Get the property that contains the data, given by 'propertyName' ...
155         Property sequencedProperty = input.getProperty(sequencedPropertyName);
156 
157         if (sequencedProperty == null || sequencedProperty.isEmpty()) {
158             String msg = RepositoryI18n.unableToFindPropertyForSequencing.text(sequencedPropertyName, input.getLocation());
159             throw new SequencerException(msg);
160         }
161 
162         // Get the binary property with the image content, and build the image metadata from the image ...
163         ValueFactories factories = context.getExecutionContext().getValueFactories();
164         SequencerOutputMap output = new SequencerOutputMap(factories);
165         InputStream stream = null;
166         Throwable firstError = null;
167         Binary binary = factories.getBinaryFactory().create(sequencedProperty.getFirstValue());
168         binary.acquire();
169         try {
170             // Parallel the JCR lemma for converting objects into streams
171             stream = binary.getStream();
172             StreamSequencerContext streamSequencerContext = createStreamSequencerContext(input,
173                                                                                          sequencedProperty,
174                                                                                          context,
175                                                                                          problems);
176             this.streamSequencer.sequence(stream, output, streamSequencerContext);
177         } catch (Throwable t) {
178             // Record the error ...
179             firstError = t;
180         } finally {
181             try {
182                 if (stream != null) {
183                     // Always close the stream, recording the error if we've not yet seen an error
184                     try {
185                         stream.close();
186                     } catch (Throwable t) {
187                         if (firstError == null) firstError = t;
188                     } finally {
189                         stream = null;
190                     }
191                 }
192                 if (firstError != null) {
193                     // Wrap and throw the first error that we saw ...
194                     throw new SequencerException(firstError);
195                 }
196             } finally {
197                 binary.release();
198             }
199         }
200 
201         // Accumulator of paths that we've added to the batch but have not yet been submitted to the graph
202         Set<Path> builtPaths = new HashSet<Path>();
203 
204         // Determine if previously-derived content should be removed ...
205         boolean replacePreviouslyDerivedContent = Boolean.parseBoolean(SequencerConfig.DEFAULT_REPLACE_PREVIOUSLY_DERIVED_CONTENT_PROPERTY_VALUE);
206         SequencerConfig config = getConfiguration();
207         if (config != null) {
208             Object value = config.getProperties().get(SequencerConfig.REPLACE_PREVIOUSLY_DERIVED_CONTENT_PROPERTY_NAME);
209             if (value != null) {
210                 replacePreviouslyDerivedContent = Boolean.parseBoolean(value.toString());
211             }
212         }
213 
214         // Find each output node and save the image metadata there ...
215         final Path inputPath = input.getLocation().getPath();
216         for (RepositoryNodePath outputPath : outputPaths) {
217             // Get the name of the repository source, workspace and the path to the output node
218             final String repositoryWorkspaceName = outputPath.getWorkspaceName();
219             final String nodePath = outputPath.getNodePath();
220 
221             // Find or create the output node in this session ...
222             context.destinationGraph().useWorkspace(repositoryWorkspaceName);
223 
224             buildPathTo(nodePath, context, builtPaths);
225             // Node outputNode = context.graph().getNodeAt(nodePath);
226 
227             // Now save the image metadata to the output node ...
228             saveOutput(inputPath, nodePath, output, context, builtPaths, replacePreviouslyDerivedContent);
229         }
230 
231         context.getDestination().submit();
232     }
233 
234     /**
235      * Creates all nodes along the given node path if they are missing. Ensures that nodePath is a valid path to a node.
236      * 
237      * @param nodePath the node path to create
238      * @param context the sequencer context under which it should be created
239      * @param builtPaths a set of the paths that have already been created but not submitted in this batch
240      */
241     private void buildPathTo( String nodePath,
242                               SequencerContext context,
243                               Set<Path> builtPaths ) {
244         PathFactory pathFactory = context.getExecutionContext().getValueFactories().getPathFactory();
245         Path targetPath = pathFactory.create(nodePath);
246 
247         buildPathTo(targetPath, context, builtPaths);
248     }
249 
250     /**
251      * Creates all nodes along the given node path if they are missing. Ensures that nodePath is a valid path to a node.
252      * 
253      * @param targetPath the node path to create
254      * @param context the sequencer context under which it should be created
255      * @param builtPaths a set of the paths that have already been created but not submitted in this batch
256      */
257     private void buildPathTo( Path targetPath,
258                               SequencerContext context,
259                               Set<Path> builtPaths ) {
260         PathFactory pathFactory = context.getExecutionContext().getValueFactories().getPathFactory();
261         PropertyFactory propFactory = context.getExecutionContext().getPropertyFactory();
262 
263         if (targetPath.isRoot()) return;
264         Path workingPath = pathFactory.createRootPath();
265         Path.Segment[] segments = targetPath.getSegmentsArray();
266         int i = 0;
267         Property primaryType = propFactory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.UNSTRUCTURED);
268         for (int max = segments.length; i < max; i++) {
269             workingPath = pathFactory.create(workingPath, segments[i]);
270 
271             if (!builtPaths.contains(workingPath)) {
272                 try {
273                     context.destinationGraph().getNodeAt(workingPath);
274                 } catch (PathNotFoundException pnfe) {
275                     context.getDestination().create(workingPath, primaryType);
276                     builtPaths.add(workingPath);
277                 }
278             }
279         }
280     }
281 
282     /**
283      * Save the sequencing output to the supplied node. This method does not need to save the output, as that is done by the
284      * caller of this method.
285      * 
286      * @param inputPath the existing node that was sequenced; never null
287      * @param outputPath the existing node onto (or below) which the output is to be written; never null
288      * @param output the (immutable) sequencing output; never null
289      * @param context the execution context for this sequencing operation; never null
290      * @param builtPaths a set of the paths that have already been created but not submitted in this batch
291      * @param replacePreviouslyDerivedContent true if any existing content that was previously derived from the same input path
292      *        should first be removed before saving the output, or false otherwise
293      */
294     protected void saveOutput( Path inputPath,
295                                String outputPath,
296                                SequencerOutputMap output,
297                                SequencerContext context,
298                                Set<Path> builtPaths,
299                                boolean replacePreviouslyDerivedContent ) {
300         if (output.isEmpty()) return;
301         final PathFactory pathFactory = context.getExecutionContext().getValueFactories().getPathFactory();
302         final PropertyFactory propertyFactory = context.getExecutionContext().getPropertyFactory();
303         final Path outputNodePath = pathFactory.create(outputPath);
304         final Path derivedFromPath = derivedFromPath(inputPath);
305 
306         // Get the existing list of children under the output path ...
307         PathStrategy pathStrategy = null;
308         try {
309             Graph graph = context.destinationGraph();
310             Graph.Batch batch = graph.batch();
311             List<Location> children = graph.getChildren().of(outputNodePath);
312             Map<Name, AtomicInteger> existingSnsByNodeName = new HashMap<Name, AtomicInteger>();
313             for (Location child : children) {
314                 Segment childSegment = child.getPath().getLastSegment();
315                 Name childName = childSegment.getName();
316                 AtomicInteger sns = existingSnsByNodeName.get(childName);
317                 if (sns == null) {
318                     sns = new AtomicInteger(childSegment.getIndex());
319                     existingSnsByNodeName.put(childName, sns);
320                 } else {
321                     if (childSegment.getIndex() > sns.get()) {
322                         sns.set(childSegment.getIndex());
323                     }
324                 }
325                 if (replacePreviouslyDerivedContent) {
326                     // Get the 'mode:derivedFrom' property on this node ...
327                     batch.readProperty(ModeShapeLexicon.DERIVED_FROM).on(child).and();
328                 }
329             }
330 
331             // Which of these children are 'mode:derived' from the same 'mode:derivedFrom' path?
332             // To figure this out, we'll use a single batch to load the 'mode:derivedFrom' property
333             // on each of the child nodes. (Not sure if this is the most efficient way to do this,
334             // but it is the least costly in terms of memory.)
335             Results derivedResults = batch.execute();
336             if (!derivedResults.getRequests().isEmpty()) {
337                 Graph.Batch deleteExistingBatch = graph.batch();
338                 for (Node child : derivedResults) {
339                     Property derivedFrom = child.getProperty(ModeShapeLexicon.DERIVED_FROM);
340                     if (derivedFrom == null) continue;
341                     for (Object value : derivedFrom) {
342                         try {
343                             Path derivedFromValue = pathFactory.create(value);
344                             if (derivedFromPath.isSameAs(derivedFromValue)) {
345                                 // Decrement the SNS index ...
346                                 Location childLocation = child.getLocation();
347                                 Segment childSegment = childLocation.getPath().getLastSegment();
348                                 Name childName = childSegment.getName();
349                                 AtomicInteger sns = existingSnsByNodeName.get(childName);
350                                 if (sns != null) {
351                                     if (sns.decrementAndGet() < 1) {
352                                         // there will be no more SNS with this name
353                                         existingSnsByNodeName.remove(childName);
354                                     }
355                                     // And delete this subgraph (via batch) ...
356                                     deleteExistingBatch.delete(childLocation).and();
357                                     break; // nothing more to do for this child
358                                 }
359                             }
360                         } catch (ValueFormatException e) {
361                             // shouldn't happen, but ignore if it does ...
362                         }
363                     }
364 
365                 }
366                 // We've already sequenced the file and we're saving it, so go ahead and remove the previously-derived
367                 // content (if there is any) ...
368                 deleteExistingBatch.execute();
369             }
370 
371             if (!existingSnsByNodeName.isEmpty()) {
372                 // Create a strategy that will compute the next SNS index for the output (if there are clashes) ...
373                 pathStrategy = new NextSnsPathStrategy(existingSnsByNodeName, pathFactory);
374             }
375         } catch (PathNotFoundException e) {
376             // The target node doesn't yet exist, so just continue ...
377         }
378 
379         if (pathStrategy == null) pathStrategy = new PassThroughStrategy();
380 
381         // Iterate over the entries in the output, in Path's natural order (shorter paths first and in lexicographical order by
382         // prefix and name)
383         final Set<Path> pathsOfTopLevelNodes = new HashSet<Path>();
384         final Destination destination = context.getDestination();
385         for (SequencerOutputMap.Entry entry : output) {
386             Path path = entry.getPath();
387             Path targetNodePath = pathStrategy.validate(path);
388 
389             // Resolve this path relative to the output node path, handling any parent or self references ...
390             Path absolutePath = targetNodePath.isAbsolute() ? targetNodePath : outputNodePath.resolve(targetNodePath);
391 
392             Collection<Property> properties = new LinkedList<Property>();
393             // Set all of the properties on this
394             for (SequencerOutputMap.PropertyValue property : entry.getPropertyValues()) {
395                 Object value = property.getValue();
396                 Property newProperty = propertyFactory.create(property.getName(), value);
397                 properties.add(newProperty);
398                 // TODO: Handle reference properties - currently passed in as Paths
399             }
400 
401             if (targetNodePath.size() <= 1 && addDerivedMixin && pathsOfTopLevelNodes.add(absolutePath)) {
402                 properties = addDerivedProperties(properties, context, derivedFromPath);
403             }
404 
405             if (absolutePath.getParent() != null) {
406                 buildPathTo(absolutePath.getParent(), context, builtPaths);
407             }
408             destination.create(absolutePath, properties);
409             builtPaths.add(absolutePath);
410         }
411     }
412 
413     protected Collection<Property> addDerivedProperties( Collection<Property> properties,
414                                                          SequencerContext context,
415                                                          Path derivedPath ) {
416         Map<Name, Property> propertiesByName = new HashMap<Name, Property>();
417         for (Property property : properties) {
418             propertiesByName.put(property.getName(), property);
419         }
420 
421         // Find the mixinTypes property and figure out the new value(s) for this property ...
422         Object values = null;
423         Property mixinTypes = propertiesByName.get(JcrLexicon.MIXIN_TYPES);
424         if (mixinTypes == null || mixinTypes.isEmpty()) {
425             values = ModeShapeLexicon.DERIVED;
426         } else {
427             // Add the mixin to the value(s) ...
428             if (mixinTypes.isSingle()) {
429                 Name name = context.getExecutionContext().getValueFactories().getNameFactory().create(mixinTypes.getFirstValue());
430                 values = new Object[] {name, ModeShapeLexicon.DERIVED};
431             } else {
432                 Object[] oldValues = mixinTypes.getValuesAsArray();
433                 Object[] newValues = new Object[oldValues.length + 1];
434                 newValues[0] = ModeShapeLexicon.DERIVED;
435                 System.arraycopy(oldValues, 0, newValues, 1, oldValues.length);
436                 values = newValues;
437             }
438         }
439         PropertyFactory propertyFactory = context.getExecutionContext().getPropertyFactory();
440         Property newMixinTypes = propertyFactory.create(JcrLexicon.MIXIN_TYPES, values);
441         propertiesByName.put(newMixinTypes.getName(), newMixinTypes);
442 
443         // Add the 'mode:derivedFrom' property ...
444         Property derivedFrom = propertiesByName.get(ModeShapeLexicon.DERIVED_FROM);
445         if (derivedFrom == null) {
446             // Only do this if the sequencer didn't already do this ...
447             derivedFrom = propertyFactory.create(ModeShapeLexicon.DERIVED_FROM, derivedPath);
448             propertiesByName.put(derivedFrom.getName(), derivedFrom);
449         }
450 
451         // Add the 'mode:derivedAt' property ...
452         Property derivedOn = propertiesByName.get(ModeShapeLexicon.DERIVED_AT);
453         if (derivedOn == null) {
454             // Only do this if the sequencer didn't already do this ...
455             // The timestamp should match that of the change event.
456             derivedOn = propertyFactory.create(ModeShapeLexicon.DERIVED_AT, context.getTimestamp());
457             propertiesByName.put(derivedOn.getName(), derivedOn);
458         }
459 
460         // Return the properties ...
461         return propertiesByName.values();
462     }
463 
464     /**
465      * Compute the path that will be used in the "mode:derivedFrom" property on the "mode:derived" node(s) output by the
466      * sequencing operation. If the supplied input path of the node being sequenced ends with "jcr:content", then the derived path
467      * is the parent path; otherwise, this method simply returns the supplied input path.
468      * 
469      * @param inputPath the path of the node being sequenced; may not be null
470      * @return the derived path; never null
471      */
472     protected Path derivedFromPath( Path inputPath ) {
473         assert inputPath != null;
474         if (!inputPath.isRoot() && inputPath.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
475             // We want to point to the sequenced 'nt:file' node, not the 'jcr:content' node ...
476             inputPath = inputPath.getParent();
477         }
478         return inputPath;
479     }
480 
481     protected String[] extractMixinTypes( Object value ) {
482         if (value instanceof String[]) return (String[])value;
483         if (value instanceof String) return new String[] {(String)value};
484         return null;
485     }
486 
487     protected StreamSequencerContext createStreamSequencerContext( Node input,
488                                                                    Property sequencedProperty,
489                                                                    SequencerContext context,
490                                                                    Problems problems ) {
491         assert input != null;
492         assert sequencedProperty != null;
493         assert context != null;
494         assert problems != null;
495         ValueFactories factories = context.getExecutionContext().getValueFactories();
496         Path path = factories.getPathFactory().create(input.getLocation().getPath());
497 
498         Set<org.modeshape.graph.property.Property> props = Collections.<Property>unmodifiableSet(input.getPropertiesByName()
499                                                                                                       .values());
500         Name fileName = path.getLastSegment().getName();
501         if (JcrLexicon.CONTENT.equals(fileName) && !path.isRoot()) {
502             // We're actually sequencing the "jcr:content" child node of an "nt:file" node, but the name of
503             // the file is actually the name of the "jcr:content" node's parent "nt:file" node.
504             fileName = path.getParent().getLastSegment().getName();
505         }
506         String mimeType = getMimeType(context, sequencedProperty, fileName.getLocalName());
507         return new StreamSequencerContext(context.getExecutionContext(), path, props, mimeType, problems);
508     }
509 
510     protected String getMimeType( SequencerContext context,
511                                   Property sequencedProperty,
512                                   String name ) {
513         SequencerException err = null;
514         String mimeType = null;
515         InputStream stream = null;
516         try {
517             // Parallel the JCR lemma for converting objects into streams
518             stream = new ByteArrayInputStream(sequencedProperty.toString().getBytes());
519             mimeType = context.getExecutionContext().getMimeTypeDetector().mimeTypeOf(name, stream);
520             return mimeType;
521         } catch (Exception error) {
522             err = new SequencerException(error);
523         } finally {
524             if (stream != null) {
525                 try {
526                     stream.close();
527                 } catch (IOException error) {
528                     // Only throw exception if an exception was not already thrown
529                     if (err == null) err = new SequencerException(error);
530                 }
531             }
532         }
533         throw err;
534     }
535 
536     protected interface PathStrategy {
537         Path validate( Path path );
538     }
539 
540     protected class PassThroughStrategy implements PathStrategy {
541         /**
542          * {@inheritDoc}
543          * 
544          * @see org.modeshape.repository.sequencer.StreamSequencerAdapter.PathStrategy#validate(org.modeshape.graph.property.Path)
545          */
546         @Override
547         public Path validate( Path path ) {
548             return path;
549         }
550     }
551 
552     protected class NextSnsPathStrategy implements PathStrategy {
553 
554         private final Map<Name, AtomicInteger> existingSnsByNodeName;
555         private final PathFactory pathFactory;
556         private final Map<Segment, Path> topLevelSegmentReplacements = new HashMap<Segment, Path>();
557 
558         protected NextSnsPathStrategy( Map<Name, AtomicInteger> existingSnsByNodeName,
559                                        PathFactory pathFactory ) {
560             this.existingSnsByNodeName = existingSnsByNodeName;
561             assert this.existingSnsByNodeName != null;
562             this.pathFactory = pathFactory;
563         }
564 
565         /**
566          * {@inheritDoc}
567          * 
568          * @see org.modeshape.repository.sequencer.StreamSequencerAdapter.PathStrategy#validate(org.modeshape.graph.property.Path)
569          */
570         @Override
571         public Path validate( Path path ) {
572             if (path.size() == 0) return path;
573             if (path.isAbsolute()) return path;
574             Segment firstSegment = path.getSegment(0);
575             if (path.size() == 1) {
576                 // This is a top-level node (there may be more than one), so we need to determine whether this
577                 // node already exists and what to do in that case ...
578                 Name nodeName = firstSegment.getName();
579                 AtomicInteger lastSns = existingSnsByNodeName.get(nodeName);
580                 if (lastSns == null) {
581                     // This name was not yet used in the children ...
582                     lastSns = new AtomicInteger(1);
583                     existingSnsByNodeName.put(nodeName, lastSns);
584                 } else {
585                     // Increment the SNS ...
586                     lastSns.incrementAndGet();
587                 }
588                 int sns = lastSns.get();
589                 if (sns != firstSegment.getIndex()) {
590                     // The SNS is different than the output expected ...
591                     Segment newSegment = pathFactory.createSegment(nodeName, sns);
592                     path = pathFactory.createRelativePath(newSegment);
593                     topLevelSegmentReplacements.put(firstSegment, path);
594                 }
595             } else {
596                 // Is the first segment in the path replaced with something else?
597                 Path newBasePath = topLevelSegmentReplacements.get(firstSegment);
598                 if (newBasePath != null) {
599                     // Yup, so replace it ...
600                     path = pathFactory.create(newBasePath, path.subpath(1));
601                 }
602             }
603             return path;
604         }
605     }
606 }