Commits

Jared Bunting committed 1b5a623

Adding constructor signature enforcement.

Comments (0)

Files changed (10)

pjcommons-base/pom.xml

 
 	<dependencies>
 		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+		</dependency>
+		<dependency>
 			<groupId>junit</groupId>
 			<artifactId>junit</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>pjcommons-test</artifactId>
+			<version>${project.version}</version>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+			    <groupId>org.codehaus.mojo</groupId>
+			    <artifactId>build-helper-maven-plugin</artifactId>
+			    <executions>
+			        <execution>
+			            <id>include-apt-services</id>
+			            <goals>
+			                <goal>add-resource</goal>
+			            </goals>
+			            <phase>prepare-package</phase>
+			            <configuration>
+			                <resources>
+			                    <resource>
+			                        <directory>src/main/apt</directory>
+			                    </resource>
+			                </resources>
+			            </configuration>
+			        </execution>
+			    </executions>
+			</plugin>
+		</plugins>
+	</build>
 </project>

pjcommons-base/src/main/apt/META-INF/services/javax.annotation.processing.Processor

+net.peachjean.commons.base.constructor.ConstructorArgsProcessor

pjcommons-base/src/main/java/net/peachjean/commons/base/constructor/ConstructorArgs.java

+package net.peachjean.commons.base.constructor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author jbunting
+ */
+@SuppressWarnings({"UnusedDeclaration"})
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
+public @interface ConstructorArgs
+{
+	Class[] value() default {};
+}

pjcommons-base/src/main/java/net/peachjean/commons/base/constructor/ConstructorArgsProcessor.java

