Types.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.asList;
import static java.util.Collections.unmodifiableMap;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.earcam.unexceptional.Exceptional;

/**
 * <p>
 * Types class.
 * </p>
 *
 */
public final class Types {

	private static final ClassLoader CLASS_LOADER = Types.class.getClassLoader();

	/**
	 * {@link java.lang.Class#isPrimitive()}
	 */
	private static final Set<String> PRIMITIVES = namesOf(
			short.class, int.class, long.class, float.class, double.class, char.class, byte.class, boolean.class, void.class);

	private static final Set<String> WRAPPERS = namesOf(
			Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class, Byte.class, Boolean.class, Void.class);

	private static final Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE;
	private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER;
	static {
		Map<Class<?>, Class<?>> map = new HashMap<>(8);
		map.put(Boolean.class, boolean.class);
		map.put(Byte.class, byte.class);
		map.put(Character.class, char.class);
		map.put(Double.class, double.class);
		map.put(Float.class, float.class);
		map.put(Integer.class, int.class);
		map.put(Long.class, long.class);
		map.put(Short.class, short.class);
		map.put(Void.class, void.class);
		WRAPPER_TO_PRIMITIVE = unmodifiableMap(map);
		PRIMITIVE_TO_WRAPPER = unmodifiableMap(WRAPPER_TO_PRIMITIVE.entrySet().stream().collect(toMap(Map.Entry::getValue, Map.Entry::getKey)));
	}


	private Types()
	{}


	private static Set<String> namesOf(Class<?>... items)
	{
		return unmodifiableSet(asList(items).stream()
				.map(Class::getCanonicalName)
				.collect(toSet()));
	}


	/**
	 * <p>
	 * wrapperToPrimitive.
	 * </p>
	 *
	 * @param type a {@link Class} object.
	 * @return a {@link Class} object.
	 */
	public static Class<?> wrapperToPrimitive(Class<?> type)
	{
		return WRAPPER_TO_PRIMITIVE.get(type);
	}


	/**
	 * <p>
	 * primitiveToWrapper.
	 * </p>
	 *
	 * @param type a {@link Class} object.
	 * @return a {@link Class} object.
	 */
	public static Class<?> primitiveToWrapper(Class<?> type)
	{
		return PRIMITIVE_TO_WRAPPER.get(type);
	}


	/**
	 * <p>
	 * isPrimitive.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a boolean.
	 */
	public static boolean isPrimitive(Type type)
	{

		Class<?> clazz = getClass(type);
		return clazz.isPrimitive();
	}


	/**
	 * <p>
	 * isPrimitive.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @return a boolean.
	 */
	public static boolean isPrimitive(String name)
	{
		return PRIMITIVES.contains(name);
	}


	/**
	 * <p>
	 * isPrimitiveWrapper.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a boolean.
	 */
	public static boolean isPrimitiveWrapper(Type type)
	{
		return isPrimitiveWrapper(type.getTypeName());
	}


	/**
	 * <p>
	 * isPrimitiveWrapper.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @return a boolean.
	 */
	public static boolean isPrimitiveWrapper(String name)
	{
		return WRAPPERS.contains(name);
	}


	/**
	 * <p>
	 * isPrimitiveArray.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a boolean.
	 */
	public static boolean isPrimitiveArray(Type type)
	{
		Class<?> clazz = getClass(type);
		return clazz.isArray() && isPrimitive(clazz.getComponentType());
	}


	/**
	 * <p>
	 * isInterface.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a boolean.
	 */
	public static boolean isInterface(Type type)
	{
		Class<?> clazz = getClass(type);
		return clazz.isInterface() && !clazz.isAnnotation();
	}


	/**
	 * <p>
	 * isInterface.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @return a boolean.
	 */
	public static boolean isInterface(String name)
	{
		return isInterface(getClass(name, CLASS_LOADER));
	}


	/**
	 * <p>
	 * isClass.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a boolean.
	 */
	public static boolean isClass(Type type)
	{
		Class<?> clazz = getClass(type);
		return clazz.getSuperclass() != null || Object.class.equals(clazz);
	}


