Names.java

/*-
 * #%L
 * io.earcam.instrumental.reflect
 * %%
 * 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.reflect;

import static java.util.Arrays.stream;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Stream.concat;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import io.earcam.utilitarian.charstar.CharSequences;

//@formatter:off
/**
 * <p>
 * Names for things.
 * </p>
 * 
 * Definitions by example, given {@code java.lang.Thread}:
 * <ul>
 *    <li>
 *       <i>internal</i>:
 *       <pre>java/lang/Thread</pre>
 *    </li>
 *    <li>
 *       <i>descriptor</i>:
 *       <pre>Ljava/lang/Thread;</pre>
 *    </li>
 *    <li>
 *       <i>resource</i>:
 *       <pre>java/lang/Thread.class</pre>
 *    </li>
 * </ul>
 * 
 * @see <a href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.2">JVMS ยง4.2</a>
 */
//@formatter:on
public final class Names {

	private static final String CLASS_SUFFIX = ".class";
	private static final char L = 'L';
	private static final Map<Character, String> PRIMITIVE_DESCRIPTORS;  // excludes 'L'
	private static final Map<String, String> PRIMITIVE_TO_DESCRIPTOR;
	static {
		Map<Character, String> map = new HashMap<>(8);
		map.put('Z', "boolean");
		map.put('B', "byte");
		map.put('C', "char");
		map.put('D', "double");
		map.put('F', "float");
		map.put('I', "int");
		map.put('J', "long");
		map.put('S', "short");
		map.put('V', "void");

		PRIMITIVE_DESCRIPTORS = unmodifiableMap(map);

		PRIMITIVE_TO_DESCRIPTOR = unmodifiableMap(map.entrySet().stream()
				.collect(toMap(Map.Entry::getValue, e -> Character.toString(e.getKey()))));
	}


	private Names()
	{}


	/**
	 * @param internalName the <i>internal</i> name
	 * @return the type name
	 */
	public static String internalToTypeName(CharSequence internalName)
	{
		if(CharSequences.endsWith(internalName, "package-info") || CharSequences.endsWith(internalName, "module-info")) {
			return CharSequences.replace(internalName, '/', '.').toString();
		}

		int depth = 0;
		while(CharSequences.indexOf(internalName, '[', depth) != -1) {
			++depth;
		}
		StringBuilder typeName = new StringBuilder();
		if(depth > 0) {
			if(internalName.charAt(depth) == 'L') {
				typeName.append(internalName, depth + 1, internalName.length() - 1);
			} else {
				typeName.append(PRIMITIVE_DESCRIPTORS.get(internalName.charAt(depth)));
			}
			while(depth-- > 0) {
				typeName.append('[').append(']');
			}
		} else {
			typeName.append(internalName);
		}
		while((depth = typeName.indexOf("/", depth)) != -1) {
			typeName.setCharAt(depth, '.');
		}
		return typeName.toString();
	}


	/**
	 * @param type the fully qualified type name
	 * @return the <i>internal</i> name
	 */
	public static String typeToInternalName(CharSequence type)
	{
		return CharSequences.replace(type, '.', '/').toString();
	}


	/**
	 * @param type the type.
	 * @return the <i>internal</i> name.
	 */
	public static String typeToInternalName(Type type)
	{
		return typeToInternalName(type.getTypeName());
	}


	/**
	 * @param type the type.
	 * @return the <i>internal</i> name with ".class" appended.
	 */
	public static String typeToResourceName(Type type)
	{
		return typeToResourceName(type.getTypeName());
	}


	/**
	 * @param type FQN of the type.
	 * @return the <i>internal</i> name with ".class" appended.
	 */
	public static String typeToResourceName(String type)
	{
		return typeToInternalName(type) + CLASS_SUFFIX;
	}