+package net.peachjean.commons.base.constructor;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.SimpleAnnotationValueVisitor6;
+import javax.lang.model.util.SimpleElementVisitor6;
+import javax.lang.model.util.SimpleTypeVisitor6;
+import javax.tools.Diagnostic;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author jbunting
+ */
+@SupportedAnnotationTypes("*")
+@SupportedSourceVersion(SourceVersion.RELEASE_6)
+public class ConstructorArgsProcessor extends AbstractProcessor
+{
+	private TypeMirror constructorArgsType;
+	private ExecutableElement valueElement;
+
+	@Override
+	public void init(final ProcessingEnvironment processingEnv)
+	{
+		super.init(processingEnv);
+		constructorArgsType = processingEnv.getElementUtils().getTypeElement(ConstructorArgs.class.getName()).asType();
+		valueElement = getValueElement((DeclaredType) constructorArgsType);
+	}
+
+	public boolean process(Set<? extends TypeElement> annotations,
+	                       RoundEnvironment env)
+	{
+		findAndValidate(env.getRootElements());
+		return true;
+	}
+
+	private void findAndValidate(final Collection<? extends Element> elements)
+	{
+		for (TypeElement type : ElementFilter.typesIn(elements))
+		{
+			findAndValidate(type.getEnclosedElements());
+			findAndValidate(type);
+		}
+	}
+
+	private void findAndValidate(final TypeElement type)
+	{
+		for (ConstructorRequirement constructorRequirement : determineRequirements(type))
+		{
+			validateRequirement(type, constructorRequirement);
+		}
+	}
+
+	private Iterable<ConstructorRequirement> determineRequirements(final TypeElement type)
+	{
+		if (type.getKind().isInterface())
+		{
+			return Collections.emptySet();
+		}
+		Set<ConstructorRequirement> requirements = Sets.newHashSet();
+		populateRequirementsSet(requirements, type);
+		return requirements;
+	}
+
+	private void populateRequirementsSet(final Set<ConstructorRequirement> requirements, final TypeElement type)
+	{
+		this.populateRequirementsSet(requirements, type, Sets.<TypeElement>newHashSet());
+	}
+
+	private void populateRequirementsSet(final Set<ConstructorRequirement> requirements, final TypeElement type,
+	                                     Set<TypeElement> previouslyConsidered)
+	{
+		if (hasAlreadyBeenConsidered(type, previouslyConsidered))
+		{
+			return;
+		}
+		populateRequirementsFromAnnotations(requirements, previouslyConsidered, type.getAnnotationMirrors());
+		populateRequirementsFromInterfaces(requirements, previouslyConsidered, type.getInterfaces());
+	}
+
+	private boolean hasAlreadyBeenConsidered(final TypeElement type, final Set<TypeElement> previouslyConsidered)
+	{
+		if (previouslyConsidered.contains(type))
+		{
+			return true;
+		}
+		else
+		{
+			previouslyConsidered.add(type);
+		}
+		return false;
+	}
+
+	private void populateRequirementsFromAnnotations(
+			final Set<ConstructorRequirement> requirements,
+			final Set<TypeElement> previouslyConsidered, final List<? extends AnnotationMirror> annotationMirrors)
+	{
+		for (AnnotationMirror mirror : annotationMirrors)
+		{
+			TypeElement annotationType = mirror.getAnnotationType().asElement().accept(typeConversionVisitor, null);
+			if (isConstructorArg(mirror))
+			{
+				requirements.add(new ConstructorRequirement(mirror));
+			}
+			populateRequirementsSet(requirements, annotationType, previouslyConsidered);
+		}
+	}
+
+	private void populateRequirementsFromInterfaces(
+			final Set<ConstructorRequirement> requirements,
+			final Set<TypeElement> previouslyConsidered, final List<? extends TypeMirror> interfaces)
+	{
+		for (TypeMirror iface : interfaces)
+		{
+			TypeElement interfaceType =
+					processingEnv.getTypeUtils().asElement(iface).accept(typeConversionVisitor, null);
+			populateRequirementsSet(requirements, interfaceType, previouslyConsidered);
+		}
+	}
+
+	private boolean isConstructorArg(final AnnotationMirror mirror)
+	{
+		boolean isType = mirror.getAnnotationType().accept(new SimpleTypeVisitor6<Boolean, Object>() {
+			@Override
+			protected Boolean defaultAction(final TypeMirror e, final Object o)
+			{
+				return false;
+			}
+
+			@Override
+			public Boolean visitDeclared(final DeclaredType t, final Object o)
+			{
+				return true;
+			}
+		}, null);
+		return isType && processingEnv.getTypeUtils().isSameType(constructorArgsType, mirror.getAnnotationType());
+	}
+
+	private void validateRequirement(final TypeElement type, final ConstructorRequirement constructorRequirement)
+	{
+		if (!doesClassMeetRequirement(type, constructorRequirement))
+		{
+			processingEnv.getMessager().printMessage(
+					Diagnostic.Kind.ERROR,
+					"Class " + type + " needs a public Constructor with parameters " + constructorRequirement.getParams());
+		}
+	}
+
+	private boolean doesClassMeetRequirement(final TypeElement type, final ConstructorRequirement constructorRequirement)
+	{
+		for (Element subelement : type.getEnclosedElements())
+		{
+			if (subelement.getKind() == ElementKind.CONSTRUCTOR &&
+			    subelement.getModifiers().contains(Modifier.PUBLIC))
+			{
+				TypeMirror mirror = subelement.asType();
+				if (mirror.accept(requirementVisitor, constructorRequirement))
+				{
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	private class ConstructorRequirement
+	{
+		private final List<TypeMirror> params;
+
+		private ConstructorRequirement(AnnotationMirror annotationMirror)
+		{
+			Preconditions.checkArgument(isConstructorArg(annotationMirror), "Requirement must be build from @" +
+			                                                                ConstructorArgs.class.getName());
+			Map<? extends ExecutableElement,? extends AnnotationValue> elementValuesWithDefaults =
+					processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
+			this.params = Collections.unmodifiableList(elementValuesWithDefaults.get(valueElement)
+			                                                           .accept(valueVisitor, null));
+		}
+
+
+		public List<TypeMirror> getParams()
+		{
+			return params;
+		}
+	}
+
+	private static ExecutableElement getValueElement(final DeclaredType type)
+	{
+		for(Element enclosed : type.asElement().getEnclosedElements())
+		{
+			if( enclosed instanceof ExecutableElement && enclosed.getSimpleName().contentEquals("value")) {
+				return (ExecutableElement) enclosed;
+			}
+		}
+		throw new IllegalStateException("Could not locate value element on " + type);
+	}
+
+	private static final SimpleAnnotationValueVisitor6<TypeMirror,Object> elementVisitor =
+			new SimpleAnnotationValueVisitor6<TypeMirror, Object>()
+			{
+				@Override
+				public TypeMirror visitType(final TypeMirror t, final Object o)
+				{
+					return t;
+				}
+			};
+
+	private static final SimpleAnnotationValueVisitor6<List<TypeMirror>, Object> valueVisitor =
+			new SimpleAnnotationValueVisitor6<List<TypeMirror>, Object>()
+			{
+
+
+				@Override
+				public List<TypeMirror> visitArray(final List<? extends AnnotationValue> vals, final Object o)
+				{
+					List<TypeMirror> returnValues = new ArrayList<TypeMirror>(vals.size());
+					for (AnnotationValue val : vals)
+					{
+						returnValues.add(val.accept(elementVisitor, null));
+					}
+					return returnValues;
+				}
+			};
+
+	private static final SimpleElementVisitor6<TypeElement, Object> typeConversionVisitor =
+			new SimpleElementVisitor6<TypeElement, Object>()
+			{
+				@Override
+				public TypeElement visitType(final TypeElement e, final Object o)
+				{
+					return e;
+				}
+			};
+
+	private static final SimpleTypeVisitor6<Boolean, ConstructorRequirement> requirementVisitor =
+			new SimpleTypeVisitor6<Boolean, ConstructorRequirement>()
+			{
+				@Override
+				public Boolean visitExecutable(final ExecutableType t, final ConstructorRequirement requirement)
+				{
+					List<? extends TypeMirror> parameterTypes = t.getParameterTypes();
+					return parameterTypes.equals(requirement.getParams());
+				}
+			};
+}

pjcommons-base/src/main/java/net/peachjean/commons/base/constructor/NoArgConstructor.java

+package net.peachjean.commons.base.constructor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author jbunting
+ */
+@ConstructorArgs
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Target(ElementType.TYPE)
+public @interface NoArgConstructor
+{
+}

pjcommons-base/src/test/java/net/peachjean/commons/base/constructor/ConstructorArgsProcessorTest.java

+package net.peachjean.commons.base.constructor;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+import java.io.IOException;
+
+import net.peachjean.commons.test.junit.CumulativeAssertionRule;
+import net.peachjean.commons.test.junit.TmpDir;
+import net.peachjean.commons.test.junit.compiler.CompilerHarness;
+import net.peachjean.commons.test.junit.compiler.CompilerResults;
+import net.peachjean.commons.test.junit.compiler.JavaSourceFromText;
+
+/**
+ * @author jbunting
+ */
+public class ConstructorArgsProcessorTest
+{
+
+	public static final JavaSourceFromText NO_ARG_INTERFACE =
+			new JavaSourceFromText("com.example.NoArgInterface", ""
+			                                                       + "package com.example;"
+			                                                       + "import net.peachjean.commons.base.constructor.NoArgConstructor;"
+			                                                       + "@NoArgConstructor "
+			                                                       + "public interface NoArgInterface { }");
+
+	public static final JavaSourceFromText ONE_ARG_INTERFACE =
+			new JavaSourceFromText("com.example.OneArgInterface", ""
+			                                                        + "package com.example;"
+			                                                        + "import net.peachjean.commons.base.constructor.ConstructorArgs;"
+			                                                        + "@ConstructorArgs(String.class) "
+			                                                        + "public interface OneArgInterface { }");
+	@Rule
+	public TmpDir tmpDir = new TmpDir();
+
+	@Rule
+	public CumulativeAssertionRule accumulator = new CumulativeAssertionRule();
+
+	@Test
+	public void testCompliantNoArg() throws IOException
+	{
+		runCompileTest(0, NO_ARG_INTERFACE,
+		               new JavaSourceFromText("com.example.GoodNoArgImpl", ""
+										 + "package com.example;"
+										 + "public class GoodNoArgImpl implements NoArgInterface {}"));
+	}
+
+	@Test
+	public void testNonCompliantNoArg() throws IOException
+	{
+		runCompileTest(1, NO_ARG_INTERFACE,
+		               new JavaSourceFromText("com.example.BadNoArgImpl", ""
+										 + "package com.example;"
+										 + "public class BadNoArgImpl implements NoArgInterface {"
+										 + "  public BadNoArgImpl(String name) {}"
+										 + "}"));
+	}
+
+	@Test
+	public void testCompliantOneArg() throws IOException
+	{
+		runCompileTest(0, ONE_ARG_INTERFACE,
+		               new JavaSourceFromText("com.example.GoodOneArgImpl", ""
+										  + "package com.example;"
+										  + "public class GoodOneArgImpl implements OneArgInterface {"
+										  + "  public GoodOneArgImpl(String name) {}"
+										  + "}"));
+	}
+
+	@Test
+	public void testNonCompliantOneArg() throws IOException
+	{
+		runCompileTest(2, ONE_ARG_INTERFACE,
+		               new JavaSourceFromText("com.example.BadOneArgImpl", ""
+										  + "package com.example;"
+										  + "public class BadOneArgImpl implements OneArgInterface { }"),
+		               new JavaSourceFromText("com.example.OtherBadOneArgImpl", ""
+										  + "package com.example;"
+										  + "public class OtherBadOneArgImpl implements OneArgInterface {"
+										  + "  public OtherBadOneArgImpl(String one, String two) {}"
+										  + "}"));
+	}
+
+	private void runCompileTest(final int expectedCompilerErrors, JavaFileObject ... sourceFiles) throws IOException
+	{
+		CompilerResults results = new CompilerHarness(tmpDir.getDir(), sourceFiles)
+				.addProcessor(new ConstructorArgsProcessor()).invoke();
+
+		for(Diagnostic<? extends JavaFileObject> diagnostic: results.getDiagnostics())
+		{
+			System.out.println(diagnostic);
+		}
+		accumulator.assertEquals("", results.getCompilerOutput());
+		accumulator.assertEquals("Unexpected number of compiler errors.", expectedCompilerErrors, results
+				.getDiagnostics().size());
+	}
+
+
+}

pjcommons-test/src/main/java/net/peachjean/commons/test/junit/compiler/CompilerHarness.java

+package net.peachjean.commons.test.junit.compiler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.processing.Processor;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import com.google.common.collect.Lists;
+
+public class CompilerHarness
+{
+	private final File rootOutput;
+	private final JavaFileObject[] sourceFiles;
+
+	private List<Processor> processors = Lists.newArrayList();
+
+	public CompilerHarness(final File rootOutput, final JavaFileObject... sourceFiles)
+	{
+		this.rootOutput = rootOutput;
+		this.sourceFiles = sourceFiles;
+	}
+
+	public CompilerHarness addProcessor(Processor processor)
+	{
+		this.processors.add(processor);
+		return this;
+	}
+
+	public CompilerResults invoke() throws IOException
+	{
+		final File classes = new File(rootOutput, "classes");
+		classes.mkdirs();
+		final File sources = new File(rootOutput, "sources");
+		sources.mkdirs();
+
+		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+		final Iterable<? extends JavaFileObject> compilationUnits = Lists.newArrayList(sourceFiles);
+
+		StringWriter compilerOutput = new StringWriter();
+		DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
+		StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
+		fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(classes));
+		fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, Arrays.asList(sources));
+		JavaCompiler.CompilationTask compilerTask =
+				compiler.getTask(compilerOutput, fileManager, diagnostics, null, null, compilationUnits);
+		compilerTask.setProcessors(processors);
+		boolean success = compilerTask.call();
+
+		compilerOutput.close();
+		fileManager.close();
+
+		return new CompilerResults(success, compilerOutput.toString(), diagnostics.getDiagnostics());
+	}
+
+}

pjcommons-test/src/main/java/net/peachjean/commons/test/junit/compiler/CompilerResults.java

+package net.peachjean.commons.test.junit.compiler;
+
+import java.util.List;
+
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+public class CompilerResults
+{
+	private final boolean successful;
+	private final String compilerOutput;
+	private final List<Diagnostic<? extends JavaFileObject>> diagnostics;
+
+	public CompilerResults(final boolean successful, final String compilerOutput,
+	                       final List<Diagnostic<? extends JavaFileObject>> diagnostics)
+	{
+		this.successful = successful;
+		this.compilerOutput = compilerOutput;
+		this.diagnostics = diagnostics;
+	}
+
+	public boolean isSuccessful()
+	{
+		return successful;
+	}
+
+	public String getCompilerOutput()
+	{
+		return compilerOutput;
+	}
+
+	public List<Diagnostic<? extends JavaFileObject>> getDiagnostics()
+	{
+		return diagnostics;
+	}
+}

pjcommons-test/src/main/java/net/peachjean/commons/test/junit/compiler/JavaSourceFromText.java

+package net.peachjean.commons.test.junit.compiler;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+
+import javax.tools.SimpleJavaFileObject;
+
+import com.google.common.io.Files;
+
+public class JavaSourceFromText extends SimpleJavaFileObject
+{
+	/**
+	 * The source code of this "file".
+	 */
+	final String code;
+
+	/**
+	 * Constructs a new JavaSourceFromString.
+	 *
+	 * @param name the name of the compilation unit represented by this file object
+	 * @param code the source code for the compilation unit represented by this file object
+	 */
+	public JavaSourceFromText(String name, String code)
+	{
+		super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
+		      Kind.SOURCE);
+		this.code = code;
+	}
+
+	public JavaSourceFromText(String name, File code) throws IOException
+	{
+		this(name, Files.toString(code, Charset.defaultCharset()));
+	}
+
+	@Override
+	public CharSequence getCharContent(boolean ignoreEncodingErrors)
+	{
+		return code;
+	}
+}
 						<autoVersionSubmodules>true</autoVersionSubmodules>
 					</configuration>
 				</plugin>
+				<plugin>
+				    <groupId>org.codehaus.mojo</groupId>
+				    <artifactId>build-helper-maven-plugin</artifactId>
+					<version>1.7</version>
+				</plugin>
 			</plugins>
 		</pluginManagement>
 	</build>