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}