View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.io
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
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   * BEWARE OF LIMITATIONS; CANNOT BE WRAPPED BY ANOTHER JarInputStream, CANNOT BE READ AS A NORMAL INPUT STREAM
43   * 
44   * TODO performance: {@link ExplodedJarEntry#loadContents()} should just be wrapping FileInputStream...
45   */
46  @SuppressWarnings("squid:MaximumInheritanceDepth") // SonarQube; Not much can be done about this... dirty hack anyhoo
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 		 * @deprecated Never intended for public use.
130 		 * Aggressively deprecated, class will become final once dropped.
131 		 * 
132 		 * @return nothing
133 		 * @throws UnsupportedOperationException everytime
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 	 * If the {@code path} parameter is a directory then returns an {@link ExplodedJarInputStream},
178 	 * otherwise, the {@code path} parameter is a file so, returns a {@link JarInputStream}
179 	 * 
180 	 * @param path the location on a filesystem
181 	 * @return a jar input stream
182 	 * @throws IOException
183 	 * 
184 	 * @see {@link #explodedJar(Path)}
185 	 */
186 	public static JarInputStream jarInputStreamFrom(Path path) throws IOException
187 	{
188 		return jarInputStreamFrom(path.toFile());
189 	}
190 
191 
192 	/**
193 	 * If the {@code path} parameter is a directory then returns an {@link ExplodedJarInputStream},
194 	 * otherwise, the {@code path} parameter is a file so, returns a {@link JarInputStream}
195 	 * 
196 	 * @param path the location on a filesystem
197 	 * @return a jar input stream
198 	 * @throws IOException
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 	 * Treat the <b>directory</b> as an exploded JAR file
208 	 * 
209 	 * @param directory
210 	 * @return a jar input stream, which can be read using {@link JarEntry} methods
211 	 * @throws IOException
212 	 */
213 	public static ExplodedJarInputStream explodedJar(File directory) throws IOException
214 	{
215 		return explodedJar(directory.toPath());
216 	}
217 
218 
219 	/**
220 	 * Treat the <b>directory</b> as an exploded JAR file
221 	 * 
222 	 * @param directory
223 	 * @return a jar input stream, which can be read using {@link JarEntry} methods
224 	 * @throws IOException
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 }