/*
* Copyright 2016 ArcBees Inc.
*
* 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.gwtplatform.mvp.processors.proxy;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Provider;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleAnnotationValueVisitor7;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.gwt.inject.client.AsyncProvider;
import com.gwtplatform.common.client.IndirectProvider;
import com.gwtplatform.mvp.client.annotations.CustomProvider;
import com.gwtplatform.mvp.client.annotations.ProxyCodeSplit;
import com.gwtplatform.mvp.client.annotations.ProxyCodeSplitBundle;
import com.gwtplatform.mvp.client.annotations.ProxyEvent;
import com.gwtplatform.mvp.client.annotations.ProxyStandard;
import com.gwtplatform.mvp.client.presenter.slots.NestedSlot;
import com.gwtplatform.mvp.processors.bundle.BundleDetails;
import com.gwtplatform.processors.tools.domain.HasImports;
import com.gwtplatform.processors.tools.domain.Type;
import com.gwtplatform.processors.tools.exceptions.UnableToProcessException;
import com.gwtplatform.processors.tools.logger.LogBuilder;
import com.gwtplatform.processors.tools.logger.Logger;
import com.gwtplatform.processors.tools.utils.Utils;
import static java.util.Arrays.asList;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.lang.model.util.ElementFilter.constructorsIn;
import static javax.lang.model.util.ElementFilter.fieldsIn;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static com.google.auto.common.AnnotationMirrors.getAnnotationValue;
import static com.google.auto.common.MoreElements.asType;
import static com.google.auto.common.MoreElements.getAnnotationMirror;
import static com.google.auto.common.MoreElements.hasModifiers;
import static com.google.auto.common.MoreElements.isAnnotationPresent;
import static com.google.auto.common.MoreTypes.asDeclared;
import static com.google.auto.common.MoreTypes.asElement;
import static com.google.auto.common.MoreTypes.asTypeElement;
import static com.google.auto.common.MoreTypes.isTypeOf;
import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.collect.FluentIterable.from;
public abstract class AbstractProxyDetails implements ProxyDetails {
private static final List<Class<? extends Annotation>> SUPPORTED_ANNOTATIONS =
asList(ProxyStandard.class, ProxyCodeSplit.class, ProxyCodeSplitBundle.class);
protected final TypeElement element;
protected final Logger logger;
protected final Utils utils;
protected final TypeMirror proxyMirror;
private Type type;
private TypeMirror presenterMirror;
private Set<String> contentSlots;
private Optional<BundleDetails> bundleDetails;
private List<ProxyEventMethod> proxyEventMethods;
private Optional<Type> customProvider;
protected AbstractProxyDetails(
Logger logger,
Utils utils,
TypeElement element,
TypeMirror proxyMirror) {
this.logger = logger;
this.utils = utils;
this.element = element;
this.proxyMirror = proxyMirror;
warnIfMultipleAnnotations();
}
private void warnIfMultipleAnnotations() {
int count = 0;
for (Class<? extends Annotation> classy : SUPPORTED_ANNOTATIONS) {
if (isAnnotationPresent(element, classy)) {
++count;
}
}
if (count > 1) {
logger.warning().context(element)
.log("Multiple proxy annotation detected. Review and make sure only one is present.");
}
}
@Override
public Type getType() {
if (type == null) {
String packageName = new Type(asType(element)).getPackageName();
String presenterName = getPresenterType().getSimpleName();
String proxyName = element.getSimpleName().toString();
type = new Type(packageName, String.format("%s$$%s", presenterName, proxyName));
}
return type;
}
@Override
public Type getProxyType() {
return new Type(element.asType());
}
@Override
public Type getPresenterType() {
return new Type(getPresenterMirror());
}
private TypeMirror getPresenterMirror() {
if (presenterMirror == null) {
List<? extends TypeMirror> proxyGenericTypes = asDeclared(proxyMirror).getTypeArguments();
if (proxyGenericTypes.isEmpty()) {
logger.error().context(element).log("This proxy must specify its presenter.");
throw new UnableToProcessException();
}
presenterMirror = proxyGenericTypes.get(0);
}
return presenterMirror;
}
@Override
public Set<String> getContentSlots() {
if (contentSlots == null) {
extractContentSlots();
}
return contentSlots;
}
private void extractContentSlots() {
TypeElement presenterElement = asTypeElement(getPresenterMirror());
List<Element> presenterMembers = utils.getAllMembers(presenterElement);
List<VariableElement> presenterFields = fieldsIn(presenterMembers);
contentSlots = new HashSet<>();
for (VariableElement field : presenterFields) {
if (isValidContentSlot(field)) {
contentSlots.add(field.getSimpleName().toString());
}
}
}
private boolean isValidContentSlot(VariableElement field) {
if (isTypeOf(NestedSlot.class, field.asType())) {
if (!hasModifiers(PRIVATE, STATIC).apply(field)) {
return true;
}
logger.mandatoryWarning()
.context(field)
.log("Content slots cannot be static or private.");
}
return false;
}
@Override
public boolean isCodeSplit() {
return element.getAnnotation(ProxyCodeSplit.class) != null;
}
@Override
public List<ProxyEventMethod> getProxyEventMethods() {
if (proxyEventMethods == null) {
TypeElement presenterElement = asTypeElement(getPresenterMirror());
List<ExecutableElement> methods = methodsIn(utils.getElements().getAllMembers(presenterElement));
proxyEventMethods = FluentIterable.from(methods)
.filter(method -> method.getAnnotation(ProxyEvent.class) != null)
.transform(element1 -> new ProxyEventMethod(logger, utils, element1))
.toList();
ensureNoHandlerMethodClashes();
}
return proxyEventMethods;
}
private void ensureNoHandlerMethodClashes() {
List<String> handlerMethodNames = FluentIterable.from(proxyEventMethods)
.transform(ProxyEventMethod::getHandlerMethodName)
.toList();
Set<String> uniqueHandlerMethodNames = new HashSet<>(handlerMethodNames);
if (handlerMethodNames.size() != uniqueHandlerMethodNames.size()) {
// TODO: Probably not worth it, but with some gymnastic we could print exactly which methods.
logger.error()
.context(asElement(getPresenterMirror()))
.log("Presenter contains multiple @ProxyEvents with handlers that have clashing method names.");
throw new UnableToProcessException();
}
}
@Override
public BundleDetails getBundleDetails() {
if (bundleDetails == null) {
extractBundleDetails();
}
return bundleDetails.orNull();
}
private void extractBundleDetails() {
Optional<AnnotationMirror> annotation = getAnnotationMirror(element, ProxyCodeSplitBundle.class);
bundleDetails = absent();
if (annotation.isPresent()) {
bundleDetails = of(new BundleDetails(logger, utils, getPresenterType(), element, annotation.get()));
}
}
@Override
public Type getCustomProvider() {
if (customProvider == null) {
customProvider = extractCustomProvider();
}
return customProvider.orNull();
}
private Optional<Type> extractCustomProvider() {
Optional<AnnotationMirror> annotationMirror = getAnnotationMirror(element, CustomProvider.class);
if (annotationMirror.isPresent()) {
AnnotationValue value = getAnnotationValue(annotationMirror.get(), "value");
DeclaredType valueType = value.accept(new SimpleAnnotationValueVisitor7<DeclaredType, Void>() {
@Override
public DeclaredType visitType(TypeMirror typeMirror, Void ignored) {
return asDeclared(typeMirror);
}
}, null);
LogBuilder errorBuilder = logger.error().context(element, annotationMirror.get(), value);
validateCustomProviderType(errorBuilder, valueType);
validateCustomProviderConstructor(errorBuilder, valueType);
return of(new Type(valueType));
}
return absent();
}
private void validateCustomProviderType(LogBuilder errorBuilder, DeclaredType type) {
TypeElement customProviderElement = asTypeElement(type);
TypeMirror expectedSuperType = utils.createWithTypeArguments(IndirectProvider.class, getPresenterMirror());
if (customProviderElement.getKind() != CLASS
|| !hasModifiers(PUBLIC).apply(customProviderElement)
|| hasModifiers(ABSTRACT).apply(customProviderElement)
|| !utils.getTypes().isSubtype(type, expectedSuperType)) {
errorBuilder.log("Element passed to @CustomProvider must be a public, non-abstract class "
+ "and implement `%s`.", new Type(expectedSuperType).getParameterizedName());
throw new UnableToProcessException();
}
}
private void validateCustomProviderConstructor(LogBuilder errorBuilder, DeclaredType type) {
TypeMirror expectedParameterMirror = utils.createWithTypeArguments(
getBundleDetails() != null || isCodeSplit() ? AsyncProvider.class : Provider.class,
getPresenterMirror());
List<ExecutableElement> constructors = constructorsIn(asTypeElement(type).getEnclosedElements());
for (ExecutableElement constructor : constructors) {
List<? extends VariableElement> parameters = constructor.getParameters();
if (hasModifiers(PUBLIC).apply(constructor)
&& parameters.size() == 1
&& utils.getTypes().isAssignable(expectedParameterMirror, parameters.get(0).asType())) {
return;
}
}
errorBuilder.log("Class passed to @CustomProvider must have a public constructor with a single parameter of "
+ "type `%s`.", new Type(expectedParameterMirror).getParameterizedName());
throw new UnableToProcessException();
}
@Override
public Collection<String> getImports() {
FluentIterable<String> imports =
from(getProxyEventMethods())
.transformAndConcat(HasImports.EXTRACT_IMPORTS_FUNCTION)
.append(getProxyType().getImports())
.append(getPresenterType().getImports());
if (getBundleDetails() != null) {
imports = imports.append(getBundleDetails().getImports());
}
if (getCustomProvider() != null) {
imports = imports.append(getCustomProvider().getImports());
}
return imports.toList();
}
}