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     /**
620      * Returns a map of the files that have been added to the archive.
621      * <p>
622      * Note: The entry names in the map use platform-specific path separators
623      * (e.g., backslashes on Windows, forward slashes on Unix). For ZIP archivers,
624      * the actual archive entries will use forward slashes as required by the ZIP
625      * specification, but this map returns names as they were added.
626      * </p>
627      * <p>
628      * For ZIP-based archivers (ZipArchiver, JarArchiver, etc.), use the overridden
629      * implementation which normalizes paths to forward slashes to match the actual
630      * ZIP entry names.
631      * </p>
632      *
633      * @return A map where keys are entry names and values are the corresponding ArchiveEntry objects.
634      * @deprecated Use {@link #getResources()} instead.
635      */
636     @Override
637     @Deprecated
638     public Map<String, ArchiveEntry> getFiles() {
639         try {
640             final Map<String, ArchiveEntry> map = new HashMap<>();
641             for (final ResourceIterator iter = getResources(); iter.hasNext(); ) {
642                 final ArchiveEntry entry = iter.next();
643                 if (includeEmptyDirs || entry.getType() == ArchiveEntry.FILE) {
644                     map.put(entry.getName(), entry);
645                 }
646             }
647             return map;
648         } catch (final ArchiverException e) {
649             throw new UndeclaredThrowableException(e);
650         }
651     }
652 
653     @Override
654     public File getDestFile() {
655         return destFile;
656     }
657 
658     @Override
659     public void setDestFile(final File destFile) {
660         this.destFile = destFile;
661 
662         if (destFile != null && destFile.getParentFile() != null) {
663             destFile.getParentFile().mkdirs();
664         }
665     }
666 
667     protected PlexusIoResourceCollection asResourceCollection(final ArchivedFileSet fileSet, Charset charset)
668             throws ArchiverException {
669         final File archiveFile = fileSet.getArchive();
670 
671         final PlexusIoResourceCollection resources;
672         try {
673             resources = archiverManagerProvider.get().getResourceCollection(archiveFile);
674         } catch (final NoSuchArchiverException e) {
675             throw new ArchiverException(
676                     "Error adding archived file-set. PlexusIoResourceCollection not found for: " + archiveFile, e);
677         }
678 
679         if (resources instanceof EncodingSupported) {
680             ((EncodingSupported) resources).setEncoding(charset);
681         }
682 
683         if (resources instanceof PlexusIoArchivedResourceCollection) {
684             ((PlexusIoArchivedResourceCollection) resources).setFile(fileSet.getArchive());
685         } else {
686             throw new ArchiverException("Expected " + PlexusIoArchivedResourceCollection.class.getName() + ", got "
687                     + resources.getClass().getName());
688         }
689 
690         if (resources instanceof AbstractPlexusIoResourceCollection) {
691             ((AbstractPlexusIoResourceCollection) resources).setStreamTransformer(fileSet.getStreamTransformer());
692         }
693         final PlexusIoProxyResourceCollection proxy = new PlexusIoProxyResourceCollection(resources);
694 
695         proxy.setExcludes(fileSet.getExcludes());
696         proxy.setIncludes(fileSet.getIncludes());
697         proxy.setIncludingEmptyDirectories(fileSet.isIncludingEmptyDirectories());
698         proxy.setCaseSensitive(fileSet.isCaseSensitive());
699         proxy.setPrefix(fileSet.getPrefix());
700         proxy.setUsingDefaultExcludes(fileSet.isUsingDefaultExcludes());
701         proxy.setFileSelectors(fileSet.getFileSelectors());
702         proxy.setStreamTransformer(fileSet.getStreamTransformer());
703         proxy.setFileMappers(fileSet.getFileMappers());
704 
705         if (getOverrideDirectoryMode() > -1 || getOverrideFileMode() > -1) {
706             proxy.setOverrideAttributes(-1, null, -1, null, getOverrideFileMode(), getOverrideDirectoryMode());
707         }
708 
709         if (getDefaultDirectoryMode() > -1 || getDefaultFileMode() > -1) {
710             proxy.setDefaultAttributes(-1, null, -1, null, getDefaultFileMode(), getDefaultDirectoryMode());
711         }
712 
713         return proxy;
714     }
715 
716     /**
717      * Adds a resource collection to the archive.
718      */
719     @Override
720     public void addResources(final PlexusIoResourceCollection collection) throws ArchiverException {
721         doAddResource(new AddedResourceCollection(collection, forcedFileMode, forcedDirectoryMode));
722     }
723 
724     private void doAddResource(Object item) {
725         resources.add(item);
726     }
727 
728     @Override
729     public void addArchivedFileSet(final ArchivedFileSet fileSet) throws ArchiverException {
730         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, null);
731         addResources(resourceCollection);
732     }
733 
734     @Override
735     public void addArchivedFileSet(final ArchivedFileSet fileSet, Charset charset) throws ArchiverException {
736         final PlexusIoResourceCollection resourceCollection = asResourceCollection(fileSet, charset);
737         addResources(resourceCollection);
738     }
739 
740     /**
741      * @since 1.0-alpha-7
742      */
743     @Override
744     @Deprecated
745     public void addArchivedFileSet(
746             @Nonnull final File archiveFile, final String prefix, final String[] includes, final String[] excludes)
747             throws ArchiverException {
748         addArchivedFileSet(archivedFileSet(archiveFile)
749                 .prefixed(prefix)
750                 .includeExclude(includes, excludes)
751                 .includeEmptyDirs(includeEmptyDirs));
752     }
753 
754     /**
755      * @since 1.0-alpha-7
756      */
757     @Override
758     @Deprecated
759     public void addArchivedFileSet(@Nonnull final File archiveFile, final String prefix) throws ArchiverException {
760         addArchivedFileSet(archivedFileSet(archiveFile).prefixed(prefix).includeEmptyDirs(includeEmptyDirs));
761     }
762 
763     /**
764      * @since 1.0-alpha-7
765      */
766     @Override
767     @Deprecated
768     public void addArchivedFileSet(@Nonnull final File archiveFile, final String[] includes, final String[] excludes)
769             throws ArchiverException {
770         addArchivedFileSet(
771                 archivedFileSet(archiveFile).includeExclude(includes, excludes).includeEmptyDirs(includeEmptyDirs));
772     }
773 
774     /**
775      * @since 1.0-alpha-7
776      */
777     @Override
778     @Deprecated
779     public void addArchivedFileSet(@Nonnull final File archiveFile) throws ArchiverException {
780         addArchivedFileSet(archivedFileSet(archiveFile).includeEmptyDirs(includeEmptyDirs));
781     }
782 
783     @Override
784     public boolean isForced() {
785         return forced;
786     }
787 
788     @Override
789     public void setForced(final boolean forced) {
790         this.forced = forced;
791     }
792 
793     @Override
794     public void addArchiveFinalizer(final ArchiveFinalizer finalizer) {
795         if (finalizers == null) {
796             finalizers = new ArrayList<>();
797         }
798 
799         finalizers.add(finalizer);
800     }
801 
802     @Override
803     public void setArchiveFinalizers(final List<ArchiveFinalizer> archiveFinalizers) {
804         finalizers = archiveFinalizers;
805     }
806 
807     @Override
808     public void setDotFileDirectory(final File dotFileDirectory) {
809         this.dotFileDirectory = dotFileDirectory;
810     }
811 
812     protected boolean isUptodate() throws ArchiverException {
813         final File zipFile = getDestFile();
814         if (!zipFile.exists()) {
815             getLogger().debug("isUp2date: false (Destination " + zipFile.getPath() + " not found.)");
816             return false; // File doesn't yet exist
817         }
818         final long destTimestamp = getFileLastModifiedTime(zipFile);
819 
820         final Iterator<Object> it = resources.iterator();
821         if (!it.hasNext()) {
822             getLogger().debug("isUp2date: false (No input files.)");
823             return false; // No timestamp to compare
824         }
825 
826         while (it.hasNext()) {
827             final Object o = it.next();
828             final long l;
829             if (o instanceof ArchiveEntry) {
830                 l = ((ArchiveEntry) o).getResource().getLastModified();
831             } else if (o instanceof AddedResourceCollection) {
832                 try {
833                     l = ((AddedResourceCollection) o).resources.getLastModified();
834                 } catch (final IOException e) {
835                     throw new ArchiverException(e.getMessage(), e);
836                 }
837             } else {
838                 throw new IllegalStateException(
839                         "Invalid object type: " + o.getClass().getName());
840             }
841             if (l == PlexusIoResource.UNKNOWN_MODIFICATION_DATE) {
842                 // Don't know what to do. Safe thing is to assume not up2date.
843                 getLogger().debug("isUp2date: false (Resource with unknown modification date found.)");
844                 return false;
845             }
846             if (l > destTimestamp) {
847                 getLogger().debug("isUp2date: false (Resource with newer modification date found.)");
848                 return false;
849             }
850         }
851 
852         getLogger().debug("isUp2date: true");
853         return true;
854     }
855 
856     /**
857      * Returns the last modified time in milliseconds of a file.
858      * It avoids the bug where milliseconds precision is lost on File#lastModified (JDK-8177809) on JDK8 and Linux.
859      * @param file The file where the last modified time will be returned for.
860      * @return The last modified time in milliseconds of the file.
861      * @throws ArchiverException In the case of an IOException, for example when the file does not exists.
862      */
863     private long getFileLastModifiedTime(File file) throws ArchiverException {
864         try {
865             return Files.getLastModifiedTime(file.toPath()).toMillis();
866         } catch (IOException e) {
867             throw new ArchiverException(e.getMessage(), e);
868         }
869     }
870 
871     protected boolean checkForced() throws ArchiverException {
872         if (!isForced() && isSupportingForced() && isUptodate()) {
873             getLogger().debug("Archive " + getDestFile() + " is uptodate.");
874             return false;
875         }
876         return true;
877     }
878 
879     @Override
880     public boolean isSupportingForced() {
881         return false;
882     }
883 
884     protected void runArchiveFinalizers() throws ArchiverException {
885         if (finalizers != null) {
886             for (final ArchiveFinalizer finalizer : finalizers) {
887                 finalizer.finalizeArchiveCreation(this);
888             }
889         }
890     }
891 
892     @Override
893     public final void createArchive() throws ArchiverException, IOException {
894         validate();
895         try {
896             try {
897                 if (dotFileDirectory != null) {
898                     addArchiveFinalizer(new DotDirectiveArchiveFinalizer(dotFileDirectory));
899                 }
900 
901                 runArchiveFinalizers();
902 
903                 execute();
904             } finally {
905                 close();
906             }
907         } catch (final IOException e) {
908             String msg = "Problem creating " + getArchiveType() + ": " + e.getMessage();
909 
910             final StringBuffer revertBuffer = new StringBuffer();
911             if (!revert(revertBuffer)) {
912                 msg += revertBuffer.toString();
913             }
914 
915             throw new ArchiverException(msg, e);
916         } finally {
917             cleanUp();
918         }
919 
920         postCreateArchive();
921     }
922 
923     protected boolean hasVirtualFiles() {
924         if (finalizers != null) {
925             for (final ArchiveFinalizer finalizer : finalizers) {
926                 final List virtualFiles = finalizer.getVirtualFiles();
927 
928                 if ((virtualFiles != null) && !virtualFiles.isEmpty()) {
929                     return true;
930                 }
931             }
932         }
933         return false;
934     }
935 
936     protected boolean revert(final StringBuffer messageBuffer) {
937         return true;
938     }
939 
940     protected void validate() throws ArchiverException, IOException {}
941 
942     /**
943      * This method is called after the archive creation
944      * completes successfully (no exceptions are thrown).
945      *
946      * Subclasses may override this method in order to
947      * augment or validate the archive after it is
948      * created.
949      *
950      * @since 3.6
951      */
952     protected void postCreateArchive() throws ArchiverException, IOException {}
953 
954     protected abstract String getArchiveType();
955 
956     private void addCloseable(Object maybeCloseable) {
957         if (maybeCloseable instanceof Closeable) {
958             closeables.add((Closeable) maybeCloseable);
959         }
960     }
961 
962     private void closeIterators() {
963         for (Closeable closeable : closeables) {
964             closeQuietlyIfCloseable(closeable);
965         }
966     }
967 
968     protected abstract void close() throws IOException;
969 
970     protected void cleanUp() throws IOException {
971         closeIterators();
972 
973         for (Object resource : resources) {
974             if (resource instanceof PlexusIoProxyResourceCollection) {
975                 resource = ((PlexusIoProxyResourceCollection) resource).getSrc();
976             }
977 
978             closeIfCloseable(resource);
979         }
980         resources.clear();
981     }
982 
983     protected abstract void execute() throws ArchiverException, IOException;
984 
985     /**
986      * @since 1.1
987      */
988     @Override
989     @Deprecated
990     public boolean isUseJvmChmod() {
991         return useJvmChmod;
992     }
993 
994     /**
995      * @since 1.1
996      */
997     @Override
998     @Deprecated
999     public void setUseJvmChmod(final boolean useJvmChmod) {
1000         this.useJvmChmod = useJvmChmod;
1001     }
1002 
1003     /**
1004      * @since 1.1
1005      */
1006     @Override
1007     public boolean isIgnorePermissions() {
1008         return ignorePermissions;
1009     }
1010 
1011     /**
1012      * @since 1.1
1013      */
1014     @Override
1015     public void setIgnorePermissions(final boolean ignorePermissions) {
1016         this.ignorePermissions = ignorePermissions;
1017     }
1018 
1019     /**
1020      * @deprecated Use {@link #setLastModifiedTime(FileTime)} instead.
1021      */
1022     @Override
1023     @Deprecated
1024     public void setLastModifiedDate(Date lastModifiedDate) {
1025         this.lastModifiedTime = lastModifiedDate != null ? FileTime.fromMillis(lastModifiedDate.getTime()) : null;
1026     }
1027 
1028     /**
1029      * @deprecated Use {@link #getLastModifiedTime()} instead.
1030      */
1031     @Override
1032     @Deprecated
1033     public Date getLastModifiedDate() {
1034         return lastModifiedTime != null ? new Date(lastModifiedTime.toMillis()) : null;
1035     }
1036 
1037     @Override
1038     public void setLastModifiedTime(FileTime lastModifiedTime) {
1039         this.lastModifiedTime = lastModifiedTime;
1040     }
1041 
1042     @Override
1043     public FileTime getLastModifiedTime() {
1044         return lastModifiedTime;
1045     }
1046 
1047     @Override
1048     public void setFilenameComparator(Comparator<String> filenameComparator) {
1049         this.filenameComparator = filenameComparator;
1050     }
1051 
1052     public Comparator<String> getFilenameComparator() {
1053         return filenameComparator;
1054     }
1055 
1056     @Override
1057     public void setOverrideUid(int uid) {
1058         overrideUid = uid;
1059     }
1060 
1061     @Override
1062     public void setOverrideUserName(String userName) {
1063         overrideUserName = userName;
1064     }
1065 
1066     @Override
1067     public int getOverrideUid() {
1068         return overrideUid;
1069     }
1070 
1071     @Override
1072     public String getOverrideUserName() {
1073         return overrideUserName;
1074     }
1075 
1076     @Override
1077     public void setOverrideGid(int gid) {
1078         overrideGid = gid;
1079     }
1080 
1081     @Override
1082     public void setOverrideGroupName(String groupName) {
1083         overrideGroupName = groupName;
1084     }
1085 
1086     @Override
1087     public int getOverrideGid() {
1088         return overrideGid;
1089     }
1090 
1091     @Override
1092     public String getOverrideGroupName() {
1093         return overrideGroupName;
1094     }
1095 
1096     @Override
1097     public void setUmask(int umask) {
1098         this.umask = umask;
1099     }
1100 
1101     @Override
1102     public int getUmask() {
1103         return umask;
1104     }
1105 
1106     /**
1107      * @deprecated Use {@link #configureReproducibleBuild(FileTime)} instead.
1108      */
1109     @Override
1110     @Deprecated
1111     public void configureReproducible(Date lastModifiedDate) {
1112         configureReproducibleBuild(FileTime.fromMillis(lastModifiedDate.getTime()));
1113     }
1114 
1115     @Override
1116     public void configureReproducibleBuild(FileTime lastModifiedTime) {
1117         // 1. force last modified date
1118         setLastModifiedTime(normalizeLastModifiedTime(lastModifiedTime));
1119 
1120         // 2. sort filenames in each directory when scanning filesystem
1121         setFilenameComparator(String::compareTo);
1122 
1123         // 3. ignore uid/gid from filesystem (for tar)
1124         setOverrideUid(0);
1125         setOverrideUserName("root"); // is it possible to avoid this, like "tar --numeric-owner"?
1126         setOverrideGid(0);
1127         setOverrideGroupName("root");
1128 
1129         // 4. set umask to 022 to avoid environment umask value particularly on group write
1130         setUmask(0_022);
1131     }
1132 
1133     /**
1134      * Normalize last modified time value to get reproducible archive entries, based on
1135      * archive binary format.
1136      *
1137      * <p>tar uses UTC timestamp, but zip uses local time then requires
1138      * tweaks to make the value reproducible whatever the current timezone is.
1139      *
1140      * @param lastModifiedTime The last modification time
1141      * @return The normalized last modification time
1142      *
1143      * @see #configureReproducibleBuild(FileTime)
1144      */
1145     protected FileTime normalizeLastModifiedTime(FileTime lastModifiedTime) {
1146         return lastModifiedTime;
1147     }
1148 }