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.List;
31  import java.util.Objects;
32  import java.util.Optional;
33  import java.util.stream.Collectors;
34  
35  import org.apache.commons.lang3.StringUtils;
36  import org.apache.commons.lang3.builder.CompareToBuilder;
37  import org.apache.commons.lang3.builder.EqualsBuilder;
38  import org.apache.commons.lang3.builder.HashCodeBuilder;
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 windows). May be empty but not null.</li>
47   *     <li>{@code dirSeparator} - the separator between name segments (e.g. "\\" on 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. */
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 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.
136      * @return a new Path object.
137      */
138     public Path asPath() {
139         return Paths.get(name);
140     }
141 
142     /**
143      * Creates a new DocumentName by adding the child to the current name.
144      * Resulting documentName will have the same base name.
145      * @param child the child to add (must use directory separator from this document name).
146      * @return the new document name with the same {@link #baseName}, directory sensitivity and case sensitivity as
147      * this one.
148      */
149     public DocumentName resolve(final String child) {
150         if (StringUtils.isBlank(child)) {
151             return this;
152         }
153         String separator = getDirectorySeparator();
154         String pattern = separator.equals("/") ? child.replace('\\', '/') :
155                 child.replace('/', '\\');
156 
157         if (!pattern.startsWith(separator)) {
158              pattern = name + separator + pattern;
159         }
160 
161         return new Builder(this).setName(fsInfo.normalize(pattern)).build();
162     }
163 
164     /**
165      * Gets the fully qualified name of the document.
166      * @return the fully qualified name of the document.
167      */
168     public String getName() {
169         return root + fsInfo.dirSeparator() + name;
170     }
171 
172     /**
173      * Gets the fully qualified basename of the document.
174      * @return the fully qualified basename of the document.
175      */
176     public String getBaseName() {
177         return baseName.getName();
178     }
179 
180     /**
181      * Gets the root for this document.
182      * @return the root for this document.
183      */
184     public String getRoot() {
185         return root;
186     }
187 
188     /**
189      * Gets the DocumentName for the basename of this DocumentName.
190      * @return the DocumentName for the basename of this document name.
191      */
192     public DocumentName getBaseDocumentName() {
193         return baseName;
194     }
195 
196     /**
197      * Returns the directory separator.
198      * @return the directory separator.
199      */
200     public String getDirectorySeparator() {
201         return fsInfo.dirSeparator();
202     }
203 
204     /**
205      * Determines if the candidate starts with the root or separator strings.
206      * @param candidate the candidate to check. If blank method will return {@code false}.
207      * @param root the root to check. If blank the root check is skipped.
208      * @param separator the separator to check. If blank the check is skipped.
209      * @return true if either the root or separator check returned {@code true}.
210      */
211     boolean startsWithRootOrSeparator(final String candidate, final String root, final String separator) {
212         if (StringUtils.isBlank(candidate)) {
213             return false;
214         }
215         boolean result = !StringUtils.isBlank(root) && candidate.startsWith(root);
216         if (!result) {
217             result = !StringUtils.isBlank(separator) && candidate.startsWith(separator);
218         }
219         return result;
220     }
221 
222     /**
223      * Gets the portion of the name that is not part of the base name.
224      * The resulting name will always start with the directory separator.
225      * @return the portion of the name that is not part of the base name.
226      */
227     public String localized() {
228         String result = getName();
229         String baseNameStr = baseName.getName();
230         if (result.startsWith(baseNameStr)) {
231             result = result.substring(baseNameStr.length());
232         }
233         if (!startsWithRootOrSeparator(result, getRoot(), fsInfo.dirSeparator())) {
234             result = fsInfo.dirSeparator() + result;
235         }
236         return result;
237     }
238 
239     /**
240      * Gets the portion of the name that is not part of the base name.
241      * The resulting name will always start with the directory separator.
242      * @param dirSeparator The character(s) to use to separate directories in the result.
243      * @return the portion of the name that is not part of the base name.
244      */
245     public String localized(final String dirSeparator) {
246         String[] tokens = fsInfo.tokenize(localized());
247         if (tokens.length == 0) {
248             return dirSeparator;
249         }
250         if (tokens.length == 1) {
251             return dirSeparator + tokens[0];
252         }
253 
254         String modifiedRoot =  dirSeparator.equals("/") ? root.replace('\\', '/') :
255                 root.replace('/', '\\');
256         String result = String.join(dirSeparator, tokens);
257         return startsWithRootOrSeparator(result, modifiedRoot, dirSeparator) ? result : dirSeparator + result;
258     }
259 
260     /**
261      * Gets the last segment of the name. This is the part after the last directory separator.
262      * @return the last segment of the name.
263      */
264     public String getShortName() {
265         int pos = name.lastIndexOf(fsInfo.dirSeparator());
266         return pos == -1 ? name : name.substring(pos + 1);
267     }
268 
269     /**
270      * Gets the case sensitivity flag.
271      * @return {@code true} if the name is case-sensitive.
272      */
273     public boolean isCaseSensitive() {
274         return fsInfo.isCaseSensitive();
275     }
276 
277     /**
278      * Returns the localized file name.
279      * @return the localized file name.
280      */
281     @Override
282     public String toString() {
283         return localized();
284     }
285 
286     @Override
287     public int compareTo(final DocumentName other) {
288         return CompareToBuilder.reflectionCompare(this, other);
289     }
290 
291     @Override
292     public boolean equals(final Object other) {
293         return EqualsBuilder.reflectionEquals(this, other);
294     }
295 
296     @Override
297     public int hashCode() {
298         return HashCodeBuilder.reflectionHashCode(this);
299     }
300 
301     /**
302      * The file system information needed to process document names.
303      */
304     public static class FSInfo implements Comparable<FSInfo> {
305         /** The common name for the file system this Info represents. */
306         private final String name;
307         /** The separator between directory names. */
308         private final String separator;
309         /** The case-sensitivity flag. */
310         private final boolean isCaseSensitive;
311         /** The list of roots for the file system. */
312         private final List<String> roots;
313 
314         public static FSInfo getDefault() {
315             FSInfo result = (FSInfo) System.getProperties().get("FSInfo");
316             return result == null ?
317                     new FSInfo("default", FileSystems.getDefault())
318                     : result;
319         }
320         /**
321          * Constructor. Extracts the necessary data from the file system.
322          * @param fileSystem the file system to extract data from.
323          */
324         public FSInfo(final FileSystem fileSystem) {
325             this("anon", fileSystem);
326         }
327 
328         /**
329          * Constructor. Extracts the necessary data from the file system.
330          * @param fileSystem the file system to extract data from.
331          */
332         public FSInfo(final String name, final FileSystem fileSystem) {
333             this.name = name;
334             this.separator = fileSystem.getSeparator();
335             this.isCaseSensitive = isCaseSensitive(fileSystem);
336             roots = new ArrayList<>();
337             fileSystem.getRootDirectories().forEach(r -> roots.add(r.toString()));
338         }
339 
340         /**
341          * Determines if the file system is case-sensitive.
342          * @param fileSystem the file system to check.
343          * @return {@code true} if the file system is case-sensitive.
344          */
345         private static boolean isCaseSensitive(final FileSystem fileSystem) {
346             boolean isCaseSensitive = false;
347             Path nameSet = null;
348             Path filea = null;
349             Path fileA = null;
350             try {
351                 try {
352                     Path root = fileSystem.getPath("");
353                     nameSet = Files.createTempDirectory(root, "NameSet");
354                     filea = nameSet.resolve("a");
355                     fileA = nameSet.resolve("A");
356                     Files.createFile(filea);
357                     Files.createFile(fileA);
358                     isCaseSensitive = true;
359                 } catch (IOException e) {
360                     // do nothing
361                 } finally {
362                     if (filea != null) {
363                         Files.deleteIfExists(filea);
364                     }
365                     if (fileA != null) {
366                         Files.deleteIfExists(fileA);
367                     }
368                     if (nameSet != null) {
369                         Files.deleteIfExists(nameSet);
370                     }
371                 }
372             } catch (IOException e) {
373                 // do nothing.
374             }
375             return isCaseSensitive;
376         }
377 
378         /**
379          * Gets the common name for the underlying file system.
380          * @return the common file system name.
381          */
382         @Override
383         public String toString() {
384             return name;
385         }
386 
387         /**
388          * Constructor for virtual/abstract file systems for example the entry names within an archive.
389          * @param separator the separator string to use.
390          * @param isCaseSensitive the case-sensitivity flag.
391          * @param roots the roots for the file system.
392          */
393         FSInfo(final String name, final String separator, final boolean isCaseSensitive, final List<String> roots) {
394             this.name = name;
395             this.separator = separator;
396             this.isCaseSensitive = isCaseSensitive;
397             this.roots = new ArrayList<>(roots);
398         }
399 
400         /**
401          * Gets the directory separator.
402          * @return The directory separator.
403          */
404         public String dirSeparator() {
405             return separator;
406         }
407 
408         /**
409          * Gets the case-sensitivity flag.
410          * @return the case-sensitivity flag.
411          */
412         public boolean isCaseSensitive() {
413             return isCaseSensitive;
414         }
415 
416         /**
417          * Retrieves the root extracted from the name.
418          * @param name the name to extract the root from
419          * @return an optional containing the root or empty.
420          */
421         public Optional<String> rootFor(final String name) {
422             for (String sysRoot : roots) {
423                 if (name.startsWith(sysRoot)) {
424                     return Optional.of(sysRoot);
425                 }
426             }
427             return Optional.empty();
428         }
429 
430         /**
431          * Tokenizes the string based on the directory separator of this DocumentName.
432          * @param source the source to tokenize.
433          * @return the array of tokenized strings.
434          */
435         public String[] tokenize(final String source) {
436             return source.split("\\Q" + dirSeparator() + "\\E");
437         }
438 
439         /**
440          * Removes {@code .} and {@code ..} from filenames.
441          * @param pattern the file name pattern
442          * @return the normalized file name.
443          */
444         public String normalize(final String pattern) {
445             if (StringUtils.isBlank(pattern)) {
446                 return "";
447             }
448             List<String> parts = new ArrayList<>(Arrays.asList(tokenize(pattern)));
449             for (int i = 0; i < parts.size(); i++) {
450                 String part = parts.get(i);
451                 if (part.equals("..")) {
452                     if (i == 0) {
453                         throw new IllegalStateException("Unable to create path before root");
454                     }
455                     parts.set(i - 1, null);
456                     parts.set(i, null);
457                 } else if (part.equals(".")) {
458                     parts.set(i, null);
459                 }
460             }
461             return parts.stream().filter(Objects::nonNull).collect(Collectors.joining(dirSeparator()));
462         }
463 
464         @Override
465         public int compareTo(final FSInfo other) {
466             return CompareToBuilder.reflectionCompare(this, other);
467         }
468 
469         @Override
470         public boolean equals(final Object other) {
471             return EqualsBuilder.reflectionEquals(this, other);
472         }
473 
474         @Override
475         public int hashCode() {
476             return HashCodeBuilder.reflectionHashCode(this);
477         }
478     }
479 
480     /**
481      * The Builder for a DocumentName.
482      */
483     public static final class Builder {
484         /** The name for the document. */
485         private String name;
486         /** The base name for the document. */
487         private DocumentName baseName;
488         /** The file system info. */
489         private final FSInfo fsInfo;
490         /** The file system root. */
491         private String root;
492         /** A flag for baseName same as this. */
493         private boolean sameNameFlag;
494 
495         /**
496          * Create with default settings.
497          */
498         private Builder(final FSInfo fsInfo) {
499             this.fsInfo = fsInfo;
500             root = "";
501         }
502 
503         /**
504          * Create with default settings.
505          */
506         private Builder(final FileSystem fileSystem) {
507             this(new FSInfo(fileSystem));
508         }
509 
510         /**
511          * Create based on the file provided.
512          * @param file the file to base the builder on.
513          */
514         private Builder(final File file) {
515             this(FSInfo.getDefault());
516             setName(file);
517         }
518 
519         /**
520          * Used in testing.
521          * @param fsInfo the FSInfo for the file.
522          * @param file the file to process.
523          */
524         Builder(final FSInfo fsInfo, final File file) {
525             this(fsInfo);
526             setName(file);
527         }
528 
529         /**
530          * Create a Builder that clones the specified DocumentName.
531          * @param documentName the DocumentName to clone.
532          */
533         Builder(final DocumentName documentName) {
534             this.root = documentName.root;
535             this.name = documentName.name;
536             this.baseName = documentName.baseName;
537             this.fsInfo = documentName.fsInfo;
538         }
539 
540         /**
541          * Get the directory separator for this builder.
542          * @return the directory separator fo this builder.
543          */
544         public String directorySeparator() {
545             return fsInfo.dirSeparator();
546         }
547 
548         /**
549          * Verify that the builder will build a proper DocumentName.
550          */
551         private void verify() {
552             Objects.requireNonNull(name, "Name must not be null");
553             if (name.startsWith(fsInfo.dirSeparator())) {
554                 name = name.substring(fsInfo.dirSeparator().length());
555             }
556             if (!sameNameFlag) {
557                 Objects.requireNonNull(baseName, "Basename must not be null");
558             }
559         }
560 
561         /**
562          * Sets the root for the DocumentName.
563          * @param root the root for the DocumentName.
564          * @return this.
565          */
566         public Builder setRoot(final String root) {
567             this.root = StringUtils.defaultIfBlank(root, "");
568             return this;
569         }
570 
571         /**
572          * Sets the name for this DocumentName relative to the baseName.
573          * If the {@code name} is {@code null} an empty string is used.
574          * <p>
575          *     To correctly parse the string it must use the directory separator specified by
576          *     this Document.
577          * </p>
578          * @param name the name for this Document name. Will be made relative to the baseName.
579          * @return this
580          */
581         public Builder setName(final String name) {
582             Pair<String, String> pair = splitRoot(StringUtils.defaultIfBlank(name, ""));
583             if (this.root.isEmpty()) {
584                 this.root = pair.getLeft();
585             }
586             this.name = fsInfo.normalize(pair.getRight());
587             if (this.baseName != null && !baseName.name.isEmpty()) {
588                 if (!this.name.startsWith(baseName.name)) {
589                     this.name = this.name.isEmpty() ? baseName.name :
590                             baseName.name + fsInfo.dirSeparator() + this.name;
591                 }
592             }
593             return this;
594         }
595 
596         /**
597          * Extracts the root/name pair from a name string.
598          * <p>
599          *     Package private for testing.
600          * </p>
601          * @param name the name to extract the root/name pair from.
602          * @return the root/name pair.
603          */
604         Pair<String, String> splitRoot(final String name) {
605             String workingName = name;
606             Optional<String> maybeRoot = fsInfo.rootFor(name);
607             String root = maybeRoot.orElse("");
608             if (!root.isEmpty()) {
609                 if (workingName.startsWith(root)) {
610                     workingName = workingName.substring(root.length());
611                     if (!workingName.startsWith(fsInfo.dirSeparator())) {
612                         if (root.endsWith(fsInfo.dirSeparator())) {
613                             root = root.substring(0, root.length() - fsInfo.dirSeparator().length());
614                         }
615                     }
616                 }
617             }
618             return ImmutablePair.of(root, workingName);
619         }
620 
621         /**
622          * Sets the builder root if it is empty.
623          * @param root the root to set the builder root to if it is empty.
624          */
625         private void setEmptyRoot(final String root) {
626             if (this.root.isEmpty()) {
627                 this.root = root;
628             }
629         }
630 
631         /**
632          * Sets the properties from the file. Will reset the baseName appropriately.
633          * @param file the file to set the properties from.
634          * @return this.
635          */
636         public Builder setName(final File file) {
637             Pair<String, String> pair = splitRoot(file.getAbsolutePath());
638             setEmptyRoot(pair.getLeft());
639             this.name = fsInfo.normalize(pair.getRight());
640             if (file.isDirectory()) {
641                 sameNameFlag = true;
642             } else {
643                 File p = file.getParentFile();
644                 if (p != null) {
645                     setBaseName(p);
646                 } else {
647                     Builder baseBuilder = new Builder(this.fsInfo).setName(this.directorySeparator());
648                     baseBuilder.sameNameFlag = true;
649                     setBaseName(baseBuilder.build());
650                 }
651             }
652             return this;
653         }
654 
655         /**
656          * Sets the baseName.
657          * Will set the root if it is not set.
658          * <p>
659          *     To correctly parse the string it must use the directory separator specified by this builder.
660          * </p>
661          * @param baseName the basename to use.
662          * @return this.
663          */
664         public Builder setBaseName(final String baseName) {
665             DocumentName.Builder builder = DocumentName.builder(fsInfo).setName(baseName);
666             builder.sameNameFlag = true;
667             setBaseName(builder);
668             return this;
669         }
670 
671         /**
672          * Sets the basename from the {@link #name} of the specified DocumentName.
673          * Will set the root the baseName has the root set.
674          * @param baseName the DocumentName to set the basename from.
675          * @return this.
676          */
677         public Builder setBaseName(final DocumentName baseName) {
678             this.baseName = baseName;
679             if (!baseName.getRoot().isEmpty()) {
680                 this.root = baseName.getRoot();
681             }
682             return this;
683         }
684 
685         /**
686          * Executes the builder, sets the base name and clears the sameName flag.
687          * @param builder the builder for the base name.
688          */
689         private void setBaseName(final DocumentName.Builder builder) {
690             this.baseName = builder.build();
691             this.sameNameFlag = false;
692         }
693 
694         /**
695          * Sets the basename from a File. Sets {@link #root} and the {@link #baseName}
696          * Will set the root.
697          * @param file the file to set the base name from.
698          * @return this.
699          */
700         public Builder setBaseName(final File file) {
701             DocumentName.Builder builder = DocumentName.builder(fsInfo).setName(file);
702             builder.sameNameFlag = true;
703             setBaseName(builder);
704             return this;
705         }
706 
707         /**
708          * Build a DocumentName from this builder.
709          * @return A new DocumentName.
710          */
711         public DocumentName build() {
712             verify();
713             return new DocumentName(this);
714         }
715     }
716 }