248 lines
8.6 KiB
Java
248 lines
8.6 KiB
Java
package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor;
|
|
|
|
import org.jetbrains.annotations.ApiStatus;
|
|
|
|
import javax.lang.model.element.*;
|
|
import javax.lang.model.type.TypeKind;
|
|
import javax.lang.model.type.TypeMirror;
|
|
import javax.lang.model.util.ElementFilter;
|
|
import javax.lang.model.util.Types;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* How a [Value] can be constructed. Either a constructor, factory method, or builder.
|
|
*/
|
|
public sealed interface ConstructionSource {
|
|
/**
|
|
* The target [Value] class to construct.
|
|
*/
|
|
TypeElement getTargetClass();
|
|
/**
|
|
* The executable element to construct the [Value]. This may be a constructor, factory method, or builder.
|
|
*/
|
|
ExecutableElement getConstructionElement();
|
|
/**
|
|
* If this source is a constructor (either of the value or the builder).
|
|
*/
|
|
boolean isConstructor();
|
|
/**
|
|
* If this source is a builder.
|
|
*/
|
|
boolean isBuilder();
|
|
|
|
final class Constructor implements ConstructionSource {
|
|
private final ExecutableElement constructor;
|
|
private TypeElement targetClass;
|
|
|
|
public Constructor(ExecutableElement constructor) {
|
|
this.constructor = Objects.requireNonNull(constructor);
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getTargetClass() {
|
|
return targetClass != null ? targetClass : (targetClass = (TypeElement) constructor.getEnclosingElement());
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getConstructionElement() {
|
|
return constructor;
|
|
}
|
|
|
|
@Override
|
|
public boolean isConstructor() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean isBuilder() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
final class Factory implements ConstructionSource {
|
|
private final Types types;
|
|
private final ExecutableElement method;
|
|
private TypeElement targetClass;
|
|
|
|
public Factory(Types types, ExecutableElement method) {
|
|
this.types = types;
|
|
this.method = method;
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getTargetClass() {
|
|
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(method.getReturnType()));
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getConstructionElement() {
|
|
return method;
|
|
}
|
|
|
|
@Override
|
|
public boolean isConstructor() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean isBuilder() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
sealed abstract class Builder implements ConstructionSource {
|
|
public abstract TypeElement getBuilderClass();
|
|
public abstract ExecutableElement getBuildMethod();
|
|
|
|
@Override
|
|
public boolean isBuilder() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
final class BuilderConstructor extends Builder {
|
|
private final Types types;
|
|
private final ExecutableElement constructor;
|
|
private TypeElement targetClass;
|
|
private TypeElement builderClass;
|
|
private ExecutableElement buildMethod;
|
|
|
|
public BuilderConstructor(Types types, ExecutableElement constructor) {
|
|
this.types = types;
|
|
this.constructor = constructor;
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getTargetClass() {
|
|
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType()));
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getConstructionElement() {
|
|
return constructor;
|
|
}
|
|
|
|
@Override
|
|
public boolean isConstructor() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getBuilderClass() {
|
|
return builderClass != null ? builderClass : (builderClass = (TypeElement) constructor.getEnclosingElement());
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getBuildMethod() {
|
|
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) constructor.getEnclosingElement()));
|
|
}
|
|
}
|
|
|
|
final class BuilderFactory extends Builder {
|
|
private final Types types;
|
|
private final ExecutableElement method;
|
|
private TypeElement targetClass;
|
|
private TypeElement builderClass;
|
|
private ExecutableElement buildMethod;
|
|
|
|
public BuilderFactory(Types types, ExecutableElement method) {
|
|
this.types = types;
|
|
this.method = method;
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getTargetClass() {
|
|
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType()));
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getConstructionElement() {
|
|
return method;
|
|
}
|
|
|
|
@Override
|
|
public boolean isConstructor() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public TypeElement getBuilderClass() {
|
|
return builderClass != null ? builderClass : (builderClass = (TypeElement) types.asElement(method.getReturnType()));
|
|
}
|
|
|
|
@Override
|
|
public ExecutableElement getBuildMethod() {
|
|
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) types.asElement(method.getReturnType())));
|
|
}
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
static ExecutableElement findBuildMethod(TypeElement builderClass) {
|
|
// Ok, maybe there is just one possible builder method.
|
|
{
|
|
ExecutableElement candidate = null;
|
|
boolean foundMultipleCandidates = false;
|
|
boolean isCandidateReasonableBuilderMethodName = false;
|
|
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
|
|
if (isPossibleBuilderMethod(method, builderClass)) {
|
|
if (candidate == null) {
|
|
candidate = method;
|
|
} else {
|
|
// Multiple possible methods, keep the one with a reasonable builder name if possible.
|
|
foundMultipleCandidates = true;
|
|
isCandidateReasonableBuilderMethodName = isCandidateReasonableBuilderMethodName || isReasonableBuilderMethodName(candidate);
|
|
if (isCandidateReasonableBuilderMethodName) {
|
|
if (isReasonableBuilderMethodName(method)) {
|
|
// both reasonable, too ambiguous.
|
|
candidate = null;
|
|
break;
|
|
}
|
|
} else {
|
|
candidate = method;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (candidate != null && (!foundMultipleCandidates || isCandidateReasonableBuilderMethodName)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
// Last try, check to see if the immediate parent class makes sense.
|
|
{
|
|
Element candidate = builderClass.getEnclosingElement();
|
|
if (candidate.getKind() == ElementKind.CLASS) {
|
|
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
|
|
if (method.getReturnType().equals(candidate.asType()) && method.getParameters().isEmpty()) {
|
|
return method;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Well, I give up.
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A possible builder method has no parameters and a return type of the class we want to
|
|
* construct. Therefore, the return type is not going to be void, primitive, or a platform
|
|
* class.
|
|
*/
|
|
@ApiStatus.Internal
|
|
static boolean isPossibleBuilderMethod(ExecutableElement method, TypeElement builderClass) {
|
|
if (!method.getParameters().isEmpty()) return false;
|
|
TypeMirror returnType = method.getReturnType();
|
|
if (returnType.getKind() == TypeKind.VOID) return false;
|
|
if (returnType.getKind().isPrimitive()) return false;
|
|
if (returnType.equals(builderClass.asType())) return false;
|
|
String returnTypeName = returnType.toString();
|
|
return !(returnTypeName.startsWith("java.") || returnTypeName.startsWith("javax.") || returnTypeName.startsWith("android."));
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
static boolean isReasonableBuilderMethodName(ExecutableElement method) {
|
|
String methodName = method.getSimpleName().toString().toLowerCase(Locale.ROOT);
|
|
return methodName.startsWith("build") || methodName.startsWith("create");
|
|
}
|
|
}
|