diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java index cc7cd5f44..9b28b0954 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java @@ -1,11 +1,14 @@ package io.avaje.http.generator.client; +import static io.avaje.http.generator.core.ProcessingContext.createMetaInfWriter; import static io.avaje.http.generator.core.ProcessingContext.logError; import static io.avaje.http.generator.core.ProcessingContext.platform; import static io.avaje.http.generator.core.ProcessingContext.setPlatform; import static io.avaje.http.generator.core.ProcessingContext.typeElement; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -15,10 +18,13 @@ import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; import io.avaje.http.generator.core.APContext; import io.avaje.http.generator.core.ClientPrism; +import io.avaje.http.generator.core.Constants; import io.avaje.http.generator.core.ControllerReader; import io.avaje.http.generator.core.ImportPrism; import io.avaje.http.generator.core.ProcessingContext; @@ -29,9 +35,9 @@ public class ClientProcessor extends AbstractProcessor { private final ComponentMetaData metaData = new ComponentMetaData(); + private final Map privateMetaData = new HashMap<>(); private SimpleComponentWriter componentWriter; - private boolean readModuleInfo; @Override @@ -76,7 +82,7 @@ private void readModule() { return; } readModuleInfo = true; - new ComponentReader(metaData).read(); + new ComponentReader(metaData, privateMetaData).read(); } private void writeForImported(Element importedElement) { @@ -91,39 +97,65 @@ private void writeClient(Element controller) { final ControllerReader reader = new ControllerReader((TypeElement) controller); reader.read(false); try { - metaData.add(writeClientAdapter(reader)); + var packagePrivate = + !controller.getModifiers().contains(Modifier.PUBLIC) + && ClientPrism.isPresent(controller); + if (packagePrivate) { + var packageName = APContext.elements().getPackageOf(controller).getQualifiedName().toString(); + var meta = privateMetaData.computeIfAbsent(packageName, k -> new ComponentMetaData()); + meta.add(writeClientAdapter(reader, true)); + } else { + metaData.add(writeClientAdapter(reader, false)); + } + } catch (final Exception e) { logError(reader.beanType(), "Failed to write client class " + e); } } } - protected String writeClientAdapter(ControllerReader reader) throws IOException { + protected String writeClientAdapter(ControllerReader reader, boolean packagePrivate) throws IOException { var suffix = ClientSuffix.fromInterface(reader.beanType().getQualifiedName().toString()); - return new ClientWriter(reader, suffix).write(); - } - - private void initialiseComponent() { - metaData.initialiseFullName(); - if (!metaData.all().isEmpty()) { - ProcessingContext.addClientComponent(metaData.fullName()); - ProcessingContext.validateModule(); - } - try { - componentWriter.init(); - } catch (final IOException e) { - logError("Error creating writer for JsonbComponent", e); - } + return new ClientWriter(reader, suffix, packagePrivate).write(); } private void writeComponent(boolean processingOver) { - initialiseComponent(); if (processingOver) { try { - componentWriter.write(); + if (!metaData.all().isEmpty()) { + ProcessingContext.addClientComponent(metaData.fullName()); + componentWriter.init(); + componentWriter.write(); + } + + for (var meta : privateMetaData.values()) { + ProcessingContext.addClientComponent(meta.fullName()); + var writer = new SimpleComponentWriter(meta); + writer.init(); + writer.write(); + } + writeMetaInf(); + ProcessingContext.validateModule(); } catch (final IOException e) { logError("Error writing component", e); } } } + + void writeMetaInf() throws IOException { + final FileObject fileObject = createMetaInfWriter(Constants.META_INF_COMPONENT); + if (fileObject != null) { + try (var fileWriter = fileObject.openWriter()) { + if (!metaData.all().isEmpty()) { + fileWriter.write(metaData.fullName()); + fileWriter.write("\n"); + } + + for (var meta : privateMetaData.values()) { + fileWriter.write(meta.fullName()); + fileWriter.write("\n"); + } + } + } + } } diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientWriter.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientWriter.java index 115b69395..f49188b3f 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientWriter.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientWriter.java @@ -1,6 +1,5 @@ package io.avaje.http.generator.client; -import io.avaje.http.generator.core.APContext; import io.avaje.http.generator.core.BaseControllerWriter; import io.avaje.http.generator.core.ClientPrism; import io.avaje.http.generator.core.ControllerReader; @@ -27,9 +26,12 @@ final class ClientWriter extends BaseControllerWriter { private final Set propertyConstants = new HashSet<>(); private final String suffix; - ClientWriter(ControllerReader reader, String suffix) throws IOException { + private final boolean packagePrivate; + + ClientWriter(ControllerReader reader, String suffix, boolean packagePrivate) throws IOException { super(reader, suffix); this.suffix = suffix; + this.packagePrivate = packagePrivate; reader.addImportType(HTTP_CLIENT); readMethods(); } @@ -76,12 +78,12 @@ private void writeMethods() { private void writeClassStart() { writer.append(AT_GENERATED).eol(); AnnotationUtil.writeAnnotations(writer, reader.beanType()); - - writer.append("public final class %s%s implements %s, AutoCloseable {", shortName, suffix, shortName).eol().eol(); + var access = packagePrivate ? "" : "public "; + writer.append("%sfinal class %s%s implements %s, AutoCloseable {", access, shortName, suffix, shortName).eol().eol(); writer.append(" private final HttpClient client;").eol().eol(); - writer.append(" public %s%s(HttpClient client) {", shortName, suffix).eol(); + writer.append(" %s%s%s(HttpClient client) {", access, shortName, suffix).eol(); writer.append(" this.client = client;").eol(); writer.append(" }").eol().eol(); } diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentMetaData.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentMetaData.java index ac0866394..28b1ec99e 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentMetaData.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentMetaData.java @@ -4,7 +4,7 @@ final class ComponentMetaData { - private final List generatedClients = new ArrayList<>(); + private final Set generatedClients = new HashSet<>(); private String fullName; @Override @@ -12,11 +12,6 @@ public String toString() { return generatedClients.toString(); } - /** Ensure the component name has been initialised. */ - void initialiseFullName() { - fullName(); - } - void add(String type) { generatedClients.add(type); } @@ -28,13 +23,13 @@ void setFullName(String fullName) { String fullName() { if (fullName == null) { String topPackage = TopPackage.of(generatedClients); - fullName = topPackage + ".GeneratedHttpComponent"; + fullName = topPackage + "." + name(topPackage) + "HttpComponent"; } return fullName; } List all() { - return generatedClients; + return new ArrayList<>(generatedClients); } /** Return the package imports for the JsonAdapters and related types. */ @@ -46,4 +41,37 @@ Collection allImports() { return packageImports; } + + + static String name(String name) { + if (name == null) { + return null; + } + final int pos = name.lastIndexOf('.'); + if (pos > -1) { + name = name.substring(pos + 1); + } + return camelCase(name).replaceFirst("Httpclient", "Generated"); + } + + private static String camelCase(String name) { + StringBuilder sb = new StringBuilder(name.length()); + boolean upper = true; + for (char aChar : name.toCharArray()) { + if (Character.isLetterOrDigit(aChar)) { + if (upper) { + aChar = Character.toUpperCase(aChar); + upper = false; + } + sb.append(aChar); + } else if (toUpperOn(aChar)) { + upper = true; + } + } + return sb.toString(); + } + + private static boolean toUpperOn(char aChar) { + return aChar == ' ' || aChar == '-' || aChar == '_'; + } } diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentReader.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentReader.java index b6d335af1..086103fff 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentReader.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ComponentReader.java @@ -1,8 +1,10 @@ package io.avaje.http.generator.client; + import static io.avaje.http.generator.core.ProcessingContext.filer; import static io.avaje.http.generator.core.ProcessingContext.logDebug; import static io.avaje.http.generator.core.ProcessingContext.logWarn; import static io.avaje.http.generator.core.ProcessingContext.typeElement; +import static java.util.stream.Collectors.toList; import java.io.FileNotFoundException; import java.io.LineNumberReader; @@ -11,14 +13,15 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.annotation.processing.FilerException; -import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeMirror; import javax.tools.FileObject; import javax.tools.StandardLocation; +import io.avaje.http.generator.core.APContext; import io.avaje.http.generator.core.Constants; import io.avaje.prism.GeneratePrism; @@ -26,42 +29,44 @@ final class ComponentReader { private final ComponentMetaData componentMetaData; + private final Map privateMetaData; - ComponentReader(ComponentMetaData metaData) { + ComponentReader(ComponentMetaData metaData, Map privateMetaData) { this.componentMetaData = metaData; + this.privateMetaData = privateMetaData; } void read() { - final String componentFullName = loadMetaInfServices(); - if (componentFullName != null) { - final TypeElement moduleType = typeElement(componentFullName); + for (String fqn : loadMetaInf()) { + final TypeElement moduleType = typeElement(fqn); if (moduleType != null) { - componentMetaData.setFullName(componentFullName); - readMetaData(moduleType); - } - } - } + var adapters = + MetaDataPrism.getInstanceOn(moduleType).value().stream() + .map(APContext::asTypeElement) + .collect(toList()); - /** Read the existing JsonAdapters from the MetaData annotation of the generated component. */ - private void readMetaData(TypeElement moduleType) { - for (final AnnotationMirror annotationMirror : moduleType.getAnnotationMirrors()) { - MetaDataPrism.getOptional(annotationMirror).map(MetaDataPrism::value).stream() - .flatMap(List::stream) - .map(TypeMirror::toString) - .forEach(componentMetaData::add); - } - } + if (adapters.get(0).getModifiers().contains(Modifier.PUBLIC)) { + componentMetaData.setFullName(fqn); + adapters.stream() + .map(TypeElement::getQualifiedName) + .map(Object::toString) + .forEach(componentMetaData::add); - private String loadMetaInfServices() { - final List lines = loadMetaInf(); - return lines.isEmpty() ? null : lines.get(0); + } else { + var packageName = APContext.elements().getPackageOf(moduleType).getQualifiedName().toString(); + var meta = privateMetaData.computeIfAbsent(packageName, k -> new ComponentMetaData()); + adapters.stream() + .map(TypeElement::getQualifiedName) + .map(Object::toString) + .forEach(meta::add); + } + } + } } private List loadMetaInf() { try { - final FileObject fileObject = filer() - .getResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_COMPONENT); - + final FileObject fileObject = filer().getResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_COMPONENT); if (fileObject != null) { final List lines = new ArrayList<>(); final Reader reader = fileObject.openReader(true); diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/SimpleComponentWriter.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/SimpleComponentWriter.java index 9a5f05809..8ffd70de3 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/SimpleComponentWriter.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/SimpleComponentWriter.java @@ -46,16 +46,6 @@ void write() throws IOException { writeRegister(); writeClassEnd(); writer.close(); - writeMetaInf(); - } - - void writeMetaInf() throws IOException { - final FileObject fileObject = createMetaInfWriter(Constants.META_INF_COMPONENT); - if (fileObject != null) { - try (var fileWriter = fileObject.openWriter()) { - fileWriter.write(fullName); - } - } } private void writeRegister() { diff --git a/http-generator-client/src/test/java/io/avaje/http/generator/client/ComponentMetaDataTest.java b/http-generator-client/src/test/java/io/avaje/http/generator/client/ComponentMetaDataTest.java new file mode 100644 index 000000000..116cbdf60 --- /dev/null +++ b/http-generator-client/src/test/java/io/avaje/http/generator/client/ComponentMetaDataTest.java @@ -0,0 +1,18 @@ +package io.avaje.http.generator.client; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ComponentMetaDataTest { + + @Test + void name() { + assertThat(ComponentMetaData.name(null)).isNull(); + assertThat(ComponentMetaData.name("org.foo")).isEqualTo("Foo"); + assertThat(ComponentMetaData.name("org.fooBar")).isEqualTo("FooBar"); + assertThat(ComponentMetaData.name("org.FooBar")).isEqualTo("FooBar"); + assertThat(ComponentMetaData.name("org.FooBarHttpclient")).isEqualTo("FooBarGenerated"); + assertThat(ComponentMetaData.name("org.FooBarHttpclientAgainHttpclient")).isEqualTo("FooBarGeneratedAgainHttpclient"); + } +} diff --git a/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient.java b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient.java new file mode 100644 index 000000000..06fc0b735 --- /dev/null +++ b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient.java @@ -0,0 +1,13 @@ +package io.avaje.http.generator.client.clients; + +import io.avaje.http.api.Client; +import io.avaje.http.api.Get; +import io.avaje.http.api.Header; + +@Client +interface PrivateClient { + + @Get("/private") + String apiCall(@Header("Accept") String accept); + +} diff --git a/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient2.java b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient2.java new file mode 100644 index 000000000..407fa636e --- /dev/null +++ b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/PrivateClient2.java @@ -0,0 +1,13 @@ +package io.avaje.http.generator.client.clients; + +import io.avaje.http.api.Client; +import io.avaje.http.api.Get; +import io.avaje.http.api.Header; + +@Client +public interface PrivateClient2 { + + @Get("/private") + String apiCall(@Header("Accept") String accept); + +} diff --git a/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/other/PrivateClient2.java b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/other/PrivateClient2.java new file mode 100644 index 000000000..f3bfe589d --- /dev/null +++ b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/other/PrivateClient2.java @@ -0,0 +1,13 @@ +package io.avaje.http.generator.client.clients.other; + +import io.avaje.http.api.Client; +import io.avaje.http.api.Get; +import io.avaje.http.api.Header; + +@Client +interface PrivateClient2 { + + @Get("/private") + String apiCall(@Header("Accept") String accept); + +} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java index f15a9ebc0..c865caa14 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Paths; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -49,7 +50,7 @@ private static final class Ctx { private final boolean instrumentAllMethods; private final boolean disableDirectWrites; private final boolean javalin6; - private String clientFQN; + private final Set clientFQN = new HashSet<>(); Ctx(ProcessingEnvironment env, PlatformAdapter adapter, boolean generateOpenAPI) { readAdapter = adapter; @@ -243,9 +244,8 @@ public static void validateModule() { logWarn(module, "io.avaje.http.api.javalin only contains SOURCE retention annotations. It should added as `requires static`"); } }); - var fqn = CTX.get().clientFQN; - reader.validateServices("io.avaje.http.client.HttpClient.GeneratedComponent", Set.of(fqn)); + reader.validateServices("io.avaje.http.client.HttpClient.GeneratedComponent", CTX.get().clientFQN); } catch (Exception e) { // can't read module @@ -285,6 +285,6 @@ private static boolean resourceExists(String relativeName) { } public static void addClientComponent(String clientFQN) { - CTX.get().clientFQN = clientFQN; + CTX.get().clientFQN.add(clientFQN); } } diff --git a/tests/test-client-generation/src/main/java/org/example/pkgprivate/PrivateClient.java b/tests/test-client-generation/src/main/java/org/example/pkgprivate/PrivateClient.java new file mode 100644 index 000000000..cb9511b44 --- /dev/null +++ b/tests/test-client-generation/src/main/java/org/example/pkgprivate/PrivateClient.java @@ -0,0 +1,13 @@ +package org.example.pkgprivate; + +import io.avaje.http.api.Client; +import io.avaje.http.api.Get; +import io.avaje.http.api.Header; + +@Client +interface PrivateClient { + + @Get("/private") + String apiCall(@Header("Accept") String accept); + +} diff --git a/tests/test-client/src/main/java/example/github/pkgprivate/SimplePkgPrivate.java b/tests/test-client/src/main/java/example/github/pkgprivate/SimplePkgPrivate.java new file mode 100644 index 000000000..674f1ccb8 --- /dev/null +++ b/tests/test-client/src/main/java/example/github/pkgprivate/SimplePkgPrivate.java @@ -0,0 +1,12 @@ +package example.github.pkgprivate; + +import io.avaje.http.api.Client; +import io.avaje.http.api.Get; +import io.avaje.http.client.HttpException; + +@Client +interface SimplePkgPrivate { + + @Get("private") + String get() throws HttpException; +} diff --git a/tests/test-client/src/main/java/module-info.java b/tests/test-client/src/main/java/module-info.java index a925d770c..a83254a65 100644 --- a/tests/test-client/src/main/java/module-info.java +++ b/tests/test-client/src/main/java/module-info.java @@ -7,5 +7,6 @@ exports example.github; - provides io.avaje.http.client.HttpClient.GeneratedComponent with example.github.httpclient.GeneratedHttpComponent; + provides io.avaje.http.client.HttpClient.GeneratedComponent + with example.github.httpclient.GeneratedHttpComponent, example.github.pkgprivate.PkgprivateHttpComponent; } diff --git a/tests/test-client/src/main/resources/META-INF/services/io.avaje.http.client.HttpClient$GeneratedComponent b/tests/test-client/src/main/resources/META-INF/services/io.avaje.http.client.HttpClient$GeneratedComponent deleted file mode 100644 index d419efe24..000000000 --- a/tests/test-client/src/main/resources/META-INF/services/io.avaje.http.client.HttpClient$GeneratedComponent +++ /dev/null @@ -1 +0,0 @@ -example.github.httpclient.GeneratedHttpComponent \ No newline at end of file diff --git a/tests/test-client/src/test/java/example/github/pkgprivate/PkgPrivateTest.java b/tests/test-client/src/test/java/example/github/pkgprivate/PkgPrivateTest.java new file mode 100644 index 000000000..54ce01373 --- /dev/null +++ b/tests/test-client/src/test/java/example/github/pkgprivate/PkgPrivateTest.java @@ -0,0 +1,43 @@ +package example.github.pkgprivate; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.avaje.http.client.HttpClient; +import io.avaje.jex.Jex; + +class PkgPrivateTest { + + static Jex.Server server = null; + String url = "http://localhost:" + server.port(); + + @BeforeAll + static void startServer() { + server = Jex.create().get("/private", ctx -> ctx.text("myPrivateResult")).port(0).start(); + } + + @AfterAll + static void stop() { + server.shutdown(); + } + + @Test + void test_create() { + final HttpClient client = HttpClient.builder().baseUrl("https://api.github.com").build(); + + final var simple = client.create(SimplePkgPrivate.class); + assertThat(simple).isNotNull(); + } + + @Test + void test_pkg_private() { + final HttpClient client = HttpClient.builder().baseUrl(url).build(); + + final var simple = client.create(SimplePkgPrivate.class); + final var result = simple.get(); + assertThat(result).isEqualTo("myPrivateResult"); + } +}