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

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

19 May 2025 01:23PM UTC coverage: 90.495% (+0.3%) from 90.169%
#430

Pull #258

github

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

276 of 305 new or added lines in 21 files covered. (90.49%)

1 existing line in 1 file now uncovered.

2009 of 2220 relevant lines covered (90.5%)

0.9 hits per line

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

95.0
/src/main/java/edu/kit/datamanager/ro_crate/reader/ReadZipStrategy.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 net.lingala.zip4j.ZipFile;
8
import org.apache.commons.io.FileUtils;
9
import org.apache.commons.io.filefilter.FileFilterUtils;
10

11
import java.io.File;
12
import java.io.IOException;
13
import java.nio.file.Path;
14
import java.util.UUID;
15

16
/**
17
 * Reads a crate from a ZIP archive (file).
18
 * <p>
19
 * This class handles reading and extraction of RO-Crate content from ZIP archives
20
 * into a temporary directory structure on the file system,
21
 * which allows accessing the contained files.
22
 * <p>
23
 * Supports <a href=https://github.com/TheELNConsortium/TheELNFileFormat>ELN-Style crates</a>,
24
 * meaning the crate may be either in the zip archive directly or in a single,
25
 * direct subfolder beneath the root folder (/folder).
26
 * <p>
27
 * Note: This implementation checks for up to 50 subdirectories if multiple are present.
28
 * This is to avoid zip bombs, which may contain a lot of subdirectories,
29
 * and at the same time gracefully handle valid crated with hidden subdirectories
30
 * (for example, thumbnails).
31
 * <p>
32
 * NOTE: The resulting crate may refer to these temporary files. Therefore,
33
 * these files are only being deleted before the JVM exits. If you need to free
34
 * space because your application is long-running or creates a lot of
35
 * crates, you may use the getters to retrieve information which will help
36
 * you to clean up manually. Keep in mind that crates may refer to this
37
 * folder after extraction. Use RoCrateWriter to export it so some
38
 * persistent location and possibly read it from there, if required. Or use
39
 * the ZipWriter to write it back to its source.
40
 */
41
public class ReadZipStrategy implements GenericReaderStrategy<String> {
42

43
  protected final String ID = UUID.randomUUID().toString();
1✔
44
  protected Path temporaryFolder = Path.of(String.format("./.tmp/ro-crate-java/zipReader/%s/", ID));
1✔
45
  protected boolean isExtracted = false;
1✔
46

47
  /**
48
   * Crates an instance with the default configuration.
49
   * <p>
50
   * The default configuration is to extract the ZipFile to
51
   * `./.tmp/ro-crate-java/zipReader/$UUID/`.
52
   */
53
  public ReadZipStrategy() {}
1✔
54

55
  /**
56
   * Creates a ZipReader which will extract the contents temporary
57
   * to the given location instead of the default location.
58
   *
59
   * @param folderPath            the custom directory to extract
60
   *                              content to for temporary access.
61
   * @param shallAddUuidSubfolder if true, the reader will extract
62
   *                              into subdirectories of the given
63
   *                              directory. These subdirectories
64
   *                              will have UUIDs as their names.
65
   */
66
  public ReadZipStrategy(Path folderPath, boolean shallAddUuidSubfolder) {
1✔
67
    if (shallAddUuidSubfolder) {
1✔
68
      this.temporaryFolder = folderPath.resolve(ID);
1✔
69
    } else {
70
      this.temporaryFolder = folderPath;
1✔
71
    }
72
  }
1✔
73

74
  /**
75
   * @return the identifier which may be used as the name for a subfolder in the temporary directory.
76
   */
77
  public String getID() {
78
    return ID;
1✔
79
  }
80

81
  /**
82
   * @return the folder (considered temporary) where the zipped crate will be or has been extracted to.
83
   */
84
  public Path getTemporaryFolder() {
85
    return temporaryFolder;
1✔
86
  }
87

88
  /**
89
   * @return whether the crate has already been extracted into the temporary folder.
90
   */
91
  public boolean isExtracted() {
92
    return isExtracted;
1✔
93
  }
94

95
  private void readCrate(String location) throws IOException {
96
    File folder = temporaryFolder.toFile();
1✔
97
    // ensure the directory is clean
98
    if (folder.isDirectory()) {
1✔
99
      FileUtils.cleanDirectory(folder);
1✔
100
    } else if (folder.isFile()) {
1✔
NEW
101
      FileUtils.delete(folder);
×
102
    }
103
    // extract
104
    try (ZipFile zf = new ZipFile(location)) {
1✔
105
      zf.extractAll(temporaryFolder.toAbsolutePath().toString());
1✔
106
      this.isExtracted = true;
1✔
107
    }
108
    // register deletion on exit
109
    FileUtils.forceDeleteOnExit(folder);
1✔
110
  }
1✔
111

112
  @Override
113
  public ObjectNode readMetadataJson(String location) throws IOException {
114
    if (!isExtracted) {
1✔
115
      this.readCrate(location);
1✔
116
    }
117

118
    ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
119
    File jsonMetadata = this.temporaryFolder.resolve(JsonDescriptor.ID).toFile();
1✔
120
    if (!jsonMetadata.isFile()) {
1✔
121
      // Try to find the metadata file in subdirectories
122
      File firstSubdir = FileUtils.listFilesAndDirs(
1✔
123
            temporaryFolder.toFile(),
1✔
124
            FileFilterUtils.directoryFileFilter(),
1✔
125
            null // not recursive
126
        )
127
        .stream()
1✔
128
        .limit(50)
1✔
129
        .filter(file -> file.toPath().toAbsolutePath().resolve(JsonDescriptor.ID).toFile().isFile())
1✔
130
        .findFirst()
1✔
131
        .orElseThrow(() -> new IllegalStateException("No %s found in zip file".formatted(JsonDescriptor.ID)));
1✔
132
      jsonMetadata = firstSubdir.toPath().resolve(JsonDescriptor.ID).toFile();
1✔
133
    }
134

135
    return objectMapper.readTree(jsonMetadata).deepCopy();
1✔
136
  }
137

138
  @Override
139
  public File readContent(String location) throws IOException {
140
    if (!isExtracted) {
1✔
141
      this.readCrate(location);
×
142
    }
143
    return temporaryFolder.toFile();
1✔
144
  }
145
}
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