Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy loading #701

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 11 additions & 53 deletions processor/src/main/java/apoc/processor/ApocProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,26 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import org.neo4j.kernel.api.QueryLanguage;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserAggregationFunction;
import org.neo4j.procedure.UserFunction;

@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class ApocProcessor extends AbstractProcessor {

private List<Map<String, List<QueryLanguage>>> procedureSignatures;

private List<Map<String, List<QueryLanguage>>> userFunctionSignatures;
private List<SignatureVisitor.Signature> signatures;

private SignatureVisitor signatureVisitor;

private ExtensionClassWriter extensionClassWriter;
private ProcedureServiceWriter procedureServiceWriter;

@Override
public Set<String> getSupportedAnnotationTypes() {
Expand All @@ -51,63 +48,24 @@ public Set<String> getSupportedAnnotationTypes() {

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
procedureSignatures = new ArrayList<>();
userFunctionSignatures = new ArrayList<>();
signatures = new ArrayList<>();
extensionClassWriter = new ExtensionClassWriter(processingEnv.getFiler());
procedureServiceWriter = new ProcedureServiceWriter(processingEnv.getFiler());
signatureVisitor = new SignatureVisitor(processingEnv.getElementUtils(), processingEnv.getMessager());
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

annotations.forEach(annotation -> extractSignature(annotation, roundEnv));

List<String> procedureSignaturesCypher5 = new ArrayList<>();
List<String> userFunctionSignaturesCypher5 = new ArrayList<>();
List<String> procedureSignaturesCypher25 = new ArrayList<>();
List<String> userFunctionSignaturesCypher25 = new ArrayList<>();

separateKeysByQueryLanguage(procedureSignatures, procedureSignaturesCypher5, procedureSignaturesCypher25);
separateKeysByQueryLanguage(
userFunctionSignatures, userFunctionSignaturesCypher5, userFunctionSignaturesCypher25);
for (final var annotation : annotations) {
for (final var method : roundEnv.getElementsAnnotatedWith(annotation)) {
signatures.add(signatureVisitor.visit(method));
}
}

if (roundEnv.processingOver()) {
extensionClassWriter.write(
procedureSignaturesCypher5,
userFunctionSignaturesCypher5,
procedureSignaturesCypher25,
userFunctionSignaturesCypher25);
extensionClassWriter.write(signatures);
procedureServiceWriter.write(signatures);
}
return false;
}

private void extractSignature(TypeElement annotation, RoundEnvironment roundEnv) {
List<Map<String, List<QueryLanguage>>> signatures = accumulator(annotation);
roundEnv.getElementsAnnotatedWith(annotation)
.forEach(annotatedElement -> signatures.add(signatureVisitor.visit(annotatedElement)));
}

private List<Map<String, List<QueryLanguage>>> accumulator(TypeElement annotation) {
if (annotation.getQualifiedName().contentEquals(Procedure.class.getName())) {
return procedureSignatures;
}
return userFunctionSignatures;
}

public static void separateKeysByQueryLanguage(
List<Map<String, List<QueryLanguage>>> list, List<String> c5Keys, List<String> c6Keys) {
for (Map<String, List<QueryLanguage>> map : list) {
for (Map.Entry<String, List<QueryLanguage>> entry : map.entrySet()) {
String key = entry.getKey();
List<QueryLanguage> values = entry.getValue();

if (values.contains(QueryLanguage.CYPHER_5)) {
c5Keys.add(key);
}
if (values.contains(QueryLanguage.CYPHER_25)) {
c6Keys.add(key);
}
}
}
}
}
51 changes: 22 additions & 29 deletions processor/src/main/java/apoc/processor/ExtensionClassWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.neo4j.kernel.api.QueryLanguage;

public class ExtensionClassWriter {

Expand All @@ -41,22 +43,10 @@ public ExtensionClassWriter(Filer filer) {
this.filer = filer;
}

public void write(
List<String> procedureSignaturesCypher5,
List<String> userFunctionSignaturesCypher5,
List<String> procedureSignaturesCypher25,
List<String> userFunctionSignaturesCypher25) {

public void write(List<SignatureVisitor.Signature> signatures) {
try {
String suffix = isExtendedProject() ? "Extended" : "";
final TypeSpec typeSpec = defineClass(
procedureSignaturesCypher5,
userFunctionSignaturesCypher5,
procedureSignaturesCypher25,
userFunctionSignaturesCypher25,
suffix);

JavaFile.builder("apoc", typeSpec).build().writeTo(filer);
final var suffix = isExtendedProject() ? "Extended" : "";
JavaFile.builder("apoc", defineClass(signatures, suffix)).build().writeTo(filer);
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand All @@ -71,33 +61,36 @@ private boolean isExtendedProject() throws IOException {
return projectPath.contains("extended/build/generated");
}

private TypeSpec defineClass(
List<String> procedureSignaturesCypher5,
List<String> userFunctionSignaturesCypher5,
List<String> procedureSignaturesCypher25,
List<String> userFunctionSignaturesCypher25,
String suffix) {
private TypeSpec defineClass(List<SignatureVisitor.Signature> signatures, String suffix) {
return TypeSpec.classBuilder("ApocSignatures" + suffix)
.addModifiers(Modifier.PUBLIC)
.addField(signatureListField("PROCEDURES_CYPHER_5", procedureSignaturesCypher5))
.addField(signatureListField("FUNCTIONS_CYPHER_5", userFunctionSignaturesCypher5))
.addField(signatureListField("PROCEDURES_CYPHER_25", procedureSignaturesCypher25))
.addField(signatureListField("FUNCTIONS_CYPHER_25", userFunctionSignaturesCypher25))
.addField(signatureListField("PROCEDURES_CYPHER_5", names(signatures, true, QueryLanguage.CYPHER_5)))
.addField(signatureListField("FUNCTIONS_CYPHER_5", names(signatures, false, QueryLanguage.CYPHER_5)))
.addField(signatureListField("PROCEDURES_CYPHER_25", names(signatures, true, QueryLanguage.CYPHER_25)))
.addField(signatureListField("FUNCTIONS_CYPHER_25", names(signatures, false, QueryLanguage.CYPHER_25)))
.build();
}

private FieldSpec signatureListField(String fieldName, List<String> signatures) {
private String[] names(List<SignatureVisitor.Signature> signatures, boolean procedure, QueryLanguage lang) {
return signatures.stream()
.filter(s -> s.isProcedure() == procedure)
.filter(s -> s.scope().contains(lang))
.map(SignatureVisitor.Signature::name)
.toArray(String[]::new);
}

private FieldSpec signatureListField(String fieldName, String[] signatures) {
ParameterizedTypeName fieldType =
ParameterizedTypeName.get(ClassName.get(List.class), ClassName.get(String.class));
return FieldSpec.builder(fieldType, fieldName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer(CodeBlock.builder()
.addStatement(String.format("List.of(%s)", placeholders(signatures)), signatures.toArray())
.addStatement(String.format("List.of(%s)", placeholders(signatures)), (Object[]) signatures)
.build())
.build();
}

private String placeholders(List<String> signatures) {
private String placeholders(String[] signatures) {
// FIXME: find a way to manage the indentation automatically
return signatures.stream().map((ignored) -> "$S").collect(Collectors.joining(",\n\t\t"));
return Arrays.stream(signatures).map((ignored) -> "$S").collect(Collectors.joining(",\n\t\t"));
}
}
60 changes: 60 additions & 0 deletions processor/src/main/java/apoc/processor/ProcedureServiceWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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 apoc.processor;

import static javax.tools.StandardLocation.CLASS_OUTPUT;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.annotation.processing.Filer;
import org.neo4j.procedure.Procedure;

public class ProcedureServiceWriter {
private final Filer filer;

public ProcedureServiceWriter(Filer filer) {
this.filer = filer;
}

public void write(List<SignatureVisitor.Signature> signatures) {
final var classNames = signatures.stream()
.map(SignatureVisitor.Signature::className)
.distinct()
.sorted()
.toList();
try {
writeProcedureClasses(classNames);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void writeProcedureClasses(Iterable<String> classNames) throws IOException {
final var path = "META-INF/services/" + Procedure.class.getCanonicalName();
var file = filer.createResource(CLASS_OUTPUT, "", path);

try (var writer =
new PrintWriter(new BufferedOutputStream(file.openOutputStream()), true, StandardCharsets.UTF_8)) {
for (final var name : classNames) writer.println(name);
}
}
}
65 changes: 29 additions & 36 deletions processor/src/main/java/apoc/processor/SignatureVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
package apoc.processor;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor9;
import javax.tools.Diagnostic;
Expand All @@ -34,72 +35,64 @@
import org.neo4j.procedure.UserAggregationFunction;
import org.neo4j.procedure.UserFunction;

public class SignatureVisitor extends SimpleElementVisitor9<Map<String, List<QueryLanguage>>, Void> {
public class SignatureVisitor extends SimpleElementVisitor9<SignatureVisitor.Signature, Void> {

private final Elements elementUtils;

private final Messager messager;

public SignatureVisitor(Elements elementUtils, Messager messager) {

this.elementUtils = elementUtils;
this.messager = messager;
}

@Override
public Map<String, List<QueryLanguage>> visitExecutable(ExecutableElement method, Void unused) {
return Map.of(
getAnnotationName(method)
.orElse(String.format("%s.%s", elementUtils.getPackageOf(method), method.getSimpleName())),
getCypherScopes(method));
public Signature visitExecutable(ExecutableElement method, Void unused) {
final var isProcedure = method.getAnnotation(Procedure.class) != null;
final var className =
((TypeElement) method.getEnclosingElement()).getQualifiedName().toString();
final var name = getProcedureName(method)
.or(() -> getUserFunctionName(method))
.or(() -> getUserAggregationFunctionName(method))
.orElse("%s.%s".formatted(elementUtils.getPackageOf(method), method.getSimpleName()));
return new Signature(name, isProcedure, cypherScopes(method), className);
}

@Override
public Map<String, List<QueryLanguage>> visitUnknown(Element e, Void unused) {
public Signature visitUnknown(Element e, Void unused) {
messager.printMessage(Diagnostic.Kind.ERROR, "unexpected .....");
return super.visitUnknown(e, unused);
}

private Optional<String> getAnnotationName(ExecutableElement method) {
return getProcedureName(method)
.or(() -> getUserFunctionName(method))
.or(() -> getUserAggregationFunctionName(method));
}

private List<QueryLanguage> getCypherScopes(ExecutableElement method) {
return Optional.ofNullable(method.getAnnotation(QueryLanguageScope.class))
.map(annotation -> {
QueryLanguage[] scope = annotation.scope();
return scope.length > 0
? Arrays.asList(scope)
: List.of(QueryLanguage.CYPHER_5, QueryLanguage.CYPHER_25);
})
.orElse(List.of(QueryLanguage.CYPHER_5, QueryLanguage.CYPHER_25));
private Set<QueryLanguage> cypherScopes(ExecutableElement method) {
final var annotation = method.getAnnotation(QueryLanguageScope.class);
if (annotation != null && annotation.scope().length > 0) {
return EnumSet.copyOf(Arrays.asList(annotation.scope()));
} else {
return QueryLanguage.ALL;
}
}

private Optional<String> getProcedureName(ExecutableElement method) {
return Optional.ofNullable(method.getAnnotation(Procedure.class))
.map((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()))
.flatMap(this::blankToEmpty);
.flatMap((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()));
}

private Optional<String> getUserFunctionName(ExecutableElement method) {
return Optional.ofNullable(method.getAnnotation(UserFunction.class))
.map((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()))
.flatMap(this::blankToEmpty);
.flatMap((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()));
}

private Optional<String> getUserAggregationFunctionName(ExecutableElement method) {
return Optional.ofNullable(method.getAnnotation(UserAggregationFunction.class))
.map((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()))
.flatMap(this::blankToEmpty);
.flatMap((annotation) -> pickFirstNonBlank(annotation.name(), annotation.value()));
}

private Optional<String> blankToEmpty(String s) {
return s.isBlank() ? Optional.empty() : Optional.of(s);
private Optional<String> pickFirstNonBlank(String name, String value) {
if (!name.isBlank()) return Optional.of(name);
else if (!value.isBlank()) return Optional.of(value);
else return Optional.empty();
}

private String pickFirstNonBlank(String name, String value) {
return name.isBlank() ? value : name;
}
public record Signature(String name, boolean isProcedure, Set<QueryLanguage> scope, String className) {}
}
Loading
Loading