View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.rat.document;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.FileSystem;
24  import java.nio.file.FileSystems;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.Paths;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Objects;
34  import java.util.Optional;
35  import java.util.concurrent.ConcurrentHashMap;
36  
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.lang3.builder.CompareToBuilder;
39  import org.apache.commons.lang3.tuple.ImmutablePair;
40  import org.apache.commons.lang3.tuple.Pair;
41  
42  /**
43   * The name for a document. The {@code DocumentName} is an immutable structure that handles all the intricacies of file
44   * naming on various operating systems. DocumentNames have several components:
45   * <ul>
46   *     <li>{@code root} - where in the file system the name starts (e.g C:\ on Microsoft Windows). May be empty but not null.</li>
47   *     <li>{@code dirSeparator} - the separator between name segments (e.g. "\" on Microsoft Windows, "/" on linux). May not be
48   *     empty or null.</li>
49   *     <li>{@code name} - the name of the file relative to the {@code root}. May not be null. Does NOT begin with a {@code dirSeparator}</li>
50   *     <li>{@code baseName} - the name of a directory or file from which this file is reported. A DocumentName with a
51   *     {@code name} of "foo/bar/baz.txt" and a {@code baseName} of "foo" will be reported as "bar/baz.txt". May not be null.</li>
52   *     <li>{@code isCaseSensitive} - identifies if the underlying file system is case-sensitive.</li>
53   * </ul>
54   * <p>
55   *     {@code DocumentName}s are generally used to represent files on the files system. However, they are also used to represent files
56   *     within an archive. When representing a file in an archive the baseName is the name of the enclosing archive document.
57   * </p>
58   */
59  public class DocumentName implements Comparable<DocumentName> {
60      /** The full name for the document. */
61      private final String name;
62      /** The name of the base directory for the document. */
63      private final DocumentName baseName;
64      /** The file system info for this document. */
65      private final FSInfo fsInfo;
66      /** The root for the DocumentName. May be empty but not null. Must be one of the roots in fsInfo. */
67      private final String root;
68  
69      /**
70       * Creates a Builder with the default file system info.
71       * @return the builder.
72       * @see FSInfo
73       */
74      public static Builder builder() {
75          return new Builder(FSInfo.getDefault());
76      }
77  
78      /**
79       * Creates a builder with the specified FSInfo instance.
80       * @param fsInfo the FSInfo to use for the builder.
81       * @return a new builder.
82       */
83      public static Builder builder(final FSInfo fsInfo) {
84          return new Builder(fsInfo);
85      }
86  
87      /**
88       * Creates a builder for the specified file system.
89       * @param fileSystem the file system to create the builder on.
90       * @return a new builder.
91       */
92      public static Builder builder(final FileSystem fileSystem) {
93          return new Builder(fileSystem);
94      }
95  
96      /**
97       * Creates a builder from a File. The {@link #baseName} is set to the file name if it is a directory otherwise
98       * it is set to the directory containing the file.
99       * @param file The file to set defaults from.
100      * @return the builder.
101      */
102     public static Builder builder(final File file) {
103         return new Builder(file);
104     }
105 
106     /**
107      * Creates a builder from a document name. The builder will be configured to create a clone of the DocumentName.
108      * @param documentName the document name to set the defaults from.
109      * @return the builder.
110      */
111     public static Builder builder(final DocumentName documentName) {
112         return new Builder(documentName);
113     }
114 
115     /**
116      * Builds the DocumentName from the builder.
117      * @param builder the builder to provide the values.
118      */
119     DocumentName(final Builder builder) {
120         this.name = builder.name;
121         this.fsInfo = builder.fsInfo;
122         this.root = builder.root;
123         this.baseName = builder.sameNameFlag ? this : builder.baseName;
124     }
125 
126     /**
127      * Creates a file from the fully qualified document name.
128      * @return a new File object.
129      */
130     public File asFile() {
131         return new File(getName());
132     }
133 
134     /**
135      * Creates a path from the document name. This method uses the fully qualified name without the root.
136      * this results in a relative file name from the root.
137      * @return a new Path object.
138      */
139     public Path asPath() {
140         return Paths.get(name);
141     }
142 
143     /**
144      * Creates a new DocumentName by adding the child to the current name.
145      * Resulting documentName will have the same base name.
146      * Directory separator is normalized to the directory separator for this file system.
147      * If the child string:
148      * <dl>
149      *     <dt>Is blank</dt>
150      *     <dd>This DocumentName is returned.</dd>
151      *     <dt>Starts with the file system root</dt>
152      *     <dd>The root must match the root of this DocumentName and the directory structure
153      *     must start with the directory structure of the basename for this DocumentName.</dd>
154      *     <dt>Starts with the directory separator character<dt>
155      *     <dd>Result will be a tree starting at the directory specified by the basename.</dd>
156      *     <dt>Does not start with a directory separator character</dt>
157      *     <dd>Result will be a tree starting at the directory specified by this DocumentName</dd>
158      * </dl>
159      * @param child the child to add (must use directory separator from this document name).
160      * @return the new document name with the same {@link #baseName}, directory sensitivity and case sensitivity as
161      * this one.
162      * @throws IllegalArgumentException if the child specifies a different root from this document name.
163      */
164     public DocumentName resolve(final String child) {
165         if (StringUtils.isBlank(child)) {
166             return this;
167         }
168         String separator = getDirectorySeparator();
169         String pattern = separator.equals("/") ? child.replace('\\', '/') :
170                 child.replace('/', '\\');
171 
172         Optional<String> expectedRoot = fsInfo.rootFor(child);
173         if (expectedRoot.isPresent()) {
174             if (!expectedRoot.get().equals(getRoot())) {
175                 throw new IllegalArgumentException(String.format("%s does not start with %s", pattern, getName()));
176             }
177             if (!getRoot().equals(separator)) {
178                 // we have something like C:\ as the root so convert the pattern to start with the separator.
179                 pattern = separator + pattern.substring(getRoot().length());
180                 if (pattern.startsWith(baseName.name)) {
181                     pattern = pattern.substring(baseName.name.length());
182                 }
183             }
184         }
185 
186         // Patterns with separators either start with the name of this document plus a relative
187         // name, or are just directory off the baseName. In either case the name is correct.
188         // So just handle the relative case.
189         if (!pattern.startsWith(separator)) {
190             pattern = name + separator + pattern;
191         }
192 
193         return new Builder(this).setName(fsInfo.normalize(pattern)).build();
194     }
195 
196     /**
197      * Gets the fully qualified name of the document.
198      * @return the fully qualified name of the document.
199      */
200     public String getName() {
201         return root + name;
202     }
203 
204     /**
205      * Gets the path of the document. This is the fully qualified name without the root but starting with a path separator.
206      * @return the path of the document.
207      */
208     public String getPath() {
209         return getDirectorySeparator() + name;
210     }
211 
212     /**
213      * Gets the fully qualified basename of the document.
214      * @return the fully qualified basename of the document.
215      */
216     public String getBaseName() {
217         return baseName.getName();
218     }
219 
220     /**
221      * Gets the root for this document.
222      * @return the root for this document.
223      */
224     public String getRoot() {
225         return root;
226     }
227 
228     /**
229      * Gets the DocumentName for the basename of this DocumentName.
230      * @return the DocumentName for the basename of this document name.
231      */
232     public DocumentName getBaseDocumentName() {
233         return baseName;
234     }
235 
236     /**
237      * Returns the directory separator.
238      * @return the directory separator.
239      */
240     public String getDirectorySeparator() {
241         return fsInfo.dirSeparator();
242     }
243 
244     /**
245      * Returns the FSInfo for this document name.
246      * @return the FSInfo for this document name.
247      */
248     public FSInfo fsInfo() {
249         return fsInfo;
250     }
251 
252     /**
253      * Determines if the candidate starts with the root or separator strings.
254      * @param candidate the candidate to check. If blank method will return {@code false}.
255      * @param root the root to check. If blank the root check is skipped.
256      * @param separator the separator to check. If blank the check is skipped.
257      * @return true if either the root or separator check returned {@code true}.
258      */
259     static boolean startsWithRootOrSeparator(final String candidate, final String root, final String separator) {
260         if (StringUtils.isBlank(candidate)) {
261             return false;
262         }
263         boolean result = !StringUtils.isBlank(root) && candidate.startsWith(root);
264         if (!result) {
265             result = !StringUtils.isBlank(separator) && candidate.startsWith(separator);
266         }
267         return result;
268     }
269 
270     /**
271      * Gets the portion of the name that is not part of the base name.
272      * The resulting name will always start with the directory separator.
273      * @return the portion of the name that is not part of the base name.
274      */
275     public String localized() {
276         String result = getName();
277         String baseNameStr = baseName.getName();
278         if (result.startsWith(baseNameStr)) {
279             result = result.substring(baseNameStr.length());
280         }
281         if (!startsWithRootOrSeparator(result, getRoot(), fsInfo.dirSeparator())) {
282             result = fsInfo.dirSeparator() + result;
283         }
284         return result;
285     }
286 
287     /**
288      * Gets the portion of the name that is not part of the base name.
289      * The resulting name will always start with the directory separator.
290      * @param dirSeparator The character(s) to use to separate directories in the result.
291      * @return the portion of the name that is not part of the base name.
292      */
293     public String localized(final String dirSeparator) {
294         String[] tokens = fsInfo.tokenize(localized());
295         if (tokens.length == 0) {
296             return dirSeparator;
297         }
298         if (tokens.length == 1) {
299             return dirSeparator + tokens[0];
300         }
301 
302         String modifiedRoot =  dirSeparator.equals("/") ? root.replace('\\', '/') :
303                 root.replace('/', '\\');
304         String result = String.join(dirSeparator, tokens);
305         return startsWithRootOrSeparator(result, modifiedRoot, dirSeparator) ? result : dirSeparator + result;
306     }
307 
308     /**
309      * Gets the last segment of the name. This is the part after the last directory separator.
310      * @return the last segment of the name.
311      */
312     public String getShortName() {
313         int pos = name.lastIndexOf(fsInfo.dirSeparator());
314         return pos == -1 ? name : name.substring(pos + 1);
315     }
316 
317     /**
318      * Gets the case sensitivity flag.
319      * @return {@code true} if the name is case-sensitive.
320      */
321     public boolean isCaseSensitive() {
322         return fsInfo.isCaseSensitive();
323     }
324 
325     /**
326      * Returns the localized file name.
327      * @return the localized file name.
328      */
329     @Override
330     public String toString() {
331         return localized();
332     }
333 
334     @Override
335     public int compareTo(final DocumentName other) {
336         return new CompareToBuilder()
337                 .append(this.root, other.root)
338                 .append(this.getBaseName(), other.getBaseName())
339                 .append(this.getName(), other.getName()).build();
340     }
341 
342     @Override
343     public final boolean equals(final Object other) {
344         if (other instanceof DocumentName otherDocumentName) {
345             return compareTo(otherDocumentName) == 0;
346         }
347         return false;
348     }
349 
350     @Override
351     public final int hashCode() {
352         return getName().hashCode();
353     }
354 
355     /**
356      * The File System Info Data for a DocumentName.
357      * Use to preserve data across DocumentNames without having to
358      * reconstruct the data for each DocumentName.
359      */
360     private static final class FSInfoData {
361         /** The case sensitivity flag */
362         private final boolean isCaseSensitive;
363         /** The list of roots for the file system. */
364         private final List<String> roots;
365         /** The separator between directory names. */
366         private final String separator;
367 
368         /**
369          * Constructor for known properties.
370          * @param separator the directory separator character(s).
371          * @param isCaseSensitive {@code true} if the file system is cases sensitive.
372          * @param roots THe list of roots for the file system.
373          */
374         FSInfoData(final String separator, final boolean isCaseSensitive, final List<String> roots) {
375             this.isCaseSensitive = isCaseSensitive;
376             this.roots = roots;
377             this.separator = separator;
378         }
379 
380         /**
381          * Constructor for an arbitrary file system.
382          * This constructor can be processor intensive as it has to check the file system for case sensitivity.
383          * @param fileSystem the file system.
384          */
385         FSInfoData(final FileSystem fileSystem) {
386             isCaseSensitive = isCaseSensitive(fileSystem);
387             roots = new ArrayList<>();
388             fileSystem.getRootDirectories().forEach(r -> roots.add(r.toString()));
389             separator = fileSystem.getSeparator();
390         }
391 
392         /**
393          * Determines if the file system is case-sensitive.
394          * @param fileSystem the file system to check.
395          * @return {@code true} if the file system is case-sensitive.
396          */
397         private static boolean isCaseSensitive(final FileSystem fileSystem) {
398             boolean isCaseSensitive = false;
399             Path nameSet = null;
400             Path filea = null;
401             Path fileA = null;
402             try {
403                 try {
404                     Path root = fileSystem.getPath("");
405                     nameSet = Files.createTempDirectory(root, "NameSet");
406                     filea = nameSet.resolve("a");
407                     fileA = nameSet.resolve("A");
408                     Files.createFile(filea);
409                     Files.createFile(fileA);
410                     isCaseSensitive = true;
411                 } catch (IOException e) {
412                     // do nothing
413                 } finally {
414                     if (filea != null) {
415                         Files.deleteIfExists(filea);
416                     }
417                     if (fileA != null) {
418                         Files.deleteIfExists(fileA);
419                     }
420                     if (nameSet != null) {
421                         Files.deleteIfExists(nameSet);
422                     }
423                 }
424             } catch (IOException e) {
425                 // do nothing.
426             }
427             return isCaseSensitive;
428         }
429 
430     }
431     /**
432      * The file system information needed to process document names.
433      */
434     public static final class FSInfo implements Comparable<FSInfo> {
435         /**
436          * The map of FileSystem to FSInfoData used to avoid expensive FileSystem processing.
437          */
438         private static final Map<FileSystem, FSInfoData> REGISTRY = new ConcurrentHashMap<>();
439 
440         /** The case-sensitivity flag. */
441         private final FSInfoData data;
442 
443         /** The common name for the file system */
444         private final String name;
445 
446         /**
447          * Gets the FSInfo for the default file system.
448          * If the System property {@code FSInfo} is set, the {@code FSInfo} stored there is used, otherwise
449          * the {@link FileSystem} returned from {@link FileSystems#getDefault()} is used.
450          * @return the FSInfo for the default file system.
451          */
452         public static FSInfo getDefault() {
453             FSInfo result = (FSInfo) System.getProperties().get("FSInfo");
454             return result == null ?
455                     new FSInfo(FileSystems.getDefault())
456                     : result;
457         }
458 
459         /**
460          * Constructor. Extracts the necessary data from the file system.
461          * @param fileSystem the file system to extract data from.
462          */
463         public FSInfo(final FileSystem fileSystem) {
464             this("anon", fileSystem);
465         }
466 
467         /**
468          * Constructor. Extracts the necessary data from the file system.
469          * @param name the common name for the file system.
470          * @param fileSystem the file system to extract data from.
471          */
472         FSInfo(final String name, final FileSystem fileSystem) {
473             this.data = REGISTRY.computeIfAbsent(fileSystem, k -> new FSInfoData(fileSystem));
474             this.name = name;
475         }
476 
477         /**
478          * Constructor for virtual/abstract file systems for example the entry names within an archive.
479          * @param name the common name for the file system.
480          * @param separator the separator string to use.
481          * @param isCaseSensitive the case-sensitivity flag.
482          * @param roots the roots for the file system.
483          */
484         FSInfo(final String name, final String separator, final boolean isCaseSensitive, final List<String> roots) {
485             data = new FSInfoData(separator, isCaseSensitive, roots);
486             this.name = name;
487         }
488 
489         /**
490          * Gets the common name for the underlying file system.
491          * @return the common file system name.
492          */
493         @Override
494         public String toString() {
495             return name;
496         }
497 
498         /**
499          * Gets the directory separator.
500          * @return The directory separator.
501          */
502         public String dirSeparator() {
503             return data.separator;
504         }
505 
506         /**
507          * Gets the case-sensitivity flag.
508          * @return the case-sensitivity flag.
509          */
510         public boolean isCaseSensitive() {
511             return data.isCaseSensitive;
512         }
513 
514         /**
515          * Retrieves the root extracted from the name.
516          * @param name the name to extract the root from
517          * @return an optional containing the root or empty.
518          */
519         public Optional<String> rootFor(final String name) {
520             for (String sysRoot : data.roots) {
521                 if (name.startsWith(sysRoot)) {
522                     return Optional.of(sysRoot);
523                 }
524             }
525             return Optional.empty();
526         }
527 
528         /**
529          * Gets the array of roots for this file system.
530          * @return an array of roots for this file system.
531          */
532         public String[] roots() {
533             return data.roots.toArray(new String[0]);
534         }
535 
536         /**
537          * Tokenizes the string based on the directory separator of this DocumentName.
538          * @param source the source to tokenize.
539          * @return the array of tokenized strings.
540          */
541         public String[] tokenize(final String source) {
542             return source.split("\\Q" + dirSeparator() + "\\E");
543         }
544 
545         /**
546          * Removes {@code .} and {@code ..} from filenames.
547          * @param pattern the file name pattern
548          * @return the normalized file name.
549          */
550         public String normalize(final String pattern) {
551             if (StringUtils.isBlank(pattern) || pattern.trim().equals(".")) {
552                 return "";
553             }
554             String adjustedPattern = dirSeparator().equals("/") ? pattern.replace("\\", "/") : pattern.replace("/", "\\");
555             if (adjustedPattern.trim().equals(dirSeparator())) {
556                 return adjustedPattern;
557             }
558             List<String> parts = new ArrayList<>(Arrays.asList(tokenize(adjustedPattern)));
559             int i = 0;
560             while (i < parts.size()) {
561                 String part = parts.get(i);
562                 if (part.equals("..")) {
563                     if (i == 0) {
564                         throw new IllegalStateException("Unable to create path before root");
565                     }
566                     parts.remove(i);
567                     parts.remove(i - 1);
568                     i--;
569                 } else if (part.equals(".")) {
570                     parts.remove(i);
571                 } else {
572                     i++;
573                 }
574             }
575             if (parts.isEmpty()) {
576                 throw new IllegalStateException("Unable to create path before root");
577             }
578             return String.join(dirSeparator(), parts);
579         }
580 
581         /**
582          * Creates a path separated by the directory separator.
583          * Starting with an empty string will cause the directory separator to appear at the beginning.
584          * @param segments the segments that make up the path.
585          * @return the path string.
586          */
587         public String mkPath(final String... segments) {
588             return String.join(dirSeparator(), segments);
589         }
590 
591         /**
592          * Determines if the candidate string starts with a root or directory separator as defined in this
593          * FSInfo.
594          * @param candidate the candidate string to test.
595          * @return {@code true} if the candidate starts with a root or a directory separator.
596          */
597         public boolean startsWithRootOrSeparator(final String candidate) {
598             if (candidate == null) {
599                 return false;
600             }
601             String target = candidate.trim();
602             if (StringUtils.isBlank(target)) {
603                 return false;
604             }
605             for (String root : roots()) {
606                 if (target.startsWith(root)) {
607                     return true;
608                 }
609             }
610             return target.startsWith(dirSeparator());
611         }
612 
613         private int compareData(final DocumentName.FSInfoData otherData) {
614             int result = Boolean.compare(this.data.isCaseSensitive, otherData.isCaseSensitive);
615             if (result == 0) {
616                 result = this.data.separator.compareTo(otherData.separator);
617                 if (result == 0) {
618                     if (new HashSet<>(this.data.roots).containsAll(otherData.roots)) {
619                         result = new HashSet<>(otherData.roots).containsAll(this.data.roots) ? 0 : 1;
620                     } else {
621                         result = -1;
622                     }
623                 }
624             }
625             return result;
626         }
627 
628         @Override
629         public int compareTo(final FSInfo other) {
630             int result = this.name.compareToIgnoreCase(other.name);
631             return result == 0 ? compareData(other.data) : result;
632         }
633 
634         @Override
635         public boolean equals(final Object other) {
636             return other instanceof FSInfo oth && this.compareTo(oth) == 0;
637         }
638 
639         @Override
640         public int hashCode() {
641             return name.hashCode();
642         }
643     }
644 
645     /**
646      * The Builder for a DocumentName.
647      */
648     public static final class Builder {
649         /** The name for the document. */
650         private String name;
651         /** The base name for the document. */
652         private DocumentName baseName;
653         /** The file system info. */
654         private final FSInfo fsInfo;
655         /** The file system root. */
656         private String root;
657         /** A flag for baseName same as this. */
658         private boolean sameNameFlag;
659 
660         /**
661          * Create with default settings.
662          */
663         private Builder(final FSInfo fsInfo) {
664             this.fsInfo = fsInfo;
665             this.root = "";
666         }
667 
668         /**
669          * Create with default settings.
670          */
671         private Builder(final FileSystem fileSystem) {
672             this(new FSInfo(fileSystem));
673         }
674 
675         /**
676          * Create based on the file provided.
677          * @param file the file to base the builder on.
678          */
679         private Builder(final File file) {
680             this(FSInfo.getDefault());
681             setName(file);
682         }
683 
684         /**
685          * Used in testing.
686          * @param fsInfo the FSInfo for the file.
687          * @param file the file to process.
688          */
689         Builder(final FSInfo fsInfo, final File file) {
690             this(fsInfo);
691             setName(file);
692         }
693 
694         /**
695          * Create a Builder that clones the specified DocumentName.
696          * @param documentName the DocumentName to clone.
697          */
698         Builder(final DocumentName documentName) {
699             this.root = documentName.root;
700             this.name = documentName.name;
701             this.baseName = documentName.baseName;
702             this.fsInfo = documentName.fsInfo;
703         }
704 
705         /**
706          * Get the directory separator for this builder.
707          * @return the directory separator fo this builder.
708          */
709         public String directorySeparator() {
710             return fsInfo.dirSeparator();
711         }
712 
713         /**
714          * Verify that the builder will build a proper DocumentName.
715          */
716         private void verify() {
717             Objects.requireNonNull(name, "Name must not be null");
718             if (name.startsWith(fsInfo.dirSeparator())) {
719                 name = name.substring(fsInfo.dirSeparator().length());
720             }
721             if (!sameNameFlag) {
722                 Objects.requireNonNull(baseName, "Basename must not be null");
723                 if (this.root.isBlank()) {
724                     this.root = this.baseName.getRoot();
725                 }
726             }
727             if (this.root.isBlank()) {
728                 this.root = this.fsInfo.roots()[0];
729             } else {
730                 if (!List.of(this.fsInfo.roots()).contains(this.root)) {
731                     throw new IllegalArgumentException(String.format("'%s' is not a valid root for %s", this.root, this.fsInfo));
732                 }
733             }
734         }
735 
736         /**
737          * Sets the root for the DocumentName.
738          * @param root the root for the DocumentName.
739          * @return this.
740          */
741         public Builder setRoot(final String root) {
742             this.root = StringUtils.defaultIfBlank(root, "");
743             return this;
744         }
745 
746         /**
747          * Sets the name for this DocumentName relative to the baseName.
748          * If the {@code name} is {@code null} an empty string is used.
749          * <p>
750          *     To correctly parse the string it must use the directory separator specified by
751          *     this Document.
752          * </p>
753          * @param name the name for this Document name. Will be made relative to the baseName.
754          * @return this
755          */
756         public Builder setName(final String name) {
757             Pair<String, String> pair = splitRoot(StringUtils.defaultIfBlank(name, ""));
758             if (this.root.isEmpty()) {
759                 setRoot(pair.getLeft());
760             }
761             this.name = fsInfo.normalize(pair.getRight());
762             return this;
763         }
764 
765         /**
766          * Extracts the root/name pair from a name string.
767          * <p>
768          *     Package private for testing.
769          * </p>
770          * @param name the name to extract the root/name pair from.
771          * @return the root/name pair.
772          */
773         Pair<String, String> splitRoot(final String name) {
774             String workingName = name;
775             String workingRoot = fsInfo.rootFor(name).orElse("");
776             if (!workingRoot.isEmpty() && workingName.startsWith(workingRoot)) {
777                 workingName = workingName.substring(workingRoot.length());
778             }
779             return ImmutablePair.of(workingRoot, workingName);
780         }
781 
782         /**
783          * Sets the builder root if it is empty.
784          * @param root the root to set the builder root to if it is empty.
785          */
786         private void setEmptyRoot(final String root) {
787             if (this.root.isEmpty()) {
788                 this.root = root;
789             }
790         }
791 
792         /**
793          * Sets the properties from the file. Will reset the baseName appropriately.
794          * @param file the file to set the properties from.
795          * @return this.
796          */
797         public Builder setName(final File file) {
798             Pair<String, String> pair = splitRoot(file.getAbsolutePath());
799             setEmptyRoot(pair.getLeft());
800             this.name = fsInfo.normalize(pair.getRight());
801             if (file.isDirectory()) {
802                 sameNameFlag = true;
803             } else {
804                 File p = file.getParentFile();
805                 if (p != null) {
806                     setBaseName(p);
807                 } else {
808                     Builder baseBuilder = new Builder(this.fsInfo).setName(this.directorySeparator());
809                     baseBuilder.sameNameFlag = true;
810                     setBaseName(baseBuilder.build());
811                 }
812             }
813             return this;
814         }
815 
816         /**
817          * Sets the baseName.
818          * Will set the root if it is not set.
819          * <p>
820          *     To correctly parse the string it must use the directory separator specified by this builder.
821          * </p>
822          * @param baseName the basename to use.
823          * @return this.
824          */
825         public Builder setBaseName(final String baseName) {
826             DocumentName.Builder builder = DocumentName.builder(fsInfo).setName(baseName);
827             builder.sameNameFlag = true;
828             setBaseName(builder);
829             return this;
830         }
831 
832         /**
833          * Sets the basename from the {@link #name} of the specified DocumentName.
834          * Will set the root the baseName has the root set.
835          * @param baseName the DocumentName to set the basename from.
836          * @return this.
837          */
838         public Builder setBaseName(final DocumentName baseName) {
839             this.baseName = baseName;
840             if (!baseName.getRoot().isEmpty()) {
841                 this.root = baseName.getRoot();
842             }
843             return this;
844         }
845 
846         /**
847          * Executes the builder, sets the base name and clears the sameName flag.
848          * @param builder the builder for the base name.
849          */
850         private void setBaseName(final DocumentName.Builder builder) {
851             this.baseName = builder.build();
852             this.sameNameFlag = false;
853         }
854 
855         /**
856          * Sets the basename from a File. Sets {@link #root} and the {@link #baseName}
857          * Will set the root.
858          * @param file the file to set the base name from.
859          * @return this.
860          */
861         public Builder setBaseName(final File file) {
862             DocumentName.Builder builder = DocumentName.builder(fsInfo).setName(file);
863             builder.sameNameFlag = true;
864             setBaseName(builder);
865             return this;
866         }
867 
868         // only called if basName is not null
869         private void verifyBaseName() {
870             if (!this.name.startsWith(baseName.name)) {
871                 this.name = this.name.isEmpty() ? baseName.name :
872                         baseName.name + fsInfo.dirSeparator() + this.name;
873             }
874             if (!this.baseName.getRoot().equals(root)) {
875                 Builder builder = new Builder(baseName).setRoot(root);
876                 if (baseName.baseName != null && baseName.baseName != baseName) {
877                     builder.setBaseName(baseName.baseName);
878                 } else {
879                     builder.baseName = null;
880                     builder.sameNameFlag = true;
881                 }
882                 this.baseName = builder.build();
883             }
884 
885         }
886 
887         /**
888          * Build a DocumentName from this builder.
889          * @return A new DocumentName.
890          */
891         public DocumentName build() {
892             verify();
893             if (this.baseName != null) {
894                 verifyBaseName();
895             } else {
896                 if (this.name.startsWith(root)) {
897                     this.name = this.name.substring(root.length());
898                 }
899             }
900             return new DocumentName(this);
901         }
902     }
903 }