1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package io.earcam.utilitarian.io;
20
21 import static io.earcam.unexceptional.Closing.closeAfterAccepting;
22
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.nio.file.attribute.BasicFileAttributes;
31 import java.nio.file.attribute.FileTime;
32 import java.util.Iterator;
33 import java.util.jar.JarEntry;
34 import java.util.jar.JarInputStream;
35 import java.util.jar.Manifest;
36
37 import javax.annotation.concurrent.NotThreadSafe;
38
39 import io.earcam.unexceptional.Exceptional;
40
41
42
43
44
45
46 @SuppressWarnings("squid:MaximumInheritanceDepth")
47 @NotThreadSafe
48 public final class ExplodedJarInputStream extends JarInputStream {
49
50 private static final Path MANIFEST_PATH = Paths.get("META-INF", "MANIFEST.MF");
51
52 private static class EmptyInputStream extends InputStream {
53
54 public static final InputStream EMPTY_INPUTSTREAM = new EmptyInputStream();
55
56
57 @Override
58 public int read()
59 {
60 return -1;
61 }
62 }
63
64 public class ExplodedJarEntry extends JarEntry {
65
66 private Path path;
67 private byte[] contents;
68 private int position = 0;
69
70
71 public ExplodedJarEntry(Path path)
72 {
73 super(directory.relativize(path).toString());
74 this.path = path;
75 setMethod(STORED);
76 }
77
78
79 public Path path()
80 {
81 return path;
82 }
83
84
85 @Override
86 public boolean isDirectory()
87 {
88 return path().toFile().isDirectory();
89 }
90
91
92 @Override
93 public FileTime getCreationTime()
94 {
95 return Exceptional.apply(Files::readAttributes, path(), BasicFileAttributes.class).creationTime();
96 }
97
98
99 @Override
100 public FileTime getLastModifiedTime()
101 {
102 return Exceptional.apply(Files::getLastModifiedTime, path());
103 }
104
105
106 @Override
107 public long getTime()
108 {
109 return getLastModifiedTime().toMillis();
110 }
111
112
113 @Override
114 public long getSize()
115 {
116 return Exceptional.apply(Files::size, path());
117 }
118
119
120 private void loadContents()
121 {
122 if(contents == null) {
123 contents = Exceptional.apply(Files::readAllBytes, path());
124 }
125 }
126
127
128
129
130
131
132
133
134
135 @Deprecated
136 public int read()
137 {
138 throw new UnsupportedOperationException("Never intended for public use. Agressively deprecated.");
139 }
140
141
142 public int read(byte[] b, int off, int len)
143 {
144 loadContents();
145 int remaining = available();
146 if(remaining == 0) {
147 return -1;
148 }
149 int length = Math.min(remaining, len);
150 System.arraycopy(contents, position, b, off, length);
151 position += length;
152 return length;
153 }
154
155
156 int available()
157 {
158 loadContents();
159 return contents.length - position;
160 }
161 }
162
163 private Iterator<Path> iterator;
164 private Path directory;
165 private ExplodedJarEntry current;
166
167
168 private ExplodedJarInputStream(Path directory, Iterator<Path> iterator) throws IOException
169 {
170 super(EmptyInputStream.EMPTY_INPUTSTREAM, false);
171 this.directory = directory.toRealPath();
172 this.iterator = iterator;
173 }
174
175
176
177
178
179
180
181
182
183
184
185
186 public static JarInputStream jarInputStreamFrom(Path path) throws IOException
187 {
188 return jarInputStreamFrom(path.toFile());
189 }
190
191
192
193
194
195
196
197
198
199
200 public static JarInputStream jarInputStreamFrom(File path) throws IOException
201 {
202 return path.isDirectory() ? explodedJar(path) : new JarInputStream(new FileInputStream(path));
203 }
204
205
206
207
208
209
210
211
212
213 public static ExplodedJarInputStream explodedJar(File directory) throws IOException
214 {
215 return explodedJar(directory.toPath());
216 }
217
218
219
220
221
222
223
224
225
226 public static ExplodedJarInputStream explodedJar(Path directory) throws IOException
227 {
228 if(!directory.toFile().isDirectory()) {
229 throw new IOException("'" + directory + "' is not a directory");
230 }
231 RecursivePathIterator rpi = new RecursivePathIterator(directory);
232 return new ExplodedJarInputStream(directory, new Filterator<Path>(rpi, MANIFEST_PATH));
233 }
234
235
236 @Override
237 public JarEntry getNextJarEntry() throws IOException
238 {
239 current = iterator.hasNext() ? new ExplodedJarEntry(iterator.next().toRealPath()) : null;
240 return current;
241 }
242
243
244 @Override
245 public Manifest getManifest()
246 {
247 Manifest manifest = null;
248 Path file = directory.resolve(MANIFEST_PATH);
249 if(file.toFile().exists()) {
250 manifest = new Manifest();
251 closeAfterAccepting(FileInputStream::new, file.toFile(), manifest::read);
252 }
253 return manifest;
254 }
255
256
257 private void checkCurrent()
258 {
259 if(current == null) {
260 throw new UnsupportedOperationException(ExplodedJarInputStream.class + " does not work as a regular InputStream");
261 }
262 }
263
264
265 @Override
266 public int read(byte[] b, int off, int len) throws IOException
267 {
268 checkCurrent();
269 return current.read(b, off, len);
270 }
271
272
273 @Override
274 public int available() throws IOException
275 {
276 checkCurrent();
277 return current.available();
278 }
279 }