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     public void addDirectory(@Nonnull final File directory) throws ArchiverException {
288         addFileSet(fileSet(directory).prefixed("").includeExclude(null, null).includeEmptyDirs(includeEmptyDirs));
289     }
290 
291     @Override
292     public void addDirectory(@Nonnull final File directory, final String prefix) throws ArchiverException {
293         addFileSet(
294                 fileSet(directory).prefixed(prefix).includeExclude(null, null).includeEmptyDirs(includeEmptyDirs));
295     }
296 
297     @Override
298     public void addDirectory(@Nonnull final File directory, final String[] includes, final String[] excludes)
299             throws ArchiverException {
300         addFileSet(fileSet(directory)
301                 .prefixed("")
302                 .includeExclude(includes, excludes)
303                 .includeEmptyDirs(includeEmptyDirs));
304     }
305 
306     @Override
307     public void addDirectory(
308             @Nonnull final File directory, final String prefix, final String[] includes, final String[] excludes)
309             throws ArchiverException {
310         addFileSet(fileSet(directory)
311                 .prefixed(prefix)
312                 .includeExclude(includes, excludes)
313                 .includeEmptyDirs(includeEmptyDirs));
314     }
315 
316     @Override
317     public void addFileSet(@Nonnull final FileSet fileSet) throws ArchiverException {
318         final File directory = fileSet.getDirectory();
319         if (directory == null) {
320             throw new ArchiverException("The file sets base directory is null.");
321         }
322 
323         if (!directory.isDirectory()) {
324             throw new ArchiverException(directory.getAbsolutePath() + " isn't a directory.");
325         }
326 
327         // The PlexusIoFileResourceCollection contains platform-specific File.separatorChar which
328         // is an interesting cause of grief, see PLXCOMP-192
329         final PlexusIoFileResourceCollection collection = new PlexusIoFileResourceCollection();
330         collection.setFollowingSymLinks(false);
331 
332         collection.setIncludes(fileSet.getIncludes());
333         collection.setExcludes(fileSet.getExcludes());
334         collection.setBaseDir(directory);
335         collection.setFileSelectors(fileSet.getFileSelectors());
336         collection.setIncludingEmptyDirectories(fileSet.isIncludingEmptyDirectories());
337         collection.setPrefix(fileSet.getPrefix());
338         collection.setCaseSensitive(fileSet.isCaseSensitive());
339         collection.setUsingDefaultExcludes(fileSet.isUsingDefaultExcludes());
340         collection.setStreamTransformer(fileSet.getStreamTransformer());
341         collection.setFileMappers(fileSet.getFileMappers());
342         collection.setFilenameComparator(getFilenameComparator());
343 
344         if (getOverrideDirectoryMode() > -1
345                 || getOverrideFileMode() > -1
346                 || getOverrideUid() > -1
347                 || getOverrideGid() > -1
348                 || getOverrideUserName() != null
349                 || getOverrideGroupName() != null) {
350             collection.setOverrideAttributes(
351                     getOverrideUid(),
352                     getOverrideUserName(),
353                     getOverrideGid(),
354                     getOverrideGroupName(),
355                     getOverrideFileMode(),
356                     getOverrideDirectoryMode());
357         }
358 
359         if (getDefaultDirectoryMode() > -1 || getDefaultFileMode() > -1) {
360             collection.setDefaultAttributes(-1, null, -1, null, getDefaultFileMode(), getDefaultDirectoryMode());
361         }
362 
363         addResources(collection);
364     }
365 
366     @Override
367     public void addFile(@Nonnull final File inputFile, @Nonnull final String destFileName) throws ArchiverException {
368         int permissions;
369         if (forcedFileMode > 0) {
370             permissions = forcedFileMode;
371         } else {
372             permissions = PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE;
373             try {
374                 permissions = PlexusIoResourceAttributeUtils.getFileAttributes(inputFile)
375                         .getOctalMode();
376             } catch (IOException ioe) {
377                 // ignore
378             }
379         }
380 
381         addFile(inputFile, destFileName, permissions);
382     }
383 
384     @Override
385     public void addSymlink(String symlinkName, String symlinkDestination) throws ArchiverException {
386         final int fileMode = getOverrideFileMode();
387 
388         addSymlink(symlinkName, fileMode, symlinkDestination);
389     }
390 
391     @Override
392     public void addSymlink(String symlinkName, int permissions, String symlinkDestination) throws ArchiverException {
393         doAddResource(
394                 ArchiveEntry.createSymlinkEntry(symlinkName, permissions, symlinkDestination, getDirectoryMode()));
395     }
396 
397     private ArchiveEntry updateArchiveEntryAttributes(ArchiveEntry entry) {
398         if (getOverrideUid() > -1
399                 || getOverrideGid() > -1
400                 || getOverrideUserName() != null
401                 || getOverrideGroupName() != null) {
402             entry.setResourceAttributes(new SimpleResourceAttributes(
403                     getOverrideUid(),
404                     getOverrideUserName(),
405                     getOverrideGid(),
406                     getOverrideGroupName(),
407                     entry.getMode()));
408         }
409         return entry;
410     }
411 
412     protected ArchiveEntry asArchiveEntry(
413             @Nonnull final PlexusIoResource resource,
414             final String destFileName,
415             int permissions,
416             PlexusIoResourceCollection collection)
417             throws ArchiverException {
418         if (!resource.isExisting()) {
419             throw new ArchiverException(resource.getName() + " not found.");
420         }
421 
422         if (umask > 0 && permissions != PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE) {
423             permissions &= ~umask;
424         }
425 
426         ArchiveEntry entry;
427         if (resource.isFile()) {
428             entry = ArchiveEntry.createFileEntry(destFileName, resource, permissions, collection, getDirectoryMode());
429         } else {
430             entry = ArchiveEntry.createDirectoryEntry(destFileName, resource, permissions, getDirectoryMode());
431         }
432 
433         return updateArchiveEntryAttributes(entry);
434     }
435 
436     private ArchiveEntry asArchiveEntry(final AddedResourceCollection collection, final PlexusIoResource resource)
437             throws ArchiverException {
438         final String destFileName = collection.resources.getName(resource);
439 
440         int fromResource = PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE;
441         if (resource instanceof ResourceAttributeSupplier) {
442             final PlexusIoResourceAttributes attrs = ((ResourceAttributeSupplier) resource).getAttributes();
443 
444             if (attrs != null) {
445                 fromResource = attrs.getOctalMode();
446             }
447         }
448 
449         return asArchiveEntry(
450                 resource,
451                 destFileName,
452                 collection.maybeOverridden(fromResource, resource.isDirectory()),
453                 collection.resources);
454     }
455 
456     @Override
457     public void addResource(final PlexusIoResource resource, final String destFileName, final int permissions)
458             throws ArchiverException {
459         doAddResource(asArchiveEntry(resource, destFileName, permissions, null));
460     }
461 
462     @Override
463     public void addFile(@Nonnull final File inputFile, @Nonnull String destFileName, int permissions)
464             throws ArchiverException {
465         if (!inputFile.isFile() || !inputFile.exists()) {
466             throw new ArchiverException(inputFile.getAbsolutePath() + " isn't a file.");
467         }
468 
469         if (replacePathSlashesToJavaPaths) {
470             destFileName = destFileName.replace('\\', '/');
471         }
472 
473         if (permissions < 0) {
474             permissions = getOverrideFileMode();
475         }
476 
477         if (umask > 0 && permissions != PlexusIoResourceAttributes.UNKNOWN_OCTAL_MODE) {
478             permissions &= ~umask;
479         }
480 
481         try {
482             // do a null check here, to avoid creating a file stream if there are no filters...
483             ArchiveEntry entry = ArchiveEntry.createFileEntry(destFileName, inputFile, permissions, getDirectoryMode());
484             doAddResource(updateArchiveEntryAttributes(entry));
485         } catch (final IOException e) {
486             throw new ArchiverException("Failed to determine inclusion status for: " + inputFile, e);
487         }
488     }
489 
490     @Nonnull
491     @Override
492     public ResourceIterator getResources() throws ArchiverException {
493         return new ResourceIterator() {
494 
495             private final Iterator<Object> addedResourceIter = resources.iterator();
496 
497             private AddedResourceCollection currentResourceCollection;
498 
499             private Iterator ioResourceIter;
500 
501             private ArchiveEntry nextEntry;
502 
503             private final Set<String> seenEntries = new HashSet<>();
504 
505             @Override
506             public boolean hasNext() {
507                 do {
508                     if (nextEntry == null) {
509                         if (ioResourceIter == null) {
510                             if (addedResourceIter.hasNext()) {
511                                 final Object o = addedResourceIter.next();
512                                 if (o instanceof ArchiveEntry) {
513                                     nextEntry = (ArchiveEntry) o;
514                                 } else if (o instanceof AddedResourceCollection) {
515                                     currentResourceCollection = (AddedResourceCollection) o;
516 
517                                     try {
518                                         ioResourceIter = currentResourceCollection.resources.getResources();
519                                     } catch (final IOException e) {
520                                         throw new ArchiverException(e.getMessage(), e);
521                                     }
522                                 } else {
523                                     return throwIllegalResourceType(o);
524                                 }
525                             } else {
526                                 nextEntry = null;
527                             }
528                         } else {
529                             if (ioResourceIter.hasNext()) {
530                                 final PlexusIoResource resource = (PlexusIoResource) ioResourceIter.next();
531                                 nextEntry = asArchiveEntry(currentResourceCollection, resource);
532                             } else {
533                                 // this will leak handles in the IO iterator if the iterator is not fully consumed.
534                                 // alternately we'd have to make this method return a Closeable iterator back
535                                 // to the client and ditch the whole issue onto the client.
536                                 // this does not really make any sense either, might equally well change the
537                                 // api into something that is not broken by design.
538                                 addCloseable(ioResourceIter);
539                                 ioResourceIter = null;
540                             }
541                         }
542                     }
543 
544                     if (nextEntry != null && seenEntries.contains(normalizedForDuplicateCheck(nextEntry))) {
545                         final String path = nextEntry.getName();
546 
547                         if (Archiver.DUPLICATES_PRESERVE.equals(duplicateBehavior)
548                                 || Archiver.DUPLICATES_SKIP.equals(duplicateBehavior)) {
549                             if (nextEntry.getType() == ArchiveEntry.FILE) {
550                                 getLogger().debug(path + " already added, skipping");
551                             }
552 
553                             nextEntry = null;
554                         } else if (Archiver.DUPLICATES_FAIL.equals(duplicateBehavior)) {
555                             throw new ArchiverException("Duplicate file " + path + " was found and the duplicate "
556                                     + "attribute is 'fail'.");
557                         } else {
558                             // duplicate equal to add, so we continue
559                             getLogger().debug("duplicate file " + path + " found, adding.");
560                         }
561                     }
562                 } while (nextEntry == null && !(ioResourceIter == null && !addedResourceIter.hasNext()));
563 
564                 return nextEntry != null;
565             }
566 
567             private boolean throwIllegalResourceType(Object o) {
568                 throw new IllegalStateException(
569                         "An invalid resource of type: " + o.getClass().getName() + " was added to archiver: "
570                                 + getClass().getName());
571             }
572 
573             @Override
574             public ArchiveEntry next() {
575                 if (!hasNext()) {
576                     throw new NoSuchElementException();
577                 }
578 
579                 final ArchiveEntry next = nextEntry;
580                 nextEntry = null;
581 
582                 seenEntries.add(normalizedForDuplicateCheck(next));
583 
584                 return next;
585             }
586 
587             @Override
588             public void remove() {
589                 throw new UnsupportedOperationException("Does not support iterator");
590             }
591 
592             private String normalizedForDuplicateCheck(ArchiveEntry entry) {
593                 return entry.getName().replace('\\', '/');
594             }
595         };
596     }
597 
598     private static void closeIfCloseable(Object resource) throws IOException {
599         if (resource == null) {
600             return;
601         }
602         if (resource instanceof Closeable) {
603             ((Closeable) resource).close();
604         }
605     }
606 
607     private static void closeQuietlyIfCloseable(Object resource) {
608         try {
609             closeIfCloseable(resource);
610         } catch (IOException e) {
611             throw new RuntimeException(e);
612         }
613     }
614 
615     @Override
616     public Map<String, ArchiveEntry> getFiles() {
617         try {
618             final Map<String, ArchiveEntry> map = new HashMap<>();
619             for (final ResourceIterator iter = getResources(); iter.hasNext(); ) {
620                 final ArchiveEntry entry = iter.next();
621                 if (includeEmptyDirs || entry.getType() == ArchiveEntry.FILE) {
622                     map.put(entry.getName(), entry);
623                 }
624             }
625             return map;
626         } catch (final ArchiverException e) {
627             throw new UndeclaredThrowableException(e);
628         }
629     }
630 
631     @Override
632     public File getDestFile() {
633         return destFile;
634     }
635 
636     @Override
637     public void setDestFile(final File destFile) {
638         this.destFile = destFile;
639 
640         if (destFile != null && destFile.getParentFile() != null) {
641             destFile.getParentFile().mkdirs();
642         }
643     }
644 
645     protected PlexusIoResourceCollection asResourceCollection(final ArchivedFileSet fileSet, Charset charset)
646             throws ArchiverException {
647         final File archiveFile = fileSet.getArchive();
648 
649         final PlexusIoResourceCollection resources;
650         try {
651             resources = archiverManagerProvider.get().getResourceCollection(archiveFile);
652         } catch (final NoSuchArchiverException e) {
653             throw new ArchiverException(
654                     "Error adding archived file-set. PlexusIoResourceCollection not found for: " + archiveFile, e);
655         }
656 
657         if (resources instanceof EncodingSupported) {
658             ((EncodingSupported) resources).setEncoding(charset);
659         }
660 
661         if (resources instanceof PlexusIoArchivedResourceCollection) {
662             ((PlexusIoArchivedResourceCollection) resources).setFile(fileSet.getArchive());
663         } else {
664             throw new ArchiverException("Expected " + PlexusIoArchivedResourceCollection.class.getName() + ", got "
665                     + resources.getClass().getName());
666         }
667 
668         if (resources instanceof AbstractPlexusIoResourceCollection) {
669             ((AbstractPlexusIoResourceCollection) resources).setStreamTransformer(fileSet.getStreamTransformer());
670         }
671         final PlexusIoProxyResourceCollection proxy = new PlexusIoProxyResourceCollection(resources);
672 
673         proxy.setExcludes(fileSet.getExcludes());
674         proxy.setIncludes(fileSet.getIncludes());
675         proxy.setIncludingEmptyDirectories(fileSet.isIncludingEmptyDirectories());
676         proxy.setCaseSensitive(fileSet.isCaseSensitive());
677         proxy.setPrefix(fileSet.getPrefix());
678         proxy.setUsingDefaultExcludes(fileSet.isUsingDefaultExcludes());
679         proxy.setFileSelectors(fileSet.getFileSelectors());
680         proxy.setStreamTransformer(fileSet.getStreamTransformer());
681         proxy.setFileMappers(fileSet.getFileMappers());
682 
683         if (getOverrideDirectoryMode() > -1 || getOverrideFileMode() > -1) {
684             proxy.setOverrideAttributes(-1, null, -1, null, getOverrideFileMode(), getOverrideDirectoryMode());
685         }
686 
687         if (getDefaultDirectoryMode() > -1 || getDefaultFileMode() > -1) {
688             proxy.setDefaultAttributes(-1, null, -1, null, getDefaultFileMode(), getDefaultDirectoryMode());
689         }
690 
691         return proxy;
692     }
693 
694     /**
695      * Adds a resource collection to the archive.
696      */
697     @Override
698     public void addResources(final PlexusIoResourceCollection collection) throws ArchiverException {
699         doAddResource(new AddedResourceCollection(collection, forcedFileMode, forcedDirectoryMode));
700     }
701 
702     private void doAddResource(Object item) {
703         resources.add(item);
704     }
705 
706     @Override
707     public void addArchivedFileSet(final ArchivedFileSet fileSet) throws ArchiverException {
708         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, null);
709         addResources(resourceCollection);
710     }
711 
712     @Override
713     public void addArchivedFileSet(final ArchivedFileSet fileSet, Charset charset) throws ArchiverException {
714         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, charset);
715         addResources(resourceCollection);
716     }
717 
718     /**
719      * @since 1.0-alpha-7
720      */
721     @Override
722     public void addArchivedFileSet(
723             @Nonnull final File archiveFile, final String prefix, final String[] includes, final String[] excludes)
724             throws ArchiverException {
725         addArchivedFileSet(archivedFileSet(archiveFile)
726                 .prefixed(prefix)
727                 .includeExclude(includes, excludes)
728                 .includeEmptyDirs(includeEmptyDirs));
729     }
730 
731     /**
732      * @since 1.0-alpha-7
733      */
734     @Override
735     public void addArchivedFileSet(@Nonnull final File archiveFile, final String prefix) throws ArchiverException {
736         addArchivedFileSet(archivedFileSet(archiveFile).prefixed(prefix).includeEmptyDirs(includeEmptyDirs));
737     }
738 
739     /**
740      * @since 1.0-alpha-7
741      */
742     @Override
743     public void addArchivedFileSet(@Nonnull final File archiveFile, final String[] includes, final String[] excludes)
744             throws ArchiverException {
745         addArchivedFileSet(
746                 archivedFileSet(archiveFile).includeExclude(includes, excludes).includeEmptyDirs(includeEmptyDirs));
747     }
748 
749     /**
750      * @since 1.0-alpha-7
751      */
752     @Override
753     public void addArchivedFileSet(@Nonnull final File archiveFile) throws ArchiverException {
754         addArchivedFileSet(archivedFileSet(archiveFile).includeEmptyDirs(includeEmptyDirs));
755     }
756 
757     @Override
758     public boolean isForced() {
759         return forced;
760     }
761 
762     @Override
763     public void setForced(final boolean forced) {
764         this.forced = forced;
765     }
766 
767     @Override
768     public void addArchiveFinalizer(final ArchiveFinalizer finalizer) {
769         if (finalizers == null) {
770             finalizers = new ArrayList<>();
771         }
772 
773         finalizers.add(finalizer);
774     }
775 
776     @Override
777     public void setArchiveFinalizers(final List<ArchiveFinalizer> archiveFinalizers) {
778         finalizers = archiveFinalizers;
779     }
780 
781     @Override
782     public void setDotFileDirectory(final File dotFileDirectory) {
783         this.dotFileDirectory = dotFileDirectory;
784     }
785 
786     protected boolean isUptodate() throws ArchiverException {
787         final File zipFile = getDestFile();
788         if (!zipFile.exists()) {
789             getLogger().debug("isUp2date: false (Destination " + zipFile.getPath() + " not found.)");
790             return false; // File doesn't yet exist
791         }
792         final long destTimestamp = getFileLastModifiedTime(zipFile);
793 
794         final Iterator<Object> it = resources.iterator();
795         if (!it.hasNext()) {
796             getLogger().debug("isUp2date: false (No input files.)");
797             return false; // No timestamp to compare
798         }
799 
800         while (it.hasNext()) {
801             final Object o = it.next();
802             final long l;
803             if (o instanceof ArchiveEntry) {
804                 l = ((ArchiveEntry) o).getResource().getLastModified();
805             } else if (o instanceof AddedResourceCollection) {
806                 try {
807                     l = ((AddedResourceCollection) o).resources.getLastModified();
808                 } catch (final IOException e) {
809                     throw new ArchiverException(e.getMessage(), e);
810                 }
811             } else {
812                 throw new IllegalStateException(
813                         "Invalid object type: " + o.getClass().getName());
814             }
815             if (l == PlexusIoResource.UNKNOWN_MODIFICATION_DATE) {
816                 // Don't know what to do. Safe thing is to assume not up2date.
817                 getLogger().debug("isUp2date: false (Resource with unknown modification date found.)");
818                 return false;
819             }
820             if (l > destTimestamp) {
821                 getLogger().debug("isUp2date: false (Resource with newer modification date found.)");
822                 return false;
823             }
824         }
825 
826         getLogger().debug("isUp2date: true");
827         return true;
828     }
829 
830     /**
831      * Returns the last modified time in milliseconds of a file.
832      * It avoids the bug where milliseconds precision is lost on File#lastModified (JDK-8177809) on JDK8 and Linux.
833      * @param file The file where the last modified time will be returned for.
834      * @return The last modified time in milliseconds of the file.
835      * @throws ArchiverException In the case of an IOException, for example when the file does not exists.
836      */
837     private long getFileLastModifiedTime(File file) throws ArchiverException {
838         try {
839             return Files.getLastModifiedTime(file.toPath()).toMillis();
840         } catch (IOException e) {
841             throw new ArchiverException(e.getMessage(), e);
842         }
843     }
844 
845     protected boolean checkForced() throws ArchiverException {
846         if (!isForced() && isSupportingForced() && isUptodate()) {
847             getLogger().debug("Archive " + getDestFile() + " is uptodate.");
848             return false;
849         }
850         return true;
851     }
852 
853     @Override
854     public boolean isSupportingForced() {
855         return false;
856     }
857 
858     protected void runArchiveFinalizers() throws ArchiverException {
859         if (finalizers != null) {
860             for (final ArchiveFinalizer finalizer : finalizers) {
861                 finalizer.finalizeArchiveCreation(this);
862             }
863         }
864     }
865 
866     @Override
867     public final void createArchive() throws ArchiverException, IOException {
868         validate();
869         try {
870             try {
871                 if (dotFileDirectory != null) {
872                     addArchiveFinalizer(new DotDirectiveArchiveFinalizer(dotFileDirectory));
873                 }
874 
875                 runArchiveFinalizers();
876 
877                 execute();
878             } finally {
879                 close();
880             }
881         } catch (final IOException e) {
882             String msg = "Problem creating " + getArchiveType() + ": " + e.getMessage();
883 
884             final StringBuffer revertBuffer = new StringBuffer();
885             if (!revert(revertBuffer)) {
886                 msg += revertBuffer.toString();
887             }
888 
889             throw new ArchiverException(msg, e);
890         } finally {
891             cleanUp();
892         }
893 
894         postCreateArchive();
895     }
896 
897     protected boolean hasVirtualFiles() {
898         if (finalizers != null) {
899             for (final ArchiveFinalizer finalizer : finalizers) {
900                 final List virtualFiles = finalizer.getVirtualFiles();
901 
902                 if ((virtualFiles != null) && !virtualFiles.isEmpty()) {
903                     return true;
904                 }
905             }
906         }
907         return false;
908     }
909 
910     protected boolean revert(final StringBuffer messageBuffer) {
911         return true;
912     }
913 
914     protected void validate() throws ArchiverException, IOException {}
915 
916     /**
917      * This method is called after the archive creation
918      * completes successfully (no exceptions are thrown).
919      *
920      * Subclasses may override this method in order to
921      * augment or validate the archive after it is
922      * created.
923      *
924      * @since 3.6
925      */
926     protected void postCreateArchive() throws ArchiverException, IOException {}
927 
928     protected abstract String getArchiveType();
929 
930     private void addCloseable(Object maybeCloseable) {
931         if (maybeCloseable instanceof Closeable) {
932             closeables.add((Closeable) maybeCloseable);
933         }
934     }
935 
936     private void closeIterators() {
937         for (Closeable closeable : closeables) {
938             closeQuietlyIfCloseable(closeable);
939         }
940     }
941 
942     protected abstract void close() throws IOException;
943 
944     protected void cleanUp() throws IOException {
945         closeIterators();
946 
947         for (Object resource : resources) {
948             if (resource instanceof PlexusIoProxyResourceCollection) {
949                 resource = ((PlexusIoProxyResourceCollection) resource).getSrc();
950             }
951 
952             closeIfCloseable(resource);
953         }
954         resources.clear();
955     }
956 
957     protected abstract void execute() throws ArchiverException, IOException;
958 
959     /**
960      * @since 1.1
961      */
962     @Override
963     public boolean isUseJvmChmod() {
964         return useJvmChmod;
965     }
966 
967     /**
968      * @since 1.1
969      */
970     @Override
971     public void setUseJvmChmod(final boolean useJvmChmod) {
972         this.useJvmChmod = useJvmChmod;
973     }
974 
975     /**
976      * @since 1.1
977      */
978     @Override
979     public boolean isIgnorePermissions() {
980         return ignorePermissions;
981     }
982 
983     /**
984      * @since 1.1
985      */
986     @Override
987     public void setIgnorePermissions(final boolean ignorePermissions) {
988         this.ignorePermissions = ignorePermissions;
989     }
990 
991     /**
992      * @deprecated Use {@link #setLastModifiedTime(FileTime)} instead.
993      */
994     @Override
995     @Deprecated
996     public void setLastModifiedDate(Date lastModifiedDate) {
997         this.lastModifiedTime = lastModifiedDate != null ? FileTime.fromMillis(lastModifiedDate.getTime()) : null;
998     }
999 
1000     /**
1001      * @deprecated Use {@link #getLastModifiedTime()} instead.
1002      */
1003     @Override
1004     @Deprecated
1005     public Date getLastModifiedDate() {
1006         return lastModifiedTime != null ? new Date(lastModifiedTime.toMillis()) : null;
1007     }
1008 
1009     @Override
1010     public void setLastModifiedTime(FileTime lastModifiedTime) {
1011         this.lastModifiedTime = lastModifiedTime;
1012     }
1013 
1014     @Override
1015     public FileTime getLastModifiedTime() {
1016         return lastModifiedTime;
1017     }
1018 
1019     @Override
1020     public void setFilenameComparator(Comparator<String> filenameComparator) {
1021         this.filenameComparator = filenameComparator;
1022     }
1023 
1024     public Comparator<String> getFilenameComparator() {
1025         return filenameComparator;
1026     }
1027 
1028     @Override
1029     public void setOverrideUid(int uid) {
1030         overrideUid = uid;
1031     }
1032 
1033     @Override
1034     public void setOverrideUserName(String userName) {
1035         overrideUserName = userName;
1036     }
1037 
1038     @Override
1039     public int getOverrideUid() {
1040         return overrideUid;
1041     }
1042 
1043     @Override
1044     public String getOverrideUserName() {
1045         return overrideUserName;
1046     }
1047 
1048     @Override
1049     public void setOverrideGid(int gid) {
1050         overrideGid = gid;
1051     }
1052 
1053     @Override
1054     public void setOverrideGroupName(String groupName) {
1055         overrideGroupName = groupName;
1056     }
1057 
1058     @Override
1059     public int getOverrideGid() {
1060         return overrideGid;
1061     }
1062 
1063     @Override
1064     public String getOverrideGroupName() {
1065         return overrideGroupName;
1066     }
1067 
1068     @Override
1069     public void setUmask(int umask) {
1070         this.umask = umask;
1071     }
1072 
1073     @Override
1074     public int getUmask() {
1075         return umask;
1076     }
1077 
1078     /**
1079      * @deprecated Use {@link #configureReproducibleBuild(FileTime)} instead.
1080      */
1081     @Override
1082     @Deprecated
1083     public void configureReproducible(Date lastModifiedDate) {
1084         configureReproducibleBuild(FileTime.fromMillis(lastModifiedDate.getTime()));
1085     }
1086 
1087     @Override
1088     public void configureReproducibleBuild(FileTime lastModifiedTime) {
1089         // 1. force last modified date
1090         setLastModifiedTime(normalizeLastModifiedTime(lastModifiedTime));
1091 
1092         // 2. sort filenames in each directory when scanning filesystem
1093         setFilenameComparator(String::compareTo);
1094 
1095         // 3. ignore uid/gid from filesystem (for tar)
1096         setOverrideUid(0);
1097         setOverrideUserName("root"); // is it possible to avoid this, like "tar --numeric-owner"?
1098         setOverrideGid(0);
1099         setOverrideGroupName("root");
1100 
1101         // 4. set umask to 022 to avoid environment umask value particularly on group write
1102         setUmask(0_022);
1103     }
1104 
1105     /**
1106      * Normalize last modified time value to get reproducible archive entries, based on
1107      * archive binary format.
1108      *
1109      * <p>tar uses UTC timestamp, but zip uses local time then requires
1110      * tweaks to make the value reproducible whatever the current timezone is.
1111      *
1112      * @param lastModifiedTime The last modification time
1113      * @return The normalized last modification time
1114      *
1115      * @see #configureReproducibleBuild(FileTime)
1116      */
1117     protected FileTime normalizeLastModifiedTime(FileTime lastModifiedTime) {
1118         return lastModifiedTime;
1119     }
1120 }