View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver;
18  
19  import javax.annotation.Nonnull;
20  import javax.inject.Inject;
21  import javax.inject.Provider;
22  
23  import java.io.Closeable;
24  import java.io.File;
25  import java.io.IOException;
26  import java.lang.reflect.UndeclaredThrowableException;
27  import java.nio.charset.Charset;
28  import java.nio.file.Files;
29  import java.nio.file.attribute.FileTime;
30  import java.util.ArrayList;
31  import java.util.Comparator;
32  import java.util.Date;
33  import java.util.HashMap;
34  import java.util.HashSet;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.NoSuchElementException;
39  import java.util.Set;
40  
41  import org.codehaus.plexus.archiver.manager.ArchiverManager;
42  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
43  import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributeUtils;
44  import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributes;
45  import org.codehaus.plexus.components.io.attributes.SimpleResourceAttributes;
46  import org.codehaus.plexus.components.io.functions.ResourceAttributeSupplier;
47  import org.codehaus.plexus.components.io.resources.AbstractPlexusIoResourceCollection;
48  import org.codehaus.plexus.components.io.resources.EncodingSupported;
49  import org.codehaus.plexus.components.io.resources.PlexusIoArchivedResourceCollection;
50  import org.codehaus.plexus.components.io.resources.PlexusIoFileResourceCollection;
51  import org.codehaus.plexus.components.io.resources.PlexusIoResource;
52  import org.codehaus.plexus.components.io.resources.PlexusIoResourceCollection;
53  import org.codehaus.plexus.components.io.resources.proxy.PlexusIoProxyResourceCollection;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import static org.codehaus.plexus.archiver.util.DefaultArchivedFileSet.archivedFileSet;
58  import static org.codehaus.plexus.archiver.util.DefaultFileSet.fileSet;
59  
60  public abstract class AbstractArchiver implements Archiver, FinalizerEnabled {
61  
62      private final Logger logger = LoggerFactory.getLogger(getClass());
63  
64      protected Logger getLogger() {
65          return logger;
66      }
67  
68      private File destFile;
69  
70      /**
71       * A list of the following objects:
72       * <ul>
73       * <li>Instances of {@link ArchiveEntry}, which are passed back by {@link #getResources()} without modifications
74       * .</li>
75       * <li>Instances of {@link PlexusIoResourceCollection}, which are converted into an {@link Iterator} over instances
76       * of {@link ArchiveEntry} by {@link #getResources()}.
77       * </ul>
78       */
79      private final List<Object> resources = new ArrayList<>();
80  
81      private boolean includeEmptyDirs = true;
82  
83      private int forcedFileMode = -1; // Will always be used
84  
85      private int forcedDirectoryMode = -1; // Will always be used
86  
87      private int defaultFileMode = -1; // Optionally used if a value is needed
88  
89      private int defaultDirectoryMode = -1; // Optionally used if a value is needed
90  
91      private int umask = -1;
92  
93      private boolean forced = true;
94  
95      private List<ArchiveFinalizer> finalizers;
96  
97      private File dotFileDirectory;
98  
99      private String duplicateBehavior = Archiver.DUPLICATES_SKIP;
100 
101     // On lunix-like systems, we replace windows backslashes with forward slashes
102     private final boolean replacePathSlashesToJavaPaths = File.separatorChar == '/';
103 
104     private final List<Closeable> closeables = new ArrayList<>();
105 
106     /**
107      * since 2.2 is on by default
108      *
109      * @since 1.1
110      */
111     private boolean useJvmChmod = true;
112 
113     private FileTime lastModifiedTime;
114 
115     /**
116      * @since 4.2.0
117      */
118     private Comparator<String> filenameComparator;
119 
120     /**
121      * @since 4.2.0
122      */
123     private int overrideUid = -1;
124 
125     /**
126      * @since 4.2.0
127      */
128     private String overrideUserName;
129 
130     /**
131      * @since 4.2.0
132      */
133     private int overrideGid = -1;
134 
135     /**
136      * @since 4.2.0
137      */
138     private String overrideGroupName;
139 
140     /**
141      * Injected: Allows us to pull the ArchiverManager instance out of the container without causing a chicken-and-egg
142      * instantiation/composition problem.
143      */
144     @Inject
145     private Provider<ArchiverManager> archiverManagerProvider;
146 
147     private static class AddedResourceCollection {
148 
149         private final PlexusIoResourceCollection resources;
150 
151         private final int forcedFileMode;
152 
153         private final int forcedDirectoryMode;
154 
155         public AddedResourceCollection(PlexusIoResourceCollection resources, int forcedFileMode, int forcedDirMode) {
156             this.resources = resources;
157             this.forcedFileMode = forcedFileMode;
158             this.forcedDirectoryMode = forcedDirMode;
159         }
160 
161         private int maybeOverridden(int suggestedMode, boolean isDir) {
162             if (isDir) {
163                 return forcedDirectoryMode >= 0 ? forcedDirectoryMode : suggestedMode;
164             } else {
165                 return forcedFileMode >= 0 ? forcedFileMode : suggestedMode;
166             }
167         }
168     }
169 
170     /**
171      * @since 1.1
172      */
173     private boolean ignorePermissions = false;
174 
175     @Override
176     public String getDuplicateBehavior() {
177         return duplicateBehavior;
178     }
179 
180     @Override
181     public void setDuplicateBehavior(final String duplicate) {
182         if (!Archiver.DUPLICATES_VALID_BEHAVIORS.contains(duplicate)) {
183             throw new IllegalArgumentException("Invalid duplicate-file behavior: '" + duplicate
184                     + "'. Please specify one of: " + Archiver.DUPLICATES_VALID_BEHAVIORS);
185         }
186 
187         duplicateBehavior = duplicate;
188     }
189 
190     @Override
191     public final void setFileMode(final int mode) {
192         if (mode >= 0) {
193             forcedFileMode = (mode & UnixStat.PERM_MASK) | UnixStat.FILE_FLAG;
194         } else {
195             forcedFileMode = -1;
196         }
197     }
198 
199     @Override
200     public final void setDefaultFileMode(final int mode) {
201         defaultFileMode = (mode & UnixStat.PERM_MASK) | UnixStat.FILE_FLAG;
202     }
203 
204     @Override
205     public final int getOverrideFileMode() {
206         return forcedFileMode;
207     }
208 
209     @Override
210     public final int getFileMode() {
211         if (forcedFileMode < 0) {
212             if (defaultFileMode < 0) {
213                 return DEFAULT_FILE_MODE;
214             }
215 
216             return defaultFileMode;
217         }
218 
219         return forcedFileMode;
220     }
221 
222     @Override
223     public final int getDefaultFileMode() {
224         return defaultFileMode;
225     }
226 
227     /**
228      * @deprecated Use {@link Archiver#getDefaultFileMode()}.
229      */
230     @Deprecated
231     public final int getRawDefaultFileMode() {
232         return getDefaultFileMode();
233     }
234 
235     @Override
236     public final void setDirectoryMode(final int mode) {
237         if (mode >= 0) {
238             forcedDirectoryMode = (mode & UnixStat.PERM_MASK) | UnixStat.DIR_FLAG;
239         } else {
240             forcedDirectoryMode = -1;
241         }
242     }
243 
244     @Override
245     public final void setDefaultDirectoryMode(final int mode) {
246         defaultDirectoryMode = (mode & UnixStat.PERM_MASK) | UnixStat.DIR_FLAG;
247     }
248 
249     @Override
250     public final int getOverrideDirectoryMode() {
251         return forcedDirectoryMode;
252     }
253 
254     @Override
255     public final int getDirectoryMode() {
256         if (forcedDirectoryMode < 0) {
257             if (defaultDirectoryMode < 0) {
258                 return DEFAULT_DIR_MODE;
259             }
260 
261             return defaultDirectoryMode;
262         }
263 
264         return forcedDirectoryMode;
265     }
266 
267     @Override
268     public final int getDefaultDirectoryMode() {
269         if (defaultDirectoryMode < 0) {
270             return DEFAULT_DIR_MODE;
271         } else {
272             return defaultDirectoryMode;
273         }
274     }
275 
276     @Override
277     public boolean getIncludeEmptyDirs() {
278         return includeEmptyDirs;
279     }
280 
281     @Override
282     public void setIncludeEmptyDirs(final boolean includeEmptyDirs) {
283         this.includeEmptyDirs = includeEmptyDirs;
284     }
285 
286     @Override
287     @Deprecated
288     public void addDirectory(@Nonnull final File directory) throws ArchiverException {
289         addFileSet(fileSet(directory).prefixed("").includeExclude(null, null).includeEmptyDirs(includeEmptyDirs));
290     }
291 
292     @Override
293     @Deprecated
294     public void addDirectory(@Nonnull final File directory, final String prefix) throws ArchiverException {
295         addFileSet(
296                 fileSet(directory).prefixed(prefix).includeExclude(null, null).includeEmptyDirs(includeEmptyDirs));
297     }
298 
299     @Override
300     @Deprecated
301     public void addDirectory(@Nonnull final File directory, final String[] includes, final String[] excludes)
302             throws ArchiverException {
303         addFileSet(fileSet(directory)
304                 .prefixed("")
305                 .includeExclude(includes, excludes)
306                 .includeEmptyDirs(includeEmptyDirs));
307     }
308 
309     @Override
310     @Deprecated
311     public void addDirectory(
312             @Nonnull final File directory, final String prefix, final String[] includes, final String[] excludes)
313             throws ArchiverException {
314         addFileSet(fileSet(directory)
315                 .prefixed(prefix)
316                 .includeExclude(includes, excludes)
317                 .includeEmptyDirs(includeEmptyDirs));
318     }
319 
320     @Override
321     public void addFileSet(@Nonnull final FileSet fileSet) throws ArchiverException {
322         final File directory = fileSet.getDirectory();
323         if (directory == null) {
324             throw new ArchiverException("The file sets base directory is null.");
325         }
326 
327         if (!directory.isDirectory()) {
328             throw new ArchiverException(directory.getAbsolutePath() + " isn't a directory.");
329         }
330 
331         // The PlexusIoFileResourceCollection contains platform-specific File.separatorChar which
332         // is an interesting cause of grief, see PLXCOMP-192
333         final PlexusIoFileResourceCollection collection = new PlexusIoFileResourceCollection();
334         collection.setFollowingSymLinks(false);
335 
336         collection.setIncludes(fileSet.getIncludes());
337         collection.setExcludes(fileSet.getExcludes());
338         collection.setBaseDir(directory);
339         collection.setFileSelectors(fileSet.getFileSelectors());
340         collection.setIncludingEmptyDirectories(fileSet.isIncludingEmptyDirectories());
341         collection.setPrefix(fileSet.getPrefix());
342         collection.setCaseSensitive(fileSet.isCaseSensitive());
343         collection.setUsingDefaultExcludes(fileSet.isUsingDefaultExcludes());
344         collection.setStreamTransformer(fileSet.getStreamTransformer());
345         collection.setFileMappers(fileSet.getFileMappers());
346         collection.setFilenameComparator(getFilenameComparator());
347 
348         if (getOverrideDirectoryMode() > -1
349                 || getOverrideFileMode() > -1
350                 || getOverrideUid() > -1
351                 || getOverrideGid() > -1
352                 || getOverrideUserName() != null
353                 || getOverrideGroupName() != null) {
354             collection.setOverrideAttributes(
355                     getOverrideUid(),
356                     getOverrideUserName(),
357                     getOverrideGid(),
358                     getOverrideGroupName(),
359                     getOverrideFileMode(),
360                     getOverrideDirectoryMode());
361         }
362 
363         if (getDefaultDirectoryMode() > -1 || getDefaultFileMode() > -1) {
364             collection.setDefaultAttributes(-1, null, -1, null, getDefaultFileMode(), getDefaultDirectoryMode());
365         }
366 
367         addResources(collection);
368     }
369 
370     @Override
371     public void addFile(@Nonnull final File inputFile, @Nonnull final String destFileName) throws ArchiverException {
372         int permissions;
373         if (forcedFileMode > 0) {
374             permissions = forcedFileMode;
375         } else {
376             permissions = PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE;
377             try {
378                 permissions = PlexusIoResourceAttributeUtils.getFileAttributes(inputFile)
379                         .getOctalMode();
380             } catch (IOException ioe) {
381                 // ignore
382             }
383         }
384 
385         addFile(inputFile, destFileName, permissions);
386     }
387 
388     @Override
389     public void addSymlink(String symlinkName, String symlinkDestination) throws ArchiverException {
390         final int fileMode = getOverrideFileMode();
391 
392         addSymlink(symlinkName, fileMode, symlinkDestination);
393     }
394 
395     @Override
396     public void addSymlink(String symlinkName, int permissions, String symlinkDestination) throws ArchiverException {
397         doAddResource(
398                 ArchiveEntry.createSymlinkEntry(symlinkName, permissions, symlinkDestination, getDirectoryMode()));
399     }
400 
401     private ArchiveEntry updateArchiveEntryAttributes(ArchiveEntry entry) {
402         if (getOverrideUid() > -1
403                 || getOverrideGid() > -1
404                 || getOverrideUserName() != null
405                 || getOverrideGroupName() != null) {
406             entry.setResourceAttributes(new SimpleResourceAttributes(
407                     getOverrideUid(),
408                     getOverrideUserName(),
409                     getOverrideGid(),
410                     getOverrideGroupName(),
411                     entry.getMode()));
412         }
413         return entry;
414     }
415 
416     protected ArchiveEntry asArchiveEntry(
417             @Nonnull final PlexusIoResource resource,
418             final String destFileName,
419             int permissions,
420             PlexusIoResourceCollection collection)
421             throws ArchiverException {
422         if (!resource.isExisting()) {
423             throw new ArchiverException(resource.getName() + " not found.");
424         }
425 
426         if (umask > 0 && permissions != PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE) {
427             permissions &= ~umask;
428         }
429 
430         ArchiveEntry entry;
431         if (resource.isFile()) {
432             entry = ArchiveEntry.createFileEntry(destFileName, resource, permissions, collection, getDirectoryMode());
433         } else {
434             entry = ArchiveEntry.createDirectoryEntry(destFileName, resource, permissions, getDirectoryMode());
435         }
436 
437         return updateArchiveEntryAttributes(entry);
438     }
439 
440     private ArchiveEntry asArchiveEntry(final AddedResourceCollection collection, final PlexusIoResource resource)
441             throws ArchiverException {
442         final String destFileName = collection.resources.getName(resource);
443 
444         int fromResource = PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE;
445         if (resource instanceof ResourceAttributeSupplier) {
446             final PlexusIoResourceAttributes attrs = ((ResourceAttributeSupplier) resource).getAttributes();
447 
448             if (attrs != null) {
449                 fromResource = attrs.getOctalMode();
450             }
451         }
452 
453         return asArchiveEntry(
454                 resource,
455                 destFileName,
456                 collection.maybeOverridden(fromResource, resource.isDirectory()),
457                 collection.resources);
458     }
459 
460     @Override
461     public void addResource(final PlexusIoResource resource, final String destFileName, final int permissions)
462             throws ArchiverException {
463         doAddResource(asArchiveEntry(resource, destFileName, permissions, null));
464     }
465 
466     @Override
467     public void addFile(@Nonnull final File inputFile, @Nonnull String destFileName, int permissions)
468             throws ArchiverException {
469         if (!inputFile.isFile() || !inputFile.exists()) {
470             throw new ArchiverException(inputFile.getAbsolutePath() + " isn't a file.");
471         }
472 
473         if (replacePathSlashesToJavaPaths) {
474             destFileName = destFileName.replace('\\', '/');
475         }
476 
477         if (permissions < 0) {
478             permissions = getOverrideFileMode();
479         }
480 
481         if (umask > 0 && permissions != PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE) {
482             permissions &= ~umask;
483         }
484 
485         try {
486             // do a null check here, to avoid creating a file stream if there are no filters...
487             ArchiveEntry entry = ArchiveEntry.createFileEntry(destFileName, inputFile, permissions, getDirectoryMode());
488             doAddResource(updateArchiveEntryAttributes(entry));
489         } catch (final IOException e) {
490             throw new ArchiverException("Failed to determine inclusion status for: " + inputFile, e);
491         }
492     }
493 
494     @Nonnull
495     @Override
496     public ResourceIterator getResources() throws ArchiverException {
497         return new ResourceIterator() {
498 
499             private final Iterator<Object> addedResourceIter = resources.iterator();
500 
501             private AddedResourceCollection currentResourceCollection;
502 
503             private Iterator ioResourceIter;
504 
505             private ArchiveEntry nextEntry;
506 
507             private final Set<String> seenEntries = new HashSet<>();
508 
509             @Override
510             public boolean hasNext() {
511                 do {
512                     if (nextEntry == null) {
513                         if (ioResourceIter == null) {
514                             if (addedResourceIter.hasNext()) {
515                                 final Object o = addedResourceIter.next();
516                                 if (o instanceof ArchiveEntry) {
517                                     nextEntry = (ArchiveEntry) o;
518                                 } else if (o instanceof AddedResourceCollection) {
519                                     currentResourceCollection = (AddedResourceCollection) o;
520 
521                                     try {
522                                         ioResourceIter = currentResourceCollection.resources.getResources();
523                                     } catch (final IOException e) {
524                                         throw new ArchiverException(e.getMessage(), e);
525                                     }
526                                 } else {
527                                     return throwIllegalResourceType(o);
528                                 }
529                             } else {
530                                 nextEntry = null;
531                             }
532                         } else {
533                             if (ioResourceIter.hasNext()) {
534                                 final PlexusIoResource resource = (PlexusIoResource) ioResourceIter.next();
535                                 nextEntry = asArchiveEntry(currentResourceCollection, resource);
536                             } else {
537                                 // this will leak handles in the IO iterator if the iterator is not fully consumed.
538                                 // alternately we'd have to make this method return a Closeable iterator back
539                                 // to the client and ditch the whole issue onto the client.
540                                 // this does not really make any sense either, might equally well change the
541                                 // api into something that is not broken by design.
542                                 addCloseable(ioResourceIter);
543                                 ioResourceIter = null;
544                             }
545                         }
546                     }
547 
548                     if (nextEntry != null && seenEntries.contains(normalizedForDuplicateCheck(nextEntry))) {
549                         final String path = nextEntry.getName();
550 
551                         if (Archiver.DUPLICATES_PRESERVE.equals(duplicateBehavior)
552                                 || Archiver.DUPLICATES_SKIP.equals(duplicateBehavior)) {
553                             if (nextEntry.getType() == ArchiveEntry.FILE) {
554                                 getLogger().debug(path + " already added, skipping");
555                             }
556 
557                             nextEntry = null;
558                         } else if (Archiver.DUPLICATES_FAIL.equals(duplicateBehavior)) {
559                             throw new ArchiverException("Duplicate file " + path + " was found and the duplicate "
560                                     + "attribute is 'fail'.");
561                         } else {
562                             // duplicate equal to add, so we continue
563                             getLogger().debug("duplicate file " + path + " found, adding.");
564                         }
565                     }
566                 } while (nextEntry == null && !(ioResourceIter == null && !addedResourceIter.hasNext()));
567 
568                 return nextEntry != null;
569             }
570 
571             private boolean throwIllegalResourceType(Object o) {
572                 throw new IllegalStateException(
573                         "An invalid resource of type: " + o.getClass().getName() + " was added to archiver: "
574                                 + getClass().getName());
575             }
576 
577             @Override
578             public ArchiveEntry next() {
579                 if (!hasNext()) {
580                     throw new NoSuchElementException();
581                 }
582 
583                 final ArchiveEntry next = nextEntry;
584                 nextEntry = null;
585 
586                 seenEntries.add(normalizedForDuplicateCheck(next));
587 
588                 return next;
589             }
590 
591             @Override
592             public void remove() {
593                 throw new UnsupportedOperationException("Does not support iterator");
594             }
595 
596             private String normalizedForDuplicateCheck(ArchiveEntry entry) {
597                 return entry.getName().replace('\\', '/');
598             }
599         };
600     }
601 
602     private static void closeIfCloseable(Object resource) throws IOException {
603         if (resource == null) {
604             return;
605         }
606         if (resource instanceof Closeable) {
607             ((Closeable) resource).close();
608         }
609     }
610 
611     private static void closeQuietlyIfCloseable(Object resource) {
612         try {
613             closeIfCloseable(resource);
614         } catch (IOException e) {
615             throw new RuntimeException(e);
616         }
617     }
618 
619     @Override
620     @Deprecated
621     public Map<String, ArchiveEntry> getFiles() {
622         try {
623             final Map<String, ArchiveEntry> map = new HashMap<>();
624             for (final ResourceIterator iter = getResources(); iter.hasNext(); ) {
625                 final ArchiveEntry entry = iter.next();
626                 if (includeEmptyDirs || entry.getType() == ArchiveEntry.FILE) {
627                     map.put(entry.getName(), entry);
628                 }
629             }
630             return map;
631         } catch (final ArchiverException e) {
632             throw new UndeclaredThrowableException(e);
633         }
634     }
635 
636     @Override
637     public File getDestFile() {
638         return destFile;
639     }
640 
641     @Override
642     public void setDestFile(final File destFile) {
643         this.destFile = destFile;
644 
645         if (destFile != null && destFile.getParentFile() != null) {
646             destFile.getParentFile().mkdirs();
647         }
648     }
649 
650     protected PlexusIoResourceCollection asResourceCollection(final ArchivedFileSet fileSet, Charset charset)
651             throws ArchiverException {
652         final File archiveFile = fileSet.getArchive();
653 
654         final PlexusIoResourceCollection resources;
655         try {
656             resources = archiverManagerProvider.get().getResourceCollection(archiveFile);
657         } catch (final NoSuchArchiverException e) {
658             throw new ArchiverException(
659                     "Error adding archived file-set. PlexusIoResourceCollection not found for: " + archiveFile, e);
660         }
661 
662         if (resources instanceof EncodingSupported) {
663             ((EncodingSupported) resources).setEncoding(charset);
664         }
665 
666         if (resources instanceof PlexusIoArchivedResourceCollection) {
667             ((PlexusIoArchivedResourceCollection) resources).setFile(fileSet.getArchive());
668         } else {
669             throw new ArchiverException("Expected " + PlexusIoArchivedResourceCollection.class.getName() + ", got "
670                     + resources.getClass().getName());
671         }
672 
673         if (resources instanceof AbstractPlexusIoResourceCollection) {
674             ((AbstractPlexusIoResourceCollection) resources).setStreamTransformer(fileSet.getStreamTransformer());
675         }
676         final PlexusIoProxyResourceCollection proxy = new PlexusIoProxyResourceCollection(resources);
677 
678         proxy.setExcludes(fileSet.getExcludes());
679         proxy.setIncludes(fileSet.getIncludes());
680         proxy.setIncludingEmptyDirectories(fileSet.isIncludingEmptyDirectories());
681         proxy.setCaseSensitive(fileSet.isCaseSensitive());
682         proxy.setPrefix(fileSet.getPrefix());
683         proxy.setUsingDefaultExcludes(fileSet.isUsingDefaultExcludes());
684         proxy.setFileSelectors(fileSet.getFileSelectors());
685         proxy.setStreamTransformer(fileSet.getStreamTransformer());
686         proxy.setFileMappers(fileSet.getFileMappers());
687 
688         if (getOverrideDirectoryMode() > -1 || getOverrideFileMode() > -1) {
689             proxy.setOverrideAttributes(-1, null, -1, null, getOverrideFileMode(), getOverrideDirectoryMode());
690         }
691 
692         if (getDefaultDirectoryMode() > -1 || getDefaultFileMode() > -1) {
693             proxy.setDefaultAttributes(-1, null, -1, null, getDefaultFileMode(), getDefaultDirectoryMode());
694         }
695 
696         return proxy;
697     }
698 
699     /**
700      * Adds a resource collection to the archive.
701      */
702     @Override
703     public void addResources(final PlexusIoResourceCollection collection) throws ArchiverException {
704         doAddResource(new AddedResourceCollection(collection, forcedFileMode, forcedDirectoryMode));
705     }
706 
707     private void doAddResource(Object item) {
708         resources.add(item);
709     }
710 
711     @Override
712     public void addArchivedFileSet(final ArchivedFileSet fileSet) throws ArchiverException {
713         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, null);
714         addResources(resourceCollection);
715     }
716 
717     @Override
718     public void addArchivedFileSet(final ArchivedFileSet fileSet, Charset charset) throws ArchiverException {
719         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, charset);
720         addResources(resourceCollection);
721     }
722 
723     /**
724      * @since 1.0-alpha-7
725      */
726     @Override
727     @Deprecated
728     public void addArchivedFileSet(
729             @Nonnull final File archiveFile, final String prefix, final String[] includes, final String[] excludes)
730             throws ArchiverException {
731         addArchivedFileSet(archivedFileSet(archiveFile)
732                 .prefixed(prefix)
733                 .includeExclude(includes, excludes)
734                 .includeEmptyDirs(includeEmptyDirs));
735     }
736 
737     /**
738      * @since 1.0-alpha-7
739      */
740     @Override
741     @Deprecated
742     public void addArchivedFileSet(@Nonnull final File archiveFile, final String prefix) throws ArchiverException {
743         addArchivedFileSet(archivedFileSet(archiveFile).prefixed(prefix).includeEmptyDirs(includeEmptyDirs));
744     }
745 
746     /**
747      * @since 1.0-alpha-7
748      */
749     @Override
750     @Deprecated
751     public void addArchivedFileSet(@Nonnull final File archiveFile, final String[] includes, final String[] excludes)
752             throws ArchiverException {
753         addArchivedFileSet(
754                 archivedFileSet(archiveFile).includeExclude(includes, excludes).includeEmptyDirs(includeEmptyDirs));
755     }
756 
757     /**
758      * @since 1.0-alpha-7
759      */
760     @Override
761     @Deprecated
762     public void addArchivedFileSet(@Nonnull final File archiveFile) throws ArchiverException {
763         addArchivedFileSet(archivedFileSet(archiveFile).includeEmptyDirs(includeEmptyDirs));
764     }
765 
766     @Override
767     public boolean isForced() {
768         return forced;
769     }
770 
771     @Override
772     public void setForced(final boolean forced) {
773         this.forced = forced;
774     }
775 
776     @Override
777     public void addArchiveFinalizer(final ArchiveFinalizer finalizer) {
778         if (finalizers == null) {
779             finalizers = new ArrayList<>();
780         }
781 
782         finalizers.add(finalizer);
783     }
784 
785     @Override
786     public void setArchiveFinalizers(final List<ArchiveFinalizer> archiveFinalizers) {
787         finalizers = archiveFinalizers;
788     }
789 
790     @Override
791     public void setDotFileDirectory(final File dotFileDirectory) {
792         this.dotFileDirectory = dotFileDirectory;
793     }
794 
795     protected boolean isUptodate() throws ArchiverException {
796         final File zipFile = getDestFile();
797         if (!zipFile.exists()) {
798             getLogger().debug("isUp2date: false (Destination " + zipFile.getPath() + " not found.)");
799             return false; // File doesn't yet exist
800         }
801         final long destTimestamp = getFileLastModifiedTime(zipFile);
802 
803         final Iterator<Object> it = resources.iterator();
804         if (!it.hasNext()) {
805             getLogger().debug("isUp2date: false (No input files.)");
806             return false; // No timestamp to compare
807         }
808 
809         while (it.hasNext()) {
810             final Object o = it.next();
811             final long l;
812             if (o instanceof ArchiveEntry) {
813                 l = ((ArchiveEntry) o).getResource().getLastModified();
814             } else if (o instanceof AddedResourceCollection) {
815                 try {
816                     l = ((AddedResourceCollection) o).resources.getLastModified();
817                 } catch (final IOException e) {
818                     throw new ArchiverException(e.getMessage(), e);
819                 }
820             } else {
821                 throw new IllegalStateException(
822                         "Invalid object type: " + o.getClass().getName());
823             }
824             if (l == PlexusIoResource.UNKNOWN_MODIFICATION_DATE) {
825                 // Don't know what to do. Safe thing is to assume not up2date.
826                 getLogger().debug("isUp2date: false (Resource with unknown modification date found.)");
827                 return false;
828             }
829             if (l > destTimestamp) {
830                 getLogger().debug("isUp2date: false (Resource with newer modification date found.)");
831                 return false;
832             }
833         }
834 
835         getLogger().debug("isUp2date: true");
836         return true;
837     }
838 
839     /**
840      * Returns the last modified time in milliseconds of a file.
841      * It avoids the bug where milliseconds precision is lost on File#lastModified (JDK-8177809) on JDK8 and Linux.
842      * @param file The file where the last modified time will be returned for.
843      * @return The last modified time in milliseconds of the file.
844      * @throws ArchiverException In the case of an IOException, for example when the file does not exists.
845      */
846     private long getFileLastModifiedTime(File file) throws ArchiverException {
847         try {
848             return Files.getLastModifiedTime(file.toPath()).toMillis();
849         } catch (IOException e) {
850             throw new ArchiverException(e.getMessage(), e);
851         }
852     }
853 
854     protected boolean checkForced() throws ArchiverException {
855         if (!isForced() && isSupportingForced() && isUptodate()) {
856             getLogger().debug("Archive " + getDestFile() + " is uptodate.");
857             return false;
858         }
859         return true;
860     }
861 
862     @Override
863     public boolean isSupportingForced() {
864         return false;
865     }
866 
867     protected void runArchiveFinalizers() throws ArchiverException {
868         if (finalizers != null) {
869             for (final ArchiveFinalizer finalizer : finalizers) {
870                 finalizer.finalizeArchiveCreation(this);
871             }
872         }
873     }
874 
875     @Override
876     public final void createArchive() throws ArchiverException, IOException {
877         validate();
878         try {
879             try {
880                 if (dotFileDirectory != null) {
881                     addArchiveFinalizer(new DotDirectiveArchiveFinalizer(dotFileDirectory));
882                 }
883 
884                 runArchiveFinalizers();
885 
886                 execute();
887             } finally {
888                 close();
889             }
890         } catch (final IOException e) {
891             String msg = "Problem creating " + getArchiveType() + ": " + e.getMessage();
892 
893             final StringBuffer revertBuffer = new StringBuffer();
894             if (!revert(revertBuffer)) {
895                 msg += revertBuffer.toString();
896             }
897 
898             throw new ArchiverException(msg, e);
899         } finally {
900             cleanUp();
901         }
902 
903         postCreateArchive();
904     }
905 
906     protected boolean hasVirtualFiles() {
907         if (finalizers != null) {
908             for (final ArchiveFinalizer finalizer : finalizers) {
909                 final List virtualFiles = finalizer.getVirtualFiles();
910 
911                 if ((virtualFiles != null) && !virtualFiles.isEmpty()) {
912                     return true;
913                 }
914             }
915         }
916         return false;
917     }
918 
919     protected boolean revert(final StringBuffer messageBuffer) {
920         return true;
921     }
922 
923     protected void validate() throws ArchiverException, IOException {}
924 
925     /**
926      * This method is called after the archive creation
927      * completes successfully (no exceptions are thrown).
928      *
929      * Subclasses may override this method in order to
930      * augment or validate the archive after it is
931      * created.
932      *
933      * @since 3.6
934      */
935     protected void postCreateArchive() throws ArchiverException, IOException {}
936 
937     protected abstract String getArchiveType();
938 
939     private void addCloseable(Object maybeCloseable) {
940         if (maybeCloseable instanceof Closeable) {
941             closeables.add((Closeable) maybeCloseable);
942         }
943     }
944 
945     private void closeIterators() {
946         for (Closeable closeable : closeables) {
947             closeQuietlyIfCloseable(closeable);
948         }
949     }
950 
951     protected abstract void close() throws IOException;
952 
953     protected void cleanUp() throws IOException {
954         closeIterators();
955 
956         for (Object resource : resources) {
957             if (resource instanceof PlexusIoProxyResourceCollection) {
958                 resource = ((PlexusIoProxyResourceCollection) resource).getSrc();
959             }
960 
961             closeIfCloseable(resource);
962         }
963         resources.clear();
964     }
965 
966     protected abstract void execute() throws ArchiverException, IOException;
967 
968     /**
969      * @since 1.1
970      */
971     @Override
972     @Deprecated
973     public boolean isUseJvmChmod() {
974         return useJvmChmod;
975     }
976 
977     /**
978      * @since 1.1
979      */
980     @Override
981     @Deprecated
982     public void setUseJvmChmod(final boolean useJvmChmod) {
983         this.useJvmChmod = useJvmChmod;
984     }
985 
986     /**
987      * @since 1.1
988      */
989     @Override
990     public boolean isIgnorePermissions() {
991         return ignorePermissions;
992     }
993 
994     /**
995      * @since 1.1
996      */
997     @Override
998     public void setIgnorePermissions(final boolean ignorePermissions) {
999         this.ignorePermissions = ignorePermissions;
1000     }
1001 
1002     /**
1003      * @deprecated Use {@link #setLastModifiedTime(FileTime)} instead.
1004      */
1005     @Override
1006     @Deprecated
1007     public void setLastModifiedDate(Date lastModifiedDate) {
1008         this.lastModifiedTime = lastModifiedDate != null ? FileTime.fromMillis(lastModifiedDate.getTime()) : null;
1009     }
1010 
1011     /**
1012      * @deprecated Use {@link #getLastModifiedTime()} instead.
1013      */
1014     @Override
1015     @Deprecated
1016     public Date getLastModifiedDate() {
1017         return lastModifiedTime != null ? new Date(lastModifiedTime.toMillis()) : null;
1018     }
1019 
1020     @Override
1021     public void setLastModifiedTime(FileTime lastModifiedTime) {
1022         this.lastModifiedTime = lastModifiedTime;
1023     }
1024 
1025     @Override
1026     public FileTime getLastModifiedTime() {
1027         return lastModifiedTime;
1028     }
1029 
1030     @Override
1031     public void setFilenameComparator(Comparator<String> filenameComparator) {
1032         this.filenameComparator = filenameComparator;
1033     }
1034 
1035     public Comparator<String> getFilenameComparator() {
1036         return filenameComparator;
1037     }
1038 
1039     @Override
1040     public void setOverrideUid(int uid) {
1041         overrideUid = uid;
1042     }
1043 
1044     @Override
1045     public void setOverrideUserName(String userName) {
1046         overrideUserName = userName;
1047     }
1048 
1049     @Override
1050     public int getOverrideUid() {
1051         return overrideUid;
1052     }
1053 
1054     @Override
1055     public String getOverrideUserName() {
1056         return overrideUserName;
1057     }
1058 
1059     @Override
1060     public void setOverrideGid(int gid) {
1061         overrideGid = gid;
1062     }
1063 
1064     @Override
1065     public void setOverrideGroupName(String groupName) {
1066         overrideGroupName = groupName;
1067     }
1068 
1069     @Override
1070     public int getOverrideGid() {
1071         return overrideGid;
1072     }
1073 
1074     @Override
1075     public String getOverrideGroupName() {
1076         return overrideGroupName;
1077     }
1078 
1079     @Override
1080     public void setUmask(int umask) {
1081         this.umask = umask;
1082     }
1083 
1084     @Override
1085     public int getUmask() {
1086         return umask;
1087     }
1088 
1089     /**
1090      * @deprecated Use {@link #configureReproducibleBuild(FileTime)} instead.
1091      */
1092     @Override
1093     @Deprecated
1094     public void configureReproducible(Date lastModifiedDate) {
1095         configureReproducibleBuild(FileTime.fromMillis(lastModifiedDate.getTime()));
1096     }
1097 
1098     @Override
1099     public void configureReproducibleBuild(FileTime lastModifiedTime) {
1100         // 1. force last modified date
1101         setLastModifiedTime(normalizeLastModifiedTime(lastModifiedTime));
1102 
1103         // 2. sort filenames in each directory when scanning filesystem
1104         setFilenameComparator(String::compareTo);
1105 
1106         // 3. ignore uid/gid from filesystem (for tar)
1107         setOverrideUid(0);
1108         setOverrideUserName("root"); // is it possible to avoid this, like "tar --numeric-owner"?
1109         setOverrideGid(0);
1110         setOverrideGroupName("root");
1111 
1112         // 4. set umask to 022 to avoid environment umask value particularly on group write
1113         setUmask(0_022);
1114     }
1115 
1116     /**
1117      * Normalize last modified time value to get reproducible archive entries, based on
1118      * archive binary format.
1119      *
1120      * <p>tar uses UTC timestamp, but zip uses local time then requires
1121      * tweaks to make the value reproducible whatever the current timezone is.
1122      *
1123      * @param lastModifiedTime The last modification time
1124      * @return The normalized last modification time
1125      *
1126      * @see #configureReproducibleBuild(FileTime)
1127      */
1128     protected FileTime normalizeLastModifiedTime(FileTime lastModifiedTime) {
1129         return lastModifiedTime;
1130     }
1131 }