View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.module.jpms
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.module.jpms;
20  
21  import static io.earcam.instrumental.module.jpms.RequireModifier.STATIC;
22  import static io.earcam.instrumental.module.jpms.RequireModifier.TRANSITIVE;
23  import static java.util.Arrays.stream;
24  import static java.util.EnumSet.of;
25  import static java.util.Locale.ROOT;
26  import static java.util.stream.Collectors.joining;
27  import static java.util.stream.Collectors.toList;
28  
29  import java.io.Serializable;
30  import java.util.Arrays;
31  import java.util.EnumSet;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Objects;
38  import java.util.Set;
39  import java.util.SortedSet;
40  import java.util.TreeSet;
41  import java.util.stream.Collectors;
42  
43  class DefaultModuleInfo implements ModuleInfo, ModuleInfoBuilder, Serializable {
44  
45  	private static final long serialVersionUID = 8059255940493321393L;
46  	private static final EnumSet<RequireModifier> MUTUALLY_EXCLUSIVE_REQUIRE_MODIFIERS = of(TRANSITIVE, STATIC);
47  	private static final String LIST_JOINING_DELIMITER = ",\n\t\t";
48  
49  	private String name;
50  	private String version;
51  	private int access;
52  
53  	private TreeSet<CharSequence> packages = new TreeSet<>();
54  	private TreeSet<String> uses = new TreeSet<>();
55  	private HashMap<String, String[]> provides = new HashMap<>();
56  	private String mainClass;
57  	private HashSet<Require> requires = new HashSet<>();
58  	private HashSet<Export> exports = new HashSet<>();
59  	private HashSet<Export> opens = new HashSet<>();
60  
61  
62  	@Override
63  	public boolean equals(Object other)
64  	{
65  		return other instanceof ModuleInfo && equals((ModuleInfo) other);
66  	}
67  
68  
69  	/**
70  	 * <p>
71  	 * equals.
72  	 * </p>
73  	 *
74  	 * @param that a {@link io.earcam.instrumental.module.jpms.ModuleInfo} object.
75  	 * @return a boolean.
76  	 */
77  	public boolean equals(ModuleInfo that)
78  	{
79  		return that != null
80  				&& Objects.equals(that.name(), name)
81  				&& Objects.equals(that.version(), version)
82  				&& Objects.equals(that.access(), access)
83  				&& Objects.equals(that.packages(), packages)
84  				&& Objects.equals(that.uses(), uses)
85  				&& deepEqualsMap(that.provides(), provides)
86  				&& Objects.equals(that.mainClass(), mainClass)
87  				&& Objects.equals(that.requires(), requires)
88  				&& Objects.equals(that.exports(), exports)
89  				&& Objects.equals(that.opens(), opens);
90  	}
91  
92  
93  	private static boolean deepEqualsMap(Map<String, String[]> a, Map<String, String[]> b)
94  	{
95  		if(Objects.deepEquals(a.keySet(), b.keySet())) {
96  			Iterator<String[]> aIt = a.values().iterator();
97  			Iterator<String[]> bIt = b.values().iterator();
98  			while(aIt.hasNext()) {
99  				if(!Arrays.equals(aIt.next(), bIt.next())) {
100 					return false;
101 				}
102 			}
103 			return true;
104 		}
105 		return false;
106 	}
107 
108 
109 	@Override
110 	public int hashCode()
111 	{
112 		return Objects.hash(
113 				name, version, packages,
114 				uses, provides.keySet(), mainClass,
115 				requires, exports, opens);
116 	}
117 
118 
119 	@Override
120 	public String name()
121 	{
122 		return name;
123 	}
124 
125 
126 	@Override
127 	public String version()
128 	{
129 		return version;
130 	}
131 
132 
133 	@Override
134 	public SortedSet<CharSequence> packages()
135 	{
136 		return packages;
137 	}
138 
139 
140 	@Override
141 	public SortedSet<String> uses()
142 	{
143 		return uses;
144 	}
145 
146 
147 	@Override
148 	public Map<String, String[]> provides()
149 	{
150 		return provides;
151 	}
152 
153 
154 	@Override
155 	public String mainClass()
156 	{
157 		return mainClass;
158 	}
159 
160 
161 	@Override
162 	public Set<Require> requires()
163 	{
164 		return requires;
165 	}
166 
167 
168 	@Override
169 	public Set<Export> exports()
170 	{
171 		return exports;
172 	}
173 
174 
175 	@Override
176 	public Set<Export> opens()
177 	{
178 		return opens;
179 	}
180 
181 
182 	@Override
183 	public ModuleInfoBuilder named(String moduleName)
184 	{
185 		name = moduleName;
186 		return this;
187 	}
188 
189 
190 	@Override
191 	public ModuleInfoBuilder versioned(String moduleVersion)
192 	{
193 		version = moduleVersion;
194 		return this;
195 	}
196 
197 
198 	@Override
199 	public ModuleInfoBuilder withAccess(int accessFlags)
200 	{
201 		access = accessFlags;
202 		return this;
203 	}
204 
205 
206 	@Override
207 	public ModuleInfoBuilder packaging(CharSequence paquet)
208 	{
209 		packages.add(paquet.toString());
210 		return this;
211 	}
212 
213 
214 	@Override
215 	public ModuleInfoBuilder using(String serviceApi)
216 	{
217 		uses.add(serviceApi);
218 		return this;
219 	}
220 
221 
222 	@Override
223 	public ModuleInfoBuilder providing(String contract, String... concretes)
224 	{
225 		provides.put(contract, concretes);
226 		return this;
227 	}
228 
229 
230 	@Override
231 	public ModuleInfoBuilder launching(String mainClass)
232 	{
233 		this.mainClass = mainClass;
234 		return this;
235 	}
236 
237 
238 	@Override
239 	public ModuleInfoBuilder requiring(String module, int access, String version)
240 	{
241 		requires.add(new Require(module, access, version));
242 		return this;
243 	}
244 
245 
246 	@Override
247 	public ModuleInfoBuilder exporting(String paquet, int access, String... modules)
248 	{
249 		exports.add(new Export(paquet, access, modules));
250 		return this;
251 	}
252 
253 
254 	@Override
255 	public ModuleInfoBuilder opening(String paquet, int access, String... modules)
256 	{
257 		opens.add(new Export(paquet, access, modules));
258 		return this;
259 	}
260 
261 
262 	@Override
263 	public ModuleInfo construct()
264 	{
265 		// TODO check validate - expose a separate method? javax.validation?
266 		Objects.requireNonNull(name, "name");
267 
268 		List<Require> erroneousRequires = requires.stream()
269 				.filter(r -> r.modifiers().containsAll(MUTUALLY_EXCLUSIVE_REQUIRE_MODIFIERS))
270 				.collect(toList());
271 		if(!erroneousRequires.isEmpty()) {
272 			throw new IllegalStateException("require statements contain mutually exclusive modifiers: " +
273 					erroneousRequires.stream().map(Require::module).collect(joining(", ")));
274 		}
275 		return this;
276 	}
277 
278 
279 	@Override
280 	public ModuleInfoBuilder deconstruct()
281 	{
282 		return this;
283 	}
284 
285 
286 	/**
287 	 * {@inheritDoc}
288 	 *
289 	 * Comments are added, because although you can add annotations to the
290 	 * module declaration, you can't to it's elements.
291 	 */
292 	@Override
293 	public String toString()
294 	{
295 		StringBuilder output = new StringBuilder();
296 
297 		addModuleComment(output);
298 
299 		output.append(visibleModifiers(modifiers()));
300 		output.append("module ").append(name).append(" {\n");
301 
302 		requiresToString(output);
303 		portsToString(output, exports, "exports");
304 		portsToString(output, opens, "opens");
305 		usesToString(output);
306 		providesToString(output);
307 
308 		return output.append('}').toString();
309 	}
310 
311 
312 	private String visibleModifiers(Set<? extends Modifier> modifiers)
313 	{
314 		String mods = modifiers.stream()
315 				.filter(Modifier::sourceVisible)
316 				.map(Modifier::name)
317 				.map(m -> m.toLowerCase(ROOT))
318 				.collect(joining(" "));
319 
320 		return (mods.length() > 0) ? mods + ' ' : mods;
321 	}
322 
323 
324 	private void addModuleComment(StringBuilder output)
325 	{
326 		if(version != null || access != 0 || !packages.isEmpty()) {
327 			startComment(output, "");
328 			versionComment(output, "", version);
329 			accessComment(output, "", Access.modifiers(ModuleModifier.class, access));
330 			packagesComment(output, packages);
331 			endComment(output, "");
332 		}
333 	}
334 
335 
336 	private static void packagesComment(StringBuilder output, Set<CharSequence> packages)
337 	{
338 		packages.forEach(p -> output.append(" * @package ").append(p).append('\n'));
339 	}
340 
341 
342 	private void requiresToString(StringBuilder output)
343 	{
344 		for(Require require : requires) {
345 			addComment(output, "\t", require.modifiers(), require.version());
346 			output.append('\t').append("requires ");
347 			output.append(visibleModifiers(require.modifiers()));
348 			output.append(require.module()).append(";\n");
349 		}
350 	}
351 
352 
353 	private void portsToString(StringBuilder output, HashSet<Export> ports, String portsLabel)
354 	{
355 		for(Export export : ports) {
356 			addComment(output, "\t", export.modifiers());
357 			output.append('\t').append(portsLabel).append(' ').append(export.paquet());
358 			if(export.modules().length > 0) {
359 				output.append(stream(export.modules()).collect(joining(LIST_JOINING_DELIMITER, " to \n\t\t", "")));
360 			}
361 			output.append(";\n");
362 		}
363 	}
364 
365 
366 	private void usesToString(StringBuilder output)
367 	{
368 		if(!uses.isEmpty()) {
369 			output.append(uses.stream().collect(Collectors.joining(";\n\tuses ", "\tuses ", ";\n")));
370 		}
371 	}
372 
373 
374 	private void providesToString(StringBuilder output)
375 	{
376 		for(Map.Entry<String, String[]> e : provides.entrySet()) {
377 			output.append('\t').append("provides ").append(e.getKey())
378 					.append(Arrays.stream(e.getValue()).collect(joining(LIST_JOINING_DELIMITER, " with \n\t\t", "")));
379 			output.append(";\n");
380 		}
381 	}
382 
383 
384 	private static <M extends Enum<M> & Modifier> void addComment(StringBuilder output, String indent, Set<M> modifiers)
385 	{
386 		if(!modifiers.isEmpty()) {
387 			startComment(output, indent);
388 			accessComment(output, indent, modifiers);
389 			endComment(output, indent);
390 		}
391 	}
392 
393 
394 	private static void addComment(StringBuilder output, String indent, Set<RequireModifier> modifiers, String version)
395 	{
396 		if(version != null || !modifiers.isEmpty()) {
397 			startComment(output, indent);
398 			versionComment(output, indent, version);
399 			accessComment(output, indent, modifiers);
400 			endComment(output, indent);
401 		}
402 	}
403 
404 
405 	private static void startComment(StringBuilder output, String indent)
406 	{
407 		output.append(indent).append("/**\n");
408 	}
409 
410 
411 	private static void versionComment(StringBuilder output, String indent, String version)
412 	{
413 		if(version != null) {
414 			output.append(indent).append(" * @version ").append(version).append('\n');
415 		}
416 	}
417 
418 
419 	private static void accessComment(StringBuilder output, String indent, Set<? extends Modifier> modifiers)
420 	{
421 		if(!modifiers.isEmpty()) {
422 			output.append(indent).append(" * @modifiers ")
423 					.append(modifiers.stream()
424 							.map(Modifier::name)
425 							.map(m -> m.toLowerCase(ROOT))
426 							.collect(joining(", ")))
427 					.append('\n');
428 		}
429 	}
430 
431 
432 	private static void endComment(StringBuilder output, String indent)
433 	{
434 		output.append(indent).append(" */\n");
435 	}
436 
437 
438 	@Override
439 	public byte[] toBytecode()
440 	{
441 		return BytecodeWriter.toBytecode(this);
442 	}
443 
444 
445 	@Override
446 	public int access()
447 	{
448 		return access;
449 	}
450 }