mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-19 02:30:21 +01:00
Add queryable store with SQLite implementation
This adds the new "queryable store" API, that allows complex queries and is backed by SQLite. This new API can be used for entities annotated with the new QueryableType annotation.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
class AnnotationHelper {
|
||||
|
||||
public Optional<? extends AnnotationValue> findAnnotationValue(AnnotationMirror annotationMirror, String name) {
|
||||
return annotationMirror.getElementValues()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getKey().getSimpleName().toString().equals(name))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.Element;
|
||||
import java.util.Optional;
|
||||
|
||||
class AnnotationProcessor {
|
||||
Optional<? extends AnnotationMirror> findAnnotation(Element element, Class<?> annotationClass) {
|
||||
return element.getAnnotationMirrors()
|
||||
.stream()
|
||||
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(annotationClass.getCanonicalName()))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.JavaFile;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import jakarta.inject.Inject;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.store.QueryableStoreFactory;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
class FactoryClassCreator {
|
||||
|
||||
private static final String STORE_PACKAGE_NAME = "sonia.scm.store";
|
||||
private static final String QUERYABLE_MUTABLE_STORE_CLASS_NAME = "QueryableMutableStore";
|
||||
private static final String QUERYABLE_STORE_CLASS_NAME = "QueryableStore";
|
||||
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
|
||||
FactoryClassCreator(ProcessingEnvironment processingEnv) {
|
||||
this.processingEnv = processingEnv;
|
||||
}
|
||||
|
||||
void createFactoryClass(Element element, String packageName, TypeElement dataClassTypeElement) throws IOException {
|
||||
TypeName typeNameOfDataClass = TypeName.get(dataClassTypeElement.asType());
|
||||
TypeSpec.Builder builder =
|
||||
TypeSpec
|
||||
.classBuilder(element.getSimpleName() + "StoreFactory")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addJavadoc("Generated queryable store factory for type {@link $T}.\nTo create conditions in queries, use the static fields in the class {@link $TQueryFields}.\n", typeNameOfDataClass, typeNameOfDataClass);
|
||||
|
||||
createStoreFactoryField(builder);
|
||||
createConstructor(builder);
|
||||
|
||||
List<ParentSpec> parents = determineParentSpecs(dataClassTypeElement);
|
||||
if (parents.isEmpty()) {
|
||||
createGetterForDataTypeWithoutParent(builder, typeNameOfDataClass);
|
||||
} else {
|
||||
createOverallGetterForDataTypeWithParents(builder, typeNameOfDataClass);
|
||||
createMutableGetterForDataTypeWithParents(typeNameOfDataClass, parents, builder);
|
||||
createPartialGetterForDataTypeWithParents(builder, typeNameOfDataClass, parents);
|
||||
}
|
||||
|
||||
JavaFile.builder(packageName, builder.build())
|
||||
.build()
|
||||
.writeTo(processingEnv.getFiler());
|
||||
}
|
||||
|
||||
private void createMutableGetterForDataTypeWithParents(TypeName typeNameOfDataClass, List<ParentSpec> parents, TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
createGetMutableMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to modify elements of the type {@link $T}.\nTo do so, an id has to be specified for each parent type.\n",
|
||||
parents,
|
||||
ParentSpec::buildParameterNameWithIdSuffix,
|
||||
ParentSpec::appendParentAsIdStringArgument
|
||||
)
|
||||
);
|
||||
|
||||
if (isEveryParentModelObject(parents)) {
|
||||
builder.addMethod(
|
||||
createGetMutableMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to modify elements of the type {@link $T}.\nTo do so, an instance of each parent type has to be specified.\n",
|
||||
parents,
|
||||
ParentSpec::buildIdGetterWithParameterName,
|
||||
ParentSpec::appendParentAsObjectArgument
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private MethodSpec createGetMutableMethodSpec(TypeName typeNameOfDataClass,
|
||||
String javaDoc,
|
||||
List<ParentSpec> parents,
|
||||
Function<ParentSpec, String> parentIdProcessor,
|
||||
BiConsumer<MethodSpec.Builder, ParentSpec> parentArgumentProcessor) {
|
||||
MethodSpec.Builder getMutableBuilder = MethodSpec
|
||||
.methodBuilder("getMutable")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addJavadoc(javaDoc, typeNameOfDataClass)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_MUTABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement(
|
||||
"return storeFactory.getMutable($T.class, $L)",
|
||||
typeNameOfDataClass,
|
||||
parents.stream().map(parentIdProcessor).reduce((s1, s2) -> s1 + ", " + s2).orElseThrow()
|
||||
);
|
||||
|
||||
parents.forEach(
|
||||
parent -> parentArgumentProcessor.accept(getMutableBuilder, parent)
|
||||
);
|
||||
|
||||
return getMutableBuilder.build();
|
||||
}
|
||||
|
||||
private boolean isEveryParentModelObject(List<ParentSpec> parents) {
|
||||
return parents.stream().allMatch(ParentSpec::isModelObject);
|
||||
}
|
||||
|
||||
private void createPartialGetterForDataTypeWithParents(TypeSpec.Builder builder, TypeName typeNameOfDataClass, List<ParentSpec> parents) {
|
||||
for (int i = 0; i < parents.size(); i++) {
|
||||
String javaDocParentDescriptor;
|
||||
if (i == 0) {
|
||||
javaDocParentDescriptor = "only to the first parent";
|
||||
} else if (i < parents.size() - 1) {
|
||||
javaDocParentDescriptor = "only to the first " + (i + 1) + "parents";
|
||||
} else {
|
||||
javaDocParentDescriptor = "to all parents";
|
||||
}
|
||||
|
||||
int currentParentLimit = i + 1;
|
||||
builder.addMethod(
|
||||
createPartialGetterMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to query elements of the type {@link $T} limited " + javaDocParentDescriptor + " specified by their ids.\n",
|
||||
parents,
|
||||
currentParentLimit,
|
||||
ParentSpec::buildParameterNameWithIdSuffix,
|
||||
ParentSpec::appendParentAsIdStringArgument
|
||||
)
|
||||
);
|
||||
|
||||
if (isEveryParentModelObject(parents)) {
|
||||
builder.addMethod(
|
||||
createPartialGetterMethodSpec(
|
||||
typeNameOfDataClass,
|
||||
"Returns a store to query elements of the type {@link $T} limited " + javaDocParentDescriptor + " specified as instances of the parent type.\n",
|
||||
parents,
|
||||
currentParentLimit,
|
||||
ParentSpec::buildIdGetterWithParameterName,
|
||||
ParentSpec::appendParentAsObjectArgument
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MethodSpec createPartialGetterMethodSpec(TypeName typeNameOfDataClass,
|
||||
String javaDoc,
|
||||
List<ParentSpec> parents,
|
||||
int parentLimit,
|
||||
Function<ParentSpec, String> parentIdProcessor,
|
||||
BiConsumer<MethodSpec.Builder, ParentSpec> parentArgumentProcessor) {
|
||||
MethodSpec.Builder getBuilder = MethodSpec
|
||||
.methodBuilder(parentLimit == parents.size() ? "get" : "getOverlapping")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addJavadoc(javaDoc, typeNameOfDataClass)
|
||||
.addStatement(
|
||||
"return storeFactory.getReadOnly($T.class, $L)",
|
||||
typeNameOfDataClass,
|
||||
parents.stream()
|
||||
.limit(parentLimit)
|
||||
.map(parentIdProcessor)
|
||||
.reduce((s1, s2) -> s1 + ", " + s2)
|
||||
.orElseThrow()
|
||||
);
|
||||
|
||||
parents.stream()
|
||||
.limit(parentLimit)
|
||||
.forEach(parent -> parentArgumentProcessor.accept(getBuilder, parent));
|
||||
|
||||
return getBuilder.build();
|
||||
}
|
||||
|
||||
private void createOverallGetterForDataTypeWithParents(TypeSpec.Builder builder, TypeName typeNameOfDataClass) {
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("getOverall")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getReadOnly($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to overall query elements of the type {@link $T} independent of any parent.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void createGetterForDataTypeWithoutParent(TypeSpec.Builder builder, TypeName typeNameOfDataClass) {
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getReadOnly($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to query elements of the type {@link $T}.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
builder.addMethod(
|
||||
MethodSpec.methodBuilder("getMutable")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(ParameterizedTypeName.get(ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_MUTABLE_STORE_CLASS_NAME), typeNameOfDataClass))
|
||||
.addStatement("return storeFactory.getMutable($T.class)", typeNameOfDataClass)
|
||||
.addJavadoc("Returns a store to modify elements of the type {@link $T}.\n", typeNameOfDataClass)
|
||||
.build());
|
||||
}
|
||||
|
||||
private List<ParentSpec> determineParentSpecs(TypeElement typeElement) {
|
||||
return new QueryableTypeParentProcessor().getQueryableTypeValues(typeElement)
|
||||
.stream()
|
||||
.map(queryableType -> {
|
||||
String parentClassPackage = queryableType.substring(0, queryableType.lastIndexOf("."));
|
||||
String parentClassName = queryableType.substring(queryableType.lastIndexOf(".") + 1);
|
||||
String parameterName = lowercaseFirstLetter(parentClassName);
|
||||
return new ParentSpec(parentClassPackage, parentClassName, parameterName, isParentModelObject(queryableType));
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean isParentModelObject(String parentType) {
|
||||
try {
|
||||
Class<?> parentClass = Class.forName(parentType);
|
||||
return Arrays.stream(parentClass.getInterfaces()).anyMatch(parentInterface -> parentInterface.getName().equals(ModelObject.class.getName()));
|
||||
} catch (ClassNotFoundException e) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, String.format("Failed to find class of parent '%s'. Unable to determine whether this is a ModelObject or not. Will not generate factory methods for parent objects, only for ids.", parentType));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String lowercaseFirstLetter(String parentClassName) {
|
||||
return parentClassName.substring(0, 1).toLowerCase(Locale.ENGLISH) + parentClassName.substring(1);
|
||||
}
|
||||
|
||||
private void createConstructor(TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
MethodSpec
|
||||
.constructorBuilder()
|
||||
.addParameter(QueryableStoreFactory.class, "storeFactory")
|
||||
.addStatement("this.storeFactory = storeFactory")
|
||||
.addAnnotation(Inject.class)
|
||||
.addJavadoc("Instances should not be created manually, but injected by dependency injection using {@link $T}.\n", Inject.class)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void createStoreFactoryField(TypeSpec.Builder builder) {
|
||||
builder.addField(QueryableStoreFactory.class, "storeFactory", Modifier.PRIVATE, Modifier.FINAL);
|
||||
}
|
||||
|
||||
private record ParentSpec(String classPackage, String className, String parameterName, boolean isModelObject) {
|
||||
String buildParameterNameWithIdSuffix() {
|
||||
return parameterName + "Id";
|
||||
}
|
||||
|
||||
String buildIdGetterWithParameterName() {
|
||||
return parameterName + ".getId()";
|
||||
}
|
||||
|
||||
static void appendParentAsIdStringArgument(MethodSpec.Builder builder, ParentSpec parent) {
|
||||
builder.addParameter(String.class, parent.buildParameterNameWithIdSuffix());
|
||||
}
|
||||
|
||||
static void appendParentAsObjectArgument(MethodSpec.Builder builder, ParentSpec parent) {
|
||||
builder.addParameter(ClassName.get(parent.classPackage(), parent.className()), parent.parameterName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
interface FieldInitializer {
|
||||
void initialize(FieldSpec.Builder fieldBuilder, TypeElement element, String fieldClass, String fieldName);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
|
||||
class NumberQueryFieldHandler extends QueryFieldHandler {
|
||||
public NumberQueryFieldHandler(String packageName, String className) {
|
||||
this(packageName, className, null);
|
||||
}
|
||||
|
||||
public NumberQueryFieldHandler(String packageName, String className, String suffix) {
|
||||
super(
|
||||
"NumberQueryField",
|
||||
new TypeName[]{ClassName.get(packageName, className)},
|
||||
(fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get("sonia.scm.store", "QueryableStore").nestedClass(fieldClass),
|
||||
fieldName
|
||||
),
|
||||
suffix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.google.auto.common.MoreElements;
|
||||
import com.google.auto.common.MoreTypes;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.JavaFile;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
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.Modifier;
|
||||
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 java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class QueryFieldClassCreator {
|
||||
|
||||
private static final String SIZE_SUFFIX = "SIZE";
|
||||
private static final String STORE_PACKAGE_NAME = "sonia.scm.store";
|
||||
private static final String QUERYABLE_STORE_CLASS_NAME = "QueryableStore";
|
||||
private static final String ID_QUERY_FIELD_CLASS_NAME = "IdQueryField";
|
||||
|
||||
private static final FieldInitializer SIMPLE_INITIALIZER = (fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(fieldClass),
|
||||
fieldName
|
||||
);
|
||||
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
|
||||
QueryFieldClassCreator(ProcessingEnvironment processingEnv) {
|
||||
this.processingEnv = processingEnv;
|
||||
}
|
||||
|
||||
void createQueryFieldClass(Element element, String packageName, TypeElement typeElement) throws IOException {
|
||||
TypeSpec.Builder builder =
|
||||
TypeSpec
|
||||
.classBuilder(element.getSimpleName() + "QueryFields")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
|
||||
.addJavadoc("Generated query fields for type {@link $T}.\nTo create a queryable store for this, use an injected instance of the {@link $TStoreFactory}.\n", TypeName.get(typeElement.asType()), TypeName.get(typeElement.asType()));
|
||||
|
||||
createPrivateConstructor(builder);
|
||||
processParents(typeElement, builder);
|
||||
processId(typeElement, builder);
|
||||
processFields(typeElement, builder);
|
||||
|
||||
JavaFile.builder(packageName, builder.build())
|
||||
.build()
|
||||
.writeTo(processingEnv.getFiler());
|
||||
}
|
||||
|
||||
private void createPrivateConstructor(TypeSpec.Builder builder) {
|
||||
builder.addMethod(
|
||||
MethodSpec
|
||||
.constructorBuilder()
|
||||
.addModifiers(Modifier.PRIVATE)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void processParents(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
new QueryableTypeParentProcessor().getQueryableTypeValues(typeElement)
|
||||
.forEach(queryableType -> {
|
||||
String parentClassPackage = queryableType.substring(0, queryableType.lastIndexOf("."));
|
||||
String parentClassName = queryableType.substring(queryableType.lastIndexOf(".") + 1);
|
||||
builder.addField(
|
||||
FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
TypeName.get(typeElement.asType())),
|
||||
parentClassName.toUpperCase(Locale.ENGLISH) + "_ID"
|
||||
)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
|
||||
.initializer(
|
||||
"new $T<>($T.class)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
ClassName.get(parentClassPackage, parentClassName))
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
private void processId(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
builder.addField(
|
||||
FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(ID_QUERY_FIELD_CLASS_NAME),
|
||||
TypeName.get(typeElement.asType())),
|
||||
"INTERNAL_ID"
|
||||
)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
|
||||
.initializer(
|
||||
"new $T<>()",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(ID_QUERY_FIELD_CLASS_NAME))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void processFields(TypeElement typeElement, TypeSpec.Builder builder) {
|
||||
processFields(typeElement, typeElement, builder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void processFields(TypeElement typeElement, TypeElement superTypeElement, TypeSpec.Builder builder) {
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getAllMembers(typeElement)
|
||||
.stream()
|
||||
.filter(member -> member.getKind() == ElementKind.FIELD)
|
||||
.filter(member -> !member.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(member -> !member.getModifiers().contains(Modifier.TRANSIENT))
|
||||
.flatMap(field ->
|
||||
createFieldSpec(
|
||||
superTypeElement,
|
||||
MoreElements.asVariable(field))
|
||||
)
|
||||
.forEach(builder::addField);
|
||||
TypeElement superclass = (TypeElement) processingEnv.getTypeUtils().asElement(typeElement.getSuperclass());
|
||||
if (superclass != null && !superclass.getQualifiedName().toString().equals(Object.class.getCanonicalName())) {
|
||||
processFields(superclass, typeElement, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<FieldSpec> createFieldSpec(TypeElement element, VariableElement field) {
|
||||
TypeMirror effectiveFieldType = determineFieldType(field);
|
||||
return createFieldHandler(effectiveFieldType).stream()
|
||||
.map(queryFieldHandler -> {
|
||||
String fieldName = field.getSimpleName().toString();
|
||||
String fieldClass = queryFieldHandler.getClazz();
|
||||
TypeName[] furtherGenerics = queryFieldHandler.getGenerics();
|
||||
TypeName[] generics = new TypeName[furtherGenerics.length + 1];
|
||||
generics[0] = TypeName.get(element.asType());
|
||||
System.arraycopy(furtherGenerics, 0, generics, 1, furtherGenerics.length);
|
||||
FieldSpec.Builder fieldBuilder = FieldSpec
|
||||
.builder(
|
||||
ParameterizedTypeName.get(
|
||||
ClassName
|
||||
.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME)
|
||||
.nestedClass(fieldClass),
|
||||
generics),
|
||||
determineFieldNameWithSuffix(fieldName, queryFieldHandler).toUpperCase(Locale.ENGLISH)
|
||||
)
|
||||
.addJavadoc("Generated query field to create conditions for field {@link $L#$L} of type {@link $L}.\n", TypeName.get(element.asType()), fieldName, effectiveFieldType)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
|
||||
queryFieldHandler.getInitializer().initialize(fieldBuilder, element, fieldClass, fieldName);
|
||||
return fieldBuilder.build();
|
||||
});
|
||||
}
|
||||
|
||||
private TypeMirror determineFieldType(VariableElement field) {
|
||||
return new AnnotationProcessor().findAnnotation(field, XmlJavaTypeAdapter.class)
|
||||
.map(this::determineTypeFromAdapter)
|
||||
.orElseGet(field::asType);
|
||||
}
|
||||
|
||||
private TypeMirror determineTypeFromAdapter(AnnotationMirror annotationMirror) {
|
||||
AnnotationValue value = new AnnotationHelper().findAnnotationValue(annotationMirror, "value").orElseThrow();
|
||||
TypeMirror adapterType = (TypeMirror) value.getValue();
|
||||
TypeMirror xmlAdapterType = processingEnv.getTypeUtils()
|
||||
.directSupertypes(adapterType)
|
||||
.stream()
|
||||
.filter(typeMirror -> processingEnv.getTypeUtils()
|
||||
.isAssignable(
|
||||
processingEnv.getTypeUtils().erasure(typeMirror),
|
||||
processingEnv.getElementUtils().getTypeElement(XmlAdapter.class.getCanonicalName()).asType()
|
||||
))
|
||||
.findFirst()
|
||||
.orElseThrow(RuntimeException::new);
|
||||
DeclaredType declaredType = MoreTypes.asDeclared(xmlAdapterType);
|
||||
return declaredType.getTypeArguments().get(0);
|
||||
}
|
||||
|
||||
private Collection<QueryFieldHandler> createFieldHandler(TypeMirror fieldType) {
|
||||
TypeMirror collectionType = processingEnv.getElementUtils().getTypeElement(Collection.class.getCanonicalName()).asType();
|
||||
TypeMirror erasure = processingEnv.getTypeUtils().erasure(fieldType);
|
||||
if (processingEnv.getTypeUtils().isAssignable(erasure, collectionType)) {
|
||||
return List.of(
|
||||
new QueryFieldHandler(
|
||||
"CollectionQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER
|
||||
),
|
||||
new QueryFieldHandler(
|
||||
"CollectionSizeQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER,
|
||||
SIZE_SUFFIX
|
||||
)
|
||||
);
|
||||
}
|
||||
TypeMirror mapType = processingEnv.getElementUtils().getTypeElement(Map.class.getCanonicalName()).asType();
|
||||
if (processingEnv.getTypeUtils().isAssignable(erasure, mapType)) {
|
||||
return List.of(
|
||||
new QueryFieldHandler(
|
||||
"MapQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER
|
||||
),
|
||||
new QueryFieldHandler(
|
||||
"MapSizeQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER,
|
||||
SIZE_SUFFIX
|
||||
)
|
||||
);
|
||||
}
|
||||
Element fieldAsElement = processingEnv.getTypeUtils().asElement(fieldType);
|
||||
if (fieldAsElement != null && fieldAsElement.getKind() == ElementKind.ENUM) {
|
||||
return List.of(new QueryFieldHandler(
|
||||
"EnumQueryField",
|
||||
new TypeName[]{TypeName.get(fieldAsElement.asType())},
|
||||
(fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
|
||||
.initializer(
|
||||
"new $T<>($S)",
|
||||
ClassName.get(STORE_PACKAGE_NAME, QUERYABLE_STORE_CLASS_NAME).nestedClass(fieldClass),
|
||||
fieldName
|
||||
)
|
||||
));
|
||||
}
|
||||
return switch (fieldType.toString()) {
|
||||
case "java.lang.String" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"StringQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
case "boolean", "java.lang.Boolean" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"BooleanQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
case "int", "java.lang.Integer" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Integer"));
|
||||
case "long", "java.lang.Long" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Long"));
|
||||
case "float", "java.lang.Float" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Float"));
|
||||
case "double", "java.lang.Double" -> List.of(
|
||||
new NumberQueryFieldHandler(
|
||||
"java.lang",
|
||||
"Double"));
|
||||
case "java.util.Date", "java.time.Instant" -> List.of(
|
||||
new QueryFieldHandler(
|
||||
"InstantQueryField",
|
||||
new TypeName[]{},
|
||||
SIMPLE_INITIALIZER));
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private String determineFieldNameWithSuffix(String fieldName, QueryFieldHandler fieldHandler) {
|
||||
return fieldHandler.getSuffix()
|
||||
.map(suffix -> String.format("%s_%s", fieldName, suffix))
|
||||
.orElse(fieldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class QueryFieldHandler {
|
||||
private final String clazz;
|
||||
private final TypeName[] generics;
|
||||
private final FieldInitializer initializer;
|
||||
private final String suffix;
|
||||
|
||||
public QueryFieldHandler(String clazz, TypeName[] generics, FieldInitializer initializer) {
|
||||
this(clazz, generics, initializer, null);
|
||||
}
|
||||
|
||||
public Optional<String> getSuffix() {
|
||||
return Optional.ofNullable(suffix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import com.google.auto.common.MoreElements;
|
||||
import org.kohsuke.MetaInfServices;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.Processor;
|
||||
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.Element;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
@SupportedAnnotationTypes("sonia.scm.store.QueryableType")
|
||||
@MetaInfServices(Processor.class)
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_17)
|
||||
public class QueryableTypeAnnotationProcessor extends AbstractProcessor {
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
|
||||
for (TypeElement annotation : annotations) {
|
||||
log("Found annotation: " + annotation.getQualifiedName());
|
||||
roundEnvironment.getElementsAnnotatedWith(annotation).forEach(element -> {
|
||||
log("Found annotated element: " + element.getSimpleName());
|
||||
tryToCreateQueryFieldClass(element);
|
||||
tryToCreateFactoryClass(element);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void tryToCreateQueryFieldClass(Element element) {
|
||||
TypeElement typeElement = MoreElements.asType(element);
|
||||
getPackageName(typeElement)
|
||||
.ifPresent(packageName -> {
|
||||
try {
|
||||
new QueryFieldClassCreator(processingEnv).createQueryFieldClass(element, packageName, typeElement);
|
||||
} catch (IOException e) {
|
||||
error("Failed to create query field class for type " + typeElement + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void tryToCreateFactoryClass(Element element) {
|
||||
TypeElement typeElement = MoreElements.asType(element);
|
||||
getPackageName(typeElement)
|
||||
.ifPresent(packageName -> {
|
||||
try {
|
||||
new FactoryClassCreator(processingEnv).createFactoryClass(element, packageName, typeElement);
|
||||
} catch (IOException e) {
|
||||
error("Failed to create factory class for type " + typeElement + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private Optional<String> getPackageName(TypeElement typeElement) {
|
||||
Element enclosingElement = typeElement.getEnclosingElement();
|
||||
try {
|
||||
return of(MoreElements.asPackage(enclosingElement).getQualifiedName().toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
error("Could not determine package name for " + typeElement + ". QueryableType annotation does not support inner classes. Exception: " + e.getMessage());
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String message) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
|
||||
}
|
||||
|
||||
private void error(String message) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.annotation;
|
||||
|
||||
import sonia.scm.store.QueryableType;
|
||||
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
class QueryableTypeParentProcessor {
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getQueryableTypeValues(TypeElement typeElement) {
|
||||
return new AnnotationProcessor().findAnnotation(typeElement, QueryableType.class)
|
||||
.map(annotationMirror -> {
|
||||
Optional<? extends AnnotationValue> value = new AnnotationHelper().findAnnotationValue(annotationMirror, "value");
|
||||
if (value.isEmpty()) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
List<AnnotationValue> parentClassTypes = (List<AnnotationValue>) value.orElseThrow().getValue();
|
||||
return parentClassTypes.stream()
|
||||
.map(AnnotationValue::getValue)
|
||||
.map(Object::toString)
|
||||
.toList();
|
||||
})
|
||||
.orElseGet(List::of);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
sonia.scm.annotation.QueryableTypeAnnotationProcessor,isolating
|
||||
Reference in New Issue
Block a user