001/*
002 * JDrupes Builder
003 * Copyright (C) 2026 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.builder.core;
020
021import com.google.common.flogger.FluentLogger;
022import io.github.azagniotov.matcher.AntPathMatcher;
023import java.io.IOException;
024import java.nio.file.Path;
025import java.nio.file.attribute.FileTime;
026import java.time.Instant;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.LinkedHashSet;
030import java.util.List;
031import java.util.Optional;
032import java.util.SequencedSet;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035import java.util.zip.ZipEntry;
036import java.util.zip.ZipFile;
037import org.jdrupes.builder.api.BuildException;
038import org.jdrupes.builder.api.InputResource;
039import org.jdrupes.builder.api.InputTree;
040import org.jdrupes.builder.api.ResourceFactory;
041import org.jdrupes.builder.api.ResourceType;
042import org.jdrupes.builder.api.Resources;
043
044/// The default implementation of a [ZipFileInputTree].
045///
046/// @param <T> the type of the [InputResource]s in the tree.
047///
048public class ZipFileInputTree<T extends InputResource> extends ResourceObject
049        implements InputTree<T> {
050    @SuppressWarnings({ "unused" })
051    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
052    @SuppressWarnings("PMD.FieldNamingConventions")
053    private static final AntPathMatcher pathMatcher
054        = new AntPathMatcher.Builder().build();
055    private Instant latestChange;
056    private final Path zipFilePath;
057    private ZipFile zipFile;
058    private final String[] patterns;
059    private final List<String> excludes = new ArrayList<>();
060
061    /// Returns a new file tree. The file tree includes all files
062    /// matching `pattern` in the tree provided by the zip file.
063    ///
064    /// @param type the resource type
065    /// @param zipFile the ZIP file
066    /// @param patterns the patterns
067    ///
068    @SuppressWarnings({ "PMD.ArrayIsStoredDirectly", "PMD.UseVarargs" })
069    protected ZipFileInputTree(ResourceType<?> type,
070            org.jdrupes.builder.api.ZipFile zipFile, String[] patterns) {
071        super(type);
072        this.zipFilePath = zipFile.path();
073        if (patterns.length == 0) {
074            this.patterns = new String[] { "**/*" };
075        } else {
076            this.patterns = patterns;
077        }
078    }
079
080    @Override
081    public Resources<T> clear() {
082        throw new UnsupportedOperationException();
083    }
084
085    @Override
086    public Resources<T> add(T resource) {
087        throw new UnsupportedOperationException();
088    }
089
090    @Override
091    public boolean isEmpty() {
092        return !zipFile().entries().hasMoreElements();
093    }
094
095    @Override
096    public ZipFileInputTree<T> exclude(String pattern) {
097        excludes.add(pattern);
098        return this;
099    }
100
101    private ZipFile zipFile() {
102        try {
103            if (zipFile == null) {
104                zipFile = new ZipFile(zipFilePath.toFile());
105            }
106            return zipFile;
107        } catch (IOException e) {
108            throw new BuildException().cause(e);
109        }
110    }
111
112    @Override
113    public Optional<Instant> asOf() {
114        if (latestChange == null) {
115            latestChange = zipFile().stream().map(ZipEntry::getLastModifiedTime)
116                .max(FileTime::compareTo).map(FileTime::toInstant)
117                .orElse(Instant.EPOCH);
118        }
119        return Optional.ofNullable(latestChange);
120    }
121
122    @SuppressWarnings({ "unchecked", "PMD.AvoidInstantiatingObjectsInLoops" })
123    @Override
124    public Stream<Entry<T>> entries() {
125        List<Entry<T>> result = new ArrayList<>();
126        var entries = zipFile().entries();
127        while (entries.hasMoreElements()) {
128            var entry = entries.nextElement();
129            if (!Arrays.stream(patterns).anyMatch(
130                pattern -> pathMatcher.isMatch(pattern, entry.getName()))
131                || excludes.stream().anyMatch(
132                    ex -> pathMatcher.isMatch(ex, entry.getName()))
133                || entry.isDirectory()) {
134                continue;
135            }
136            try {
137                result.add(new Entry<>(Path.of(entry.getName()),
138                    ResourceFactory.create(
139                        (ResourceType<T>) type().containedType(),
140                        entry.getLastModifiedTime().toInstant(),
141                        zipFile.getInputStream(entry))));
142            } catch (IOException e) {
143                throw new BuildException().cause(e);
144            }
145        }
146        return result.stream();
147    }
148
149    @Override
150    public Stream<Path> paths() {
151        return entries().map(Entry::path);
152    }
153
154    @Override
155    public Stream<T> stream() {
156        return entries().map(Entry::resource);
157    }
158
159    @Override
160    public SequencedSet<T> get() {
161        return stream().collect(Collectors.toCollection(LinkedHashSet::new));
162    }
163}