• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kit-data-manager / ro-crate-java / #429

19 May 2025 12:15PM UTC coverage: 90.5% (+0.3%) from 90.169%
#429

Pull #258

github

web-flow
Merge e4d91d01c into 810d1995c
Pull Request #258: Support .ELN-style crates in all zip readers and writers

224 of 245 new or added lines in 15 files covered. (91.43%)

3 existing lines in 3 files now uncovered.

2010 of 2221 relevant lines covered (90.5%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.98
/src/main/java/edu/kit/datamanager/ro_crate/reader/ZipStreamStrategy.java
1
package edu.kit.datamanager.ro_crate.reader;
2

3
import com.fasterxml.jackson.databind.ObjectMapper;
4
import com.fasterxml.jackson.databind.node.ObjectNode;
5
import edu.kit.datamanager.ro_crate.entities.contextual.JsonDescriptor;
6
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;
7
import java.io.File;
8
import java.io.FileOutputStream;
9
import java.io.IOException;
10
import java.io.InputStream;
11
import java.io.OutputStream;
12
import java.nio.file.Path;
13
import java.util.UUID;
14
import net.lingala.zip4j.io.inputstream.ZipInputStream;
15
import net.lingala.zip4j.model.LocalFileHeader;
16
import org.apache.commons.io.FileUtils;
17
import org.apache.commons.io.filefilter.FileFilterUtils;
18
import org.slf4j.Logger;
19
import org.slf4j.LoggerFactory;
20

21
/**
22
 * A ZIP file reader implementation of the StreamReaderStrategy interface.
23
 * This class handles reading and extraction of RO-Crate content from ZIP archives
24
 * into a temporary directory structure, which allows for accessing the contained files.
25
 *
26
 * @author jejkal
27
 */
28
public class ZipStreamStrategy implements GenericReaderStrategy<InputStream> {
29

30
    private static final Logger logger = LoggerFactory.getLogger(ZipStreamStrategy.class);
1✔
31
    protected final String ID = UUID.randomUUID().toString();
1✔
32
    protected Path temporaryFolder = Path.of(String.format("./.tmp/ro-crate-java/zipStreamReader/%s/", ID));
1✔
33
    protected boolean isExtracted = false;
1✔
34

35
    /**
36
     * Crates a ZipStreamReader with the default configuration as described in
37
     * the class documentation.
38
     */
39
    public ZipStreamStrategy() {
1✔
40
    }
1✔
41

42
    /**
43
     * Creates a ZipStreamReader which will extract the contents temporary to
44
     * the given location instead of the default location.
45
     *
46
     * @param folderPath the custom directory to extract content to for
47
     * temporary access.
48
     * @param shallAddUuidSubfolder if true, the reader will extract into
49
     * subdirectories of the given directory. These subdirectories will have
50
     * UUIDs as their names.
51
     */
52
    public ZipStreamStrategy(Path folderPath, boolean shallAddUuidSubfolder) {
1✔
53
        if (shallAddUuidSubfolder) {
1✔
54
            this.temporaryFolder = folderPath.resolve(ID);
1✔
55
        } else {
56
            this.temporaryFolder = folderPath;
1✔
57
        }
58
    }
1✔
59

60
    /**
61
     * @return the identifier which may be used as the name for a subfolder in
62
     * the temporary directory.
63
     */
64
    public String getID() {
65
        return ID;
1✔
66
    }
67

68
    /**
69
     * @return the folder (considered temporary) where the zipped crate will be
70
     * or has been extracted to.
71
     */
72
    public Path getTemporaryFolder() {
73
        return temporaryFolder;
1✔
74
    }
75

76
    /**
77
     * @return whether the crate has already been extracted into the temporary
78
     * folder.
79
     */
80
    public boolean isExtracted() {
81
        return isExtracted;
1✔
82
    }
83

84
    /**Read the crate metadata and content from the provided input stream.
85
     * 
86
     * @param stream The input stream.
87
     */
88
    private void readCrate(InputStream stream) throws IOException {
89
        File folder = temporaryFolder.toFile();
1✔
90
        // ensure the directory is clean
91
        if (folder.exists()) {
1✔
92
            if (folder.isDirectory()) {
1✔
93
                FileUtils.cleanDirectory(folder);
1✔
NEW
94
            } else if (folder.isFile()) {
×
NEW
95
                FileUtils.delete(folder);
×
96
            }
97
        } else {
98
            FileUtils.forceMkdir(folder);
1✔
99
        }
100

101
        LocalFileHeader localFileHeader;
102
        int readLen;
103
        byte[] readBuffer = new byte[4096];
1✔
104

105
        try (ZipInputStream zipInputStream = new ZipInputStream(stream)) {
1✔
106
            while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
1✔
107
                String fileName = localFileHeader.getFileName();
1✔
108
                File extractedFile = new File(folder, fileName).getCanonicalFile();
1✔
109
                if (!extractedFile.toPath().startsWith(folder.getCanonicalPath())) {
1✔
NEW
110
                    throw new IOException("Entry is outside of target directory: " + fileName);
×
111
                }
112
                if (localFileHeader.isDirectory()) {
1✔
113
                    FileUtils.forceMkdir(extractedFile);
1✔
114
                    continue;
1✔
115
                }
116
                FileUtils.forceMkdir(extractedFile.getParentFile());
1✔
117
                try (OutputStream outputStream = new FileOutputStream(extractedFile)) {
1✔
118
                    while ((readLen = zipInputStream.read(readBuffer)) != -1) {
1✔
119
                        outputStream.write(readBuffer, 0, readLen);
1✔
120
                    }
121
                }
122
            }
1✔
123
        }
124
        this.isExtracted = true;
1✔
125
        // register deletion on exit
126
        FileUtils.forceDeleteOnExit(folder);
1✔
127
    }
1✔
128

129
    @Override
130
    public ObjectNode readMetadataJson(InputStream stream) throws IOException {
131
        if (!isExtracted) {
1✔
132
            this.readCrate(stream);
1✔
133
        }
134

135
        ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
136
        File jsonMetadata = temporaryFolder.resolve(JsonDescriptor.ID).toFile();
1✔
137
        if (!jsonMetadata.isFile()) {
1✔
138
            // Try to find the metadata file in subdirectories
139
            File firstSubdir = FileUtils.listFilesAndDirs(
1✔
140
                            temporaryFolder.toFile(),
1✔
141
                            FileFilterUtils.directoryFileFilter(),
1✔
142
                            null
143
                    )
144
                    .stream()
1✔
145
                    .limit(50)
1✔
146
                    .filter(file -> file.toPath().toAbsolutePath().resolve(JsonDescriptor.ID).toFile().isFile())
1✔
147
                    .findFirst()
1✔
148
                    .orElseThrow(() -> new IllegalStateException("No %s found in zip file".formatted(JsonDescriptor.ID)));
1✔
149
            jsonMetadata = firstSubdir.toPath().resolve(JsonDescriptor.ID).toFile();
1✔
150
        }
151
        return objectMapper.readTree(jsonMetadata).deepCopy();
1✔
152
    }
153

154
    @Override
155
    public File readContent(InputStream stream) throws IOException {
156
        if (!isExtracted) {
1✔
UNCOV
157
            this.readCrate(stream);
×
158
        }
159
        return temporaryFolder.toFile();
1✔
160
    }
161
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc