View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.archive.osgi
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.instrumental.archive.osgi;
20  
21  import static io.earcam.instrumental.archive.Archive.archive;
22  import static io.earcam.instrumental.archive.osgi.AsOsgiBundle.asOsgiBundle;
23  import static io.earcam.instrumental.module.osgi.BundleInfoBuilder.bundle;
24  import static io.earcam.instrumental.module.osgi.BundleManifestHeaders.IMPORT_PACKAGE;
25  import static io.earcam.instrumental.module.osgi.Clause.clause;
26  import static io.earcam.instrumental.module.osgi.ClauseParameters.EMPTY_PARAMETERS;
27  import static io.earcam.instrumental.module.osgi.ClauseParameters.ClauseParameter.attribute;
28  import static java.nio.charset.StandardCharsets.UTF_8;
29  import static org.hamcrest.MatcherAssert.assertThat;
30  import static org.hamcrest.Matchers.contains;
31  import static org.hamcrest.Matchers.containsString;
32  import static org.hamcrest.Matchers.equalTo;
33  import static org.hamcrest.Matchers.is;
34  import static org.hamcrest.Matchers.not;
35  import static org.hamcrest.Matchers.nullValue;
36  import static org.junit.jupiter.api.Assertions.fail;
37  
38  import java.io.ByteArrayInputStream;
39  import java.io.ByteArrayOutputStream;
40  import java.util.Collections;
41  import java.util.List;
42  import java.util.function.Predicate;
43  import java.util.jar.Manifest;
44  
45  import org.junit.jupiter.api.DisplayName;
46  import org.junit.jupiter.api.Nested;
47  import org.junit.jupiter.api.Test;
48  
49  import io.earcam.acme.DummyInterface;
50  import io.earcam.acme.TakesExt;
51  import io.earcam.acme.auto.ImportsUnexceptional;
52  import io.earcam.acme.ext.Ext;
53  import io.earcam.acme.internal.DummyActivator;
54  import io.earcam.instrumental.archive.Archive;
55  import io.earcam.instrumental.archive.osgi.auto.AbstractPackageBundleMapper;
56  import io.earcam.instrumental.module.osgi.BundleInfo;
57  import io.earcam.instrumental.module.osgi.BundleInfoBuilder;
58  import io.earcam.instrumental.module.osgi.ClauseParameters;
59  import io.earcam.instrumental.module.osgi.ClauseParameters.ClauseParameter;
60  import io.earcam.instrumental.reflect.Names;
61  import io.earcam.unexceptional.Exceptional;
62  
63  public class AsOsgiBundleTest {
64  
65  	@Nested
66  	public class SymbolicNames {
67  
68  		@Test
69  		public void emptyBundle()
70  		{
71  			Archive archive = archive()
72  					.configured(asOsgiBundle()
73  							.named("foo"))
74  					.toObjectModel();
75  
76  			BundleInfo bundleInfo = bundleInfoFrom(archive);
77  
78  			assertThat(bundleInfo.symbolicName(), is(equalTo(clause("foo"))));
79  		}
80  
81  
82  		@Test
83  		public void parameterizedSymbolicName()
84  		{
85  			ClauseParameters parameters = attribute("attri", "bute").directive("direct", "ive");
86  
87  			Archive archive = archive()
88  					.configured(asOsgiBundle()
89  							.named("sym.nom", parameters))
90  					.toObjectModel();
91  
92  			BundleInfo bundleInfo = bundleInfoFrom(archive);
93  
94  			assertThat(bundleInfo.symbolicName(), is(equalTo(clause("sym.nom", parameters))));
95  		}
96  	}
97  
98  
99  	private static BundleInfo bundleInfoFrom(Archive archive)
100 	{
101 		Manifest manifest = archive.manifest().orElseThrow(AssertionError::new);
102 		ByteArrayOutputStream baos = new ByteArrayOutputStream();
103 		Exceptional.accept(manifest::write, baos);
104 		BundleInfoBuilder builder = BundleInfoBuilder.bundleFrom(new ByteArrayInputStream(baos.toByteArray()));
105 		return builder.construct();
106 	}
107 
108 
109 	private static String pkg(Class<?> type)
110 	{
111 		return type.getPackage().getName();
112 	}
113 
114 
115 	private static byte[] bytes(String text)
116 	{
117 		return text.getBytes(UTF_8);
118 	}
119 
120 	@Nested
121 	public class Exports {
122 
123 		@Test
124 		public void singleExportedClassWithNoParameters()
125 		{
126 			Archive archive = archive()
127 					.configured(asOsgiBundle()
128 							.named("mandatory.value")
129 							.exporting(DummyInterface.class))
130 					.toObjectModel();
131 
132 			BundleInfo bundleInfo = bundleInfoFrom(archive);
133 
134 			assertThat(bundleInfo.exportPackage(), contains(clause(pkg(DummyInterface.class))));
135 			assertThat(archive.content(Names.typeToResourceName(DummyInterface.class)), is(not(nullValue())));
136 		}
137 
138 
139 		@Test
140 		public void exportPatternWithParameters()
141 		{
142 			Predicate<String> matcher = Predicate.isEqual(pkg(DummyInterface.class));
143 			ClauseParameters parameters = ClauseParameter.attribute("attr", "ibute");
144 
145 			Archive archive = archive()
146 					.configured(asOsgiBundle()
147 							.named("mandatory.value")
148 							.exporting(matcher, parameters))
149 					.with(DummyInterface.class)
150 					.with(DummyActivator.class)
151 					.toObjectModel();
152 
153 			BundleInfo bundleInfo = bundleInfoFrom(archive);
154 
155 			assertThat(bundleInfo.exportPackage(), contains(clause(pkg(DummyInterface.class), parameters)));
156 		}
157 
158 
159 		@DisplayName("Given multiple export patterns, When multiple potential matches occur, Then only first pattern applied")
160 		@Test
161 		public void givenMultipleExportPatternsWhenMultiplePotentialMatchesOccurThenOnlyFirstPatternApplied()
162 		{
163 			Predicate<String> matcher = Predicate.isEqual(pkg(DummyInterface.class));
164 			ClauseParameters parameters1 = ClauseParameter.attribute("matcher", "1").directive("matcherNum", "one");
165 			ClauseParameters parameters2 = ClauseParameter.attribute("matcher", "2");
166 
167 			Archive archive = archive()
168 					.configured(asOsgiBundle()
169 							.named("mandatory.sym.nom")
170 							.exporting(matcher, parameters1)
171 							.exporting(matcher, parameters2))
172 					.with(DummyInterface.class)
173 					.with(DummyActivator.class)
174 					.toObjectModel();
175 
176 			BundleInfo bundleInfo = bundleInfoFrom(archive);
177 
178 			assertThat(bundleInfo.exportPackage(), contains(clause(pkg(DummyInterface.class), parameters1)));
179 		}
180 
181 
182 		@Test
183 		public void exportPatternDoesNotExportResourcePaths() throws ReflectiveOperationException
184 		{
185 
186 			Archive archive = archive()
187 					.configured(asOsgiBundle()
188 							.named("syn.nom")
189 							.exporting(s -> true, EMPTY_PARAMETERS))
190 					.with(DummyInterface.class)
191 					.with("some/resource/path/config.properties", bytes("key1=valueA\nkey2=valueB"))
192 					.with("/some/other/path/legal.md", bytes("IANAL Sorry"))
193 					.toObjectModel();
194 
195 			BundleInfo bundleInfo = bundleInfoFrom(archive);
196 
197 			assertThat(bundleInfo.exportPackage(), contains(clause(pkg(DummyInterface.class))));
198 		}
199 
200 
201 		@Test
202 		public void exportPatternDoesNotExportTheDefaultPackage() throws ReflectiveOperationException
203 		{
204 
205 			Class<?> noPackage = getClass().getClassLoader().loadClass("NoPackage");
206 			assertThat(noPackage, is(not(nullValue())));
207 
208 			Archive archive = archive()
209 					.configured(asOsgiBundle()
210 							.named("syn.nom")
211 							.exporting(s -> true, EMPTY_PARAMETERS))
212 					.with(DummyInterface.class)
213 					.with(noPackage)
214 					.toObjectModel();
215 
216 			BundleInfo bundleInfo = bundleInfoFrom(archive);
217 
218 			assertThat(bundleInfo.exportPackage(), contains(clause(pkg(DummyInterface.class))));
219 		}
220 	}
221 
222 	@Nested
223 	public class Activators {
224 
225 		@Test
226 		public void validActivator()
227 		{
228 			// EARCAM_SNIPPET_BEGIN: activator
229 			Archive archive = archive()
230 					.configured(asOsgiBundle()
231 							.named("sym.nom")
232 							.withActivator(DummyActivator.class))
233 					.toObjectModel();
234 			// EARCAM_SNIPPET_END: activator
235 
236 			BundleInfo bundleInfo = bundleInfoFrom(archive);
237 
238 			assertThat(bundleInfo.activator(), is(equalTo(cn(DummyActivator.class))));
239 		}
240 
241 
242 		private String cn(Class<?> type)
243 		{
244 			return type.getCanonicalName();
245 		}
246 
247 
248 		@Test
249 		public void invalidActivator()
250 		{
251 			try {
252 				asOsgiBundle()
253 						.named("mandatory.value")
254 						.withActivator(DummyInterface.class);
255 				fail();
256 			} catch(IllegalArgumentException e) {
257 
258 			}
259 		}
260 
261 
262 		@Test
263 		public void invalidActivatorIgnoredWhenValidationDisabled()
264 		{
265 			Archive archive = archive()
266 					.configured(asOsgiBundle()
267 							.disableValidation()
268 							.named("mandatory.value")
269 							.withActivator(DummyInterface.class))
270 					.toObjectModel();
271 
272 			BundleInfo bundleInfo = bundleInfoFrom(archive);
273 
274 			assertThat(bundleInfo.activator(), is(equalTo(cn(DummyInterface.class))));
275 		}
276 	}
277 
278 	@Nested
279 	public class PatternImports {
280 
281 		@Test
282 		public void importWithParameters()
283 		{
284 			String paquet = "com.acme.core.of.cores.root.of.all.apis";
285 			Archive archive = archive()
286 					.configured(asOsgiBundle()
287 							.named("mandatory.value")
288 							.importing(paquet))
289 					.with(DummyInterface.class)
290 					.toObjectModel();
291 
292 			BundleInfo bundleInfo = bundleInfoFrom(archive);
293 
294 			assertThat(bundleInfo.importPackage(), contains(clause(paquet)));
295 		}
296 
297 	}
298 
299 	@Nested
300 	public class AutoImports {
301 
302 		@Test
303 		void autoImportsViaDefaultClasspathBundleMapper()
304 		{
305 			Archive built = archive()
306 					.configured(asOsgiBundle()
307 							.named("dependee")
308 							.autoImporting())
309 					.with(ImportsUnexceptional.class)
310 					.with("some/resource.file", "Hello, World".getBytes(UTF_8))
311 					.toObjectModel();
312 
313 			String value = built.manifest().get().getMainAttributes().getValue(IMPORT_PACKAGE.header());
314 
315 			assertThat(value, containsString(Exceptional.class.getPackage().getName()));
316 		}
317 
318 
319 		@Test
320 		void autoImportsViaSuppliedPackageBundleMapper()
321 		{
322 			ClauseParameters version = attribute("version", "0.0.1-alpha");
323 			String exported = pkg(Ext.class);
324 
325 			BundleInfo dependency = bundle()
326 					.symbolicName("dependency")
327 					.exportPackages(exported, version)
328 					.construct();
329 
330 			PackageBundleMapper mapper = new AbstractPackageBundleMapper() {
331 				@Override
332 				protected List<BundleInfo> bundles()
333 				{
334 					return Collections.singletonList(dependency);
335 				}
336 			};
337 
338 			Archive built = archive()
339 					.configured(asOsgiBundle()
340 							.named("dependee")
341 							.autoImporting(mapper))
342 					.with(TakesExt.class)
343 					.toObjectModel();
344 
345 			String value = built.manifest().get().getMainAttributes().getValue(IMPORT_PACKAGE.header());
346 
347 			assertThat(value, is(equalTo(clause(exported, version).toString())));
348 		}
349 
350 
351 		@Test
352 		void autoImportsFailsWhenImportsUnmapped()
353 		{
354 
355 			PackageBundleMapper mapper = new AbstractPackageBundleMapper() {
356 				@Override
357 				protected List<BundleInfo> bundles()
358 				{
359 					return Collections.emptyList();
360 				}
361 			};
362 
363 			try {
364 				archive()
365 						.configured(asOsgiBundle()
366 								.named("dependee")
367 								.autoImporting(mapper))
368 						.with(TakesExt.class)
369 						.toObjectModel();
370 				fail();
371 			} catch(IllegalStateException e) {
372 
373 			}
374 		}
375 
376 
377 		@Test
378 		void unmappedImportsIgnoredWhenValidationDisabled()
379 		{
380 
381 			PackageBundleMapper mapper = new AbstractPackageBundleMapper() {
382 				@Override
383 				protected List<BundleInfo> bundles()
384 				{
385 					return Collections.emptyList();
386 				}
387 			};
388 
389 			archive()
390 					.configured(asOsgiBundle()
391 							.disableValidation()
392 							.named("dependee")
393 							.autoImporting(mapper))
394 					.with(TakesExt.class)
395 					.toObjectModel();
396 		}
397 
398 	}
399 }