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 java.nio.file.LinkOption.NOFOLLOW_LINKS;
22  import static org.hamcrest.MatcherAssert.assertThat;
23  import static org.hamcrest.Matchers.*;
24  import static org.junit.jupiter.api.Assertions.*;
25  
26  import java.io.FileNotFoundException;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.nio.file.attribute.BasicFileAttributes;
34  import java.util.UUID;
35  import java.util.jar.Attributes;
36  import java.util.jar.JarEntry;
37  import java.util.jar.JarInputStream;
38  import java.util.jar.Manifest;
39  
40  import org.hamcrest.Matchers;
41  import org.junit.jupiter.api.Nested;
42  import org.junit.jupiter.api.Test;
43  
44  import io.earcam.unexceptional.Exceptional;
45  import io.earcam.utilitarian.io.ExplodedJarInputStream.ExplodedJarEntry;
46  
47  public class ExplodedJarInputStreamTest {
48  
49  	private static final Path TEST_DIR = Paths.get(".", "target", "test", ExplodedJarInputStreamTest.class.getSimpleName(), UUID.randomUUID().toString());
50  
51  
52  	// FIXME clean this up, it's hideous
53  	@Test
54  	public void disgustingTest() throws Exception
55  	{
56  		Path jarDir = TEST_DIR.resolve(Paths.get("explodesToFilesystem"));
57  
58  		final Class<?> archivedType = ExplodedJarInputStreamTest.class;
59  
60  		Path archivedPath = writeArchiveClass(jarDir, archivedType);
61  
62  		archiveManifest(jarDir);
63  
64  		boolean manifestChecked = false;
65  		boolean archivedClassChecked = false;
66  
67  		try(JarInputStream input = ExplodedJarInputStream.jarInputStreamFrom(jarDir)) {
68  
69  			Manifest manifest = input.getManifest();
70  			assertManifest(manifest);
71  
72  			JarEntry entry;
73  			while((entry = input.getNextJarEntry()) != null) {
74  
75  				if(entry.isDirectory()) {
76  					assertThat(entry.getName(), anyOf(
77  							startsWith("io"),
78  							is(equalTo("META-INF"))));
79  				} else {
80  
81  					if("META-INF/MANIFEST.MF".equals(entry.getName())) {
82  						assertArchivedManifest(input);
83  						manifestChecked = true;
84  					} else if(!entry.getName().contains("$")) {
85  
86  						assertArchivedClass(archivedType, archivedPath, input, entry);
87  						archivedClassChecked = true;
88  					}
89  				}
90  			}
91  			assertThat(manifestChecked, is(true));
92  			assertThat(archivedClassChecked, is(true));
93  		}
94  	}
95  
96  
97  	private void assertArchivedManifest(JarInputStream input) throws IOException
98  	{
99  		Manifest manifest = new Manifest(input);
100 		assertManifest(manifest);
101 	}
102 
103 
104 	private void assertManifest(Manifest manifest)
105 	{
106 		assertThat(manifest.getMainAttributes().getValue("SomeKey"), is(equalTo("SomeValue")));
107 	}
108 
109 
110 	private void assertArchivedClass(final Class<?> archivedType, Path archivedPath, JarInputStream input, JarEntry entry) throws IOException
111 	{
112 		byte[] bytecode = IoStreams.readAllBytes(input);
113 
114 		assertThat(entry.getSize(), is((long) bytecode.length));
115 		assertThat(entry.getSize(), is(archivedPath.toFile().length()));
116 
117 		assertThat(entry.getCreationTime(),
118 				is(equalTo(Exceptional.apply(Files::readAttributes, archivedPath, BasicFileAttributes.class).creationTime())));
119 		assertThat(entry.getLastModifiedTime(), is(equalTo(Files.getLastModifiedTime(archivedPath, NOFOLLOW_LINKS))));
120 		assertThat(entry.getTime(), is(equalTo(entry.getLastModifiedTime().toMillis())));
121 
122 		new ClassLoader(null) {
123 			{
124 				Class<?> type = defineClass(archivedType.getCanonicalName(), bytecode, 0, bytecode.length);
125 
126 				assertThat(type.getCanonicalName(), is(equalTo(archivedType.getCanonicalName())));
127 
128 				assertThat(type, is(not(equalTo(archivedType))));
129 			}
130 		};
131 	}
132 
133 
134 	private void archiveManifest(Path jarDir) throws IOException, FileNotFoundException
135 	{
136 		Path mf = jarDir.resolve(Paths.get("META-INF", "MANIFEST.MF"));
137 		mf.getParent().toFile().mkdirs();
138 
139 		Manifest archivedManifest = new Manifest();
140 		archivedManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
141 		archivedManifest.getMainAttributes().putValue("SomeKey", "SomeValue");
142 		archivedManifest.write(new FileOutputStream(mf.toFile()));
143 	}
144 
145 
146 	private Path writeArchiveClass(Path jarDir, final Class<?> archivedType) throws IOException, FileNotFoundException
147 	{
148 		String archivedFile = archivedType.getCanonicalName().replace('.', '/') + ".class";
149 		Path archivedPath = jarDir.resolve(archivedFile);
150 		archivedPath.getParent().toFile().mkdirs();
151 		try(InputStream in = archivedType.getClassLoader().getResourceAsStream(archivedFile)) {
152 			try(FileOutputStream out = new FileOutputStream(archivedPath.toFile())) {
153 				IoStreams.transfer(in, out);
154 			}
155 		}
156 		return archivedPath;
157 	}
158 
159 
160 	@Test
161 	public void explodedFromFilesystemWithoutManifest() throws Exception
162 	{
163 		Path jarDir = TEST_DIR.resolve(Paths.get("explodedFromFilesystemWithoutManifest"));
164 
165 		final Class<?> archivedType = ExplodedJarInputStreamTest.class;
166 
167 		Path archivedPath = writeArchiveClass(jarDir, archivedType);
168 
169 		boolean manifestChecked = false;
170 		boolean archivedClassChecked = false;
171 
172 		// EARCAM_SNIPPET_BEGIN: exploded-jar-0
173 		try(JarInputStream input = ExplodedJarInputStream.jarInputStreamFrom(jarDir)) {
174 
175 			Manifest manifest = input.getManifest();
176 			// ...
177 			// EARCAM_SNIPPET_END: exploded-jar-0
178 			assertThat(manifest, is(nullValue()));
179 
180 			JarEntry entry;
181 			while((entry = input.getNextJarEntry()) != null) {
182 
183 				if(entry.isDirectory()) {
184 					assertThat(entry.getName(), anyOf(
185 							startsWith("io"),
186 							is(equalTo("META-INF"))));
187 				} else {
188 
189 					if("META-INF/MANIFEST.MF".equals(entry.getName())) {
190 						manifestChecked = true;
191 					} else if(!entry.getName().contains("$")) {
192 
193 						assertArchivedClass(archivedType, archivedPath, input, entry);
194 						archivedClassChecked = true;
195 					}
196 				}
197 			}
198 			assertThat(manifestChecked, is(false));
199 			assertThat(archivedClassChecked, is(true));
200 			// EARCAM_SNIPPET_BEGIN: exploded-jar-1
201 		}
202 		// EARCAM_SNIPPET_END: exploded-jar-1
203 	}
204 
205 
206 	@Test
207 	public void explodedJarThrowsWhenPathIsNotADirectory()
208 	{
209 		try {
210 			ExplodedJarInputStream.explodedJar(Paths.get(".", "pom.xml"));
211 			fail();
212 		} catch(IOException e) {
213 
214 		}
215 	}
216 
217 
218 	@Test
219 	public void jarInputStreamFromJarFile() throws IOException
220 	{
221 		String resource = Matchers.class.getClassLoader().getResource(Matchers.class.getCanonicalName().replace('.', '/') + ".class").toString();
222 		resource = resource.replaceFirst("jar:file:", "").replaceAll("!.*", "");
223 		Path jarFile = Paths.get(resource);
224 
225 		try(JarInputStream input = ExplodedJarInputStream.jarInputStreamFrom(jarFile)) {
226 
227 			Manifest manifest = input.getManifest();
228 			assertThat(manifest.getMainAttributes().getValue("Implementation-Vendor"), is(equalTo("hamcrest.org")));
229 		}
230 	}
231 
232 	@Nested // TODO ExplodedJarInputStream doesn't behave like a proper stream... yet
233 	public class CurrentUndesirableBehaviour {
234 
235 		@Test
236 		public void failsAsNormalInputStream() throws IOException
237 		{
238 			Path outputDir = Paths.get("target", "test-classes");
239 			JarInputStream explodedJar = ExplodedJarInputStream.explodedJar(outputDir);
240 
241 			try {
242 				IoStreams.readAllBytes(explodedJar);
243 				fail();
244 			} catch(UnsupportedOperationException uoe) {}
245 		}
246 
247 
248 		@Test
249 		public void failsAsNormalInputStreamWithNothingAvailable() throws IOException
250 		{
251 			Path outputDir = Paths.get("target", "test-classes");
252 			JarInputStream explodedJar = ExplodedJarInputStream.explodedJar(outputDir);
253 
254 			try {
255 				explodedJar.available();
256 				fail();
257 			} catch(UnsupportedOperationException uoe) {}
258 		}
259 
260 
261 		@Test
262 		public void availableCanBeInvokedOnTheEntries() throws IOException
263 		{
264 			Path outputDir = Paths.get("target", "test-classes");
265 			JarInputStream explodedJar = ExplodedJarInputStream.explodedJar(outputDir);
266 
267 			JarEntry nextJarEntry;
268 			do {
269 				nextJarEntry = explodedJar.getNextJarEntry();
270 			} while(nextJarEntry.isDirectory());
271 
272 			assertThat(explodedJar.available(), is(greaterThanOrEqualTo(0)));
273 		}
274 
275 
276 		@Deprecated
277 		@Test
278 		public void explodedJarEntrySingleReadMethodIsEffectivelyUseless() throws IOException
279 		{
280 			ExplodedJarInputStream in = (ExplodedJarInputStream) ExplodedJarInputStream.jarInputStreamFrom(Paths.get(".").toAbsolutePath());
281 			ExplodedJarEntry explodedJarEntry = in.new ExplodedJarEntry(Paths.get(".", "target").toAbsolutePath());
282 			try {
283 				explodedJarEntry.read();
284 				fail();
285 			} catch(UnsupportedOperationException e) {}
286 		}
287 	}
288 }