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 }