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 }