DefaultModuleInfo.java

/*-
 * #%L
 * io.earcam.instrumental.module.jpms
 * %%
 * Copyright (C) 2018 earcam
 * %%
 * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
 * 
 * You <b>must</b> choose to accept, in full - any individual or combination of 
 * the following licenses:
 * <ul>
 * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
 * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
 * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
 * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
 * </ul>
 * #L%
 */
package io.earcam.instrumental.module.jpms;

import static io.earcam.instrumental.module.jpms.RequireModifier.STATIC;
import static io.earcam.instrumental.module.jpms.RequireModifier.TRANSITIVE;
import static java.util.Arrays.stream;
import static java.util.EnumSet.of;
import static java.util.Locale.ROOT;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.io.Serializable;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;

class DefaultModuleInfo implements ModuleInfo, ModuleInfoBuilder, Serializable {

	private static final long serialVersionUID = 8059255940493321393L;
	private static final EnumSet<RequireModifier> MUTUALLY_EXCLUSIVE_REQUIRE_MODIFIERS = of(TRANSITIVE, STATIC);
	private static final String LIST_JOINING_DELIMITER = ",\n\t\t";

	private String name;
	private String version;
	private int access;

	private TreeSet<CharSequence> packages = new TreeSet<>();
	private TreeSet<String> uses = new TreeSet<>();
	private HashMap<String, String[]> provides = new HashMap<>();
	private String mainClass;
	private HashSet<Require> requires = new HashSet<>();
	private HashSet<Export> exports = new HashSet<>();
	private HashSet<Export> opens = new HashSet<>();


	@Override
	public boolean equals(Object other)
	{
		return other instanceof ModuleInfo && equals((ModuleInfo) other);
	}


	/**
	 * <p>
	 * equals.
	 * </p>
	 *
	 * @param that a {@link io.earcam.instrumental.module.jpms.ModuleInfo} object.
	 * @return a boolean.
	 */
	public boolean equals(ModuleInfo that)
	{
		return that != null
				&& Objects.equals(that.name(), name)
				&& Objects.equals(that.version(), version)
				&& Objects.equals(that.access(), access)
				&& Objects.equals(that.packages(), packages)
				&& Objects.equals(that.uses(), uses)
				&& deepEqualsMap(that.provides(), provides)
				&& Objects.equals(that.mainClass(), mainClass)
				&& Objects.equals(that.requires(), requires)
				&& Objects.equals(that.exports(), exports)
				&& Objects.equals(that.opens(), opens);
	}


	private static boolean deepEqualsMap(Map<String, String[]> a, Map<String, String[]> b)
	{
		if(Objects.deepEquals(a.keySet(), b.keySet())) {
			Iterator<String[]> aIt = a.values().iterator();
			Iterator<String[]> bIt = b.values().iterator();
			while(aIt.hasNext()) {
				if(!Arrays.equals(aIt.next(), bIt.next())) {
					return false;
				}
			}
			return true;
		}
		return false;
	}


	@Override
	public int hashCode()
	{
		return Objects.hash(
				name, version, packages,
				uses, provides.keySet(), mainClass,
				requires, exports, opens);
	}


	@Override
	public String name()
	{
		return name;
	}


	@Override
	public String version()
	{
		return version;
	}


	@Override
	public SortedSet<CharSequence> packages()
	{
		return packages;
	}


	@Override
	public SortedSet<String> uses()
	{
		return uses;
	}


	@Override
	public Map<String, String[]> provides()
	{
		return provides;
	}


	@Override
	public String mainClass()
	{
		return mainClass;
	}


	@Override
	public Set<Require> requires()
	{
		return requires;
	}


	@Override
	public Set<Export> exports()
	{
		return exports;
	}


	@Override
	public Set<Export> opens()
	{
		return opens;
	}


	@Override
	public ModuleInfoBuilder named(String moduleName)
	{
		name = moduleName;
		return this;
	}


	@Override
	public ModuleInfoBuilder versioned(String moduleVersion)
	{
		version = moduleVersion;
		return this;
	}


	@Override
	public ModuleInfoBuilder withAccess(int accessFlags)
	{
		access = accessFlags;
		return this;
	}


	@Override
	public ModuleInfoBuilder packaging(CharSequence paquet)
	{
		packages.add(paquet.toString());
		return this;
	}


	@Override
	public ModuleInfoBuilder using(String serviceApi)
	{
		uses.add(serviceApi);
		return this;
	}


	@Override
	public ModuleInfoBuilder providing(String contract, String... concretes)
	{
		provides.put(contract, concretes);
		return this;
	}


	@Override
	public ModuleInfoBuilder launching(String mainClass)
	{
		this.mainClass = mainClass;
		return this;
	}


	@Override
	public ModuleInfoBuilder requiring(String module, int access, String version)
	{
		requires.add(new Require(module, access, version));
		return this;
	}


	@Override
	public ModuleInfoBuilder exporting(String paquet, int access, String... modules)
	{
		exports.add(new Export(paquet, access, modules));
		return this;
	}