	/**
	 * <p>
	 * requireClass.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 */
	public static void requireClass(Type type)
	{
		Objects.requireNonNull(type, "type cannot be null");
		if(!isClass(type)) {
			throw new IllegalArgumentException("type '" + type.getTypeName() + "' is not a class");
		}
	}


	/**
	 * <p>
	 * isClass.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @return a boolean.
	 */
	public static boolean isClass(String name)
	{
		return isClass(getClass(name, CLASS_LOADER));
	}


	/**
	 * <p>
	 * getClass.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @return a {@link Class} object.
	 */
	public static Class<?> getClass(Type type)
	{
		return getClass(type, CLASS_LOADER);
	}


	/**
	 * <p>
	 * getClass.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 * @param classLoader a {@link java.lang.ClassLoader} object.
	 * @return a {@link Class} object.
	 */
	public static Class<?> getClass(Type type, ClassLoader classLoader)
	{
		return type instanceof Class ? ((Class<?>) type) : getClass(type.getTypeName(), classLoader);
	}


	/**
	 * <p>
	 * getClass.
	 * </p>
	 *
	 * @param typeName a {@link java.lang.String} object.
	 * @param classLoader a {@link java.lang.ClassLoader} object.
	 * @return a {@link Class} object.
	 */
	public static Class<?> getClass(String typeName, ClassLoader classLoader)
	{
		return Exceptional.apply(classLoader::loadClass, typeName);
	}


	/**
	 * <p>
	 * requireInterface.
	 * </p>
	 *
	 * @param type a {@link java.lang.reflect.Type} object.
	 */
	public static void requireInterface(Type type)
	{
		Objects.requireNonNull(type, "type cannot be null");
		if(!isInterface(type)) {
			throw new IllegalArgumentException("type '" + type.getTypeName() + "' is not an interface");
		}
	}


	/**
	 * <p>
	 * implementsAll.
	 * </p>
	 *
	 * @param type a {@link Class} object.
	 * @param interfaces a {@link Class} object.
	 * @return a boolean.
	 */
	public static boolean implementsAll(Class<?> type, Class<?>... interfaces)
	{
		return implementsAll(type, Arrays.asList(interfaces));

	}


	/**
	 * <p>
	 * Returns {@code true} IFF {@code type} implements all {@code interfaces}.
	 * </p>
	 *
	 * @param type a {@link Class} object.
	 * @param interfaces a {@link Collection} object.
	 * @return a boolean.
	 */
	public static boolean implementsAll(Class<?> type, Collection<Class<?>> interfaces)
	{
		List<Class<?>> implemented = allInterfacesOf(type).collect(Collectors.toList());
		Predicate<? super Class<?>> contained = implemented::contains;
		return !(implemented.isEmpty() || interfaces.stream().anyMatch(contained.negate()));
	}


	/**
	 * <p>
	 * Return all the interfaces of a given class.
	 * </p>
	 *
	 * @param type a {@link Class}.
	 * @return a {@link Stream} consisting of all interfaces of {@code type}.
	 */
	public static Stream<Class<?>> allInterfacesOf(Class<?> type)
	{
		Stream<Class<?>> interfaces = Arrays.stream(type.getInterfaces());
		return type.getSuperclass() == null ? interfaces : Stream.concat(interfaces, allInterfacesOf(type.getSuperclass()));
	}


	/**
	 * <p>
	 * Return {@code true} IFF the given {@code type} extends from the {@code superType}.
	 * </p>
	 *
	 * @param type a {@link Class} to check.
	 * @param superType a {@link Class} to check against.
	 * @return {@code true} IFF {@code type extends superType}.
	 */
	public static boolean extendsFrom(Class<?> type, Class<?> superType)
	{
		Stream<Class<?>> supers = allSuperTypesOf(type);
		return supers.anyMatch(Predicate.isEqual(superType));
	}


	/**
	 * <p>
	 * Return all the super-types of a given class.
	 * </p>
	 *
	 * @param type a {@link Class} object.
	 * @return a {@link Stream} object.
	 */
	public static Stream<Class<?>> allSuperTypesOf(Class<?> type)
	{
		Stream<Class<?>> supers = Stream.of(type);
		return type.getSuperclass() == null ? supers : Stream.concat(supers, allSuperTypesOf(type.getSuperclass()));
	}
}