org.enginehub.piston.gen.CommandRegistrationGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of FastAsyncWorldEdit-Libs-Ap Show documentation
Show all versions of FastAsyncWorldEdit-Libs-Ap Show documentation
Blazingly fast Minecraft world manipulation for artists, builders and everyone else.
/*
* Piston, a flexible command management system.
* Copyright (C) EngineHub
* Copyright (C) Piston contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package org.enginehub.piston.gen;
import com.google.common.collect.ImmutableList;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import org.enginehub.piston.CommandManager;
import org.enginehub.piston.CommandParameters;
import org.enginehub.piston.gen.util.CodeBlockUtil;
import org.enginehub.piston.gen.value.CommandInfo;
import org.enginehub.piston.gen.value.CommandParamInfo;
import org.enginehub.piston.gen.value.ExtractSpec;
import org.enginehub.piston.gen.value.RegistrationInfo;
import org.enginehub.piston.gen.value.RequiredVariable;
import org.enginehub.piston.gen.value.ReservedNames;
import org.enginehub.piston.inject.Key;
import org.enginehub.piston.internal.RegistrationUtil;
import org.enginehub.piston.part.CommandParts;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Streams.concat;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;
import static org.enginehub.piston.gen.util.CodeBlockUtil.listForGen;
import static org.enginehub.piston.gen.util.CodeBlockUtil.stringListForGen;
import static org.enginehub.piston.gen.util.CodeBlockUtil.textCompOf;
/**
* Class that handles the generation of command registration classes.
*
*
* These classes are named {@code [CommandContainer class name] + "Registration"}.
* Generated methods will be used to convert the annotation-based configuration into a runtime
* configuration, allowing for type-safe and efficient implementation while maintaining the ease
* of annotation-based configuration.
*
*/
class CommandRegistrationGenerator {
private static final ParameterSpec COMMAND_PARAMETERS_SPEC
= ParameterSpec.builder(CommandParameters.class, "parameters").build();
public static final RequiredVariable LISTENERS_REQ_VAR = RequiredVariable.builder()
.name(ReservedNames.LISTENERS)
.type(ParameterizedTypeName.get(
ImmutableList.class,
CommandCallListener.class
))
.inherited(true)
.build();
private final RegistrationInfo info;
private final ImmutableList injectedVariables;
private static boolean isStaticImportable(Method method) {
int mods = method.getModifiers();
if (!java.lang.reflect.Modifier.isStatic(mods)) {
return false;
}
return !method.isSynthetic();
}
CommandRegistrationGenerator(RegistrationInfo info) {
this.info = info;
this.injectedVariables = concat(
additionalVariables(info),
info.getInjectedVariables().stream()
).collect(toImmutableList());
}
private Stream cmdsFlatMap(Function> map) {
return info.getCommands().stream().flatMap(map);
}
private static Stream additionalVariables(RegistrationInfo info) {
Stream.Builder b = Stream.builder();
b.add(RequiredVariable.builder()
.type(ClassName.get(CommandManager.class))
.name(ReservedNames.COMMAND_MANAGER)
.inherited(true)
.build());
b.add(RequiredVariable.builder()
.type(info.getTargetClassName())
.name(ReservedNames.CONTAINER_INSTANCE)
.inherited(true)
.build());
return b.build();
}
private Stream getInjectedVariables() {
return injectedVariables.stream();
}
private static boolean isCommandStatic(CommandInfo info) {
return info.getCommandMethod().getModifiers().contains(STATIC);
}
private Modifier[] getApiVisibilityModifiers() {
return info.getClassVisibility() == null
? new Modifier[0]
: new Modifier[] {info.getClassVisibility()};
}
private ClassName getThisClass() {
return ClassName.get(info.getTargetClassName().packageName(), info.getName());
}
void generate(Element originalElement, String pkgName, Filer filer) throws IOException {
TypeSpec.Builder spec = TypeSpec.classBuilder(info.getName())
.addOriginatingElement(originalElement)
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "deprecation")
.addMember("value", "$S", "removal")
.build())
.addModifiers(FINAL)
.addModifiers(getApiVisibilityModifiers())
.addSuperinterface(ParameterizedTypeName.get(
ClassName.get(CommandRegistration.class),
info.getTargetClassName()
));
boolean hasSuperClass = false;
for (TypeElement superType : info.getSuperTypes()) {
if (superType.getKind() == ElementKind.CLASS) {
checkState(!hasSuperClass, "Super class already present");
hasSuperClass = true;
spec.superclass(TypeName.get(superType.asType()));
} else if (superType.getKind() == ElementKind.INTERFACE) {
spec.addSuperinterface(TypeName.get(superType.asType()));
} else {
throw new IllegalStateException("Not a possible super-type: "
+ superType.getKind() + " " + superType.getQualifiedName().toString());
}
}
spec.addFields(generateFields());
spec.addMethod(generateConstructor());
// static methods
spec.addMethod(generateNewBuilderMethod());
// instance methods
spec.addMethods(generateBuilderSetMethods());
spec.addMethod(generateBuildMethod());
spec.addMethods(generateCommandBindings());
spec.addMethods(getParameterMethods());
JavaFile.builder(pkgName, spec.build())
.indent(" ")
.addFileComment("Generated by $L on $L", getClass().getName(), Instant.now().toString())
.addStaticImport(CommandParts.class, "flag", "arg")
.addStaticImport(RegistrationUtil.class, Stream.of(RegistrationUtil.class.getDeclaredMethods())
.filter(CommandRegistrationGenerator::isStaticImportable)
.map(Method::getName)
.toArray(String[]::new))
.build()
.writeTo(filer);
}
private MethodSpec generateConstructor() {
return MethodSpec.constructorBuilder()
.addModifiers(PRIVATE)
.addStatement("this.$L = $T.of()",
ReservedNames.LISTENERS, ImmutableList.class)
.build();
}
private MethodSpec.Builder setSpec(RequiredVariable var) {
Modifier[] modifiers = var.isInherited()
? new Modifier[] {Modifier.PUBLIC}
: getApiVisibilityModifiers();
return MethodSpec.methodBuilder(var.getName())
.addModifiers(modifiers)
.returns(getThisClass());
}
private Iterable generateBuilderSetMethods() {
Stream injectedVariableSets = getInjectedVariables()
.map(var ->
setSpec(var)
.addParameter(ParameterSpec.builder(var.getType(), var.getName())
.addAnnotations(var.getAnnotations())
.build())
.addStatement("this.$1L = $1L", var.getName())
.addStatement("return this")
.build()
);
Stream customSets = Stream.of(
setSpec(LISTENERS_REQ_VAR)
.addParameter(ParameterizedTypeName.get(
Collection.class,
CommandCallListener.class
), LISTENERS_REQ_VAR.getName())
.addStatement("this.$1L = $2T.copyOf($1L)",
LISTENERS_REQ_VAR.getName(), ImmutableList.class)
.addStatement("return this")
.build()
);
return concat(
injectedVariableSets,
customSets
).collect(toList());
}
private Iterable getParameterMethods() {
return cmdsFlatMap(cmd -> cmd.getParams().stream())
.map(param -> {
ExtractSpec spec = param.getExtractSpec();
return MethodSpec.methodBuilder(spec.getName())
.addModifiers(PRIVATE)
.addParameter(CommandParameters.class, ReservedNames.PARAMETERS)
.returns(spec.getType())
.addCode(spec.getExtractMethodBody().generate(param.getName()))
.build();
})
.distinct()
.collect(toList());
}
private Iterable generateFields() {
Stream staticFields = getKeyTypeFields();
Stream instanceFields = concat(
getInjectedVariables(),
info.getDeclaredFields().stream(),
Stream.of(LISTENERS_REQ_VAR)
).map(var -> FieldSpec.builder(
var.getType(), var.getName(),
PRIVATE
).build());
Stream partFields = getPartFields();
return concat(staticFields, instanceFields, partFields).collect(toList());
}
private Stream getKeyTypeFields() {
return info.getKeyTypes().stream()
.map(keyInfo ->
FieldSpec.builder(
keyInfo.wrappedTypeName(Key.class),
keyInfo.getVariableName(),
PRIVATE, STATIC, FINAL
).initializer(keyInfo.keyMaker()).build()
);
}
private Stream getPartFields() {
return cmdsFlatMap(c -> c.getParams().stream())
.filter(p -> p.getType() != null && p.getName() != null && p.getConstruction() != null)
.distinct()
.map(p -> FieldSpec.builder(
p.getType(), p.getName(),
PRIVATE, FINAL)
.initializer(CodeBlock.of("$[$L$]", p.getConstruction()))
.build());
}
private MethodSpec generateBuildMethod() {
MethodSpec.Builder build = MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC);
for (CommandInfo cmd : info.getCommands()) {
build.addCode(generateRegisterCommandCode(cmd));
}
return build.build();
}
private CodeBlock generateRegisterCommandCode(CommandInfo cmd) {
// Workaround no `addStatement` for now, see:
// https://github.com/square/javapoet/issues/711
return CodeBlock.builder()
.add("$L.register($S, $L);\n", ReservedNames.COMMAND_MANAGER, cmd.getName(),
generateRegistrationLambda(cmd))
.build();
}
private CodeBlock generateRegistrationLambda(CommandInfo cmd) {
CodeBlock.Builder lambda = CodeBlock.builder()
.add("b -> {\n").indent();
lambda.addStatement("b.aliases($L)", stringListForGen(cmd.getAliases().stream()));
lambda.addStatement("b.description($L)", textCompOf(cmd.getDescription()));
cmd.getFooter().ifPresent(footer ->
lambda.addStatement("b.footer($L)", textCompOf(footer))
);
lambda.addStatement("b.parts($L)",
listForGen(cmd.getParams().stream()
.map(CommandParamInfo::getName)
.filter(Objects::nonNull)
.map(name -> CodeBlock.of("$L", name))));
lambda.addStatement("b.action(this::$L)", cmd.getGeneratedName());
cmd.getCondition().ifPresent(cond -> {
lambda.add(cond.getConstruction());
lambda.addStatement("b.condition($L)", cond.getCondVariable());
});
return lambda.unindent().add("}").build();
}
private Iterable generateCommandBindings() {
return info.getCommands().stream()
.map(this::generateCommandBinding)
.collect(toList());
}
private MethodSpec generateCommandBinding(CommandInfo commandInfo) {
MethodSpec.Builder spec = MethodSpec.methodBuilder(commandInfo.getGeneratedName())
.addModifiers(PRIVATE)
.returns(int.class)
.addParameter(COMMAND_PARAMETERS_SPEC);
for (TypeMirror thrownType : commandInfo.getCommandMethod().getThrownTypes()) {
spec.addException(TypeName.get(thrownType));
}
CodeBlock.Builder body = CodeBlock.builder();
// grab the command method
body.add(CodeBlockUtil.scopeCommandMethod(commandInfo.getCommandMethod(), "cmdMethod"));
// call beforeCall
body.addStatement("$T.listenersBeforeCall(listeners, cmdMethod, $L)",
RegistrationUtil.class, ReservedNames.PARAMETERS);
// open up a `try` for calling after* listeners
body.beginControlFlow("try");
body.addStatement("$T result", int.class);
CodeBlock callCommandMethod = generateCallCommandMethod(commandInfo);
TypeName rawReturnType = TypeName.get(commandInfo.getCommandMethod().getReturnType()).unbox();
if (TypeName.INT.equals(rawReturnType)) {
// call the method, return what it does
body.addStatement("result = $L", callCommandMethod);
} else {
// call the method, return 1
body.addStatement(callCommandMethod)
.addStatement("result = 1");
}
// call afterCall
body.addStatement("$T.listenersAfterCall(listeners, cmdMethod, $L)",
RegistrationUtil.class, ReservedNames.PARAMETERS)
.addStatement("return result");
body.nextControlFlow("catch ($T t)", Throwable.class);
// call afterThrow & re-throw
body.addStatement("$T.listenersAfterThrow(listeners, cmdMethod, $L, t)",
RegistrationUtil.class, ReservedNames.PARAMETERS)
.addStatement("throw t");
body.endControlFlow();
spec.addCode(body.build());
return spec.build();
}
private CodeBlock generateCallCommandMethod(CommandInfo commandInfo) {
CodeBlock target;
if (commandInfo.getCommandMethod().getModifiers().contains(STATIC)) {
// call it statically
target = CodeBlock.of("$T", info.getTargetClassName());
} else {
// use the instance
target = CodeBlock.of("$L", ReservedNames.CONTAINER_INSTANCE);
}
String args = commandInfo.getParams().stream()
.map(param -> CodeBlock.of("this.$L($L)",
param.getExtractSpec().getName(),
ReservedNames.PARAMETERS).toString())
.collect(joining(", "));
return CodeBlock.of("$L.$L($L)",
target,
commandInfo.getCommandMethod().getSimpleName(),
args);
}
private MethodSpec generateNewBuilderMethod() {
ClassName thisClass = getThisClass();
return MethodSpec.methodBuilder(ReservedNames.BUILDER)
.addModifiers(getApiVisibilityModifiers())
.addModifiers(STATIC)
.returns(thisClass)
.addStatement("return new $T()", thisClass)
.build();
}
}