	@Override
	public ModuleInfoBuilder opening(String paquet, int access, String... modules)
	{
		opens.add(new Export(paquet, access, modules));
		return this;
	}


	@Override
	public ModuleInfo construct()
	{
		// TODO check validate - expose a separate method? javax.validation?
		Objects.requireNonNull(name, "name");

		List<Require> erroneousRequires = requires.stream()
				.filter(r -> r.modifiers().containsAll(MUTUALLY_EXCLUSIVE_REQUIRE_MODIFIERS))
				.collect(toList());
		if(!erroneousRequires.isEmpty()) {
			throw new IllegalStateException("require statements contain mutually exclusive modifiers: " +
					erroneousRequires.stream().map(Require::module).collect(joining(", ")));
		}
		return this;
	}


	@Override
	public ModuleInfoBuilder deconstruct()
	{
		return this;
	}


	/**
	 * {@inheritDoc}
	 *
	 * Comments are added, because although you can add annotations to the
	 * module declaration, you can't to it's elements.
	 */
	@Override
	public String toString()
	{
		StringBuilder output = new StringBuilder();

		addModuleComment(output);

		output.append(visibleModifiers(modifiers()));
		output.append("module ").append(name).append(" {\n");

		requiresToString(output);
		portsToString(output, exports, "exports");
		portsToString(output, opens, "opens");
		usesToString(output);
		providesToString(output);

		return output.append('}').toString();
	}


	private String visibleModifiers(Set<? extends Modifier> modifiers)
	{
		String mods = modifiers.stream()
				.filter(Modifier::sourceVisible)
				.map(Modifier::name)
				.map(m -> m.toLowerCase(ROOT))
				.collect(joining(" "));

		return (mods.length() > 0) ? mods + ' ' : mods;
	}


	private void addModuleComment(StringBuilder output)
	{
		if(version != null || access != 0 || !packages.isEmpty()) {
			startComment(output, "");
			versionComment(output, "", version);
			accessComment(output, "", Access.modifiers(ModuleModifier.class, access));
			packagesComment(output, packages);
			endComment(output, "");
		}
	}


	private static void packagesComment(StringBuilder output, Set<CharSequence> packages)
	{
		packages.forEach(p -> output.append(" * @package ").append(p).append('\n'));
	}


	private void requiresToString(StringBuilder output)
	{
		for(Require require : requires) {
			addComment(output, "\t", require.modifiers(), require.version());
			output.append('\t').append("requires ");
			output.append(visibleModifiers(require.modifiers()));
			output.append(require.module()).append(";\n");
		}
	}


	private void portsToString(StringBuilder output, HashSet<Export> ports, String portsLabel)
	{
		for(Export export : ports) {
			addComment(output, "\t", export.modifiers());
			output.append('\t').append(portsLabel).append(' ').append(export.paquet());
			if(export.modules().length > 0) {
				output.append(stream(export.modules()).collect(joining(LIST_JOINING_DELIMITER, " to \n\t\t", "")));
			}
			output.append(";\n");
		}
	}


	private void usesToString(StringBuilder output)
	{
		if(!uses.isEmpty()) {
			output.append(uses.stream().collect(Collectors.joining(";\n\tuses ", "\tuses ", ";\n")));
		}
	}


	private void providesToString(StringBuilder output)
	{
		for(Map.Entry<String, String[]> e : provides.entrySet()) {
			output.append('\t').append("provides ").append(e.getKey())
					.append(Arrays.stream(e.getValue()).collect(joining(LIST_JOINING_DELIMITER, " with \n\t\t", "")));
			output.append(";\n");
		}
	}


	private static <M extends Enum<M> & Modifier> void addComment(StringBuilder output, String indent, Set<M> modifiers)
	{
		if(!modifiers.isEmpty()) {
			startComment(output, indent);
			accessComment(output, indent, modifiers);
			endComment(output, indent);
		}
	}


	private static void addComment(StringBuilder output, String indent, Set<RequireModifier> modifiers, String version)
	{
		if(version != null || !modifiers.isEmpty()) {
			startComment(output, indent);
			versionComment(output, indent, version);
			accessComment(output, indent, modifiers);
			endComment(output, indent);
		}
	}


	private static void startComment(StringBuilder output, String indent)
	{
		output.append(indent).append("/**\n");
	}


	private static void versionComment(StringBuilder output, String indent, String version)
	{
		if(version != null) {
			output.append(indent).append(" * @version ").append(version).append('\n');
		}
	}


	private static void accessComment(StringBuilder output, String indent, Set<? extends Modifier> modifiers)
	{
		if(!modifiers.isEmpty()) {
			output.append(indent).append(" * @modifiers ")
					.append(modifiers.stream()
							.map(Modifier::name)
							.map(m -> m.toLowerCase(ROOT))
							.collect(joining(", ")))
					.append('\n');
		}
	}


	private static void endComment(StringBuilder output, String indent)
	{
		output.append(indent).append(" */\n");
	}


	@Override
	public byte[] toBytecode()
	{
		return BytecodeWriter.toBytecode(this);
	}


	@Override
	public int access()
	{
		return access;
	}
}