	/**
	 * @see <a href="http://download.forge.objectweb.org/asm/asm4-guide.pdf">2.1.3 of asm4-guide</a>
	 * @param desc the type descriptor.
	 * @return the canonical name of the type.
	 */
	public static String descriptorToTypeName(String desc)
	{
		List<String> binaryNames = descriptorsToTypeNames(desc);
		if(binaryNames.size() != 1) {
			throw new IllegalArgumentException("No single type name in description: " + binaryNames);
		}
		return binaryNames.get(0);
	}


	private static String primitiveDescriptor(char descriptorChar)
	{
		return PRIMITIVE_DESCRIPTORS.get(descriptorChar);
	}


	/**
	 * @param desc a {@link java.lang.String} object.
	 * @return a {@link java.util.List} object.
	 */
	public static List<String> descriptorsToTypeNames(String desc)
	{
		int pos = 0;
		List<String> types = new ArrayList<>();

		while(pos < desc.length()) {
			StringBuilder arraySuffix = new StringBuilder();
			while(desc.charAt(pos) == '[') {
				arraySuffix.append("[]");
				++pos;
			}
			char current = desc.charAt(pos);
			if(current != L) {
				types.add(primitiveDescriptor(current) + arraySuffix);
				++pos;
				continue;
			}
			int end = desc.indexOf(';', pos);

			if(end == -1) {
				throw new IllegalArgumentException("Expecting object end marker ';'.  "
						+ "Could not parse next descriptor in: '" + desc + "' with: '" + desc.substring(pos) + "' remaining");
			}
			types.add(desc.substring(pos + 1, end).replace('/', '.') + arraySuffix);
			pos = end + 1;
		}
		return types;
	}


	/**
	 * @param c a {@link java.lang.Class} object.
	 * @return a {@link java.lang.CharSequence} object.
	 */
	public static CharSequence typeToDescriptor(Type c)
	{
		return typeToDescriptor(c.getTypeName());
	}


	/**
	 * @param type a {@link java.lang.CharSequence} object.
	 * @return a {@link java.lang.CharSequence} object.
	 */
	public static CharSequence typeToDescriptor(CharSequence type)
	{
		StringBuilder name = new StringBuilder();
		CharSequence t = type;
		while(CharSequences.endsWith(t, "[]")) {
			name.append('[');
			t = t.subSequence(0, t.length() - 2);
		}
		name.append(PRIMITIVE_TO_DESCRIPTOR.getOrDefault(t, classTypeToDescriptor(t)));
		return name;
	}


	private static String classTypeToDescriptor(CharSequence type)
	{
		return "L" + typeToInternalName(type) + ";";
	}


	/**
	 * @param method a {@link java.lang.reflect.Method} object.
	 * @return a {@link java.lang.CharSequence} object.
	 */
	public static CharSequence descriptorFor(Method method)
	{
		StringBuilder builder = new StringBuilder().append('(');
		for(Class<?> type : method.getParameterTypes()) {
			builder.append(typeToDescriptor(type));
		}
		return builder
				.append(')')
				.append(typeToDescriptor(method.getReturnType()));
	}


	/**
	 * @param type a {@link java.lang.Class} object.
	 * @return a stream of all class names declared <i>inside</i> the {@code type} argument
	 */
	public static Stream<String> declaredInternalNamesOf(Class<?> type)
	{
		return concat(
				concat(
						stream(type.getDeclaredClasses()).map(Names::typeToInternalName),
						stream(type.getDeclaredClasses()).flatMap(Names::declaredInternalNamesOf)),
				anonymousClassesOf(type));
	}


	private static Stream<String> anonymousClassesOf(Class<?> type)
	{
		List<String> anonymousClasses = new ArrayList<>();
		int i = 1;
		String baseName = typeToInternalName(type);
		String name = anonymousName(baseName, i);
		while(type.getClassLoader().getResource(name + CLASS_SUFFIX) != null) {
			anonymousClasses.add(name);
			name = anonymousName(baseName, ++i);
		}
		return anonymousClasses.stream();
	}


	private static String anonymousName(String baseName, int i)
	{
		return baseName + '$' + i;
	}
}