/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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 com.asakusafw.compiler.operator;
import java.lang.annotation.Annotation;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
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.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import com.asakusafw.compiler.common.Precondition;
import com.asakusafw.utils.collections.Maps;
import com.asakusafw.vocabulary.operator.OperatorHelper;
/**
* Collects operator classes.
* @since 0.1.0
* @version 0.7.0
*/
public class OperatorClassCollector {
private final OperatorCompilingEnvironment environment;
private final RoundEnvironment round;
private final List<TargetMethod> targetMethods;
private boolean sawError;
/**
* Creates a new instance.
* @param environment the current environment
* @param round the round environment
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public OperatorClassCollector(OperatorCompilingEnvironment environment, RoundEnvironment round) {
Precondition.checkMustNotBeNull(environment, "environment"); //$NON-NLS-1$
Precondition.checkMustNotBeNull(round, "round"); //$NON-NLS-1$
this.environment = environment;
this.round = round;
this.targetMethods = new ArrayList<>();
}
/**
* Adds an operator processor for detecting operator methods.
* @param processor the operator processor
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public void add(OperatorProcessor processor) {
Precondition.checkMustNotBeNull(processor, "processor"); //$NON-NLS-1$
Class<? extends Annotation> target = processor.getTargetAnnotationType();
assert target != null;
TypeElement annotation = environment.getElementUtils().getTypeElement(target.getCanonicalName());
assert annotation != null;
TypeMirror annotationType = annotation.asType();
Set<? extends Element> elements = round.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
ExecutableElement method = toOperatorMethodElement(element);
if (method == null) {
continue;
}
AnnotationMirror annotationMirror = findAnnotation(annotationType, method);
if (annotationMirror == null) {
raiseInvalid(element,
Messages.getString("OperatorClassCollector.errorMissingAnnotation")); //$NON-NLS-1$
continue;
}
registerMethod(annotationMirror, processor, method);
}
}
private AnnotationMirror findAnnotation(TypeMirror annotationType, Element element) {
for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
if (environment.getTypeUtils().isSameType(annotationType, annotation.getAnnotationType())) {
return annotation;
}
}
return null;
}
private void registerMethod(
AnnotationMirror annotation,
OperatorProcessor processor,
ExecutableElement method) {
assert annotation != null;
assert processor != null;
assert method != null;
targetMethods.add(new TargetMethod(annotation, method, processor));
}
private ExecutableElement toOperatorMethodElement(Element element) {
assert element != null;
if (element.getKind() != ElementKind.METHOD) {
raiseInvalid(element, Messages.getString("OperatorClassCollector.errorNotMethod")); //$NON-NLS-1$
return null;
}
ExecutableElement method = (ExecutableElement) element;
validateMethodModifiers(method);
return method;
}
private void validateMethodModifiers(ExecutableElement method) {
assert method != null;
if (method.getModifiers().contains(Modifier.PUBLIC) == false) {
raiseInvalid(method, Messages.getString("OperatorClassCollector.errorNotPublicMethod")); //$NON-NLS-1$
}
if (method.getModifiers().contains(Modifier.STATIC)) {
raiseInvalid(method, Messages.getString("OperatorClassCollector.errorStaticMethod")); //$NON-NLS-1$
}
if (method.getThrownTypes().isEmpty() == false) {
raiseInvalid(method, Messages.getString("OperatorClassCollector.errorThrowsMethod")); //$NON-NLS-1$
}
}
private void raiseInvalid(Element member, String message) {
assert member != null;
assert message != null;
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
MessageFormat.format(
message,
member.getSimpleName()),
member);
sawError = true;
}
/**
* Analyzes and returns the previously added operator classes.
* @return the analyzed operator classes
* @throws OperatorCompilerException if exception was occurred while analyzing operator classes
*/
public List<OperatorClass> collect() {
if (sawError) {
throw new OperatorCompilerException(null,
Messages.getString("OperatorClassCollector.errorFailedToAnalyzeMethod")); //$NON-NLS-1$
}
Map<TypeElement, List<TargetMethod>> mapping = new HashMap<>();
for (TargetMethod target : targetMethods) {
Maps.addToList(mapping, target.type, target);
}
List<OperatorClass> results = new ArrayList<>();
for (Map.Entry<TypeElement, List<TargetMethod>> entry : mapping.entrySet()) {
OperatorClass klass = toOperatorClass(entry.getKey(), entry.getValue());
results.add(klass);
}
if (sawError) {
throw new OperatorCompilerException(null,
Messages.getString("OperatorClassCollector.errorFailedToAnalyzeClass")); //$NON-NLS-1$
}
return results;
}
private OperatorClass toOperatorClass(
TypeElement type,
List<TargetMethod> targets) {
assert type != null;
assert targets != null;
validateClassModifiers(type);
validateConstructorWithNoParameters(type);
validateMemberNames(type);
validateCoverage(type, targets);
OperatorClass result = new OperatorClass(type);
for (TargetMethod target : targets) {
result.add(target.annotation, target.method, target.processor);
}
return result;
}
private void validateClassModifiers(TypeElement type) {
assert type != null;
if (type.getKind() != ElementKind.CLASS) {
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorNotClass")); //$NON-NLS-1$
}
if (type.getEnclosingElement().getKind() != ElementKind.PACKAGE) {
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorEnclosedClass")); //$NON-NLS-1$
}
if (type.getTypeParameters().isEmpty() == false) {
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorTypeParameterClass")); //$NON-NLS-1$
}
if (type.getModifiers().contains(Modifier.PUBLIC) == false) {
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorNotPublicClass")); //$NON-NLS-1$
}
if (type.getModifiers().contains(Modifier.ABSTRACT) == false) {
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorNotAbstractClass")); //$NON-NLS-1$
}
}
private void validateConstructorWithNoParameters(TypeElement type) {
assert type != null;
List<ExecutableElement> ctors = ElementFilter.constructorsIn(type.getEnclosedElements());
if (ctors.isEmpty()) {
return;
}
for (ExecutableElement ctor : ctors) {
if (ctor.getParameters().isEmpty()
&& ctor.getTypeParameters().isEmpty()
&& ctor.getThrownTypes().isEmpty()) {
return;
}
}
raiseInvalidClass(type,
Messages.getString("OperatorClassCollector.errorMissingDefaultConstructor")); //$NON-NLS-1$
}
private void validateMemberNames(TypeElement type) {
Map<String, Element> saw = new HashMap<>();
for (Element member : type.getEnclosedElements()) {
ElementKind kind = member.getKind();
if (kind != ElementKind.METHOD
&& kind.isClass() == false
&& kind.isInterface() == false) {
continue;
}
if (member.getModifiers().contains(Modifier.PUBLIC) == false) {
continue;
}
String id = member.getSimpleName().toString().toUpperCase();
if (saw.containsKey(id)) {
raiseInvalid(member,
Messages.getString("OperatorClassCollector.errorConflictMethodName")); //$NON-NLS-1$
} else {
saw.put(id, member);
}
}
}
private void validateCoverage(TypeElement type, List<TargetMethod> targets) {
assert type != null;
assert targets != null;
Set<ExecutableElement> methods = new HashSet<>();
methods.addAll(ElementFilter.methodsIn(type.getEnclosedElements()));
Set<ExecutableElement> saw = new HashSet<>();
for (TargetMethod target : targets) {
ExecutableElement method = target.method;
if (saw.contains(method)) {
raiseInvalid(method,
Messages.getString("OperatorClassCollector.errorConflictOperatorAnnotation")); //$NON-NLS-1$
} else {
saw.add(method);
boolean removed = methods.remove(method);
assert removed : method;
}
}
for (ExecutableElement method : methods) {
boolean helper = isOperatorHelper(method);
boolean open = method.getModifiers().contains(Modifier.PUBLIC);
if (helper && open == false) {
raiseInvalid(method,
Messages.getString("OperatorClassCollector.errorNotPublicHelperMethod")); //$NON-NLS-1$
} else if (helper == false && open) {
raiseInvalid(method,
Messages.getString("OperatorClassCollector.errorPublicOtherMethod")); //$NON-NLS-1$
}
}
}
private boolean isOperatorHelper(ExecutableElement method) {
assert method != null;
for (AnnotationMirror mirror : method.getAnnotationMirrors()) {
DeclaredType annotationType = mirror.getAnnotationType();
Element element = annotationType.asElement();
if (element != null && element.getAnnotation(OperatorHelper.class) != null) {
return true;
}
}
return false;
}
private void raiseInvalidClass(TypeElement element, String message) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
MessageFormat.format(
message,
element.getQualifiedName()),
element);
sawError = true;
}
private static class TargetMethod {
final AnnotationMirror annotation;
final TypeElement type;
final ExecutableElement method;
final OperatorProcessor processor;
TargetMethod(
AnnotationMirror annotation,
ExecutableElement method,
OperatorProcessor processor) {
assert annotation != null;
assert method != null;
assert processor != null;
this.annotation = annotation;
this.type = (TypeElement) method.getEnclosingElement();
this.method = method;
this.processor = processor;
}
}
}