NamedParameterMapper.java

/***************************************************************************
   Copyright 2012 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.util;


import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Reflectively examine a class to generate a mapping of parameter names to parameter order using the
{@link javax.inject.Named} annotation. The output of this class is a {@link java.util.Map} with method names as keys,
and lists of parameter names as values. If all the parameter names cannot be extracted, the
{@link net.metanotion.util.NamedParameterMapper#isNamed} method returns false. To determine the name of a parameter it
must have an {@link javax.inject.Named} annotation present whose value is used for the parameter name. The JVM, at
present, does not make the actual parameter name available through any reliable mechanism, however if a class (not an
interface) was compiled with debug information and no attempts at obfuscation have been applied, byte-code disassembly
can possibly recover the names from the source code. If you wish to use that technique see the
{@link net.metanotion.util.ObjectParameterMapper}. Also, if the interface has the annotation {@link javax.inject.Named}
applied to the whole interface, this class uses the non-normative assumption that the interface was supposed to 100%
annotated, and will throw a runtime exception when trying to scan the class if this condition is not met.
	@see net.metanotion.util.MapToListDispatcher
	@see net.metanotion.util.ObjectParameterMapper
*/
public final class NamedParameterMapper {
	private static final Logger logger = LoggerFactory.getLogger(NamedParameterMapper.class);

	/** This method returns a list of the method names declared on {@link java.lang.Object}, excluding "equals",
		"hashCode", and "toString".
		@return A list of the names of the methods declared on {@link java.lang.Object} excluding "equals", "hashCode",
		and "toString".
	*/
	public static Set<String> objectMethods() {
		final HashSet<String> methods = new HashSet<>();
		for(final Method m: Object.class.getMethods()) {
			final String name = m.getName();
			if("equals".equals(name)) { continue; }
			if("hashCode".equals(name)) { continue; }
			if("toString".equals(name)) { continue; }
			methods.add(m.getName());
		}
		return methods;
	}
	private static final Set<String> emptySet = Collections.emptySet();

	private boolean isNamed = false;
	/** Determine if class is "fully named".
		@return true if EVERY parameter of EVERY method is {@link javax.inject.Named}
	*/
	public boolean isNamed() { return isNamed; }

	/** Extract the methods and their parameter names from the class and store them in a map. If the class is not an
		interface, this will by default ignore methods that have the same name as {@link java.lang.Object}'s methods
		except for "equals", "hashCode", and "toString".
		@param klazz The class to look at for names.
		@return Map whose keys are method names, and values are lists of the parameter names. If a parameter is not
			annotated, the value of the list will be the empty string.
		@throws RuntimeException if the entire interface has a {@link javax.inject.Named} annotation and a parameter is missing.
	*/
	public Map<String,Iterable<String>> read(final Class klazz) {
		return this.read(klazz, klazz.isInterface() ? emptySet : objectMethods());
	}

	/** Extract the methods and their parameter names from the class and store them in a map.
		@param klazz The class to look at for names.
		@param ignoreMethods a set of methods to ignore.
		@return Map whose keys are method names, and values are lists of the parameter names. If a parameter is not
			annotated, the value of the list will be the empty string. This will be the map passed in as the parameter.
		@throws RuntimeException if the entire interface has a {@link javax.inject.Named} annotation and a parameter is
			missing.
	*/
	public Map<String,Iterable<String>> read(final Class klazz, final Set<String> ignoreMethods) {
		return this.read(klazz, new HashMap<String,Iterable<String>>(), ignoreMethods);
	}

	/** Extract the methods and their parameter names from the class and store them in a map. If the class is not an
		interface, this will by default ignore methods that have the same name as {@link java.lang.Object}'s methods
		except for "equals", "hashCode", and "toString".
		@param klazz The class to look at for names.
		@param methods A map instance to store the results in. Any keys already in the map will be overwritten if a
			method with the same name exists.
		@return Map whose keys are method names, and values are lists of the parameter names. If a parameter is not
			annotated, the value of the list will be the empty string.
		@throws RuntimeException if the entire interface has a {@link javax.inject.Named} annotation and a parameter is
			missing.
	*/
	public Map<String,Iterable<String>> read(final Class klazz, final Map<String,Iterable<String>> methods) {
		return this.read(klazz, methods, klazz.isInterface() ? emptySet : objectMethods());
	}

	/** Extract the methods and their parameter names from the class and store them in a map.
		@param klazz The class to look at for names.
		@param methods A map instance to store the results in. Any keys already in the map will be overwritten if a
			method with the same name exists.
		@param ignoreMethods a set of methods to ignore.
		@return Map whose keys are method names, and values are lists of the parameter names. If a parameter is not
			annotated, the value of the list will be the empty string. This will be the map passed in as the parameter.
		@throws RuntimeException if the entire interface has a {@link javax.inject.Named} annotation and a parameter is
			missing.
	*/
	public Map<String,Iterable<String>> read(final Class klazz,
			final Map<String,Iterable<String>> methods,
			final Set<String> ignoreMethods) {
		isNamed = true;
		for(final Method m: klazz.getMethods()) {
			final String name = m.getName();
			if(ignoreMethods.contains(name)) { continue; }
			logger.debug(name);
			final ArrayList<String> params = new ArrayList<>();
			methods.put(name, params);
			next: for(final Annotation[] param: m.getParameterAnnotations()) {
				for(final Annotation a: param) {
					if(a instanceof Named) {
						params.add(((Named) a).value());
						continue next;
					}
				}
				logger.debug(" Failed to get names");
				params.add("");
				isNamed = false;
			}
		}
		if((klazz.isAnnotationPresent(Named.class)) && !isNamed) {
			throw new RuntimeException("Class " + klazz + " is not fully named but has @Named on the class.");
		}
		return methods;
	}
}