From 8c0bc638cb4be2b450ab4938317e16a83d8b7cb7 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Thu, 25 Jan 2024 11:52:48 +0100 Subject: [PATCH] 4.x: Inject update - collateral (#8281) * Introduction of codegen modules * Refactoring of modules to use new builder codegen (remove use of config metadata for builders) * Deprecated old processor modules (such as builder processor) Co-authored-by: Daniel Kec Co-authored-by: Jeff Trent Co-authored-by: David Kral Signed-off-by: Tomas Langer --- all/pom.xml | 28 + bom/pom.xml | 43 +- .../java/io/helidon/builder/api/Option.java | 21 +- .../io/helidon/builder/api/Prototype.java | 43 +- builder/codegen/pom.xml | 58 ++ .../codegen/AnnotationDataBlueprint.java | 33 + .../codegen/AnnotationDataConfigured.java | 41 + .../builder/codegen/AnnotationDataOption.java | 375 +++++++ .../builder/codegen/BuilderCodegen.java | 439 ++++++++ .../codegen/BuilderCodegenProvider.java | 51 + .../builder/codegen/CustomConstant.java | 23 + .../builder/codegen/CustomMethods.java | 359 +++++++ .../builder/codegen/DeprecationData.java | 69 ++ .../builder/codegen/FactoryMethods.java | 396 ++++++++ .../codegen/GenerateAbstractBuilder.java | 947 ++++++++++++++++++ .../builder/codegen/GenerateBuilder.java | 98 ++ .../builder/codegen/MethodSignature.java | 32 + .../builder/codegen/PrototypeProperty.java | 189 ++++ .../helidon/builder/codegen/TypeContext.java | 436 ++++++++ .../helidon/builder/codegen/TypeHandler.java | 629 ++++++++++++ .../codegen/TypeHandlerCollection.java | 453 +++++++++ .../builder/codegen/TypeHandlerList.java | 30 + .../builder/codegen/TypeHandlerMap.java | 501 +++++++++ .../builder/codegen/TypeHandlerOptional.java | 267 +++++ .../builder/codegen/TypeHandlerSet.java | 36 + .../builder/codegen/TypeHandlerSupplier.java | 210 ++++ .../io/helidon/builder/codegen/Types.java | 79 ++ .../builder/codegen/ValidationTask.java | 409 ++++++++ .../helidon/builder/codegen/package-info.java | 20 + .../codegen/src/main/java/module-info.java | 31 + builder/pom.xml | 1 + builder/processor/pom.xml | 2 + .../builder/processor/BlueprintProcessor.java | 6 +- .../builder/processor/package-info.java | 6 +- .../processor/src/main/java/module-info.java | 6 +- builder/tests/builder/pom.xml | 22 +- .../test/testsubjects/RuntimeTypeExample.java | 47 + .../RuntimeTypeExampleConfigBlueprint.java | 24 + .../RuntimeTypeExampleInterface.java | 51 + ...meTypeExampleInterfaceConfigBlueprint.java | 24 + .../builder/test/RuntimeTypeExampleTest.java | 36 + builder/tests/common-types/pom.xml | 22 +- .../common/types/TypeInfoBlueprint.java | 12 +- .../io/helidon/common/types/TypeNames.java | 7 +- codegen/README.md | 57 ++ codegen/apt/pom.xml | 55 + .../codegen/apt/AptAnnotationFactory.java | 112 +++ .../io/helidon/codegen/apt/AptContext.java | 47 + .../helidon/codegen/apt/AptContextImpl.java | 160 +++ .../java/io/helidon/codegen/apt/AptFiler.java | 102 ++ .../io/helidon/codegen/apt/AptLogger.java | 128 +++ .../io/helidon/codegen/apt/AptOptions.java | 69 ++ .../io/helidon/codegen/apt/AptProcessor.java | 180 ++++ .../helidon/codegen/apt/AptTypeFactory.java | 206 ++++ .../codegen/apt/AptTypeInfoFactory.java | 561 +++++++++++ .../codegen/apt/ToAnnotationValueVisitor.java | 177 ++++ .../io/helidon/codegen/apt/package-info.java | 24 + codegen/apt/src/main/java/module-info.java | 31 + codegen/class-model/pom.xml | 51 + .../classmodel/AnnotatedComponent.java | 117 +++ .../codegen/classmodel/Annotation.java | 215 ++++ .../classmodel/AnnotationParameter.java | 120 +++ .../helidon/codegen/classmodel/ClassBase.java | 691 +++++++++++++ .../codegen/classmodel/ClassModel.java | 214 ++++ .../classmodel/ClassModelException.java | 27 + .../helidon/codegen/classmodel/ClassType.java | 41 + .../codegen/classmodel/CommonComponent.java | 224 +++++ .../codegen/classmodel/ConcreteType.java | 190 ++++ .../codegen/classmodel/Constructor.java | 91 ++ .../helidon/codegen/classmodel/Content.java | 210 ++++ .../codegen/classmodel/ContentBuilder.java | 187 ++++ .../codegen/classmodel/ContentSupport.java | 187 ++++ .../classmodel/DescribableComponent.java | 129 +++ .../codegen/classmodel/Executable.java | 273 +++++ .../io/helidon/codegen/classmodel/Field.java | 281 ++++++ .../codegen/classmodel/ImportOrganizer.java | 285 ++++++ .../codegen/classmodel/ImportSorter.java | 51 + .../codegen/classmodel/InnerClass.java | 66 ++ .../helidon/codegen/classmodel/Javadoc.java | 565 +++++++++++ .../codegen/classmodel/JavadocParser.java | 143 +++ .../io/helidon/codegen/classmodel/Method.java | 386 +++++++ .../codegen/classmodel/ModelComponent.java | 62 ++ .../codegen/classmodel/ModelWriter.java | 85 ++ .../helidon/codegen/classmodel/Parameter.java | 143 +++ .../helidon/codegen/classmodel/Returns.java | 88 ++ .../io/helidon/codegen/classmodel/Throws.java | 88 ++ .../io/helidon/codegen/classmodel/Type.java | 108 ++ .../codegen/classmodel/TypeArgument.java | 299 ++++++ .../codegen/classmodel/package-info.java | 20 + .../src/main/java/module-info.java | 25 + .../classmodel/ImportOrganizerTest.java | 62 ++ .../classmodel/TestContentBuilder.java | 98 ++ .../helidon/codegen/classmodel/TypeTest.java | 75 ++ .../codegen/classmodel/TypesCodegenTest.java | 96 ++ codegen/codegen/pom.xml | 58 ++ .../java/io/helidon/codegen/ClassCode.java | 31 + .../main/java/io/helidon/codegen/Codegen.java | 311 ++++++ .../io/helidon/codegen/CodegenContext.java | 147 +++ .../helidon/codegen/CodegenContextBase.java | 163 +++ .../codegen/CodegenContextDelegate.java | 110 ++ .../java/io/helidon/codegen/CodegenEvent.java | 460 +++++++++ .../codegen/CodegenEventBlueprint.java | 94 ++ .../io/helidon/codegen/CodegenException.java | 114 +++ .../java/io/helidon/codegen/CodegenFiler.java | 90 ++ .../io/helidon/codegen/CodegenLogger.java | 54 + .../io/helidon/codegen/CodegenOptions.java | 158 +++ .../java/io/helidon/codegen/CodegenScope.java | 52 + .../java/io/helidon/codegen/CodegenUtil.java | 117 +++ .../io/helidon/codegen/CopyrightHandler.java | 59 ++ .../codegen/ElementInfoPredicates.java | 199 ++++ .../codegen/GeneratedAnnotationHandler.java | 84 ++ .../java/io/helidon/codegen/IndentType.java | 47 + .../io/helidon/codegen/ListOptionImpl.java | 95 ++ .../java/io/helidon/codegen/ModuleInfo.java | 735 ++++++++++++++ .../helidon/codegen/ModuleInfoRequires.java | 27 + .../codegen/ModuleInfoSourceParser.java | 529 ++++++++++ .../main/java/io/helidon/codegen/Option.java | 173 ++++ .../java/io/helidon/codegen/OptionImpl.java | 80 ++ .../java/io/helidon/codegen/RoundContext.java | 88 ++ .../io/helidon/codegen/RoundContextImpl.java | 110 ++ .../io/helidon/codegen/SetOptionImpl.java | 96 ++ .../java/io/helidon/codegen/SystemLogger.java | 30 + .../helidon/codegen/TypeInfoFactoryBase.java | 169 ++++ .../java/io/helidon/codegen/package-info.java | 27 + .../helidon/codegen/spi/AnnotationMapper.java | 48 + .../codegen/spi/AnnotationMapperProvider.java | 33 + .../helidon/codegen/spi/CodegenExtension.java | 41 + .../codegen/spi/CodegenExtensionProvider.java | 36 + .../helidon/codegen/spi/CodegenProvider.java | 60 ++ .../codegen/spi/CopyrightProvider.java | 34 + .../io/helidon/codegen/spi/ElementMapper.java | 44 + .../codegen/spi/ElementMapperProvider.java | 34 + .../spi/GeneratedAnnotationProvider.java | 41 + .../io/helidon/codegen/spi/TypeMapper.java | 46 + .../codegen/spi/TypeMapperProvider.java | 33 + .../io/helidon/codegen/spi/package-info.java | 27 + .../codegen/src/main/java/module-info.java | 34 + .../io/helidon/codegen/CodegenUtilTest.java | 47 + codegen/compiler/pom.xml | 99 ++ .../io/helidon/codegen/compiler/Compiler.java | 42 + .../compiler/CompilerOptionsBlueprint.java | 94 ++ .../io/helidon/codegen/compiler/JavaC.java | 216 ++++ .../codegen/compiler/package-info.java | 23 + .../compiler/src/main/java/module-info.java | 26 + codegen/helidon-copyright/pom.xml | 43 + .../copyright/HelidonCopyrightProvider.java | 56 ++ .../helidon/copyright/package-info.java | 20 + .../src/main/java/module-info.java | 29 + codegen/pom.xml | 44 + codegen/scan/pom.xml | 59 ++ .../codegen/scan/ScanAnnotationFactory.java | 99 ++ .../io/helidon/codegen/scan/ScanContext.java | 33 + .../helidon/codegen/scan/ScanModuleInfo.java | 85 ++ .../helidon/codegen/scan/ScanTypeFactory.java | 53 + .../codegen/scan/ScanTypeInfoFactory.java | 586 +++++++++++ .../io/helidon/codegen/scan/package-info.java | 24 + codegen/scan/src/main/java/module-info.java | 26 + .../java/io/helidon/common/GenericType.java | 6 +- .../java/io/helidon/common/config/Config.java | 4 +- common/configurable/pom.xml | 28 +- common/key-util/pom.xml | 38 +- .../io/helidon/common/pki/KeysBlueprint.java | 10 +- .../common/pki/KeystoreKeysBlueprint.java | 25 +- .../helidon/common/pki/PemKeysBlueprint.java | 16 +- .../key-util/src/main/java/module-info.java | 4 +- .../helidon/common/mapper/MapperManager.java | 3 +- .../processor/classmodel/Annotation.java | 5 +- .../classmodel/AnnotationParameter.java | 5 +- .../processor/classmodel/ClassBase.java | 5 +- .../processor/classmodel/ClassModel.java | 5 +- .../classmodel/ClassModelException.java | 5 +- .../processor/classmodel/ClassType.java | 5 +- .../processor/classmodel/Constructor.java | 5 +- .../common/processor/classmodel/Field.java | 5 +- .../processor/classmodel/InnerClass.java | 5 +- .../common/processor/classmodel/Javadoc.java | 5 +- .../common/processor/classmodel/Method.java | 5 +- .../processor/classmodel/Parameter.java | 5 +- .../common/processor/classmodel/Returns.java | 5 +- .../common/processor/classmodel/Throws.java | 5 +- .../processor/classmodel/TypeArgument.java | 5 +- .../processor/classmodel/package-info.java | 5 +- .../src/main/java/module-info.java | 5 +- .../copyright/HelidonCopyrightProvider.java | 5 +- .../helidon/copyright/package-info.java | 5 +- .../src/main/java/module-info.java | 5 +- .../processor/ElementInfoPredicates.java | 4 +- .../processor/GeneratedAnnotationHandler.java | 5 +- .../common/processor/GeneratorTools.java | 5 +- .../helidon/common/processor/TypeFactory.java | 5 +- .../common/processor/TypeInfoFactory.java | 19 +- .../common/processor/package-info.java | 5 +- .../common/processor/spi/package-info.java | 4 +- .../processor/src/main/java/module-info.java | 5 +- common/socket/pom.xml | 33 +- .../common/socket/SocketOptionsBlueprint.java | 25 +- common/socket/src/main/java/module-info.java | 4 +- common/tls/pom.xml | 39 +- .../common/tls/TlsConfigBlueprint.java | 48 +- common/tls/src/main/java/module-info.java | 3 +- .../io/helidon/common/types/TypeInfo.java | 58 +- .../common/types/TypeInfoBlueprint.java | 10 +- .../common/types/TypeNameBlueprint.java | 11 +- .../helidon/common/types/TypeNameSupport.java | 53 +- .../io/helidon/common/types/TypeNames.java | 65 +- .../common/types/TypedElementInfoTest.java | 83 +- common/uri/pom.xml | 27 +- .../helidon/common/uri/UriInfoBlueprint.java | 14 +- common/uri/src/main/java/module-info.java | 4 +- dbclient/hikari/pom.xml | 34 +- dbclient/jdbc/pom.xml | 35 +- .../jdbc/JdbcParametersConfigBlueprint.java | 25 +- examples/integrations/oci/metrics/pom.xml | 4 + fault-tolerance/fault-tolerance/pom.xml | 38 +- .../faulttolerance/AsyncConfigBlueprint.java | 9 +- .../BulkheadConfigBlueprint.java | 13 +- .../CircuitBreakerConfigBlueprint.java | 18 +- .../faulttolerance/RetryConfigBlueprint.java | 21 +- .../TimeoutConfigBlueprint.java | 13 +- .../src/main/java/module-info.java | 3 +- http/encoding/encoding/pom.xml | 41 +- ...ContentEncodingContextConfigBlueprint.java | 10 +- .../encoding/src/main/java/module-info.java | 3 +- .../io/helidon/http/http2/Http2Headers.java | 6 +- http/media/media/pom.xml | 42 +- .../media/MediaContextConfigBlueprint.java | 15 +- .../media/src/main/java/module-info.java | 5 +- .../configdriven/processor/ConfigBean.java | 23 +- .../cdi/jpa/PersistenceExtension.java | 2 +- .../graal/native-image-extension/pom.xml | 4 + .../integrations/jta/jdbc/JtaConnection.java | 4 +- .../jta/jdbc/LocalXAResource.java | 24 +- .../SecretBundleNodeConfigSource.java | 4 +- integrations/openapi-ui/pom.xml | 33 +- .../openapi/ui/OpenApiUiConfigBlueprint.java | 14 +- .../openapi-ui/src/main/java/module-info.java | 3 +- metrics/api/pom.xml | 38 +- ...rmanceIndicatorMetricsConfigBlueprint.java | 16 +- .../metrics/api/MetricsConfigBlueprint.java | 26 +- .../metrics/api/ScopeConfigBlueprint.java | 16 +- .../metrics/api/ScopingConfigBlueprint.java | 14 +- .../microprofile/config/FieldTypes.java | 4 +- microprofile/openapi/pom.xml | 32 +- .../MpOpenApiManagerConfigBlueprint.java | 11 +- .../openapi/src/main/java/module-info.java | 3 +- openapi/openapi/pom.xml | 33 +- .../OpenApiFeatureConfigBlueprint.java | 26 +- .../openapi/src/main/java/module-info.java | 4 +- pom.xml | 7 +- .../main/java/io/helidon/tracing/Span.java | 3 +- webclient/api/pom.xml | 33 +- .../api/HttpClientConfigBlueprint.java | 43 +- .../api/HttpConfigBaseBlueprint.java | 25 +- .../api/WebClientConfigBlueprint.java | 9 +- ...WebClientCookieManagerConfigBlueprint.java | 14 +- webclient/api/src/main/java/module-info.java | 3 +- webclient/http1/pom.xml | 50 +- .../http1/Http1ClientConfigBlueprint.java | 6 +- .../Http1ClientProtocolConfigBlueprint.java | 25 +- .../http1/src/main/java/module-info.java | 3 +- webclient/http2/pom.xml | 27 +- .../http2/Http2ClientConfigBlueprint.java | 6 +- .../Http2ClientProtocolConfigBlueprint.java | 31 +- .../http2/src/main/java/module-info.java | 3 +- webclient/websocket/pom.xml | 41 +- .../websocket/WsClientConfigBlueprint.java | 10 +- .../WsClientProtocolConfigBlueprint.java | 11 +- .../websocket/src/main/java/module-info.java | 3 +- webserver/access-log/pom.xml | 38 +- webserver/context/pom.xml | 34 +- webserver/cors/pom.xml | 34 +- webserver/grpc/pom.xml | 35 +- .../webserver/grpc/GrpcConfigBlueprint.java | 6 +- webserver/grpc/src/main/java/module-info.java | 3 +- webserver/http2/pom.xml | 41 +- .../webserver/http2/Http2ConfigBlueprint.java | 40 +- .../http2/src/main/java/module-info.java | 3 +- webserver/observe/config/pom.xml | 22 +- webserver/observe/health/pom.xml | 22 +- webserver/observe/info/pom.xml | 22 +- webserver/observe/log/pom.xml | 22 +- webserver/observe/metrics/pom.xml | 22 +- webserver/observe/observe/pom.xml | 34 +- webserver/observe/tracing/pom.xml | 22 +- webserver/security/pom.xml | 34 +- webserver/sse/pom.xml | 36 +- .../webserver/tests/RoutingTestBase.java | 4 +- webserver/webserver/pom.xml | 37 +- .../webserver/http1/Http1ConfigBlueprint.java | 33 +- .../webserver/src/main/java/module-info.java | 4 +- webserver/websocket/pom.xml | 35 +- .../websocket/WsConfigBlueprint.java | 15 +- .../webserver/websocket/WsConnection.java | 4 +- .../websocket/src/main/java/module-info.java | 3 +- 294 files changed, 22724 insertions(+), 902 deletions(-) create mode 100644 builder/codegen/pom.xml create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataBlueprint.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataConfigured.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegenProvider.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/CustomConstant.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/DeprecationData.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/MethodSignature.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java create mode 100644 builder/codegen/src/main/java/io/helidon/builder/codegen/package-info.java create mode 100644 builder/codegen/src/main/java/module-info.java create mode 100644 builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExample.java create mode 100644 builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleConfigBlueprint.java create mode 100644 builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterface.java create mode 100644 builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterfaceConfigBlueprint.java create mode 100644 builder/tests/builder/src/test/java/io/helidon/builder/test/RuntimeTypeExampleTest.java create mode 100644 codegen/README.md create mode 100644 codegen/apt/pom.xml create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptLogger.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptOptions.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java create mode 100644 codegen/apt/src/main/java/io/helidon/codegen/apt/package-info.java create mode 100644 codegen/apt/src/main/java/module-info.java create mode 100644 codegen/class-model/pom.xml create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModelException.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassType.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Constructor.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Content.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportSorter.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/InnerClass.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Javadoc.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/JavadocParser.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelComponent.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelWriter.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Returns.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Throws.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java create mode 100644 codegen/class-model/src/main/java/io/helidon/codegen/classmodel/package-info.java create mode 100644 codegen/class-model/src/main/java/module-info.java create mode 100644 codegen/class-model/src/test/java/io/helidon/codegen/classmodel/ImportOrganizerTest.java create mode 100644 codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TestContentBuilder.java create mode 100644 codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypeTest.java create mode 100644 codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java create mode 100644 codegen/codegen/pom.xml create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ClassCode.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenEvent.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenEventBlueprint.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenException.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenLogger.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenOptions.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenScope.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CodegenUtil.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/CopyrightHandler.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/GeneratedAnnotationHandler.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/IndentType.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ListOptionImpl.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfo.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoRequires.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoSourceParser.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/Option.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/SystemLogger.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/package-info.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapper.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapperProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtension.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtensionProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/CopyrightProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapper.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapperProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/GeneratedAnnotationProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapper.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapperProvider.java create mode 100644 codegen/codegen/src/main/java/io/helidon/codegen/spi/package-info.java create mode 100644 codegen/codegen/src/main/java/module-info.java create mode 100644 codegen/codegen/src/test/java/io/helidon/codegen/CodegenUtilTest.java create mode 100644 codegen/compiler/pom.xml create mode 100644 codegen/compiler/src/main/java/io/helidon/codegen/compiler/Compiler.java create mode 100644 codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java create mode 100644 codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java create mode 100644 codegen/compiler/src/main/java/io/helidon/codegen/compiler/package-info.java create mode 100644 codegen/compiler/src/main/java/module-info.java create mode 100644 codegen/helidon-copyright/pom.xml create mode 100644 codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/HelidonCopyrightProvider.java create mode 100644 codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/package-info.java create mode 100644 codegen/helidon-copyright/src/main/java/module-info.java create mode 100644 codegen/pom.xml create mode 100644 codegen/scan/pom.xml create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/ScanContext.java create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/ScanModuleInfo.java create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeFactory.java create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java create mode 100644 codegen/scan/src/main/java/io/helidon/codegen/scan/package-info.java create mode 100644 codegen/scan/src/main/java/module-info.java diff --git a/all/pom.xml b/all/pom.xml index 36a9d2ebb47..938982eb245 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -385,6 +385,30 @@ io.helidon.common helidon-common-tls + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-apt + + + io.helidon.codegen + helidon-codegen-scan + + + io.helidon.codegen + helidon-codegen-compiler + + + io.helidon.codegen + helidon-codegen-class-model + + + io.helidon.codegen + helidon-codegen-helidon-copyright + io.helidon.common.processor helidon-common-processor @@ -986,6 +1010,10 @@ io.helidon.builder helidon-builder-processor + + io.helidon.builder + helidon-builder-codegen + io.helidon.inject helidon-inject-api diff --git a/bom/pom.xml b/bom/pom.xml index 537d8a795ae..88a10fcf973 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -237,11 +237,6 @@ helidon-security-integration-common ${helidon.version} - - io.helidon.security.integration - helidon-security-integration-webserver - ${helidon.version} - io.helidon.security helidon-security-annotations @@ -507,16 +502,49 @@ ${helidon.version} + io.helidon.codegen + helidon-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-scan + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-compiler + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-class-model + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + io.helidon.common.processor helidon-common-processor ${helidon.version} + io.helidon.common.processor helidon-common-processor-helidon-copyright ${helidon.version} + io.helidon.common.processor helidon-common-processor-class-model ${helidon.version} @@ -1296,6 +1324,11 @@ helidon-builder-processor ${helidon.version} + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + diff --git a/builder/api/src/main/java/io/helidon/builder/api/Option.java b/builder/api/src/main/java/io/helidon/builder/api/Option.java index a7083ffec46..f1ac6d14a0b 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Option.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Option.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -426,4 +426,23 @@ private Option() { */ String value(); } + + /** + * Define an option decorator. + * This is useful for example when setting a compound option, where we need to set additional options on this builder. + *

+ * Decorator on collection based options will be ignored. + * Decorator on optional values must accept an option (as it would be called both from the setter and unset methods). + */ + @Target(ElementType.METHOD) + // note: class retention needed for cases when derived builders are inherited across modules + @Retention(RetentionPolicy.CLASS) + public @interface Decorator { + /** + * Type declaration including generic types (must match the declared generic type on the blueprint). + * + * @return type name with generic declaration + */ + Class> value(); + } } diff --git a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java index 0024730abaf..719e066d03e 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -209,7 +209,7 @@ public interface Factory { public @interface Blueprint { /** * The generated interface is public by default. We can switch it to package local - * by setting this property to {@code false}- + * by setting this property to {@code false}. * * @return whether the generated interface should be public */ @@ -231,9 +231,10 @@ public interface Factory { boolean createFromConfigPublic() default true; /** - * Method create() is public by default. + * Method create() is created whenever there are no fields required (or all have default values). + * This property can disable generation of the method. * - * @return whether factory method create() should be public on prototype + * @return whether factory method create() should be created on prototype */ boolean createEmptyPublic() default true; @@ -309,7 +310,7 @@ public interface Factory { * The builder provides accessors to all types, using {@link java.util.Optional} for any field that is optional, * or any other field unless it has a default value. Primitive types are an exception (unless declared as required). * - * @param the type of the bean builder to intercept + * @param the type of the builder to decorate * @see io.helidon.builder.api.Prototype.Blueprint#decorator() */ @FunctionalInterface @@ -322,6 +323,28 @@ public interface BuilderDecorator { void decorate(T target); } + /** + * Provides a way to decorate a single option when it is applied to the builder. + * The decorator must have an accessible no argument constructor (at least package local). + * + * @param the type of the builder to decorate + * @param the type of the option to decorate + * @see io.helidon.builder.api.Prototype.Blueprint#decorator() + */ + @FunctionalInterface + public interface OptionDecorator { + /** + * Provides the ability to decorate option as it is being set on the target builder. + * This method is invoked from within the setter of the value before the value is set on the builder (i.e. the + * builder still contains previous value). + * Do not call the same setter again from within this method, as it would end in a stack overflow. + * + * @param builder the target builder being decorated + * @param optionValue option value set by the caller of the setter method + */ + void decorate(B builder, T optionValue); + } + /** * Adding this annotation in conjunction with the {@link Prototype.Blueprint} on a target interface * type or method causes the {@link #value()} be added to the generated implementation class and methods respectfully. @@ -407,6 +430,15 @@ public interface BuilderDecorator { public @interface PrototypeMethod { } + /** + * Annotated constant of a custom methods type to be added to prototype interface. + * The constant will be generated as a reference to the annotated constant (so it must be package local). + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.CLASS) + public @interface Constant { + } + /** * Add additional interfaces to implement by the prototype. Provide correct types (fully qualified) for generics. */ @@ -418,6 +450,5 @@ public interface BuilderDecorator { */ String[] value(); } - } diff --git a/builder/codegen/pom.xml b/builder/codegen/pom.xml new file mode 100644 index 00000000000..fd808a5685d --- /dev/null +++ b/builder/codegen/pom.xml @@ -0,0 +1,58 @@ + + + + + + io.helidon.builder + helidon-builder-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-builder-codegen + Helidon Builder Code Generator + + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataBlueprint.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataBlueprint.java new file mode 100644 index 00000000000..f4c2bd6e31c --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataBlueprint.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +record AnnotationDataBlueprint( + boolean prototypePublic, + boolean builderPublic, + boolean createFromConfigPublic, + boolean createEmptyPublic, + boolean isFactory, + Set extendsList, + String javadoc, + List typeArguments) { +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataConfigured.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataConfigured.java new file mode 100644 index 00000000000..82592a67bc6 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataConfigured.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; + +record AnnotationDataConfigured(boolean configured, String rootPrefix, boolean isRoot) { + static AnnotationDataConfigured create(TypeInfo typeInfo) { + boolean configured = false; + boolean isRoot = false; + String prefix = null; + + if (typeInfo.hasAnnotation(Types.PROTOTYPE_CONFIGURED)) { + configured = true; + + Annotation annotation = typeInfo.annotation(Types.PROTOTYPE_CONFIGURED); + // if the annotation is present, the value has to be defined (may be empty string) + prefix = annotation.stringValue().orElse(null); + if (prefix != null) { + isRoot = annotation.booleanValue("root").orElse(true); + } + } + + return new AnnotationDataConfigured(configured, prefix, isRoot); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java new file mode 100644 index 00000000000..8f0576be0ae --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.builder.codegen.Types.BUILDER_DESCRIPTION; +import static io.helidon.builder.codegen.Types.DEPRECATED; +import static io.helidon.builder.codegen.Types.OPTION_ACCESS; +import static io.helidon.builder.codegen.Types.OPTION_ALLOWED_VALUES; +import static io.helidon.builder.codegen.Types.OPTION_CONFIDENTIAL; +import static io.helidon.builder.codegen.Types.OPTION_CONFIGURED; +import static io.helidon.builder.codegen.Types.OPTION_DECORATOR; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_BOOLEAN; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_CODE; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_DOUBLE; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_INT; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_LONG; +import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_METHOD; +import static io.helidon.builder.codegen.Types.OPTION_PROVIDER; +import static io.helidon.builder.codegen.Types.OPTION_REDUNDANT; +import static io.helidon.builder.codegen.Types.OPTION_REQUIRED; +import static io.helidon.builder.codegen.Types.OPTION_SAME_GENERIC; +import static io.helidon.builder.codegen.Types.OPTION_SINGULAR; +import static io.helidon.common.types.TypeNames.LIST; +import static io.helidon.common.types.TypeNames.MAP; +import static io.helidon.common.types.TypeNames.OPTIONAL; +import static io.helidon.common.types.TypeNames.SET; + +record AnnotationDataOption(Javadoc javadoc, + boolean configured, + String configKey, + Boolean configMerge, + AccessModifier accessModifier, + boolean required, + boolean validateNotNull, + boolean provider, + TypeName providerType, + boolean providerDiscoverServices, + boolean singular, + String singularName, + boolean sameGeneric, + boolean equalityRedundant, + boolean toStringRedundant, + boolean confidential, + List allowedValues, + Consumer> defaultValue, + DeprecationData deprecationData, + List annotations, + TypeName decorator // support for decorating an option when setting it +) { + + static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element) { + Javadoc javadoc = javadoc(element); + boolean configured = false; + String configKey = null; + boolean configMerge = false; + AccessModifier accessModifier; + boolean providerBased = false; + TypeName providerType = null; + boolean discoverServices = false; + List allowedValues = null; + boolean singular; + String singularName; + boolean equalityRedundant; + boolean toStringRedundant; + + if (element.hasAnnotation(OPTION_CONFIGURED)) { + Annotation annotation = element.annotation(OPTION_CONFIGURED); + configured = true; + configKey = annotation.stringValue() + .filter(Predicate.not(String::isBlank)) + .orElseGet(() -> toConfigKey(handler.name())); + configMerge = annotation.booleanValue("merge") + .orElse(false); + } + accessModifier = accessModifier(element); + if (element.hasAnnotation(OPTION_PROVIDER)) { + Annotation annotation = element.annotation(OPTION_PROVIDER); + providerBased = true; + providerType = annotation.stringValue().map(TypeName::create).orElseThrow(); + discoverServices = annotation.booleanValue("discoverServices").orElse(true); + } + if (element.hasAnnotation(OPTION_ALLOWED_VALUES)) { + Annotation annotation = element.annotation(OPTION_ALLOWED_VALUES); + allowedValues = annotation.annotationValues() + .orElseGet(List::of) + .stream() + .map(it -> { + String value = it.stringValue().orElseThrow(); + String description = it.stringValue("description").orElseThrow(); + return new AllowedValue(value, description); + }) + .toList(); + } + if (element.hasAnnotation(OPTION_SINGULAR)) { + singular = true; + singularName = element.annotation(OPTION_SINGULAR) + .value() + .filter(Predicate.not(String::isBlank)) + .orElseGet(() -> singularName(handler.name())); + } else { + singular = false; + singularName = null; + } + if (element.hasAnnotation(OPTION_REDUNDANT)) { + Annotation annotation = element.annotation(OPTION_REDUNDANT); + equalityRedundant = annotation.booleanValue("equality").orElse(true); + toStringRedundant = annotation.booleanValue("stringValue").orElse(true); + } else { + equalityRedundant = false; + toStringRedundant = false; + } + + var defaultValue = defaultValue(element, + handler); + + TypeName genericType = handler.declaredType().genericTypeName(); + boolean validateNotNull = shouldValidateNotNull(defaultValue == null, genericType); + + boolean required = element.hasAnnotation(OPTION_REQUIRED); + validateNotNull = validateNotNull || required; + + if (javadoc == null) { + javadoc = element.description() + .map(Javadoc::parse) + .orElseGet(() -> Javadoc.builder() + .addLine("Option " + handler.name()) + .returnDescription(handler.name()) + .build()); + } + + DeprecationData deprecationData = DeprecationData.create(element, javadoc); + + List annotations = new ArrayList<>(); + javadoc = processDeprecation(deprecationData, annotations, javadoc); + + TypeName decorator = optionDecorator(element); + + // default/is required only based on annotations + return new AnnotationDataOption(javadoc, + configured, + configKey, + configMerge, + accessModifier, + required, + validateNotNull, + providerBased, + providerType, + discoverServices, + singular, + singularName, + element.hasAnnotation(OPTION_SAME_GENERIC), + equalityRedundant, + toStringRedundant, + element.hasAnnotation(OPTION_CONFIDENTIAL), + allowedValues, + defaultValue, + deprecationData, + annotations, + decorator); + } + + private static TypeName optionDecorator(TypedElementInfo element) { + if (element.hasAnnotation(OPTION_DECORATOR)) { + return element.annotation(OPTION_DECORATOR).typeValue() + .orElseThrow(() -> new IllegalStateException("There is no value defined on " + + OPTION_DECORATOR.fqName() + + " on element " + + element + + ", even though it is a required property.")); + } + return null; + } + + boolean hasDefault() { + return defaultValue != null; + } + + boolean hasAllowedValues() { + return allowedValues != null && !allowedValues.isEmpty(); + } + + private static AccessModifier accessModifier(TypedElementInfo element) { + return element.findAnnotation(OPTION_ACCESS) + .flatMap(Annotation::stringValue) + .map(it -> it.isBlank() ? AccessModifier.PACKAGE_PRIVATE : AccessModifier.valueOf(it)) + .orElse(AccessModifier.PUBLIC); + } + + private static Javadoc javadoc(TypedElementInfo element) { + if (element.hasAnnotation(BUILDER_DESCRIPTION)) { + return Javadoc.parse(element.annotation(BUILDER_DESCRIPTION).stringValue().orElseThrow()); + } + return null; + } + + private static Consumer> defaultValue(TypedElementInfo element, + TypeHandler handler) { + + List defaultValues = null; + List defaultInts = null; + List defaultLongs = null; + List defaultDoubles = null; + List defaultBooleans = null; + DefaultMethod defaultMethod = null; + String defaultCode = null; + /* + Now all the defaults + */ + if (element.hasAnnotation(OPTION_DEFAULT)) { + Annotation annotation = element.annotation(OPTION_DEFAULT); + defaultValues = annotation.stringValues().orElseGet(List::of); + } + if (element.hasAnnotation(OPTION_DEFAULT_INT)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_INT); + defaultInts = annotation.intValues().orElseGet(List::of); + } + if (element.hasAnnotation(OPTION_DEFAULT_LONG)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_LONG); + defaultLongs = annotation.longValues().orElseGet(List::of); + } + if (element.hasAnnotation(OPTION_DEFAULT_DOUBLE)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_DOUBLE); + defaultDoubles = annotation.doubleValues().orElseGet(List::of); + } + if (element.hasAnnotation(OPTION_DEFAULT_BOOLEAN)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_BOOLEAN); + defaultBooleans = annotation.booleanValues().orElseGet(List::of); + } + if (element.hasAnnotation(OPTION_DEFAULT_METHOD)) { + Annotation annotation = element.annotation(OPTION_DEFAULT_METHOD); + TypeName type = annotation.typeValue("type").orElse(OPTION_DEFAULT_METHOD); + if (OPTION_DEFAULT_METHOD.equals(type)) { + type = handler.declaredType(); + } + String name = annotation.stringValue().orElseThrow(); + defaultMethod = new DefaultMethod(type, name); + } + if (element.hasAnnotation(OPTION_DEFAULT_CODE)) { + defaultCode = element.annotation(OPTION_DEFAULT_CODE).stringValue().orElseThrow(); + } + + boolean noDefault = defaultValues == null + && defaultInts == null + && defaultLongs == null + && defaultDoubles == null + && defaultBooleans == null + && defaultCode == null + && defaultMethod == null; + + if (noDefault) { + return null; + } else { + return handler.toDefaultValue(defaultValues, + defaultInts, + defaultLongs, + defaultDoubles, + defaultBooleans, + defaultCode, + defaultMethod); + } + + } + + private static boolean shouldValidateNotNull(boolean noDefault, TypeName genericType) { + return noDefault + && !( + genericType.equals(OPTIONAL) + || (genericType.primitive() && !genericType.array()) + || genericType.equals(MAP) + || genericType.equals(SET) + || genericType.equals(LIST)); + } + + private static Javadoc processDeprecation(DeprecationData deprecationData, List annotations, Javadoc javadoc) { + if (javadoc == null) { + return null; + } + + if (!deprecationData.deprecated()) { + return javadoc; + } + + io.helidon.common.types.Annotation.Builder deprecated = io.helidon.common.types.Annotation.builder() + .typeName(DEPRECATED); + if (deprecationData.since() != null) { + deprecated.putValue("since", deprecationData.since()); + } + if (deprecationData.forRemoval()) { + deprecated.putValue("forRemoval", true); + } + + if (Annotations.findFirst(DEPRECATED, annotations).isEmpty()) { + annotations.add(deprecated.build()); + } + + if (deprecationData.alternativeOption() != null || deprecationData.description() != null) { + Javadoc.Builder javadocBuilder = Javadoc.builder(javadoc); + if (deprecationData.alternativeOption() == null) { + javadocBuilder.deprecation(deprecationData.description()); + } else { + javadocBuilder.deprecation("use {@link #" + deprecationData.alternativeOption() + "()} instead"); + } + javadoc = javadocBuilder.build(); + } + return javadoc; + } + + private static String singularName(String optionName) { + if (optionName.endsWith("s")) { + return optionName.substring(0, optionName.length() - 1); + } + return optionName; + } + + /* + Method name is camel case (such as maxInitialLineLength) + result is kebab-case (such as max-initial-line-length). + Note that this same method was created in ConfigUtils in common-config, but since this + module should not have any dependencies in it a copy was left here as well. + */ + private static String toConfigKey(String propertyName) { + StringBuilder result = new StringBuilder(); + + char[] chars = propertyName.toCharArray(); + for (char aChar : chars) { + if (Character.isUpperCase(aChar)) { + if (result.isEmpty()) { + result.append(Character.toLowerCase(aChar)); + } else { + result.append('-') + .append(Character.toLowerCase(aChar)); + } + } else { + result.append(aChar); + } + } + + return result.toString(); + } + + record AllowedValue(String value, String description) { + } + + record DefaultMethod(TypeName type, String method) { + } + +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java new file mode 100644 index 00000000000..778058532b0 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.builder.codegen.ValidationTask.ValidateConfiguredType; +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.RoundContext; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.TypeArgument; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.common.Errors; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +import static io.helidon.builder.codegen.Types.RUNTIME_PROTOTYPE; + +class BuilderCodegen implements CodegenExtension { + private static final TypeName GENERATOR = TypeName.create(BuilderCodegen.class); + + // all types annotated with prototyped by (for validation) + private final Set runtimeTypes = new HashSet<>(); + // all blueprint types (for validation) + private final Set blueprintTypes = new HashSet<>(); + + private final CodegenContext ctx; + + BuilderCodegen(CodegenContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(RoundContext roundContext) { + // see need to keep the type names, as some types may not be available, as we are generating them + runtimeTypes.addAll(roundContext.annotatedTypes(Types.RUNTIME_PROTOTYPED_BY) + .stream() + .map(TypeInfo::typeName) + .toList()); + Collection blueprints = roundContext.annotatedTypes(Types.PROTOTYPE_BLUEPRINT); + blueprintTypes.addAll(blueprints.stream() + .map(TypeInfo::typeName) + .toList()); + + List blueprintInterfaces = blueprints.stream() + .filter(it -> it.kind() == ElementKind.INTERFACE) + .toList(); + + for (TypeInfo blueprintInterface : blueprintInterfaces) { + process(roundContext, blueprintInterface); + } + } + + @Override + public void processingOver(RoundContext roundContext) { + process(roundContext); + + // we must collect validation information after all types are generated - so + // we also listen on @Generated, so there is another round of annotation processing where we have all + // types nice and ready + List validationTasks = new ArrayList<>(); + validationTasks.addAll(addRuntimeTypesForValidation(this.runtimeTypes)); + validationTasks.addAll(addBlueprintsForValidation(this.blueprintTypes)); + + Errors.Collector collector = Errors.collector(); + for (ValidationTask task : validationTasks) { + task.validate(collector); + } + + Errors errors = collector.collect(); + if (errors.hasFatal()) { + for (Errors.ErrorMessage error : errors) { + CodegenEvent.Builder builder = CodegenEvent.builder() + .message(error.getMessage().replace('\n', ' ')) + .addObject(error.getSource()); + + switch (error.getSeverity()) { + case FATAL -> builder.level(System.Logger.Level.ERROR); + case WARN -> builder.level(System.Logger.Level.WARNING); + case HINT -> builder.level(System.Logger.Level.INFO); + default -> builder.level(System.Logger.Level.DEBUG); + } + + ctx.logger().log(builder.build()); + } + } + } + + private void process(RoundContext roundContext, TypeInfo blueprint) { + TypeContext typeContext = TypeContext.create(ctx, blueprint); + AnnotationDataBlueprint blueprintDef = typeContext.blueprintData(); + AnnotationDataConfigured configuredData = typeContext.configuredData(); + TypeContext.PropertyData propertyData = typeContext.propertyData(); + TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); + CustomMethods customMethods = typeContext.customMethods(); + + TypeInfo typeInfo = typeInformation.blueprintType(); + TypeName prototype = typeContext.typeInfo().prototype(); + String ifaceName = prototype.className(); + List typeGenericArguments = blueprintDef.typeArguments(); + String typeArgumentString = createTypeArgumentString(typeGenericArguments); + + // prototype interface (with inner class Builder) + ClassModel.Builder classModel = ClassModel.builder() + .type(prototype) + .classType(ElementKind.INTERFACE) + .copyright(CodegenUtil.copyright(GENERATOR, + typeInfo.typeName(), + prototype)); + + String javadocString = blueprintDef.javadoc(); + List typeArguments = new ArrayList<>(); + if (javadocString == null) { + classModel.description("Interface generated from definition. Please add javadoc to the definition interface."); + typeGenericArguments.forEach(arg -> typeArguments.add(TypeArgument.builder() + .token(arg.className()) + .build())); + } else { + Javadoc javadoc = Javadoc.parse(blueprintDef.javadoc()); + classModel.javadoc(javadoc); + typeGenericArguments.forEach(arg -> { + TypeArgument.Builder tokenBuilder = TypeArgument.builder().token(arg.className()); + if (javadoc.genericsTokens().containsKey(arg.className())) { + tokenBuilder.description(javadoc.genericsTokens().get(arg.className())); + } + typeArguments.add(tokenBuilder.build()); + }); + } + typeArguments.forEach(classModel::addGenericArgument); + + if (blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#builder()"); + } + if (!propertyData.hasRequired() && blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { + classModel.addJavadocTag("see", "#create()"); + } + + typeContext.typeInfo() + .annotationsToGenerate() + .forEach(annotation -> classModel.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation))); + + classModel.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + typeInfo.typeName(), + prototype, + "1", + "")); + + if (typeContext.blueprintData().prototypePublic()) { + classModel.accessModifier(AccessModifier.PUBLIC); + } else { + classModel.accessModifier(AccessModifier.PACKAGE_PRIVATE); + } + blueprintDef.extendsList() + .forEach(classModel::addInterface); + + generateCustomConstants(customMethods, classModel); + + TypeName builderTypeName = TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".Builder")) + .typeArguments(prototype.typeArguments()) + .build(); + + + // static Builder builder() + addBuilderMethod(classModel, builderTypeName, typeArguments, ifaceName); + + // static Builder builder(T instance) + addCopyBuilderMethod(classModel, builderTypeName, prototype, typeArguments, ifaceName, typeArgumentString); + + // static T create(Config config) + addCreateFromConfigMethod(blueprintDef, + configuredData, + prototype, + typeArguments, + ifaceName, + typeArgumentString, + classModel); + + // static X create() + addCreateDefaultMethod(blueprintDef, propertyData, classModel, prototype, ifaceName, typeArgumentString, typeArguments); + + generateCustomMethods(customMethods, classModel); + + // abstract class BuilderBase... + GenerateAbstractBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeContext); + // class Builder extends BuilderBase ... + GenerateBuilder.generate(classModel, + typeInformation.prototype(), + typeInformation.runtimeObject().orElseGet(typeInformation::prototype), + typeArguments, + typeContext.blueprintData().isFactory(), + typeContext); + + roundContext.addGeneratedType(prototype, + classModel, + blueprint.typeName(), + blueprint.originatingElement().orElse(blueprint.typeName())); + } + + private static void addCreateDefaultMethod(AnnotationDataBlueprint blueprintDef, + TypeContext.PropertyData propertyData, + ClassModel.Builder classModel, + TypeName prototype, + String ifaceName, + String typeArgumentString, + List typeArguments) { + if (blueprintDef.createEmptyPublic() && blueprintDef.builderPublic()) { + /* + static X create() + */ + if (!propertyData.hasRequired()) { + classModel.addMethod(builder -> { + builder.isStatic(true) + .name("create") + .description("Create a new instance with default values.") + .returnType(prototype, "a new instance") + .addContentLine("return " + ifaceName + "." + typeArgumentString + "builder().buildPrototype();"); + typeArguments.forEach(builder::addGenericArgument); + }); + } + } + } + + private static void addCreateFromConfigMethod(AnnotationDataBlueprint blueprintDef, + AnnotationDataConfigured configuredData, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString, + ClassModel.Builder classModel) { + if (blueprintDef.createFromConfigPublic() && configuredData.configured()) { + Method.Builder method = Method.builder() + .name("create") + .isStatic(true) + .description("Create a new instance from configuration.") + .returnType(prototype, "a new instance configured from configuration") + .addParameter(paramBuilder -> paramBuilder.type(Types.COMMON_CONFIG) + .name("config") + .description("used to configure the new instance")); + typeArguments.forEach(method::addGenericArgument); + if (blueprintDef.builderPublic()) { + method.addContentLine("return " + ifaceName + "." + typeArgumentString + "builder().config(config)" + + ".buildPrototype();"); + } else { + if (typeArguments.isEmpty()) { + method.addContentLine("return new Builder().config(config).build();"); + } else { + method.addContentLine("return new Builder()<>.config(config).build();"); + } + } + classModel.addMethod(method); + } + } + + private static void addCopyBuilderMethod(ClassModel.Builder classModel, + TypeName builderTypeName, + TypeName prototype, + List typeArguments, + String ifaceName, + String typeArgumentString) { + classModel.addMethod(builder -> { + builder.isStatic(true) + .name("builder") + .description("Create a new fluent API builder from an existing instance.") + .returnType(builderTypeName, "a builder based on an instance") + .addParameter(paramBuilder -> paramBuilder.type(prototype) + .name("instance") + .description("an existing instance used as a base for the builder")); + typeArguments.forEach(builder::addGenericArgument); + builder.addContentLine("return " + ifaceName + "." + typeArgumentString + "builder().from(instance);"); + }); + } + + private static void addBuilderMethod(ClassModel.Builder classModel, + TypeName builderTypeName, + List typeArguments, + String ifaceName) { + classModel.addMethod(builder -> { + builder.isStatic(true) + .name("builder") + .description("Create a new fluent API builder to customize configuration.") + .returnType(builderTypeName, "a new builder"); + typeArguments.forEach(builder::addGenericArgument); + if (typeArguments.isEmpty()) { + builder.addContentLine("return new " + ifaceName + ".Builder();"); + } else { + builder.addContentLine("return new " + ifaceName + ".Builder<>();"); + } + }); + } + + private static void generateCustomConstants(CustomMethods customMethods, ClassModel.Builder classModel) { + for (CustomConstant customConstant : customMethods.customConstants()) { + classModel.addField(constant -> constant + .type(customConstant.fieldType()) + .name(customConstant.name()) + .javadoc(customConstant.javadoc()) + .addContent(customConstant.declaringType()) + .addContent(".") + .addContent(customConstant.name())); + } + } + + private static void generateCustomMethods(CustomMethods customMethods, ClassModel.Builder classModel) { + for (CustomMethods.CustomMethod customMethod : customMethods.factoryMethods()) { + // prototype definition - custom static factory methods + // static TypeName create(Type type); + CustomMethods.Method generated = customMethod.generatedMethod().method(); + Method.Builder method = Method.builder() + .name(generated.name()) + .javadoc(Javadoc.parse(generated.javadoc())) + .isStatic(true) + .returnType(generated.returnType()); + customMethod.generatedMethod().generateCode().accept(method); + + for (String annotation : customMethod.generatedMethod().annotations()) { + method.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation)); + } + for (CustomMethods.Argument argument : generated.arguments()) { + method.addParameter(param -> param.name(argument.name()) + .type(argument.typeName())); + } + classModel.addMethod(method); + } + + for (CustomMethods.CustomMethod customMethod : customMethods.prototypeMethods()) { + // prototype definition - custom methods must have a new method defined on this interface, missing on blueprint + CustomMethods.Method generated = customMethod.generatedMethod().method(); + if (generated.javadoc().isEmpty() + && customMethod.generatedMethod() + .annotations() + .contains(Override.class.getName())) { + // there is no javadoc, and this is overriding a method from super interface, ignore + continue; + } + + // TypeName boxed(); + Method.Builder method = Method.builder() + .name(generated.name()) + .javadoc(Javadoc.parse(generated.javadoc())) + .returnType(generated.returnType()); + for (String annotation : customMethod.generatedMethod().annotations()) { + method.addAnnotation(io.helidon.codegen.classmodel.Annotation.parse(annotation)); + } + for (CustomMethods.Argument argument : generated.arguments()) { + method.addParameter(param -> param.name(argument.name()) + .type(argument.typeName())); + } + classModel.addMethod(method); + } + } + + private Collection addBlueprintsForValidation(Set blueprints) { + List result = new ArrayList<>(); + + for (TypeName blueprintType : blueprints) { + TypeInfo blueprint = ctx.typeInfo(blueprintType) + .orElseThrow(() -> new CodegenException("Could not get TypeInfo for " + blueprintType.fqName())); + result.add(new ValidationTask.ValidateBlueprint(blueprint)); + TypeContext typeContext = TypeContext.create(ctx, blueprint); + + if (typeContext.blueprintData().isFactory()) { + result.add(new ValidationTask.ValidateBlueprintExtendsFactory(typeContext.typeInfo().prototype(), + blueprint, + toTypeInfo(blueprint, + typeContext.typeInfo() + .runtimeObject() + .get()))); + } + } + + return result; + } + + private TypeInfo toTypeInfo(TypeInfo typeInfo, TypeName typeName) { + return ctx.typeInfo(typeName.genericTypeName()) + .orElseThrow(() -> new IllegalArgumentException("Type " + typeName.fqName() + " is not a valid type for Factory" + + " declared on type " + typeInfo.typeName() + .fqName())); + } + + private List addRuntimeTypesForValidation(Set runtimeTypes) { + return runtimeTypes.stream() + .map(ctx::typeInfo) + .flatMap(Optional::stream) + .map(it -> new ValidateConfiguredType(it, + annotationTypeValue(it, RUNTIME_PROTOTYPE))) + .toList(); + } + + private TypeName annotationTypeValue(TypeInfo typeInfo, TypeName annotationType) { + return typeInfo.findAnnotation(annotationType) + .flatMap(Annotation::typeValue) + .orElseThrow(() -> new IllegalArgumentException("Type " + typeInfo.typeName() + .fqName() + " has invalid ConfiguredBy annotation")); + } + + private String createTypeArgumentString(List typeArguments) { + if (!typeArguments.isEmpty()) { + String arguments = typeArguments.stream() + .map(TypeName::className) + .collect(Collectors.joining(", ")); + return "<" + arguments + ">"; + } + return ""; + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegenProvider.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegenProvider.java new file mode 100644 index 00000000000..93c8edfd49d --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegenProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.common.types.TypeName; + +/** + * {@link java.util.ServiceLoader} provider implementation for {@link io.helidon.codegen.spi.CodegenExtensionProvider}, + * that code generates builders and implementations for blueprints. + */ +public class BuilderCodegenProvider implements CodegenExtensionProvider { + /** + * Public constructor is required for {@link java.util.ServiceLoader}. + * + * @deprecated please do not use directly + */ + @Deprecated + public BuilderCodegenProvider() { + } + + @Override + public CodegenExtension create(CodegenContext ctx, TypeName generatorType) { + return new BuilderCodegen(ctx); + } + + @Override + public Set supportedAnnotations() { + return Set.of(Types.PROTOTYPE_BLUEPRINT, + Types.RUNTIME_PROTOTYPED_BY, + Types.GENERATED); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomConstant.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomConstant.java new file mode 100644 index 00000000000..8695d0fe670 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomConstant.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.common.types.TypeName; + +record CustomConstant(TypeName declaringType, TypeName fieldType, String name, Javadoc javadoc) { +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java new file mode 100644 index 00000000000..03d919f9125 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.common.Errors; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.builder.codegen.Types.PROTOTYPE_BUILDER_METHOD; +import static io.helidon.builder.codegen.Types.PROTOTYPE_CONSTANT; +import static io.helidon.builder.codegen.Types.PROTOTYPE_CUSTOM_METHODS; +import static io.helidon.builder.codegen.Types.PROTOTYPE_FACTORY_METHOD; +import static io.helidon.builder.codegen.Types.PROTOTYPE_PROTOTYPE_METHOD; + +record CustomMethods(List factoryMethods, + List builderMethods, + List prototypeMethods, + List customConstants) { + + CustomMethods() { + this(List.of(), List.of(), List.of(), List.of()); + } + + static CustomMethods create(CodegenContext ctx, TypeContext.TypeInformation typeInformation) { + Optional annotation = typeInformation.blueprintType().findAnnotation(PROTOTYPE_CUSTOM_METHODS); + if (annotation.isEmpty()) { + return new CustomMethods(); + } + // value is mandatory for this annotation + String customMethodType = annotation.get().value().orElseThrow(); + // we must get the type info, as otherwise this is an invalid declaration + TypeInfo customMethodsInfo = ctx.typeInfo(TypeName.create(customMethodType)) + .orElseThrow(() -> new CodegenException("Failed to get type info for a type declared as custom methods type: " + + customMethodType)); + + Errors.Collector errors = Errors.collector(); + List factoryMethods = findMethods(typeInformation, + customMethodsInfo, + errors, + PROTOTYPE_FACTORY_METHOD, + CustomMethods::factoryMethod); + List builderMethods = findMethods(typeInformation, + customMethodsInfo, + errors, + PROTOTYPE_BUILDER_METHOD, + CustomMethods::builderMethod); + List prototypeMethods = findMethods(typeInformation, + customMethodsInfo, + errors, + PROTOTYPE_PROTOTYPE_METHOD, + CustomMethods::prototypeMethod); + List customConstants = findConstants(customMethodsInfo, + errors); + + errors.collect().checkValid(); + return new CustomMethods(factoryMethods, builderMethods, prototypeMethods, customConstants); + } + + // methods to be part of prototype interface (signature), and implement in both builder and impl + private static GeneratedMethod prototypeMethod(Errors.Collector errors, + TypeContext.TypeInformation typeInformation, + TypeName customMethodsType, + List annotations, + Method customMethod) { + List customMethodArgs = customMethod.arguments(); + if (customMethodArgs.isEmpty()) { + errors.fatal(customMethodsType.fqName(), + "Methods annotated with @Prototype.PrototypeMethod must accept the prototype " + + "as the first parameter, but method: " + customMethod.name() + " has no parameters"); + } else if (!correctType(typeInformation.prototype(), customMethodArgs.getFirst().typeName())) { + errors.fatal(customMethodsType.fqName(), + "Methods annotated with @Prototype.PrototypeMethod must accept the prototype " + + "as the first parameter, but method: " + customMethod.name() + + " expected: " + typeInformation.prototypeBuilder().fqName() + + " actual: " + customMethodArgs.getFirst().typeName().fqName()); + } + List generatedArgs = customMethodArgs.subList(1, customMethodArgs.size()); + List argumentNames = new ArrayList<>(); + argumentNames.add("this"); + argumentNames.addAll(generatedArgs.stream() + .map(Argument::name) + .toList()); + + Consumer> codeGenerator = contentBuilder -> { + if (!customMethod.returnType().equals(TypeNames.PRIMITIVE_VOID)) { + contentBuilder.addContent("return "); + } + contentBuilder.addContent(customMethodsType.genericTypeName()) + .addContent(".") + .addContent(customMethod.name()) + .addContent("(") + .addContent(String.join(", ", argumentNames)) + .addContentLine(");"); + }; + + return new GeneratedMethod( + new Method(typeInformation.prototypeBuilder(), + customMethod.name(), + customMethod.returnType(), + generatedArgs, + // todo the javadoc may differ (such as when we have an additional parameter for instance methods) + customMethod.javadoc()), + annotations, + codeGenerator); + } + + // methods to be part of prototype builder only + private static GeneratedMethod builderMethod(Errors.Collector errors, + TypeContext.TypeInformation typeInformation, + TypeName customMethodsType, + List annotations, + Method customMethod) { + + List customMethodArgs = customMethod.arguments(); + if (customMethodArgs.isEmpty()) { + errors.fatal(customMethodsType.fqName(), + "Methods annotated with @Prototype.BuilderMethod must accept the prototype builder base " + + "as the first parameter, but method: " + customMethod.name() + " has no parameters"); + } else if (!correctType(typeInformation.prototypeBuilderBase(), + customMethodArgs.getFirst().typeName().genericTypeName())) { + errors.fatal(customMethodsType.fqName(), + "Methods annotated with @Prototype.BuilderMethod must accept the prototype builder " + + "base as the first parameter, but method: " + customMethod.name() + + " expected: " + typeInformation.prototypeBuilderBase().fqName() + + " actual: " + customMethodArgs.getFirst().typeName().fqName()); + } + + List generatedArgs = customMethodArgs.subList(1, customMethodArgs.size()); + List argumentNames = new ArrayList<>(); + argumentNames.add("this"); + argumentNames.addAll(generatedArgs.stream() + .map(Argument::name) + .toList()); + + // return CustomMethodsType.methodName(this, param1, param2) + Consumer> codeGenerator = contentBuilder -> { + contentBuilder.addContent(customMethodsType.genericTypeName()) + .addContent(".") + .addContent(customMethod.name()) + .addContent("(") + .addContent(String.join(", ", argumentNames)) + .addContentLine(");") + .addContent("return self();"); + }; + + return new GeneratedMethod( + new Method(typeInformation.prototypeBuilder(), + customMethod.name(), + typeInformation.prototypeBuilder(), + generatedArgs, + customMethod.javadoc()), + annotations, + codeGenerator); + } + + private static boolean correctType(TypeName knownType, TypeName processingType) { + // processing type may be for a generated class, which does not contain package information + if (processingType.packageName().isEmpty()) { + if (processingType.className().equals("")) { + // cannot be resolved as this is part of our round, good faith it is a correct parameter + // this type name is used for types that are part of this round and that have a generic declaration + // such as BuilderBase, also compilation will fail with a correct exception if the type is wrong + // it will just fail on the generated class + return true; + } + // the type name is known, but package could not be determined as the type is generated as part of this + // annotation processing round - if the class name is correct, assume we have the right type + return knownType.className().equals(processingType.className()) + && knownType.enclosingNames().equals(processingType.enclosingNames()); + } + return knownType.equals(processingType); + } + + // static methods on prototype + private static GeneratedMethod factoryMethod(Errors.Collector errors, + TypeContext.TypeInformation typeInformation, + TypeName customMethodsType, + List annotations, + Method customMethod) { + + // if void: CustomMethodsType.methodName(param1, param2) + // if returns: return CustomMethodsType.methodName(param1, param2) + Consumer> codeGenerator = contentBuilder -> { + if (!customMethod.returnType().equals(TypeNames.PRIMITIVE_VOID)) { + contentBuilder.addContent("return "); + } + contentBuilder.addContent(customMethodsType.genericTypeName()) + .addContent(".") + .addContent(customMethod.name()) + .addContent("(") + .addContent(customMethod.arguments().stream().map(Argument::name).collect(Collectors.joining(", "))) + .addContentLine(");"); + }; + + // factory methods are just copied to the generated prototype + return new GeneratedMethod(new Method(typeInformation.prototype(), + customMethod.name(), + customMethod.returnType(), + customMethod.arguments(), + customMethod.javadoc()), + annotations, + codeGenerator); + } + + private static List findConstants(TypeInfo customMethodsType, + Errors.Collector errors) { + return customMethodsType.elementInfo() + .stream() + .filter(ElementInfoPredicates::isField) + .filter(ElementInfoPredicates.hasAnnotation(PROTOTYPE_CONSTANT)) + .map(it -> { + if (!it.elementModifiers().contains(Modifier.STATIC)) { + errors.fatal(it, + "A field annotated with @Prototype.Constant must be static, final, " + + "and at least package local. Field \"" + it.elementName() + "\" is not static."); + } + if (!it.elementModifiers().contains(Modifier.FINAL)) { + errors.fatal(it, + "A field annotated with @Prototype.Constant must be static, final, " + + "and at least package local. Field \"" + it.elementName() + "\" is not final."); + } + if (it.accessModifier() == AccessModifier.PRIVATE) { + errors.fatal(it, + "A field annotated with @Prototype.Constant must be static, final, " + + "and at least package local. Field \"" + it.elementName() + "\" is private."); + } + TypeName fieldType = it.typeName(); + String name = it.elementName(); + Javadoc javadoc = it.description() + .map(Javadoc::parse) + .orElseGet(() -> Javadoc.builder() + .add(fieldType.equals(TypeNames.STRING) + ? "Constant for {@value}." + : "Code generated constant.") + .build()); + + return new CustomConstant(customMethodsType.typeName(), + fieldType, + name, + javadoc); + }) + .toList(); + } + + private static List findMethods(TypeContext.TypeInformation typeInformation, + TypeInfo customMethodsType, + Errors.Collector errors, + TypeName requiredAnnotation, + MethodProcessor methodProcessor) { + // all custom methods must be static + // parameter and return type validation is to be done by method processor + return customMethodsType.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates::isStatic) + .filter(ElementInfoPredicates.hasAnnotation(requiredAnnotation)) + .map(it -> { + // return type + TypeName returnType = it.typeName(); + // method name + String methodName = it.elementName(); + // parameters + List arguments = it.parameterArguments() + .stream() + .map(arg -> new Argument(arg.elementName(), arg.typeName())) + .toList(); + + // javadoc, if present + List javadoc = it.description() + .map(String::trim) + .stream() + .filter(Predicate.not(String::isBlank)) + .findAny() + .map(description -> description.split("\n")) + .map(List::of) + .orElseGet(List::of); + + // annotations to be added to generated code + List annotations = it.findAnnotation(Types.PROTOTYPE_ANNOTATED) + .flatMap(Annotation::stringValues) + .orElseGet(List::of) + .stream() + .map(String::trim) // to remove spaces after commas when used + .filter(Predicate.not(String::isBlank)) // we do not care about blank values + .toList(); + + Method customMethod = new Method(customMethodsType.typeName(), methodName, returnType, arguments, javadoc); + + return new CustomMethod(customMethod, + methodProcessor.process(errors, + typeInformation, + customMethodsType.typeName(), + annotations, + customMethod)); + }) + .toList(); + } + + interface MethodProcessor { + GeneratedMethod process(Errors.Collector collector, + TypeContext.TypeInformation typeInformation, + TypeName customMethodsType, + List annotations, + Method customMethod); + } + + record CustomMethod(Method declaredMethod, + GeneratedMethod generatedMethod) { + + } + + record Method(TypeName declaringType, + String name, + TypeName returnType, + List arguments, + List javadoc) { + + } + + record GeneratedMethod(Method method, + List annotations, + Consumer> generateCode) { + } + + record Argument(String name, + TypeName typeName) { + + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/DeprecationData.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/DeprecationData.java new file mode 100644 index 00000000000..de11009b533 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/DeprecationData.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; + +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.builder.codegen.Types.DEPRECATED; +import static io.helidon.builder.codegen.Types.OPTION_DEPRECATED; +import static java.util.function.Predicate.not; + +/** + * Deprecation information - combined from {@link java.lang.Deprecated} and {@code Option.Deprecated} annotations. + * all options are nullable (except for the booleans of course) + * + * @param deprecated whether this option is deprecated + * @param forRemoval whether the deprecated method is planned to be removed in next major version + * @param since since if defined (version that introduced this deprecation) + * @param alternativeOption alternative option to be used instead of the deprecated one + * @param description description (if no alternative option is defined) + */ +record DeprecationData(boolean deprecated, + boolean forRemoval, + String since, + String alternativeOption, + List description) { + static DeprecationData create(TypedElementInfo element, Javadoc javadoc) { + boolean deprecated = false; + boolean forRemoval = false; + String since = null; + String alternative = null; + List description = javadoc.deprecation(); + + if (element.hasAnnotation(DEPRECATED)) { + deprecated = true; + Annotation annotation = element.annotation(DEPRECATED); + forRemoval = annotation.booleanValue("forRemoval").orElse(false); + since = annotation.stringValue("since").filter(not(String::isBlank)).orElse(null); + } + + if (element.hasAnnotation(OPTION_DEPRECATED)) { + deprecated = true; + // alternative overrides description, and it is a required property + alternative = element.annotation(OPTION_DEPRECATED) + .value() + .orElse(null); + description = null; + } + + return new DeprecationData(deprecated, forRemoval, since, alternative, description); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java new file mode 100644 index 00000000000..5d061896a18 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/FactoryMethods.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.builder.codegen.Types.COMMON_CONFIG; +import static io.helidon.builder.codegen.Types.PROTOTYPE_API; +import static io.helidon.builder.codegen.Types.PROTOTYPE_FACTORY_METHOD; +import static io.helidon.builder.codegen.Types.RUNTIME_API; +import static io.helidon.codegen.CodegenUtil.capitalize; +import static io.helidon.common.types.TypeNames.OBJECT; + +/* + We need the following factory methods: + 1. RuntimeType create(Prototype) (either on Blueprint, or on RuntimeType) + 2. Prototype create(Config) (either on Blueprint, or on ConfigObject) + */ + +/** + * Factory methods for a specific prototype property. + * + * @param createTargetType factory method to create runtime type + * @param createFromConfig + * @param builder + */ +record FactoryMethods(Optional createTargetType, + Optional createFromConfig, + Optional builder) { + static FactoryMethods create(CodegenContext ctx, + TypeInfo blueprint, + TypeHandler typeHandler) { + + Optional targetFactory = targetTypeMethod(ctx, blueprint, typeHandler); + Set configObjectCandidates = new LinkedHashSet<>(); + if (targetFactory.isPresent()) { + configObjectCandidates.add(targetFactory.get().argumentType()); + } + configObjectCandidates.add(typeHandler.actualType()); + configObjectCandidates.add(typeHandler.declaredType()); + + // the candidate from factory method is first, as it is more significant + Optional configFactory = createFromConfigMethod(ctx, + blueprint, + typeHandler, + configObjectCandidates); + configObjectCandidates = new LinkedHashSet<>(); + if (targetFactory.isPresent()) { + configObjectCandidates.add(targetFactory.get().argumentType()); + } + if (configFactory.isPresent()) { + configObjectCandidates.add(configFactory.get().factoryMethodReturnType()); + } + + return new FactoryMethods(targetFactory, + configFactory, + builder(ctx, typeHandler, configObjectCandidates)); + } + + private static Optional builder(CodegenContext ctx, + TypeHandler typeHandler, + Set builderCandidates) { + if (typeHandler.actualType().equals(OBJECT)) { + return Optional.empty(); + } + builderCandidates.add(typeHandler.actualType()); + FactoryMethod found = null; + FactoryMethod secondary = null; + for (TypeName builderCandidate : builderCandidates) { + if (typeHandler.actualType().primitive()) { + // primitive methods do not have builders + continue; + } + TypeInfo typeInfo = ctx.typeInfo(builderCandidate.genericTypeName()).orElse(null); + if (typeInfo == null) { + if (secondary == null) { + // this may be part of annotation processing where type info is not available + // our assumption is that the type is code generated and is a correct builder, if this assumption + // is not correct, we will need to improve this "algorithm" (please file an issue if that happens) + if (builderCandidate.fqName().endsWith(".Builder")) { + // this is already a builder + continue; + } + TypeName builderTypeName = TypeName.builder(builderCandidate) + .className("Builder") + .enclosingNames(List.of(builderCandidate.className())) + .build(); + secondary = new FactoryMethod(builderCandidate, builderTypeName, "builder", null); + } + continue; + } + + found = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates::isStatic) + .filter(ElementInfoPredicates.elementName("builder")) + .filter(ElementInfoPredicates::hasNoArgs) + .findFirst() + .map(it -> new FactoryMethod(builderCandidate, it.typeName(), "builder", null)) + .orElse(null); + if (found != null) { + break; + } + } + + FactoryMethod secondaryMethod = secondary; + return Optional.ofNullable(found).or(() -> Optional.ofNullable(secondaryMethod)); + } + + private static Optional createFromConfigMethod(CodegenContext ctx, + TypeInfo blueprint, + TypeHandler typeHandler, + Set configObjectCandidates) { + + // first look at declared type and blueprint + String methodName = "create" + capitalize(typeHandler.name()); + Optional returnType = findFactoryMethodByParamType(blueprint, + COMMON_CONFIG, + methodName); + + if (returnType.isPresent()) { + TypeName typeWithFactoryMethod = blueprint.typeName(); + return Optional.of(new FactoryMethod(typeWithFactoryMethod, + returnType.get(), + methodName, + COMMON_CONFIG)); + } + + // there is no factory method on definition, let's check if the return type itself is a config object + + // factory method + String createMethod = "create"; + + List candidates = configObjectCandidates.stream() + .map(ctx::typeInfo) + .flatMap(Optional::stream) + .toList(); + + for (TypeInfo typeInfo : candidates) { + // is this a config object? + if (doesImplement(typeInfo, PROTOTYPE_API)) { + // it should have create(Config) with the correct typing + Optional foundMethod = findMethod( + new MethodSignature(typeInfo.typeName(), createMethod, List.of(COMMON_CONFIG)), + typeInfo, + ElementInfoPredicates::isStatic) + .map(it -> new FactoryMethod(typeInfo.typeName(), + typeInfo.typeName(), + createMethod, + COMMON_CONFIG)); + if (foundMethod.isPresent()) { + return foundMethod; + } + } + } + + for (TypeInfo typeInfo : candidates) { + // if the target type implements ConfiguredType, we use the generic parameter of that interface to look for our config + // look for "implements ConfiguredType" + + if (doesImplement(typeInfo, RUNTIME_API)) { + // there is no config factory method available for the type that we have + TypeName candidateTypeName = typeInfo.typeName(); + // we are now interested in a method with signature "static T create(Config)" where T is the type we are handling + Optional foundMethod = findMethod( + new MethodSignature(candidateTypeName, createMethod, List.of(COMMON_CONFIG)), + typeInfo, + ElementInfoPredicates::isStatic) + .map(it -> new FactoryMethod(candidateTypeName, candidateTypeName, createMethod, COMMON_CONFIG)); + if (foundMethod.isPresent()) { + return foundMethod; + } + } + } + + // if there is a "public static T create(io.helidon.commmon.config.Config)" method available, just use it + for (TypeInfo typeInfo : candidates) { + // similar to above - but we first want to find the best candidate, this is a fallback + TypeName candidateTypeName = typeInfo.typeName(); + Optional foundMethod = findMethod( + new MethodSignature(candidateTypeName, createMethod, List.of(COMMON_CONFIG)), + typeInfo, + ElementInfoPredicates::isStatic, + ElementInfoPredicates::isPublic) + .map(it -> new FactoryMethod(candidateTypeName, candidateTypeName, createMethod, COMMON_CONFIG)); + if (foundMethod.isPresent()) { + return foundMethod; + } + } + + // this a best effort guess - it is a wrong type (we do not have a package) + // if this ever fails, please file an issue, and we will improve this "algorithm" + // we can actually find out if a type is not yet generated (it has Kind ERROR on its mirror) + for (TypeName configObjectCandidate : configObjectCandidates) { + if (configObjectCandidate.packageName().isEmpty()) { + // most likely a generated type that is created as part of this round, let's assume it is a config object + return Optional.of(new FactoryMethod(configObjectCandidate, configObjectCandidate, "create", COMMON_CONFIG)); + } + } + + return Optional.empty(); + } + + private static boolean doesImplement(TypeInfo typeInfo, TypeName interfaceType) { + return typeInfo.interfaceTypeInfo() + .stream() + .anyMatch(it -> interfaceType.equals(it.typeName().genericTypeName())); + + } + + private static Optional targetTypeMethod(CodegenContext ctx, + TypeInfo blueprint, + TypeHandler typeHandler) { + // let's look for a method on definition that takes the type + + // first look at declared type and blueprint + String createMethodName = "create" + capitalize(typeHandler.name()); + TypeName typeWithFactoryMethod = blueprint.typeName(); + TypeName factoryMethodReturnType = typeHandler.declaredType(); + Optional argumentType = findFactoryMethodByReturnType(blueprint, + factoryMethodReturnType, + createMethodName); + + if (argumentType.isPresent()) { + return Optional.of(new FactoryMethod(typeWithFactoryMethod, + factoryMethodReturnType, + createMethodName, + argumentType.get())); + } + + // then look at actual type + factoryMethodReturnType = typeHandler.actualType(); + argumentType = findFactoryMethodByReturnType(blueprint, factoryMethodReturnType, createMethodName); + if (argumentType.isPresent()) { + return Optional.of(new FactoryMethod(typeWithFactoryMethod, + factoryMethodReturnType, + createMethodName, + argumentType.get())); + } + + // there is no factory method on definition, let's check if the return type itself is a config object + + // if the type we return implements ConfiguredType, we will generate additional setters + Optional configuredTypeInterface = ctx.typeInfo(typeHandler.actualType()) + .flatMap(it -> it.interfaceTypeInfo() + .stream() + .filter(typeInfo -> RUNTIME_API.equals(typeInfo.typeName().genericTypeName())) + .findFirst()); + + createMethodName = "create"; + + if (configuredTypeInterface.isPresent()) { + // MyTargetType MyTargetType.create(ConfigObject object) + factoryMethodReturnType = typeHandler.actualType(); + typeWithFactoryMethod = factoryMethodReturnType; + argumentType = Optional.of(configuredTypeInterface.get().typeName().typeArguments().get(0)); + + return Optional.of(new FactoryMethod(typeWithFactoryMethod, + factoryMethodReturnType, + createMethodName, + argumentType.get())); + } + + // and finally we should have the factory method of the actual type we return + + return Optional.empty(); + } + + private static Optional findFactoryMethodByReturnType(TypeInfo declaringType, + TypeName returnType, + String methodName) { + return declaringType.elementInfo() + .stream() + // methods + .filter(ElementInfoPredicates::isMethod) + // static + .filter(ElementInfoPredicates::isStatic) + // @FactoryMethod + .filter(it -> it.hasAnnotation(PROTOTYPE_FACTORY_METHOD)) + // createMyProperty + .filter(it -> methodName.equals(it.elementName())) + // returns the same type that is the method return type + .filter(it -> it.typeName().equals(returnType)) + // if all of the above is true, we use the parameters as the config type + .filter(it -> it.parameterArguments().size() == 1) + .map(it -> it.parameterArguments().get(0)) + .map(TypedElementInfo::typeName) + .findFirst(); + } + + private static Optional findFactoryMethodByParamType(TypeInfo declaringType, + TypeName paramType, + String methodName) { + return declaringType.elementInfo() + .stream() + // methods + .filter(ElementInfoPredicates::isMethod) + // static + .filter(ElementInfoPredicates::isStatic) + // @FactoryMethod + .filter(ElementInfoPredicates.hasAnnotation(PROTOTYPE_FACTORY_METHOD)) + // createMyProperty + .filter(ElementInfoPredicates.elementName(methodName)) + // must have a single parameter of the correct type + .filter(ElementInfoPredicates.hasParams(paramType)) + .map(TypedElementInfo::typeName) + .findFirst(); + } + + /** + * Find a method matching the filters from {@link TypeInfo#elementInfo()}. + * + * @param signatureFilter expected signature + * @param typeInfo type info to search + * @param predicates predicates to test against + * @return found method, ord empty if method does not exist, if more than one exist, the first one is returned + */ + @SafeVarargs + private static Optional findMethod(MethodSignature signatureFilter, + TypeInfo typeInfo, + Predicate... predicates) { + return typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(it -> { + for (Predicate predicate : predicates) { + boolean res = predicate.test(it); + if (!res) { + return res; + } + } + return true; + }) + .filter(it -> { + if (signatureFilter.returnType() != null) { + if (!it.typeName().equals(signatureFilter.returnType())) { + return false; + } + } + if (signatureFilter.name() != null) { + if (!it.elementName().equals(signatureFilter.name())) { + return false; + } + } + List expectedArguments = signatureFilter.arguments(); + if (expectedArguments != null) { + List actualArguments = it.parameterArguments(); + if (actualArguments.size() != expectedArguments.size()) { + return false; + } + for (int i = 0; i < expectedArguments.size(); i++) { + TypeName expected = expectedArguments.get(i); + TypeName actualArgument = actualArguments.get(i).typeName(); + if (!expected.equals(actualArgument)) { + return false; + } + } + } + return true; + }) + .findFirst(); + + } + + record FactoryMethod(TypeName typeWithFactoryMethod, + TypeName factoryMethodReturnType, + String createMethodName, + TypeName argumentType) { + } + +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java new file mode 100644 index 00000000000..648b89816c2 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java @@ -0,0 +1,947 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.Annotation; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Constructor; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.TypeArgument; +import io.helidon.common.Errors; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.codegen.CodegenUtil.capitalize; +import static io.helidon.common.types.TypeNames.LIST; +import static io.helidon.common.types.TypeNames.MAP; +import static io.helidon.common.types.TypeNames.OPTIONAL; +import static io.helidon.common.types.TypeNames.SET; + +final class GenerateAbstractBuilder { + + private GenerateAbstractBuilder() { + } + + static void generate(ClassModel.Builder classModel, + TypeName prototype, + TypeName runtimeType, + List typeArguments, + TypeContext typeContext) { + Optional superType = typeContext.typeInfo() + .superPrototype(); + + classModel.addInnerClass(builder -> { + typeArguments.forEach(builder::addGenericArgument); + builder.name("BuilderBase") + .isAbstract(true) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .description("Fluent API builder base for {@link " + runtimeType.className() + "}.") + .addGenericArgument(token -> token.token("BUILDER") + .description("type of the builder extending this abstract builder") + .bound(TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".BuilderBase")) + .addTypeArguments(typeArguments) + .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) + .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) + .build())) + .addGenericArgument(token -> token.token("PROTOTYPE") + .description("type of the prototype interface that would be built by {@link #buildPrototype()}") + .bound(prototype)) + .addConstructor(constructor -> createConstructor(constructor, typeContext)); + superType.ifPresent(type -> { + builder.superType(TypeName.builder() + .from(TypeName.create(type.fqName() + ".BuilderBase")) + .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) + .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) + .build()); + }); + + if (typeContext.configuredData().configured() || hasConfig(typeContext.propertyData().properties())) { + builder.addInterface(TypeName.builder() + .from(Types.PROTOTYPE_CONFIGURED_BUILDER) + .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) + .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) + .build()); + } else { + builder.addInterface(TypeName.builder() + .from(Types.PROTOTYPE_BUILDER) + .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) + .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) + .build()); + } + + fields(builder, typeContext, true); + + // method "from(prototype)" + fromInstanceMethod(builder, typeContext, prototype); + fromBuilderMethod(builder, typeContext, typeArguments); + + // method preBuildPrototype() - handles providers, decorator + preBuildPrototypeMethod(builder, typeContext); + validatePrototypeMethod(builder, typeContext); + + //custom method adding + addCustomBuilderMethods(typeContext, builder); + + // setters and getters of builder + builderMethods(builder, typeContext); + toString(builder, + typeContext, + prototype.className() + "Builder", + superType.isPresent(), + typeContext.customMethods().prototypeMethods(), + true); + + // before the builder class is finished, we also generate a protected implementation + generatePrototypeImpl(builder, typeContext, typeArguments); + }); + } + + static void buildRuntimeObjectMethod(InnerClass.Builder classBuilder, TypeContext typeContext, boolean isBuilder) { + TypeContext.TypeInformation typeInformation = typeContext.typeInfo(); + boolean hasRuntimeObject = typeInformation.runtimeObject().isPresent(); + TypeName builtObject = typeInformation.runtimeObject().orElse(typeInformation.prototype()); + + Method.Builder builder = Method.builder() + .name("build") + .addAnnotation(Annotations.OVERRIDE) + .returnType(builtObject) + .addContent("return "); + if (hasRuntimeObject) { + builder.addContent(builtObject.genericTypeName()); + if (isBuilder) { + builder.addContentLine(".create(this.buildPrototype());"); + } else { + builder.addContentLine(".create(this);"); + } + } else { + if (isBuilder) { + builder.addContentLine("build();"); + } else { + builder.addContentLine("this;"); + } + } + classBuilder.addMethod(builder); + + // if impl, we also need to add the `get()` method from supplier + if (!isBuilder) { + classBuilder.addMethod(method -> method.name("get") + .returnType(builtObject) + .addAnnotation(Annotations.OVERRIDE) + .addContentLine("return build();")); + } + } + + static boolean hasConfig(List properties) { + return properties.stream() + .anyMatch(GenerateAbstractBuilder::isConfigProperty); + } + + private static void addCustomBuilderMethods(TypeContext typeContext, InnerClass.Builder builder) { + for (CustomMethods.CustomMethod customMethod : typeContext.customMethods().builderMethods()) { + // builder specific custom methods (not part of interface) + CustomMethods.Method generated = customMethod.generatedMethod().method(); + // public Builder type(Type) with implementation + Method.Builder method = Method.builder() + .name(generated.name()) + .returnType(TypeName.createFromGenericDeclaration("BUILDER")); + customMethod.generatedMethod().generateCode().accept(method); + for (String annotation : customMethod.generatedMethod().annotations()) { + method.addAnnotation(Annotation.parse(annotation)); + } + for (CustomMethods.Argument argument : generated.arguments()) { + method.addParameter(param -> param.name(argument.name()) + .type(argument.typeName())); + } + if (!generated.javadoc().isEmpty()) { + Javadoc javadoc = Javadoc.builder() + .from(Javadoc.parse(generated.javadoc())) + .returnDescription("updated builder instance") + .build(); + method.javadoc(javadoc); + } + builder.addMethod(method); + } + } + + private static void createConstructor(Constructor.Builder constructor, TypeContext typeContext) { + constructor.description("Protected to support extensibility.") + .accessModifier(AccessModifier.PROTECTED); + // overriding defaults + for (var prop : typeContext.propertyData().overridingProperties()) { + if (prop.configuredOption().hasDefault()) { + constructor.addContent(prop.setterName()) + .addContent("("); + prop.configuredOption().defaultValue().accept(constructor); + constructor.addContent(");"); + } + } + } + + private static void builderMethods(InnerClass.Builder classBuilder, TypeContext typeContext) { + List properties = typeContext.propertyData().properties(); + AnnotationDataConfigured configured = typeContext.configuredData(); + + if (configured.configured() || hasConfig(properties)) { + createConfigMethod(classBuilder, typeContext, configured, properties); + } + + TypeName returnType = TypeName.createFromGenericDeclaration("BUILDER"); + // first setters + for (PrototypeProperty child : properties) { + if (isConfigProperty(child)) { + // this is never done here, config must be defined as a standalone method + // for methods not named config, we consider this to be "just another" property + continue; + } + child.setters(classBuilder, returnType, child.configuredOption().javadoc()); + } + // then getters + /* + If has default value - return type + If primitive & optional - return type + If collection - return type + Otherwise return Optional + */ + for (PrototypeProperty child : properties) { + String getterName = child.getterName(); + if ("config".equals(getterName) && configured.configured()) { + if (child.typeHandler().actualType().equals(Types.COMMON_CONFIG)) { + // this will always exist + continue; + } + // now we have a method called config with wrong return type - this is not supported + throw new IllegalArgumentException("Configured property named \"config\" can only be of type " + + Types.COMMON_CONFIG.declaredName() + ", but is: " + + child.typeName().declaredName()); + } + /* + String host() { + return host; + } + */ + Method.Builder method = Method.builder() + .name(getterName) + .returnType(child.builderGetterType()); + child.builderGetter(method); + + for (io.helidon.common.types.Annotation annotation : child.configuredOption().annotations()) { + method.addAnnotation(annotation); + } + + Javadoc javadoc = child.configuredOption().javadoc(); + + if (javadoc != null) { + method.javadoc(Javadoc.builder(javadoc) + .returnDescription("the " + toHumanReadable(child.name())) + .build()); + } + classBuilder.addMethod(method); + } + + if (configured.configured()) { + TypeName configReturnType = TypeName.builder() + .type(Optional.class) + .addTypeArgument(Types.COMMON_CONFIG) + .build(); + Method.Builder method = Method.builder() + .name("config") + .description("If this instance was configured, this would be the config instance used.") + .returnType(configReturnType, "config node used to configure this builder, or empty if not configured") + .addContent("return ") + .addContent(Optional.class) + .addContentLine(".ofNullable(config);"); + classBuilder.addMethod(method); + } + } + + private static void createConfigMethod(InnerClass.Builder classBuilder, TypeContext typeContext, + AnnotationDataConfigured configured, + List properties) { + /* + public BUILDER config(Config config) { + this.config = config; + config.get("server").as(String.class).ifPresent(this::server); + return self(); + } + */ + Javadoc javadoc; + if (configured.configured()) { + javadoc = Javadoc.builder() + .addLine("Update builder from configuration (node of this type).") + .addLine("If a value is present in configuration, it would override currently configured values.") + .build(); + } else { + javadoc = Javadoc.builder() + .addLine("Config to use.") + .build(); + } + Method.Builder builder = Method.builder() + .name("config") + .javadoc(javadoc) + .returnType(TypeArgument.create("BUILDER"), "updated builder instance") + .addParameter(param -> param.name("config") + .type(Types.COMMON_CONFIG) + .description("configuration instance used to obtain values to update this builder")) + .addAnnotation(Annotations.OVERRIDE) + .addContent(Objects.class) + .addContentLine(".requireNonNull(config);") + .addContentLine("this.config = config;"); + + if (typeContext.typeInfo().superPrototype().isPresent()) { + builder.addContentLine("super.config(config);"); + } + + if (configured.configured()) { + for (PrototypeProperty child : properties) { + if (child.configuredOption().configured() && !child.configuredOption().provider()) { + child.typeHandler().generateFromConfig(builder, + child.configuredOption(), + child.factoryMethods()); + } + } + } + builder.addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private static void fromInstanceMethod(InnerClass.Builder builder, TypeContext typeContext, TypeName prototype) { + Method.Builder methodBuilder = Method.builder() + .name("from") + .returnType(TypeArgument.create("BUILDER")) + .description("Update this builder from an existing prototype instance.") + .addParameter(param -> param.name("prototype") + .type(prototype) + .description("existing prototype to update this builder from")) + .returnType(TypeArgument.create("BUILDER"), "updated builder instance"); + + typeContext.typeInfo() + .superPrototype() + .ifPresent(it -> methodBuilder.addContentLine("super.from(prototype);")); + for (PrototypeProperty property : typeContext.propertyData().properties()) { + TypeName declaredType = property.typeHandler().declaredType(); + if (declaredType.isSet() || declaredType.isList() || declaredType.isMap()) { + methodBuilder.addContent("add"); + methodBuilder.addContent(capitalize(property.name())); + methodBuilder.addContent("(prototype."); + methodBuilder.addContent(property.typeHandler().getterName()); + methodBuilder.addContentLine("());"); + } else { + /* + Special handling from config - we have to assign it to field, we cannot go through (config(Config)) + */ + if (isConfigProperty(property)) { + methodBuilder.addContent("this.config = prototype.config()"); + if (declaredType.isOptional()) { + methodBuilder.addContent(".orElse(null)"); + } + methodBuilder.addContentLine(";"); + } else { + methodBuilder.addContent(property.typeHandler().setterName()); + methodBuilder.addContent("(prototype."); + methodBuilder.addContent(property.typeHandler().getterName()); + methodBuilder.addContentLine("());"); + } + } + } + methodBuilder.addContentLine("return self();"); + builder.addMethod(methodBuilder); + } + + private static void fromBuilderMethod(InnerClass.Builder classBuilder, + TypeContext typeContext, + List arguments) { + TypeName prototype = typeContext.typeInfo().prototype(); + TypeName parameterType = TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".BuilderBase")) + .addTypeArguments(arguments) + .addTypeArgument(TypeName.createFromGenericDeclaration("?")) + .addTypeArgument(TypeName.createFromGenericDeclaration("?")) + .build(); + Method.Builder methodBuilder = Method.builder() + .name("from") + .addParameter(param -> param.name("builder") + .type(parameterType) + .description("existing builder prototype to update this builder from")) + .returnType(TypeArgument.create("BUILDER"), "updated builder instance") + .description("Update this builder from an existing prototype builder instance."); + + typeContext.typeInfo() + .superPrototype() + .ifPresent(it -> methodBuilder.addContentLine("super.from(builder);")); + + for (PrototypeProperty property : typeContext.propertyData().properties()) { + TypeName declaredType = property.typeHandler().declaredType(); + String setterName = property.typeHandler().setterName(); + String getterName = property.typeHandler().getterName(); + if (property.builderGetterOptional()) { + // property that is either mandatory or internally nullable + methodBuilder.addContentLine("builder." + getterName + "().ifPresent(this::" + setterName + ");"); + } else { + if (declaredType.isSet() || declaredType.isList() || declaredType.isMap()) { + methodBuilder.addContent("add" + capitalize(property.name())); + } else { + methodBuilder.addContent(setterName); + } + methodBuilder.addContentLine("(builder." + getterName + "());"); + } + + } + methodBuilder.addContentLine("return self();"); + classBuilder.addMethod(methodBuilder); + } + + private static void fields(InnerClass.Builder classBuilder, TypeContext typeContext, boolean isBuilder) { + if (isBuilder && (typeContext.configuredData().configured() || hasConfig(typeContext.propertyData().properties()))) { + classBuilder.addField(builder -> builder.type(Types.COMMON_CONFIG).name("config")); + } + for (PrototypeProperty child : typeContext.propertyData().properties()) { + if (isBuilder && child.configuredOption().hasAllowedValues()) { + String allowedValues = child.configuredOption().allowedValues() + .stream() + .map(AnnotationDataOption.AllowedValue::value) + .map(it -> "\"" + it + "\"") + .collect(Collectors.joining(", ")); + // private static final Set PROPERTY_ALLOWED_VALUES = Set.of("a", "b", "c"); + classBuilder.addField(it -> it.isFinal(true) + .isStatic(true) + .name(child.name().toUpperCase(Locale.ROOT) + "_ALLOWED_VALUES") + .type(TypeName.builder(SET).addTypeArgument(TypeNames.STRING).build()) + .addContent(Set.class) + .addContent(".of(") + .addContent(allowedValues) + .addContent(")")); + } + if (!isBuilder || !isConfigProperty(child)) { + classBuilder.addField(child.fieldDeclaration(isBuilder)); + } + if (isBuilder && child.configuredOption().provider()) { + classBuilder.addField(builder -> builder.type(boolean.class) + .name(child.name() + "DiscoverServices") + .defaultValue(String.valueOf(child.configuredOption().providerDiscoverServices()))); + } + } + } + + private static boolean isConfigProperty(PrototypeProperty property) { + return TypeHandler.isConfigProperty(property.typeHandler()); + } + + private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder, + TypeContext typeContext) { + Method.Builder preBuildBuilder = Method.builder() + .name("preBuildPrototype") + .accessModifier(AccessModifier.PROTECTED) + .description("Handles providers and decorators."); + + if (typeContext.propertyData().hasProvider()) { + preBuildBuilder.addAnnotation(builder -> builder.type(SuppressWarnings.class) + .addParameter("value", "unchecked")); + } + typeContext.typeInfo() + .superPrototype() + .ifPresent(it -> preBuildBuilder.addContentLine("super.preBuildPrototype();")); + if (typeContext.propertyData().hasProvider()) { + boolean configured = typeContext.configuredData().configured(); + if (configured) { + // need to have a non-null config instance + preBuildBuilder.addContentLine("this.config = config == null ? Config.empty() : config;"); + } + for (PrototypeProperty property : typeContext.propertyData().properties()) { + AnnotationDataOption configuredOption = property.configuredOption(); + if (configuredOption.provider()) { + boolean defaultDiscoverServices = configuredOption.providerDiscoverServices(); + + // using a code block, so we can reuse the same variable names for multiple providers + preBuildBuilder.addContentLine("{"); + TypeName providerType = configuredOption.providerType(); + preBuildBuilder.addContent("var serviceLoader = ") + .addContent(HelidonServiceLoader.class) + .addContent(".create(") + .addContent(ServiceLoader.class) + .addContent(".load(") + .addContent(providerType.genericTypeName()) + .addContentLine(".class));"); + if (configured) { + TypeName typeName = property.typeHandler().declaredType(); + if (typeName.isList() || typeName.isSet()) { + preBuildBuilder.addContent("this.add") + .addContent(capitalize(property.name())) + .addContent("(discoverServices(config, \"") + .addContent(configuredOption.configKey()) + .addContent("\", serviceLoader, ") + .addContent(providerType.genericTypeName()) + .addContent(".class, ") + .addContent(property.typeHandler().actualType().genericTypeName()) + .addContent(".class, ") + .addContent(property.name()) + .addContent("DiscoverServices, ") + .addContent(property.name()) + .addContentLine("));"); + } else { + preBuildBuilder.addContent("discoverService(config, \"") + .addContent(configuredOption.configKey()) + .addContent("\", serviceLoader, ") + .addContent(providerType) + .addContent(".class, ") + .addContent(property.typeHandler().actualType().genericTypeName()) + .addContent(".class, ") + .addContent(property.name()) + .addContent("DiscoverServices, @java.util.Optional@.ofNullable(") + .addContent(property.name()) + .addContent(")).ifPresent(this::") + .addContent(property.setterName()) + .addContentLine(");"); + } + } else { + if (defaultDiscoverServices) { + preBuildBuilder.addContentLine("this." + property.name() + "(serviceLoader.asList());"); + } + } + preBuildBuilder.addContentLine("}"); + } + } + } + if (typeContext.typeInfo().decorator().isPresent()) { + preBuildBuilder.addContent("new ") + .addContent(typeContext.typeInfo().decorator().get()) + .addContentLine("().decorate(this);"); + } + classBuilder.addMethod(preBuildBuilder); + } + + private static void validatePrototypeMethod(InnerClass.Builder classBuilder, TypeContext typeContext) { + Method.Builder validateBuilder = Method.builder() + .name("validatePrototype") + .accessModifier(AccessModifier.PROTECTED) + .description("Validates required properties."); + + typeContext.typeInfo() + .superPrototype() + .ifPresent(it -> validateBuilder.addContentLine("super.validatePrototype();")); + + TypeContext.PropertyData propertyData = typeContext.propertyData(); + if (propertyData.hasRequired() + || propertyData.hasNonNulls() + || propertyData.hasAllowedValues()) { + requiredValidation(validateBuilder, typeContext); + } + classBuilder.addMethod(validateBuilder); + } + + private static void requiredValidation(Method.Builder validateBuilder, TypeContext typeContext) { + validateBuilder.addContent(Errors.Collector.class) + .addContent(" collector = ") + .addContent(Errors.class) + .addContentLine(".collector();"); + + for (PrototypeProperty property : typeContext.propertyData().properties()) { + String configKey = property.configuredOption().configKey(); + String propertyName = property.name(); + + if (property.configuredOption().validateNotNull() && !property.configuredOption().hasDefault()) { + validateBuilder.addContentLine("if (" + propertyName + " == null) {") + .addContent("collector.fatal(getClass(), \"Property \\\"") + .addContent(configKey == null ? propertyName : configKey); + + if (property.configuredOption().required()) { + validateBuilder.addContentLine("\\\" is required, but not set\");"); + } else { + validateBuilder.addContentLine("\\\" must not be null, but not set\");"); + } + validateBuilder.addContentLine("}"); + } + if (property.configuredOption().hasAllowedValues()) { + String allowedValuesConstant = propertyName.toUpperCase(Locale.ROOT) + "_ALLOWED_VALUES"; + TypeName declaredType = property.typeHandler().declaredType(); + + if (declaredType.isList() || declaredType.isSet()) { + String single = "single" + capitalize(propertyName); + validateBuilder.addContentLine("for (var " + single + " : " + propertyName + ") {"); + validateBuilder.addContentLine("if (!" + allowedValuesConstant + ".contains(String.valueOf(" + single + "))" + + ") {") + .addContent("collector.fatal(getClass(), \"Property \\\"") + .addContent(configKey == null ? propertyName : configKey) + .addContent("\\\" contains value that is not within allowed values. Configured: \\\"\" + " + + single + " + \"\\\"") + .addContentLine(", expected one of: \\\"\" + " + allowedValuesConstant + " + \"\\\"\");"); + validateBuilder.addContentLine("}"); + validateBuilder.addContentLine("}"); + + } else { + validateBuilder.addContent("if ("); + if (!declaredType.primitive()) { + validateBuilder.addContent(propertyName + " != null && "); + } + validateBuilder.addContentLine("!" + allowedValuesConstant + ".contains(String.valueOf(" + propertyName + + "))) {") + .addContent("collector.fatal(getClass(), \"Property \\\"") + .addContent(configKey == null ? propertyName : configKey) + .addContent("\\\" value is not within allowed values. Configured: \\\"\" + " + propertyName + " + " + + "\"\\\"") + .addContentLine(", expected one of: \\\"\" + " + allowedValuesConstant + " + \"\\\"\");"); + validateBuilder.addContentLine("}"); + } + } + } + validateBuilder.addContentLine("collector.collect().checkValid();"); + } + + private static void generatePrototypeImpl(InnerClass.Builder classBuilder, + TypeContext typeContext, + List typeArguments) { + Optional superPrototype = typeContext.typeInfo() + .superPrototype(); + TypeName prototype = typeContext.typeInfo().prototype(); + TypeName prototypeImpl = typeContext.typeInfo().prototypeImpl(); + + String ifaceName = prototype.className(); + String implName = prototypeImpl.className(); + + // inner class of builder base + classBuilder.addInnerClass(builder -> { + typeArguments.forEach(builder::addGenericArgument); + builder.name(implName) + .accessModifier(AccessModifier.PROTECTED) + .isStatic(true) + .description("Generated implementation of the prototype, " + + "can be extended by descendant prototype implementations."); + superPrototype.ifPresent(it -> { + builder.superType(TypeName.create(it.className() + "Impl")); + }); + builder.addInterface(prototype); + if (typeContext.blueprintData().isFactory()) { + builder.addInterface(TypeName.builder() + .type(Supplier.class) + .addTypeArgument(typeContext.typeInfo().runtimeObject() + .orElse(typeContext.typeInfo().prototype())) + .build()); + } + /* + Fields + */ + fields(builder, typeContext, false); + /* + Constructor + */ + builder.addConstructor(constructor -> { + constructor.description("Create an instance providing a builder.") + .accessModifier(AccessModifier.PROTECTED) + .addParameter(param -> param.name("builder") + .type(TypeName.builder() + .from(TypeName.create(ifaceName + ".BuilderBase")) + .addTypeArguments(typeArguments) + .addTypeArgument(TypeArgument.create("?")) + .addTypeArgument(TypeArgument.create("?")) + .build()) + .description("extending builder base of this prototype")); + superPrototype.ifPresent(it -> { + constructor.addContentLine("super(builder);"); + }); + implAssignToFields(constructor, typeContext); + }); + /* + RuntimeType build() + */ + if (typeContext.blueprintData().isFactory()) { + buildRuntimeObjectMethod(builder, typeContext, false); + } + /* + Custom prototype methods + */ + for (CustomMethods.CustomMethod customMethod : typeContext.customMethods().prototypeMethods()) { + // builder - custom implementation methods for new prototype interface methods + CustomMethods.Method generated = customMethod.generatedMethod().method(); + Method.Builder method = Method.builder() + .name(generated.name()) + .returnType(generated.returnType()); + + // public TypeName boxed() - with implementation + // no javadoc on impl, it is package local anyway + for (String annotation : customMethod.generatedMethod().annotations()) { + method.addAnnotation(Annotation.parse(annotation)); + } + if (!customMethod.generatedMethod().annotations().contains(Override.class.getName())) { + method.addAnnotation(Annotations.OVERRIDE); + } + generated.arguments() + .forEach(argument -> method.addParameter(param -> param.name(argument.name()).type(argument.typeName()))); + customMethod.generatedMethod().generateCode().accept(method); + builder.addMethod(method); + } + /* + Implementation methods of prototype interface + */ + implMethods(builder, typeContext); + /* + To string + */ + toString(builder, + typeContext, + ifaceName, + superPrototype.isPresent(), + typeContext.customMethods().prototypeMethods(), + false); + /* + Hash code and equals + */ + hashCodeAndEquals(builder, typeContext, ifaceName, superPrototype.isPresent()); + }); + } + + private static void hashCodeAndEquals(InnerClass.Builder classBuilder, + TypeContext typeContext, + String ifaceName, + boolean hasSuper) { + List equalityFields = typeContext.propertyData() + .properties() + .stream() + .filter(PrototypeProperty::equality) + .toList(); + + equalsMethod(classBuilder, ifaceName, hasSuper, equalityFields); + hashCodeMethod(classBuilder, hasSuper, equalityFields); + } + + private static void equalsMethod(InnerClass.Builder classBuilder, + String ifaceName, + boolean hasSuper, + List equalityFields) { + String newLine = "\n" + ClassModel.PADDING_TOKEN + ClassModel.PADDING_TOKEN + "&& "; + Method.Builder method = Method.builder() + .name("equals") + .returnType(TypeName.create(boolean.class)) + .addAnnotation(Annotations.OVERRIDE) + .addParameter(param -> param.name("o") + .type(Object.class)) + // same instance + .addContentLine("if (o == this) {") + .addContentLine("return true;") + .addContentLine("}") + // same type + .addContentLine("if (!(o instanceof " + ifaceName + " other)) {") + .addContentLine("return false;") + .addContentLine("}"); + // compare fields + method.addContent("return "); + if (hasSuper) { + method.addContent("super.equals(other)"); + if (!equalityFields.isEmpty()) { + method.addContent(newLine); + } + } + if (!hasSuper && equalityFields.isEmpty()) { + method.addContent("true"); + } else { + method.addContent(equalityFields.stream() + .map(field -> { + if (field.typeName().array()) { + return "java.util.Arrays.equals(" + field.name() + ", other." + + field.getterName() + "())"; + } + if (field.typeName().primitive()) { + return field.name() + " == other." + field.getterName() + "()"; + } + return "Objects.equals(" + field.name() + ", other." + field.getterName() + "())"; + }) + .collect(Collectors.joining(newLine))); + } + method.addContentLine(";"); + classBuilder.addMethod(method); + } + + private static void hashCodeMethod(InnerClass.Builder classBuilder, + boolean hasSuper, + List equalityFields) { + Method.Builder method = Method.builder() + .name("hashCode") + .returnType(TypeName.create(int.class)) + .addAnnotation(Annotations.OVERRIDE); + if (equalityFields.isEmpty()) { + // no fields on this type + if (hasSuper) { + method.addContentLine("return super.hashCode();"); + } else { + // hashcode is a constant, as there are no fields and no super type + method.addContentLine("return 1;"); + } + } else { + if (hasSuper) { + method.addContent("return 31 * super.hashCode() + ") + .addContent(Objects.class) + .addContent(".hash("); + } else { + method.addContent("return ") + .addContent(Objects.class) + .addContent(".hash("); + } + + method.addContent(equalityFields.stream() + .map(PrototypeProperty::name) + .collect(Collectors.joining(", "))) + .addContentLine(");"); + } + + classBuilder.addMethod(method); + } + + private static void toString(InnerClass.Builder classBuilder, + TypeContext typeContext, + String typeName, + boolean hasSuper, + List prototypeMethods, + boolean isBuilder) { + if (prototypeMethods.stream() + .map(CustomMethods.CustomMethod::generatedMethod) + .map(CustomMethods.GeneratedMethod::method) + .filter(it -> "toString".equals(it.name())) + .filter(it -> it.returnType().equals(TypeNames.STRING)) + .anyMatch(it -> it.arguments().isEmpty())) { + // do not create toString() if defined as a custom method + return; + } + // only create to string if not part of prototype methods + Method.Builder method = Method.builder() + .name("toString") + .returnType(TypeName.create(String.class)) + .addAnnotation(Annotations.OVERRIDE) + .addContent("return \"" + typeName); + + List toStringFields = typeContext.propertyData() + .properties() + .stream() + .filter(PrototypeProperty::toStringValue) + .toList(); + + if (toStringFields.isEmpty()) { + method.addContentLine("{};\""); + } else { + method.addContentLine("{\"") + .increaseContentPadding() + .increaseContentPadding() + .addContentLine(toStringFields.stream() + .map(it -> { + boolean secret = it.confidential() || it.typeHandler().actualType() + .equals(Types.CHAR_ARRAY); + + String name = it.name(); + if (secret) { + if (it.typeName().primitive() && !it.typeName().array()) { + return "+ \"" + name + "=****\""; + } + // builder stores fields without optional wrapper + if (!isBuilder && it.typeName().genericTypeName().equals(OPTIONAL)) { + return "+ \"" + name + "=\" + (" + name + ".isPresent() ? \"****\" : " + + "\"null\")"; + } + return "+ \"" + name + "=\" + (" + name + " == null ? \"null\" : " + + "\"****\")"; + } + return "+ \"" + name + "=\" + " + name; + + }) + .collect(Collectors.joining(" + \",\"\n"))); + if (hasSuper) { + method.addContentLine("+ \"};\""); + } else { + method.addContent("+ \"}\""); + } + } + if (hasSuper) { + method.addContent("+ super.toString()"); + } + method.addContentLine(";"); + classBuilder.addMethod(method); + } + + private static void implMethods(InnerClass.Builder classBuilder, TypeContext typeContext) { + // then getters + for (PrototypeProperty child : typeContext.propertyData().properties()) { + String fieldName = child.name(); + String getterName = child.getterName(); + + classBuilder.addMethod(method -> method.name(getterName) + .returnType(child.typeHandler().declaredType()) + .addAnnotation(Annotations.OVERRIDE) + .addContentLine("return " + fieldName + ";")); + } + } + + private static void implAssignToFields(Constructor.Builder constructor, TypeContext typeContext) { + for (PrototypeProperty child : typeContext.propertyData().properties()) { + constructor.addContent("this." + child.name() + " = "); + TypeName declaredType = child.typeHandler().declaredType(); + if (declaredType.genericTypeName().equals(LIST)) { + constructor.addContent(List.class) + .addContentLine(".copyOf(builder." + child.getterName() + "());"); + } else if (declaredType.genericTypeName().equals(SET)) { + constructor.addContent(Collections.class) + .addContent(".unmodifiableSet(new ") + .addContent(LinkedHashSet.class) + .addContentLine("<>(builder." + child.getterName() + "()));"); + } else if (declaredType.genericTypeName().equals(MAP)) { + constructor.addContent(Collections.class) + .addContent(".unmodifiableMap(new ") + .addContent(LinkedHashMap.class) + .addContentLine("<>(builder." + child.getterName() + "()));"); + } else { + if (child.builderGetterOptional() && !declaredType.isOptional()) { + // builder getter optional, but type not, we call get (must be present - is validated) + constructor.addContentLine("builder." + child.getterName() + "().get();"); + } else { + // optional and other types are just plainly assigned + constructor.addContentLine("builder." + child.getterName() + "();"); + } + } + } + } + + private static String toHumanReadable(String name) { + StringBuilder result = new StringBuilder(); + + char[] nameChars = name.toCharArray(); + for (char nameChar : nameChars) { + if (Character.isUpperCase(nameChar)) { + if (!result.isEmpty()) { + result.append(' '); + } + result.append(Character.toLowerCase(nameChar)); + } else { + result.append(nameChar); + } + } + + return result.toString(); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java new file mode 100644 index 00000000000..6a5e780bbf3 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.TypeArgument; +import io.helidon.common.Builder; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotations; +import io.helidon.common.types.TypeName; + +/** + * Builder is an inner class of the prototype. + * It extends the base builder, so we can support further extensibility. + * Class name is always "Builder" + * Super class name is always "BuilderBase" + */ +final class GenerateBuilder { + + private GenerateBuilder() { + } + + static void generate(ClassModel.Builder classBuilder, + TypeName prototype, + TypeName runtimeType, + List typeArguments, + boolean isFactory, + TypeContext typeContext) { + classBuilder.addInnerClass(builder -> { + TypeName builderType = TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".Builder")) + .addTypeArguments(typeArguments) + .build(); + typeArguments.forEach(builder::addGenericArgument); + builder.name("Builder") + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .description("Fluent API builder for {@link " + runtimeType.className() + "}.") + .superType(TypeName.builder() + .from(TypeName.create(prototype.fqName() + ".BuilderBase")) + .addTypeArguments(typeArguments) + .addTypeArgument(builderType) + .addTypeArgument(prototype) + .build()) + .addInterface(TypeName.builder() + .from(TypeName.create(Builder.class)) + .addTypeArgument(builderType) + .addTypeArgument(runtimeType) + .build()) + .addConstructor(constructor -> { + if (typeContext.blueprintData().builderPublic()) { + constructor.accessModifier(AccessModifier.PRIVATE); + } else { + // package private to allow instantiation + constructor.accessModifier(AccessModifier.PACKAGE_PRIVATE); + } + }) + .addMethod(method -> { + method.name("buildPrototype") + .returnType(prototype) + .addAnnotation(Annotations.OVERRIDE) + .addContentLine("preBuildPrototype();") + .addContentLine("validatePrototype();") + .addContent("return new ") + .addContent(prototype.genericTypeName()) + .addContent("Impl"); + if (!typeArguments.isEmpty()) { + method.addContent("<>"); + } + method.addContentLine("(this);"); + }); + if (isFactory) { + GenerateAbstractBuilder.buildRuntimeObjectMethod(builder, typeContext, true); + } else { + // build method returns the same as buildPrototype method + builder.addMethod(method -> method.name("build") + .addAnnotation(Annotations.OVERRIDE) + .returnType(runtimeType) + .addContentLine("return buildPrototype();")); + } + }); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/MethodSignature.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/MethodSignature.java new file mode 100644 index 00000000000..db45f7228a9 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/MethodSignature.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; + +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +record MethodSignature(TypeName returnType, String name, List arguments) { + public static MethodSignature create(TypedElementInfo info) { + return new MethodSignature(info.typeName(), + info.elementName(), + info.parameterArguments().stream() + .map(TypedElementInfo::typeName) + .toList()); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java new file mode 100644 index 00000000000..f4b2954d52e --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.codegen.CodegenUtil.capitalize; + +// builder property +record PrototypeProperty(MethodSignature signature, + TypeHandler typeHandler, + AnnotationDataOption configuredOption, + FactoryMethods factoryMethods, + boolean equality, // part of equals and hash code + boolean toStringValue, // part of toString + boolean confidential // if part of toString, do not print the actual value +) { + // cannot be identifiers - such as field name or method name + private static final Set RESERVED_WORDS = Set.of( + "abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", + "class", "const", "continue", "default", + "do", "double", "else", "enum", + "extends", "final", "finally", "float", + "for", "goto", "if", "implements", + "import", "instanceof", "int", "interface", + "long", "native", "new", "package", + "private", "protected", "public", "return", + "short", "static", "super", "switch", + "synchronized", "this", "throw", "throws", + "transient", "try", "void", "volatile", + "while", "true", "false", "null" + ); + + static PrototypeProperty create(CodegenContext ctx, + TypeInfo blueprint, + TypedElementInfo element, + boolean beanStyleAccessors) { + + boolean isBoolean = element.typeName().boxed().equals(TypeNames.BOXED_BOOLEAN); + String getterName = element.elementName(); // this is always correct + String name = propertyName(getterName, + isBoolean, + beanStyleAccessors); // name of the property, such as "withDefault", "optional", "list", "map" + String setterName = setterName(name, beanStyleAccessors); + if (RESERVED_WORDS.contains(name)) { + name = "the" + capitalize(name); + } + + // real return type (String, Optional, List, Map) + TypeName returnType = propertyTypeName(element); + + boolean sameGeneric = element.hasAnnotation(Types.OPTION_SAME_GENERIC); + // to help with defaults, setters, config mapping etc. + TypeHandler typeHandler = TypeHandler.create(name, getterName, setterName, returnType, sameGeneric); + + // all information from @ConfiguredOption annotation + AnnotationDataOption configuredOption = AnnotationDataOption.create(typeHandler, element); + FactoryMethods factoryMethods = FactoryMethods.create(ctx, + blueprint, + typeHandler); + + boolean confidential = element.hasAnnotation(Types.OPTION_CONFIDENTIAL); + + Optional redundantAnnotation = element.findAnnotation(Types.OPTION_REDUNDANT); + boolean toStringValue = !redundantAnnotation.flatMap(it -> it.getValue("stringValue")) + .map(Boolean::parseBoolean) + .orElse(false); + boolean equality = !redundantAnnotation.flatMap(it -> it.getValue("equality")) + .map(Boolean::parseBoolean) + .orElse(false); + + return new PrototypeProperty( + MethodSignature.create(element), + typeHandler, + configuredOption, + factoryMethods, + equality, + toStringValue, + confidential + ); + } + + private static TypeName propertyTypeName(TypedElementInfo element) { + return element.findAnnotation(Types.OPTION_TYPE) + .flatMap(Annotation::value) + .map(TypeName::create) + .orElseGet(element::typeName); + } + + Field.Builder fieldDeclaration(boolean isBuilder) { + return typeHandler.fieldDeclaration(configuredOption(), isBuilder, !isBuilder); + } + + void setters(InnerClass.Builder classBuilder, TypeName builderType, Javadoc blueprintJavadoc) { + typeHandler().setters(classBuilder, + configuredOption(), + factoryMethods(), + builderType, + blueprintJavadoc); + } + + String name() { + return typeHandler.name(); + } + + String getterName() { + return typeHandler.getterName(); + } + + String setterName() { + return typeHandler.setterName(); + } + + TypeName typeName() { + return typeHandler.declaredType(); + } + + TypeName builderGetterType() { + return typeHandler.builderGetterType(configuredOption.required(), + configuredOption.hasDefault()); + } + + void builderGetter(ContentBuilder contentBuilder) { + typeHandler.generateBuilderGetter(contentBuilder, + configuredOption.required(), + configuredOption.hasDefault()); + } + + boolean builderGetterOptional() { + return typeHandler.builderGetterOptional(configuredOption.required(), + configuredOption.hasDefault()); + } + + private static String setterName(String name, boolean beanStyleAccessors) { + if (beanStyleAccessors || RESERVED_WORDS.contains(name)) { + return "set" + capitalize(name); + } + + return name; + } + + private static String propertyName(String getterName, boolean isBoolean, boolean beanStyleAccessors) { + if (beanStyleAccessors) { + if (isBoolean) { + if (getterName.startsWith("is")) { + return deCapitalize(getterName.substring(2)); + } + } + if (getterName.startsWith("get")) { + return deCapitalize(getterName.substring(3)); + } + } + return getterName; + } + + private static String deCapitalize(String string) { + if (string.isBlank()) { + return string; + } + return Character.toLowerCase(string.charAt(0)) + string.substring(1); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java new file mode 100644 index 00000000000..b613f44f466 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java @@ -0,0 +1,436 @@ + +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.Errors; +import io.helidon.common.Severity; +import io.helidon.common.types.Annotated; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.builder.codegen.Types.PROTOTYPE_BUILDER_DECORATOR; +import static io.helidon.builder.codegen.Types.PROTOTYPE_FACTORY; +import static io.helidon.common.types.TypeNames.OBJECT; +import static io.helidon.common.types.TypeNames.OPTIONAL; + +record TypeContext( + TypeInformation typeInfo, + AnnotationDataBlueprint blueprintData, + AnnotationDataConfigured configuredData, + PropertyData propertyData, + CustomMethods customMethods) { + + private static final Set IGNORED_NAMES = Set.of("build", + "get", + "buildPrototype"); + private static final String BLUEPRINT = "Blueprint"; + private static final Set IGNORED_METHODS = Set.of( + // equals, hash code and toString + new MethodSignature(TypeName.create(boolean.class), "equals", List.of(OBJECT)), + new MethodSignature(TypeName.create(int.class), "hashCode", List.of()), + new MethodSignature(TypeNames.STRING, "toString", List.of()) + ); + + @SuppressWarnings("checkstyle:MethodLength") // use a lot of lines for parameter formatting + static TypeContext create(CodegenContext ctx, TypeInfo blueprint) { + String javadoc = blueprint.description().orElse(null); + // we need to have Blueprint + Optional blueprintAnnotationOpt = blueprint.findAnnotation(Types.PROTOTYPE_BLUEPRINT); + Optional implementAnnoOpt = blueprint.findAnnotation(Types.PROTOTYPE_IMPLEMENT); + + + if (blueprintAnnotationOpt.isEmpty()) { + throw new IllegalStateException("Cannot get @Prototype.Blueprint annotation when processing it for type " + + blueprint); + } + + Annotation blueprintAnnotation = + blueprintAnnotationOpt.orElseGet(() -> Annotation.create(Types.PROTOTYPE_BLUEPRINT)); + List prototypeImplements = + implementAnnoOpt.map(TypeContext::prototypeImplements) + .orElseGet(List::of); + + Set extendList = new LinkedHashSet<>(); + Set superPrototypes = new LinkedHashSet<>(); + Set ignoreInterfaces = new LinkedHashSet<>(); + + // add my blueprint + extendList.add(blueprint.typeName()); + /// prototype (marker interface) + extendList.add(Types.PROTOTYPE_API); + + gatherExtends(blueprint, extendList, superPrototypes, ignoreInterfaces); + extendList.addAll(prototypeImplements); + + Optional superPrototype; + if (superPrototypes.isEmpty()) { + superPrototype = Optional.empty(); + } else { + // the first prototype we reach is the one we extend, this is "best effort" approach + // we could traverse the hierarchy more granularly to find the right one or throw + // if we extend more than one prototype interface on the same level + superPrototype = Optional.of(superPrototypes.iterator().next()); + } + + boolean beanStyleAccessors = blueprintAnnotation.getValue("beanStyle") + .map(Boolean::parseBoolean) + .orElse(false); + + + /* + * Find all valid builder methods + */ + Errors.Collector errors = Errors.collector(); + List propertyMethods = new ArrayList<>(); + // default methods discovered on any interface (this one or extended) - these should not become properties + Set ignoredMethods = new HashSet<>(IGNORED_METHODS); + // all method signatures defined on super prototypes + Set superPrototypeMethods = new HashSet<>(); + gatherBuilderProperties(ctx, + blueprint, + errors, + propertyMethods, + ignoredMethods, + ignoreInterfaces, + beanStyleAccessors, + superPrototypeMethods); + errors.collect().checkValid(); + + /* + now some properties on the current blueprint may be overriding properties from on of the super prototypes + in such a case, we must handle it specifically + - if it has a different default value, update it in the supertype (constructor should call setter with the default + */ + List overridingProperties = new ArrayList<>(); + propertyMethods = propertyMethods.stream() + .filter(it -> { + // filter out all properties from super prototypes + if (superPrototypeMethods.contains(it.signature())) { + overridingProperties.add(it); + return false; + } + return true; + }) + .toList(); + + // filter out duplicates + Set addedSignatures = new HashSet<>(); + propertyMethods = propertyMethods.stream() + .filter(it -> addedSignatures.add(it.signature())) + .toList(); + + boolean hasOptional = propertyMethods.stream() + .map(PrototypeProperty::typeHandler) + .anyMatch(it -> it.declaredType().genericTypeName().equals(OPTIONAL)); + boolean hasRequired = propertyMethods.stream() + .map(PrototypeProperty::configuredOption) + .anyMatch(AnnotationDataOption::required); + boolean hasNonNulls = propertyMethods.stream() + .map(PrototypeProperty::configuredOption) + .anyMatch(AnnotationDataOption::validateNotNull); + boolean hasAllowedValues = propertyMethods.stream() + .map(PrototypeProperty::configuredOption) + .anyMatch(AnnotationDataOption::hasAllowedValues); + boolean prototypePublic = blueprintAnnotation.getValue("isPublic") + .map(Boolean::parseBoolean) + .orElse(true); + // does not make sense to create public builder, if prototype interface is package local + boolean builderPublic = blueprintAnnotation.getValue("builderPublic") + .map(Boolean::parseBoolean) + .orElse(true); + boolean createFromConfigPublic = blueprintAnnotation.getValue("createFromConfigPublic") + .map(Boolean::parseBoolean) + .orElse(true); + boolean createEmptyPublic = blueprintAnnotation.getValue("createEmptyPublic") + .map(Boolean::parseBoolean) + .orElse(true); + boolean hasProvider = propertyMethods.stream() + .map(PrototypeProperty::configuredOption) + .map(AnnotationDataOption::provider) + .filter(it -> it) // filter our falses + .findFirst() + .orElse(false); + Optional decorator = blueprintAnnotation.getValue("decorator") + .map(TypeName::create) + .filter(Predicate.not(PROTOTYPE_BUILDER_DECORATOR::equals)); + + // factory is if the blueprint implements Factory + Optional factoryInterface = blueprint.interfaceTypeInfo() + .stream() + .filter(it -> PROTOTYPE_FACTORY.equals(it.typeName().genericTypeName())) + .findFirst(); + boolean isFactory = factoryInterface.isPresent(); + Optional runtimeObject = factoryInterface.map(it -> it.typeName().typeArguments().getFirst()); + + AnnotationDataConfigured configured = AnnotationDataConfigured.create(blueprint); + + TypeName prototype = generatedTypeName(blueprint); + + TypeName prototypeImpl = TypeName.builder(prototype) + .className(prototype.className() + "Impl") + .build(); + + TypeName prototypeBuilder = TypeName.builder(prototype) + .addEnclosingName(prototype.className()) + .className("Builder") + .build(); + + TypeInformation typeInformation = new TypeInformation(blueprint, + prototype, + prototypeBuilder, + prototypeImpl, + runtimeObject, + decorator, + superPrototype, + annotationsToGenerate(blueprint)); + + return new TypeContext( + typeInformation, + new AnnotationDataBlueprint( + prototypePublic, + builderPublic, + createFromConfigPublic, + createEmptyPublic, + isFactory, + extendList, + javadoc, + blueprint.typeName().typeArguments()), + configured, + new PropertyData(hasOptional, + hasRequired, + hasNonNulls, + hasProvider, + hasAllowedValues, + propertyMethods, + overridingProperties), + CustomMethods.create(ctx, typeInformation)); + } + + static List annotationsToGenerate(Annotated annotated) { + return annotated.findAnnotation(Types.PROTOTYPE_ANNOTATED) + .flatMap(Annotation::stringValues) + .stream() + .flatMap(List::stream) + .toList(); + } + + private static List prototypeImplements(Annotation annotation) { + return annotation.stringValues() + .stream() + .flatMap(List::stream) + .map(TypeName::create) + .toList(); + } + + private static void gatherExtends(TypeInfo typeInfo, Set extendList, + Set superPrototypes, + Set ignoredInterfaces) { + // if any implemented interface is a @Blueprint, we must extend the target type as well + // as any implemented interface is already a Prototype, we ignore additional annotations (it is our super type) + List typeInfos = typeInfo.interfaceTypeInfo(); + for (TypeInfo info : typeInfos) { + if (info.findAnnotation(Types.PROTOTYPE_BLUEPRINT).isPresent()) { + // this is a blueprint, we must implement its built type + TypeName typeName = info.typeName(); + String className = typeName.className(); + TypeName toExtend = TypeName.builder(typeName) + .className(className.substring(0, className.length() - 9)) + .build(); + extendList.add(toExtend); + superPrototypes.add(toExtend); + ignoredInterfaces.add(toExtend); + ignoredInterfaces.add(typeName); + } + boolean gatherAll = true; + for (TypeInfo implementedInterface : info.interfaceTypeInfo()) { + if (implementedInterface.typeName().equals(Types.PROTOTYPE_API)) { + extendList.add(info.typeName()); + // this is a prototype itself, ignore additional interfaces + gatherAll = false; + superPrototypes.add(info.typeName()); + + // we need to ignore ANY interface implemented by "info" and its super interfaces + if (ignoredInterfaces.add(info.typeName().genericTypeName())) { + ignoredInterfaces.add(TypeName.builder(info.typeName()) + .className(info.typeName().className() + "Blueprint") + .build()); + ignoreAllInterfaces(ignoredInterfaces, info); + } + break; + } + } + if (gatherAll) { + gatherExtends(info, extendList, superPrototypes, ignoredInterfaces); + } + } + } + + private static void ignoreAllInterfaces(Set ignoredInterfaces, TypeInfo info) { + // also add all super interfaces of the prototype + List superIfaces = info.interfaceTypeInfo(); + + for (TypeInfo superIface : superIfaces) { + if (ignoredInterfaces.add(superIface.typeName().genericTypeName())) { + ignoreAllInterfaces(ignoredInterfaces, superIface); + } + } + } + + @SuppressWarnings("checkstyle:ParameterNumber") // we need all of them + private static void gatherBuilderProperties(CodegenContext ctx, + TypeInfo typeInfo, + Errors.Collector errors, + List properties, + Set ignoredMethods, + Set ignoreInterfaces, + boolean beanStyleAccessors, + Set superPrototypeMethods) { + + // we are only interested in getter methods + TypeName typeName = typeInfo.typeName(); + properties.addAll(typeInfo.elementInfo().stream() + .filter(ElementInfoPredicates::isMethod) + .filter(Predicate.not(ElementInfoPredicates::isStatic)) + .filter(Predicate.not(ElementInfoPredicates::isPrivate)) + .filter(it -> { + if (it.elementModifiers().contains(Modifier.DEFAULT)) { + ignoredMethods.add(MethodSignature.create(it)); + return false; + } + return true; + }) + .filter(it -> { + if (IGNORED_NAMES.contains(it.elementName())) { + return false; + } + return !ignoredMethods.contains(MethodSignature.create(it)); + }) + .filter(it -> { + // if the method is defined on a super prototype, add it to the set + if (ignoreInterfaces.contains(it.enclosingType().get())) { + // collect all methods from super prototypes, so we know how to handle overrides + superPrototypeMethods.add(MethodSignature.create(it)); + } + if (ignoreInterfaces.contains(it.enclosingType().get())) { + // if this method is defined on an ignored interface, filter it out + return false; + } + return true; + }) + .filter(it -> { + Severity severity = Severity.WARN; + + // parameters and return type + if (it.typeName().equals(TypeNames.PRIMITIVE_VOID)) { + // invalid return type for builder + + errors.message("Builder definition methods cannot have void return type " + + "(must be getters): " + + typeName + "." + it.elementName(), + severity); + return false; + } + if (!it.parameterArguments().isEmpty()) { + errors.message("Builder definition methods cannot have " + + "parameters (must be getters): " + + typeName + "." + it.elementName(), + severity); + return false; + } + + return true; + }) + // filter out Supplier.get() + .filter(it -> !("get".equals(it.elementName()) && "T".equals(it.typeName().className()))) + .map(it -> PrototypeProperty.create(ctx, + typeInfo, + it, + beanStyleAccessors)) + .toList()); + + // we also need to add info for all implemented interfaces + List interfaces = typeInfo.interfaceTypeInfo(); + + for (TypeInfo anInterface : interfaces) { + gatherBuilderProperties(ctx, + anInterface, + errors, + properties, + ignoredMethods, + ignoreInterfaces, + beanStyleAccessors, + superPrototypeMethods); + } + } + + private static TypeName generatedTypeName(TypeInfo typeInfo) { + String typeName = typeInfo.typeName().className(); + if (typeName.endsWith(BLUEPRINT)) { + typeName = typeName.substring(0, typeName.length() - BLUEPRINT.length()); + } else { + throw new IllegalArgumentException("Blueprint interface name must end with " + BLUEPRINT + + ", this is invalid type: " + typeInfo.typeName().fqName()); + } + + return TypeName.builder(typeInfo.typeName()) + .enclosingNames(List.of()) + .className(typeName) + .build(); + } + + record TypeInformation( + TypeInfo blueprintType, + TypeName prototype, + TypeName prototypeBuilder, + TypeName prototypeImpl, + Optional runtimeObject, + Optional decorator, + Optional superPrototype, + List annotationsToGenerate) { + public TypeName prototypeBuilderBase() { + return TypeName.builder(prototypeBuilder) + .className(prototypeBuilder.className() + "Base") + .build() + .genericTypeName(); + } + } + + record PropertyData( + boolean hasOptional, + boolean hasRequired, + boolean hasNonNulls, + boolean hasProvider, + boolean hasAllowedValues, + List properties, + List overridingProperties) { + } + +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java new file mode 100644 index 00000000000..5eee9bc9550 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java @@ -0,0 +1,629 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +class TypeHandler { + private final String name; + private final String getterName; + private final String setterName; + private final TypeName declaredType; + + TypeHandler(String name, String getterName, String setterName, TypeName declaredType) { + this.name = name; + this.getterName = getterName; + this.setterName = setterName; + this.declaredType = declaredType; + } + + static TypeHandler create(String name, String getterName, String setterName, TypeName returnType, boolean sameGeneric) { + if (TypeNames.OPTIONAL.equals(returnType)) { + return new TypeHandlerOptional(name, getterName, setterName, returnType); + } + if (TypeNames.SUPPLIER.equals(returnType)) { + return new TypeHandlerSupplier(name, getterName, setterName, returnType); + } + if (TypeNames.SET.equals(returnType)) { + return new TypeHandlerSet(name, getterName, setterName, returnType); + } + + if (TypeNames.LIST.equals(returnType)) { + return new TypeHandlerList(name, getterName, setterName, returnType); + } + if (TypeNames.MAP.equals(returnType)) { + return new TypeHandlerMap(name, getterName, setterName, returnType, sameGeneric); + } + + return new TypeHandler(name, getterName, setterName, returnType); + } + + static AccessModifier setterAccessModifier(AnnotationDataOption configured) { + return configured.accessModifier(); + } + + static TypeName toWildcard(TypeName typeName) { + if (typeName.wildcard()) { + return typeName; + } + return TypeName.builder(typeName).wildcard(true).build(); + } + + static boolean isConfigProperty(TypeHandler handler) { + return "config".equals(handler.name()) + && handler.actualType().equals(Types.COMMON_CONFIG); + } + + protected static TypeName collectionImplType(TypeName typeName) { + TypeName genericTypeName = typeName.genericTypeName(); + if (genericTypeName.equals(TypeNames.MAP)) { + return Types.LINKED_HASH_MAP; + } + if (genericTypeName.equals(TypeNames.LIST)) { + return Types.ARRAY_LIST; + } + + return Types.LINKED_HASH_SET; + } + + @Override + public String toString() { + return declaredType.fqName() + " " + name; + } + + TypeName builderGetterType(boolean required, boolean hasDefault) { + if (builderGetterOptional(required, hasDefault)) { + if (declaredType().isOptional()) { + // already wrapped + return declaredType(); + } else { + return TypeName.builder(TypeNames.OPTIONAL) + .addTypeArgument(declaredType().boxed()) + .build(); + } + } + return declaredType(); + } + + void generateBuilderGetter(ContentBuilder contentBuilder, + boolean required, + boolean hasDefault) { + contentBuilder.addContent("return "); + if (builderGetterOptional(required, hasDefault)) { + contentBuilder.addContent(Optional.class) + .addContent(".ofNullable(") + .addContent(name) + .addContent(")"); + } else { + contentBuilder.addContent(name); + } + contentBuilder.addContentLine(";"); + } + + Field.Builder fieldDeclaration(AnnotationDataOption configured, + boolean isBuilder, + boolean alwaysFinal) { + Field.Builder builder = Field.builder() + .name(name) + .isFinal(alwaysFinal || !isBuilder); + + if (isBuilder && (configured.required())) { + // we need to use object types to be able to see if this was configured + builder.type(declaredType.boxed()); + } else { + builder.type(declaredType); + } + + if (isBuilder && configured.hasDefault()) { + configured.defaultValue().accept(builder); + } + + return builder; + } + + Consumer> toDefaultValue(String defaultValue) { + TypeName typeName = actualType(); + if (TypeNames.STRING.equals(typeName)) { + return content -> content.addContent("\"") + .addContent(defaultValue) + .addContent("\""); + } + if (TypeNames.DURATION.equals(typeName)) { + return content -> content.addContent(Duration.class) + .addContent(".parse(\"") + .addContent(defaultValue) + .addContent("\")"); + } + if (Types.CHAR_ARRAY.equals(typeName)) { + return content -> content.addContent("\"") + .addContent(defaultValue) + .addContent("\".toCharArray()"); + } + if (typeName.primitive()) { + if (typeName.fqName().equals("char")) { + return content -> content.addContent("'") + .addContent(defaultValue) + .addContent("'"); + } + return content -> content.addContent(defaultValue); + } + if (typeName.name().startsWith("java.")) { + return content -> content.addContent(defaultValue); + } + // should be an enum + return content -> content.addContent(typeName.genericTypeName()) + .addContent(".") + .addContent(defaultValue); + } + + Consumer> toDefaultValue(List defaultValues, + List defaultInts, + List defaultLongs, + List defaultDoubles, + List defaultBooleans, + String defaultCode, + AnnotationDataOption.DefaultMethod defaultMethod) { + if (defaultCode != null) { + return content -> content.addContent(defaultCode); + } + if (defaultMethod != null) { + // must return the correct type + return toDefaultFromMethod(defaultMethod); + } + + return toDefaultValue(defaultValues, + defaultInts, + defaultLongs, + defaultDoubles, + defaultBooleans); + } + + Consumer> toDefaultValue(List defaultValues, + List defaultInts, + List defaultLongs, + List defaultDoubles, + List defaultBooleans) { + if (defaultValues != null) { + String string = singleDefault(defaultValues); + return toDefaultValue(string); + } + if (defaultInts != null) { + return content -> content.addContent(String.valueOf(singleDefault(defaultInts))); + } + if (defaultLongs != null) { + return content -> content.addContent(singleDefault(defaultLongs) + "L"); + } + if (defaultDoubles != null) { + return content -> content.addContent(String.valueOf(singleDefault(defaultDoubles))); + } + if (defaultBooleans != null) { + return content -> content.addContent(String.valueOf(singleDefault(defaultBooleans))); + } + + return contentBuilder -> { + }; + } + + protected Consumer> toDefaultFromMethod(AnnotationDataOption.DefaultMethod defaultMethod) { + return content -> content.addContent(defaultMethod.type().genericTypeName()) + .addContent(".") + .addContent(defaultMethod.method()) + .addContent("()"); + } + + TypeName declaredType() { + return declaredType; + } + + TypeName actualType() { + return declaredType; + } + + String name() { + return name; + } + + String getterName() { + return getterName; + } + + String setterName() { + return setterName; + } + + void generateFromConfig(Method.Builder method, + AnnotationDataOption configured, + FactoryMethods factoryMethods) { + method.addContent(configGet(configured)); + String fqName = actualType().fqName(); + + if (fqName.endsWith(".Builder")) { + // this is a special case - we have a builder field + if (configured.hasDefault()) { + method.addContent(".as(") + .addContent(Types.COMMON_CONFIG) + .addContent(".class).ifPresent(") + .addContent(name()) + .addContentLine("::config);"); + } else { + // a bit dirty hack - we expect builder() method to exist on the class that owns the builder + int lastDot = fqName.lastIndexOf('.'); + String builderMethod = fqName.substring(0, lastDot) + ".builder()"; + method.addContentLine(".map(" + builderMethod + "::config).ifPresent(this::" + setterName() + ");"); + } + } else { + generateFromConfig(method, factoryMethods); + method.addContentLine(".ifPresent(this::" + setterName() + ");"); + } + } + + String configGet(AnnotationDataOption configured) { + if (configured.configMerge()) { + return "config"; + } + return "config.get(\"" + configured.configKey() + "\")"; + } + + String generateFromConfig(FactoryMethods factoryMethods) { + if (actualType().fqName().equals("char[]")) { + return ".asString().as(String::toCharArray)"; + } + + TypeName boxed = actualType().boxed(); + return factoryMethods.createFromConfig() + .map(it -> ".map(" + it.typeWithFactoryMethod().genericTypeName().fqName() + "::" + it.createMethodName() + ")") + .orElseGet(() -> ".as(" + boxed.fqName() + ".class)"); + + } + + void generateFromConfig(Method.Builder method, FactoryMethods factoryMethods) { + if (actualType().fqName().equals("char[]")) { + method.addContent(".asString().as(") + .addContent(String.class) + .addContent("::toCharArray)"); + return; + } + + Optional fromConfig = factoryMethods.createFromConfig(); + if (fromConfig.isPresent()) { + FactoryMethods.FactoryMethod factoryMethod = fromConfig.get(); + method.addContent(".map(") + .addContent(factoryMethod.typeWithFactoryMethod().genericTypeName()) + .addContent("::" + factoryMethod.createMethodName() + ")"); + } else { + TypeName boxed = actualType().boxed(); + method.addContent(".as(") + .addContent(boxed) + .addContent(".class)"); + } + } + + TypeName argumentTypeName() { + return declaredType(); + } + + void setters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + FactoryMethods factoryMethod, + TypeName returnType, + Javadoc blueprintJavadoc) { + + declaredSetter(classBuilder, configured, returnType, blueprintJavadoc); + + if (actualType().equals(Types.CHAR_ARRAY)) { + charArraySetter(classBuilder, configured, returnType, blueprintJavadoc); + } + + // if there is a factory method for the return type, we also have setters for the type (probably config object) + if (factoryMethod.createTargetType().isPresent()) { + factorySetter(classBuilder, configured, returnType, blueprintJavadoc, factoryMethod.createTargetType().get()); + } + + // if there is a builder factory method, we create a method with builder consumer + if (factoryMethod.builder().isPresent()) { + factorySetterConsumer(classBuilder, configured, returnType, blueprintJavadoc, factoryMethod.builder().get()); + factorySetterSupplier(classBuilder, configured, returnType, blueprintJavadoc); + } + + String fqName = actualType().fqName(); + if (fqName.endsWith(".Builder")) { + // this is a special case - we have a builder field, we want to generate consumer (special, same instance) + setterConsumer(classBuilder, configured, returnType, blueprintJavadoc); + } + } + + void setterConsumer(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + String argumentName = "consumer"; + + List paramLines = new ArrayList<>(); + paramLines.add("consumer of builder for"); + paramLines.addAll(blueprintJavadoc.returnDescription()); + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, paramLines) + .build(); + + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(actualType()) + .build(); + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .javadoc(javadoc) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("var builder = "); + + if (configured.hasDefault()) { + builder.addContentLine("this." + name() + ";"); + } else { + String fqName = actualType().fqName(); + // a bit dirty hack - we expect builder() method to exist on the class that owns the builder + int lastDot = fqName.lastIndexOf('.'); + String builderMethod = fqName.substring(0, lastDot) + ".builder()"; + builder.addContentLine(builderMethod + ";"); + } + + builder.addContentLine("consumer.accept(builder);") + .addContentLine("this." + name() + "(builder);") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + protected Javadoc.Builder setterJavadoc(Javadoc blueprintJavadoc) { + return Javadoc.builder(blueprintJavadoc) + .addTag("see", "#" + getterName() + "()") + .returnDescription("updated builder instance"); + } + + protected void charArraySetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + classBuilder.addMethod(builder -> builder.name(setterName()) + .returnType(returnType) + .addParameter(param -> param.name(name()) + .type(TypeNames.STRING)) + .javadoc(setterJavadoc(blueprintJavadoc) + .addParameter(name(), blueprintJavadoc.returnDescription()) + .build()) + .accessModifier(setterAccessModifier(configured)) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + " = " + name() + ".toCharArray();") + .addContentLine("return self();")); + } + + boolean builderGetterOptional(boolean required, boolean hasDefault) { + // optional and collections - good return types + if (declaredType().isList() + || declaredType().isMap() + || declaredType().isSet()) { + return false; + } + if (declaredType().isOptional()) { + return true; + } + // optional and primitive type - good return type (uses default for primitive if not customized) + if (!required && declaredType().primitive()) { + return false; + } + // has default, and not Optional - return type (never can be null) + // any other case (required, optional without defaults) - return optional + return !hasDefault; + + } + + protected void declaredSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .javadoc(setterJavadoc(blueprintJavadoc) + .addParameter(name(), blueprintJavadoc.returnDescription()) + .build()) + .returnType(returnType) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(name()) + .type(argumentTypeName())) + .accessModifier(setterAccessModifier(configured)); + if (!declaredType.primitive()) { + builder.addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");"); + } + + if (configured.decorator() != null) { + builder.addContent("new ") + .addContent(configured.decorator()) + .addContent("().decorate(this, ") + .addContent(name()) + .addContentLine(");"); + } + + builder.addContentLine("this." + name() + " = " + name() + ";"); + + builder.addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private T singleDefault(List defaultValues) { + if (defaultValues.isEmpty()) { + throw new IllegalArgumentException("Default values configured for " + name() + " are empty, one value is expected."); + } + if (defaultValues.size() > 1) { + throw new IllegalArgumentException("Default values configured for " + name() + " contain more than one value," + + " exactly one value is expected."); + } + return defaultValues.getFirst(); + } + + private void factorySetterConsumer(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc, + FactoryMethods.FactoryMethod factoryMethod) { + TypeName builderType; + if (factoryMethod.factoryMethodReturnType().className().equals("Builder")) { + builderType = factoryMethod.factoryMethodReturnType(); + } else if (factoryMethod.factoryMethodReturnType().className().endsWith(".Builder")) { + builderType = factoryMethod.factoryMethodReturnType(); + } else { + builderType = TypeName.create(factoryMethod.factoryMethodReturnType().fqName() + ".Builder"); + } + + String argumentName = "consumer"; + + List paramLines = new ArrayList<>(); + paramLines.add("consumer of builder for"); + paramLines.addAll(blueprintJavadoc.returnDescription()); + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, paramLines) + .build(); + + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build(); + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .accessModifier(setterAccessModifier(configured)) + .javadoc(javadoc) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("var builder = ") + .addContent(factoryMethod.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + factoryMethod.createMethodName() + "();") + .addContentLine("consumer.accept(builder);") + .addContentLine("this." + name() + "(builder.build());") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private void factorySetterSupplier(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + TypeName supplierType = actualType(); + if (!supplierType.wildcard()) { + supplierType = TypeName.builder(supplierType) + .wildcard(true) + .build(); + } + supplierType = TypeName.builder(TypeNames.SUPPLIER) + .addTypeArgument(supplierType) + .build(); + + String argumentName = "supplier"; + + List paramLines = new ArrayList<>(); + paramLines.add("supplier of"); + paramLines.addAll(blueprintJavadoc.returnDescription()); + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, paramLines) + .build(); + + TypeName argumentType = supplierType; + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .accessModifier(setterAccessModifier(configured)) + .javadoc(javadoc) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContentLine("this." + name() + "(" + argumentName + ".get());") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private void factorySetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc, + FactoryMethods.FactoryMethod factoryMethod) { + String argumentName = name() + "Config"; + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .javadoc(setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, blueprintJavadoc.returnDescription()) + .build()) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(argumentName) + .type(factoryMethod.argumentType())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("this." + name() + " = ") + .addContent(factoryMethod.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + factoryMethod.createMethodName() + "(" + argumentName + ");") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + static class OneTypeHandler extends TypeHandler { + private final TypeName actualType; + + OneTypeHandler(String name, String getterName, String setterName, TypeName declaredType) { + super(name, getterName, setterName, declaredType); + + if (declaredType.typeArguments().isEmpty()) { + this.actualType = TypeNames.STRING; + } else { + this.actualType = declaredType.typeArguments().getFirst(); + } + } + + @Override + TypeName actualType() { + return actualType; + } + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java new file mode 100644 index 00000000000..e8884fc57af --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java @@ -0,0 +1,453 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.builder.codegen.Types.COMMON_CONFIG; +import static io.helidon.codegen.CodegenUtil.capitalize; + +abstract class TypeHandlerCollection extends TypeHandler.OneTypeHandler { + private static final Set BUILT_IN_MAPPERS = Set.of( + TypeNames.STRING, + TypeNames.BOXED_BOOLEAN, + TypeNames.BOXED_BYTE, + TypeNames.BOXED_SHORT, + TypeNames.BOXED_INT, + TypeNames.BOXED_LONG, + TypeNames.BOXED_CHAR, + TypeNames.BOXED_FLOAT, + TypeNames.BOXED_DOUBLE, + TypeNames.BOXED_VOID, + TypeName.create(BigDecimal.class), + TypeName.create(BigInteger.class), + TypeName.create(Pattern.class), + TypeName.create(Class.class), + TypeName.create(Duration.class), + TypeName.create(Period.class), + TypeName.create(LocalDate.class), + TypeName.create(LocalDateTime.class), + TypeName.create(LocalTime.class), + TypeName.create(ZonedDateTime.class), + TypeName.create(ZoneId.class), + TypeName.create(ZoneOffset.class), + TypeName.create(Instant.class), + TypeName.create(OffsetTime.class), + TypeName.create(OffsetDateTime.class), + TypeName.create(YearMonth.class), + TypeName.create(File.class), + TypeName.create(Path.class), + TypeName.create(Charset.class), + TypeName.create(URI.class), + TypeName.create(URL.class), + TypeName.create(UUID.class) + ); + private final TypeName collectionType; + private final TypeName collectionImplType; + private final String collector; + private final Optional configMapper; + + TypeHandlerCollection(String name, + String getterName, + String setterName, + TypeName declaredType, + TypeName collectionType, + String collector, + Optional configMapper) { + super(name, getterName, setterName, declaredType); + this.collectionType = collectionType; + this.collectionImplType = collectionImplType(collectionType); + this.collector = collector; + this.configMapper = configMapper; + } + + @Override + Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilder, boolean alwaysFinal) { + Field.Builder builder = super.fieldDeclaration(configured, isBuilder, true); + if (isBuilder && !configured.hasDefault()) { + newCollectionInstanceWithoutParams(builder); + builder.addContent("()"); + } + return builder; + } + + @Override + Consumer> toDefaultValue(List defaultValues, + List defaultInts, + List defaultLongs, + List defaultDoubles, + List defaultBooleans) { + + if (defaultValues != null) { + return content -> { + newCollectionInstanceWithoutParams(content); + content.addContent("(") + .addContent(collectionType.genericTypeName()) + .addContent(".of("); + + for (int i = 0; i < defaultValues.size(); i++) { + toDefaultValue(defaultValues.get(i)).accept(content); + if (i != defaultValues.size() - 1) { + content.addContent(", "); + } + } + content.addContent("))"); + }; + } + + if (defaultInts != null) { + return defaultCollection(defaultInts); + } + if (defaultLongs != null) { + return content -> { + newCollectionInstanceWithoutParams(content); + content.addContent("(") + .addContent(collectionType.genericTypeName()) + .addContent(".of("); + + for (int i = 0; i < defaultLongs.size(); i++) { + content.addContent(String.valueOf(defaultLongs.get(i))) + .addContent("L"); + if (i != defaultLongs.size() - 1) { + content.addContent(", "); + } + } + content.addContent("))"); + }; + } + if (defaultDoubles != null) { + return defaultCollection(defaultDoubles); + } + if (defaultBooleans != null) { + return defaultCollection(defaultBooleans); + } + + return null; + } + + @Override + void generateFromConfig(Method.Builder method, + AnnotationDataOption configured, + FactoryMethods factoryMethods) { + if (configured.provider()) { + return; + } + TypeName actualType = actualType().genericTypeName(); + + if (factoryMethods.createFromConfig().isPresent()) { + FactoryMethods.FactoryMethod factoryMethod = factoryMethods.createFromConfig().get(); + TypeName returnType = factoryMethod.factoryMethodReturnType(); + boolean mapList = true; + if (returnType.isList() || returnType.isSet()) { + mapList = false; + } else { + // return type is some other type, we must check it is the same as this one, + // or we expect another method to be used + mapList = returnType.equals(actualType); + } + if (mapList) { + method.addContentLine(configGet(configured) + + ".mapList(" + + generateMapListFromConfig(factoryMethods) + + ").ifPresent(this::" + setterName() + ");"); + } else { + method.addContentLine(configGet(configured) + + generateFromConfig(factoryMethods) + + ".ifPresent(this::" + setterName() + ");"); + } + } else if (BUILT_IN_MAPPERS.contains(actualType)) { + // types we support in config can be simplified, + // this also supports comma separated lists for string based types + method.addContent(configGet(configured)) + .addContent(".asList(") + .addContent(actualType.genericTypeName()) + .addContent(".class") + .addContent(")"); + configMapper.ifPresent(method::addContent); + method.addContent(".ifPresent(this::") + .addContent(setterName()) + .addContentLine(");"); + } else { + method.addContentLine(configGet(configured) + + ".asNodeList()" + + ".map(nodeList -> nodeList.stream()" + + ".map(cfg -> cfg" + + generateFromConfig(factoryMethods) + + ".get())" + + "." + collector + ")" + + ".ifPresent(this::" + setterName() + ");"); + } + } + + String generateMapListFromConfig(FactoryMethods factoryMethods) { + return factoryMethods.createFromConfig() + .map(it -> it.typeWithFactoryMethod().genericTypeName().fqName() + "::" + it.createMethodName()) + .orElseThrow(() -> new IllegalStateException("This should have been called only if factory method is present for " + + declaredType() + " " + name())); + + } + + @Override + TypeName argumentTypeName() { + return TypeName.builder(collectionType) + .addTypeArgument(toWildcard(actualType())) + .build(); + } + + @Override + void setters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + FactoryMethods factoryMethods, + TypeName returnType, + Javadoc blueprintJavadoc) { + + if (configured.provider()) { + discoverServicesSetter(classBuilder, configured, returnType, blueprintJavadoc); + } + + // we cannot call super. as collections are always final + // there is always a setter with the declared type, replacing values + declaredSetters(classBuilder, configured, returnType, blueprintJavadoc); + + if (factoryMethods.createTargetType().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + factorySetter(classBuilder, configured, returnType, blueprintJavadoc, factoryMethods.createTargetType().get()); + } + + if (configured.singular()) { + singularSetter(classBuilder, configured, returnType, blueprintJavadoc, configured.singularName()); + } + + if (factoryMethods.builder().isPresent()) { + factorySetterConsumer(classBuilder, + configured, + returnType, + blueprintJavadoc, + factoryMethods, + factoryMethods.builder().get()); + } + } + + private void newCollectionInstanceWithoutParams(ContentBuilder content) { + content.addContent("new ") + .addContent(collectionImplType.genericTypeName()) + .addContent("<>"); + } + + private Consumer> defaultCollection(List list) { + return content -> { + newCollectionInstanceWithoutParams(content); + content.addContent("(") + .addContent(collectionType.genericTypeName()) + .addContent(".of("); + + for (int i = 0; i < list.size(); i++) { + content.addContent(String.valueOf(list.get(i))); + if (i != list.size() - 1) { + content.addContent(", "); + } + } + content.addContent("))"); + }; + } + + private void discoverServicesSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + classBuilder.addMethod(builder -> builder.name(setterName() + "DiscoverServices") + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name("discoverServices") + .type(boolean.class) + .description("whether to discover implementations through service loader")) + .accessModifier(setterAccessModifier(configured)) + .addContentLine("this." + name() + "DiscoverServices = discoverServices;") + .addContentLine("return self();")); + } + + private void factorySetterConsumer(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc, + FactoryMethods factoryMethods, + FactoryMethods.FactoryMethod factoryMethod) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + TypeName builderType; + if (factoryMethod.factoryMethodReturnType().className().equals("Builder")) { + builderType = factoryMethod.factoryMethodReturnType(); + } else { + builderType = TypeName.create(factoryMethod.factoryMethodReturnType().fqName() + ".Builder"); + } + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build(); + String argumentName = "consumer"; + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, blueprintJavadoc.returnDescription()) + .build(); + + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .javadoc(javadoc) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("var builder = ") + .addContent(factoryMethod.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + factoryMethod.createMethodName() + "();") + .addContentLine("consumer.accept(builder);"); + + if (factoryMethods.createTargetType() + .map(FactoryMethods.FactoryMethod::factoryMethodReturnType) + .map(m -> m.genericTypeName().equals(collectionType)) + .orElse(false)) { + builder.addContentLine("this." + name() + "(builder.build());") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } else if (configured.singular()) { + String singularName = configured.singularName(); + String methodName = "add" + capitalize(singularName); + builder.name(methodName) + .addContentLine("this." + name() + ".add(builder.build());") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + } + + private void singularSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc, + String singularName) { + String methodName = "add" + capitalize(singularName); + + Method.Builder builder = Method.builder() + .name(methodName) + .javadoc(setterJavadoc(blueprintJavadoc) + .addParameter(singularName, blueprintJavadoc.returnDescription()) + .build()) + .returnType(returnType) + .update(it -> configured.annotations().forEach(it::addAnnotation)) + .addParameter(param -> param.name(singularName) + .type(actualType())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + singularName + ");") + .addContentLine("this." + name() + ".add(" + singularName + ");") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private void factorySetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc, + FactoryMethods.FactoryMethod factoryMethod) { + if (factoryMethod.argumentType().equals(COMMON_CONFIG)) { + // if the factory method uses config as a parameter, then it is not desired on the builder + return; + } + String argumentName = name() + "Config"; + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(argumentName) + .type(factoryMethod.argumentType()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContentLine("this." + name() + ".clear();") + .addContent("this." + name() + ".addAll(") + .addContent(factoryMethod.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + factoryMethod.createMethodName() + "(" + argumentName + "));") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } + + private void declaredSetters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + // we cannot call super. as collections are always final + // there is always a setter with the declared type, replacing values + Method.Builder builder = Method.builder() + .name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + ".clear();") + .addContentLine("this." + name() + ".addAll(" + name() + ");") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + + builder.name("add" + capitalize(name())) + .clearContent() + .addContentLine("Objects.requireNonNull(" + name() + ");") //Overwrites existing content + .addContentLine("this." + name() + ".addAll(" + name() + ");") + .addContentLine("return self();"); + classBuilder.addMethod(builder); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java new file mode 100644 index 00000000000..90277f33b9b --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Optional; + +import io.helidon.common.types.TypeName; + +import static io.helidon.common.types.TypeNames.LIST; + +class TypeHandlerList extends TypeHandlerCollection { + + TypeHandlerList(String name, String getterName, String setterName, TypeName declaredType) { + super(name, getterName, setterName, declaredType, LIST, "toList()", Optional.empty()); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java new file mode 100644 index 00000000000..0fbfda0e07e --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerMap.java @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.ContentBuilder; +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.codegen.classmodel.TypeArgument; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.codegen.CodegenUtil.capitalize; +import static io.helidon.common.types.TypeNames.LIST; +import static io.helidon.common.types.TypeNames.MAP; +import static io.helidon.common.types.TypeNames.OBJECT; +import static io.helidon.common.types.TypeNames.SET; + +class TypeHandlerMap extends TypeHandler { + private static final TypeName SAME_GENERIC_TYPE = TypeName.createFromGenericDeclaration("TYPE"); + private final TypeName actualType; + private final TypeName implTypeName; + private final boolean sameGeneric; + + TypeHandlerMap(String name, String getterName, String setterName, TypeName declaredType, boolean sameGeneric) { + super(name, getterName, setterName, declaredType); + this.sameGeneric = sameGeneric; + + this.implTypeName = collectionImplType(MAP); + if (declaredType.typeArguments().size() < 2) { + this.actualType = TypeNames.STRING; + } else { + this.actualType = declaredType.typeArguments().get(1); + } + } + + @Override + Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilder, boolean alwaysFinal) { + Field.Builder builder = super.fieldDeclaration(configured, isBuilder, true); + if (isBuilder && !configured.hasDefault()) { + builder.addContent("new ") + .addContent(implTypeName.genericTypeName()) + .addContent("<>()"); + } + return builder; + } + + @Override + Consumer> toDefaultValue(List defaultValues, + List defaultInts, + List defaultLongs, + List defaultDoubles, + List defaultBooleans) { + + if (defaultValues != null) { + if (defaultValues.size() % 2 != 0) { + throw new IllegalArgumentException("Default value for a map does not have even number of entries:" + + defaultValues); + } + + return content -> { + content.addContent(Map.class) + .addContent(".of("); + + for (int i = 1; i < defaultValues.size(); i = i + 2) { + content.addContent("\"") + .addContent(defaultValues.get(i - 1)) + .addContent("\", "); + super.toDefaultValue(defaultValues.get(i)).accept(content); + if (i < defaultValues.size() - 2) { + content.addContentLine(", "); + } + if (i == 1) { + content.increaseContentPadding() + .increaseContentPadding(); + } + } + + content.addContent(")") + .decreaseContentPadding() + .decreaseContentPadding(); + }; + } + + return null; + } + + @Override + TypeName actualType() { + return actualType; + } + + @Override + void generateFromConfig(Method.Builder method, + AnnotationDataOption configured, + FactoryMethods factoryMethods) { + List typeArguments = declaredType().typeArguments(); + if (TypeNames.STRING.equals(typeArguments.get(0)) && TypeNames.STRING.equals(typeArguments.get(1))) { + // the special case of Map + method.addContentLine(configGet(configured) + ".detach().asMap().ifPresent(this::" + name() + ");"); + } else { + method.addContentLine(configGet(configured) + + ".asNodeList().ifPresent(nodes -> nodes.forEach" + + "(node -> " + + name() + ".put(node.get(\"name\").asString().orElse(node.name()), node" + + generateFromConfig(factoryMethods) + + ".get())));"); + } + } + + @Override + TypeName argumentTypeName() { + return TypeName.builder(MAP) + .addTypeArgument(toWildcard(declaredType().typeArguments().get(0))) + .addTypeArgument(toWildcard(declaredType().typeArguments().get(1))) + .build(); + } + + @SuppressWarnings("checkstyle:MethodLength") // will be shorter when we switch to class model + @Override + void setters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + FactoryMethods factoryMethod, + TypeName returnType, + Javadoc blueprintJavadoc) { + + declaredSetter(classBuilder, configured, returnType, blueprintJavadoc); + declaredSetterAdd(classBuilder, configured, returnType, blueprintJavadoc); + + if (factoryMethod.createTargetType().isPresent()) { + // factory method + FactoryMethods.FactoryMethod fm = factoryMethod.createTargetType().get(); + String name = name(); + String argumentName = name + "Config"; + classBuilder.addMethod(builder -> { + builder.name(name + "Config") + .description(blueprintJavadoc.content()) + .accessModifier(setterAccessModifier(configured)) + .addDescriptionLine("This method keeps existing values, then puts all new values into the map.") + .addParameter(param -> param.name(argumentName) + .type(fm.argumentType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .returnType(returnType, "updated builder instance") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContentLine("this." + name + ".clear();") + .addContent("this." + name + ".putAll(") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "(" + argumentName + "));") + .addContentLine("return self();"); + }); + } + + TypeName keyType = declaredType().typeArguments().get(0); + + if (configured.singular() && isCollection(actualType())) { + // value is a collection as well, we need to generate `add` methods for adding a single value, and adding + // collection values + // builder.addValue(String key, String value) + // builder.addValues(String key, Set values) + String singularName = configured.singularName(); + setterAddValueToCollection(classBuilder, + configured, + singularName, + keyType, + actualType().typeArguments().get(0), + returnType, + blueprintJavadoc); + + setterAddValuesToCollection(classBuilder, + configured, + "add" + capitalize(name()), + keyType, + returnType, + blueprintJavadoc); + } + if (configured.singular()) { + // Builder putValue(String key, String value) + String singularName = configured.singularName(); + String methodName = "put" + capitalize(singularName); + + Method.Builder method = Method.builder() + .name(methodName) + .accessModifier(setterAccessModifier(configured)) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method adds a new value to the map, or replaces it if the key already exists.") + .addJavadocTag("see", "#" + getterName() + "()"); + if (sameGeneric) { + sameGenericArgs(method, keyType, singularName, actualType()); + } else { + method.addParameter(param -> param.name("key") + .type(keyType) + .description("key to add or replace")) + .addParameter(param -> param.name(singularName) + .type(actualType()) + .description("new value for the key")); + } + method.addContent(Objects.class) + .addContentLine(".requireNonNull(key);") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + singularName + ");") + .addContent("this." + name() + ".put(key, "); + secondArgToPut(method, actualType(), singularName); + method.addContentLine(");") + .addContentLine("return self();"); + + classBuilder.addMethod(method); + + if (factoryMethod.builder().isPresent()) { + FactoryMethods.FactoryMethod fm = factoryMethod.builder().get(); + TypeName builderType; + if (fm.factoryMethodReturnType().className().equals("Builder")) { + builderType = fm.factoryMethodReturnType(); + } else { + builderType = TypeName.create(fm.factoryMethodReturnType().fqName() + ".Builder"); + } + classBuilder.addMethod(builder -> builder.name(methodName) + .accessModifier(setterAccessModifier(configured)) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method adds a new value to the map, or replaces it if the key already exists.") + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name("key") + .type(keyType) + .description("key to add or replace")) + .addParameter(param -> param.name("consumer") + .type(TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build()) + .description("builder consumer to create new value for the key")) + .addContent(Objects.class) + .addContentLine(".requireNonNull(key);") + .addContent(Objects.class) + .addContentLine(".requireNonNull(consumer);") + .addContent("var builder = ") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "();") + .addContentLine("consumer.accept(builder);") + .addContentLine("this." + methodName + "(key, builder.build());") + .addContentLine("return self();")); + } + } + } + + @Override + protected void declaredSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + // declared type (such as Map) - replace content + classBuilder.addMethod(builder -> builder.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method replaces all values with the new ones.") + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + ".clear();") + .addContentLine("this." + name() + ".putAll(" + name() + ");") + .addContentLine("return self();")); + } + + private void sameGenericArgs(Method.Builder method, + TypeName keyType, + String value, + TypeName valueType) { + + String typeDeclaration; + TypeName genericTypeBase; + TypeName resolvedKeyType; + TypeName resolvedValueType; + + if (keyType.typeArguments().isEmpty()) { + /* + Map> + put(TYPE, List) + */ + // this is good + genericTypeBase = keyType; + resolvedKeyType = SAME_GENERIC_TYPE; + } else if (keyType.typeArguments().size() == 1) { + /* + Map, Provider> + put(Class, List) + */ + // this is also good + TypeName typeArg = keyType.typeArguments().get(0); + if (typeArg.wildcard()) { + // ?, or ? extends Something + if (typeArg.generic()) { + genericTypeBase = OBJECT; + } else { + genericTypeBase = TypeName.builder(typeArg) + .wildcard(false) + .build(); + } + } else { + genericTypeBase = typeArg; + } + resolvedKeyType = TypeName.builder(keyType) + .typeArguments(List.of(SAME_GENERIC_TYPE)) + .build(); + } else { + throw new IllegalArgumentException("Property " + name() + " with type " + declaredType().fqName() + " is annotated" + + " with @SameGeneric, yet the key generic type cannot be determined." + + " Either the key must be a simple type, or a type with one type" + + " argument."); + } + + method.addGenericArgument(TypeArgument.builder() + .token("TYPE") + .bound(genericTypeBase) + .description("Type to correctly map key and value") + .build()); + + // now resolve value + if (valueType.typeArguments().isEmpty()) { + if (!genericTypeBase.equals(valueType)) { + throw new IllegalArgumentException("Property " + name() + " with type " + declaredType().fqName() + " is " + + "annotated" + + " with @SameGeneric, yet the type of value is not the" + + " same as type found on key: " + genericTypeBase.fqName()); + } + resolvedValueType = SAME_GENERIC_TYPE; + } else if (valueType.typeArguments().size() == 1) { + if (!genericTypeBase.equals(valueType.typeArguments().get(0))) { + throw new IllegalArgumentException("Property " + name() + " with type " + declaredType().fqName() + " is " + + "annotated" + + " with @SameGeneric, yet type of value is not the" + + " same as type found on key: " + genericTypeBase.fqName()); + } + resolvedValueType = TypeName.builder(valueType) + .typeArguments(List.of(SAME_GENERIC_TYPE)) + .build(); + } else { + throw new IllegalArgumentException("Property " + name() + " with type " + declaredType().fqName() + " is annotated" + + " with @SameGeneric, yet the value generic type cannot be determined." + + " Either the value must be a simple type, or a type with one type" + + " argument."); + } + + method.addParameter(param -> param.name("key") + .type(resolvedKeyType) + .description("key to add or replace")) + .addParameter(param -> param.name(value) + .type(resolvedValueType) + .description("new value for the key")); + } + + private void setterAddValueToCollection(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + String singularName, + TypeName keyType, + TypeName valueType, + TypeName returnType, + Javadoc blueprintJavadoc) { + String methodName = "add" + capitalize(singularName); + TypeName implType = collectionImplType(actualType()); + + classBuilder.addMethod(builder -> builder.name(methodName) + .accessModifier(setterAccessModifier(configured)) + .addParameter(param -> param.name("key") + .type(keyType) + .description("key to add to")) + .addParameter(param -> param.name(singularName) + .type(valueType) + .description("additional value for the key")) + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method adds a new value to the map value, or creates a new value.") + .addJavadocTag("see", "#" + getterName() + "()") + .returnType(returnType, "updated builder instance") + .addContent(Objects.class) + .addContentLine(".requireNonNull(key);") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + singularName + ");") + .addContentLine("this." + name() + ".compute(key, (k, v) -> {") + .addContent("v = v == null ? new ") + .addContent(implType) + .addContent("<>() : new ") + .addContent(implType) + .addContentLine("<>(v);") + .addContentLine("v.add(" + singularName + ");") + .addContentLine("return v;") + .decreaseContentPadding() + .addContentLine("});") + .addContentLine("return self();")); + } + + private void setterAddValuesToCollection(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + String methodName, + TypeName keyType, + TypeName returnType, + Javadoc blueprintJavadoc) { + TypeName implType = collectionImplType(actualType()); + String name = name(); + + classBuilder.addMethod(builder -> builder.name(methodName) + .accessModifier(setterAccessModifier(configured)) + .addParameter(param -> param.name("key") + .type(keyType) + .description("key to add to")) + .addParameter(param -> param.name(name) + .type(actualType()) + .description("additional values for the key")) + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method adds a new value to the map value, or creates a new value.") + .addJavadocTag("see", "#" + getterName() + "()") + .returnType(returnType, "updated builder instance") + .addContent(Objects.class) + .addContentLine(".requireNonNull(key);") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name + ");") + .addContentLine("this." + name + ".compute(key, (k, v) -> {") + .addContent("v = v == null ? new ") + .addContent(implType) + .addContent("<>() : new ") + .addContent(implType) + .addContentLine("<>(v);") + .addContentLine("v.addAll(" + name + ");") + .addContentLine("return v;") + .decreaseContentPadding() + .addContentLine("});") + .addContentLine("return self();")); + } + + private void declaredSetterAdd(InnerClass.Builder classBuilder, AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + // declared type - add content + classBuilder.addMethod(builder -> builder.name("add" + capitalize(name())) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method keeps existing values, then puts all new values into the map.") + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + ".putAll(" + name() + ");") + .addContentLine("return self();")); + } + + private void secondArgToPut(Method.Builder method, TypeName typeName, String singularName) { + TypeName genericTypeName = typeName.genericTypeName(); + if (genericTypeName.equals(LIST)) { + method.addContent(List.class) + .addContent(".copyOf(" + singularName + ")"); + } else if (genericTypeName.equals(SET)) { + method.addContent(Set.class) + .addContent(".copyOf(" + singularName + ")"); + } else if (genericTypeName.equals(MAP)) { + method.addContent(Map.class) + .addContent(".copyOf(" + singularName + ")"); + } else { + method.addContent(singularName); + } + } + + private boolean isCollection(TypeName typeName) { + if (typeName.typeArguments().size() != 1) { + return false; + } + TypeName genericTypeName = typeName.genericTypeName(); + if (genericTypeName.equals(LIST)) { + return true; + } + return genericTypeName.equals(SET); + } + +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java new file mode 100644 index 00000000000..4b5a5a0b45e --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerOptional.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.builder.codegen.Types.CHAR_ARRAY; +import static io.helidon.codegen.CodegenUtil.capitalize; +import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; +import static io.helidon.common.types.TypeNames.BOXED_BYTE; +import static io.helidon.common.types.TypeNames.BOXED_CHAR; +import static io.helidon.common.types.TypeNames.BOXED_DOUBLE; +import static io.helidon.common.types.TypeNames.BOXED_FLOAT; +import static io.helidon.common.types.TypeNames.BOXED_INT; +import static io.helidon.common.types.TypeNames.BOXED_LONG; +import static io.helidon.common.types.TypeNames.BOXED_SHORT; +import static io.helidon.common.types.TypeNames.BOXED_VOID; +import static io.helidon.common.types.TypeNames.OPTIONAL; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BOOLEAN; +import static io.helidon.common.types.TypeNames.PRIMITIVE_BYTE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_CHAR; +import static io.helidon.common.types.TypeNames.PRIMITIVE_DOUBLE; +import static io.helidon.common.types.TypeNames.PRIMITIVE_FLOAT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_INT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_LONG; +import static io.helidon.common.types.TypeNames.PRIMITIVE_SHORT; +import static io.helidon.common.types.TypeNames.PRIMITIVE_VOID; + +// declaration in builder is always non-generic, so no need to modify default values +class TypeHandlerOptional extends TypeHandler.OneTypeHandler { + + private static final Map BOXED_TO_PRIMITIVE = Map.of( + BOXED_BOOLEAN, PRIMITIVE_BOOLEAN, + BOXED_BYTE, PRIMITIVE_BYTE, + BOXED_SHORT, PRIMITIVE_SHORT, + BOXED_INT, PRIMITIVE_INT, + BOXED_LONG, PRIMITIVE_LONG, + BOXED_CHAR, PRIMITIVE_CHAR, + BOXED_FLOAT, PRIMITIVE_FLOAT, + BOXED_DOUBLE, PRIMITIVE_DOUBLE, + BOXED_VOID, PRIMITIVE_VOID + ); + + TypeHandlerOptional(String name, String getterName, String setterName, TypeName declaredType) { + super(name, getterName, setterName, declaredType); + } + + @Override + Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilder, boolean alwaysFinal) { + Field.Builder builder = Field.builder() + .isFinal(alwaysFinal || !isBuilder) + .name(name()); + TypeName usedType = isBuilder ? actualType() : declaredType(); + + if (isBuilder && (configured.required() || !configured.hasDefault())) { + // we need to use object types to be able to see if this was configured + builder.type(usedType.boxed()); + } else { + builder.type(usedType); + } + + if (isBuilder && configured.hasDefault()) { + configured.defaultValue().accept(builder); + } + + return builder; + } + + @Override + TypeName argumentTypeName() { + TypeName type = actualType(); + if (TypeNames.STRING.equals(type) || toPrimitive(type).primitive()) { + return TypeName.builder(OPTIONAL) + .addTypeArgument(type) + .build(); + } + + return TypeName.builder(OPTIONAL) + .addTypeArgument(toWildcard(actualType())) + .build(); + } + + @Override + void setters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + FactoryMethods factoryMethod, + TypeName returnType, + Javadoc blueprintJavadoc) { + + declaredSetter(classBuilder, returnType, blueprintJavadoc); + clearSetter(classBuilder, returnType, configured); + + // and add the setter with the actual type + // config is special - handled directly when configuration is handled, as it also must be used when this type + // is @Configured + if (!isConfigProperty(this)) { + // declared setter - optional is package local, field is never optional in builder + Method.Builder method = Method.builder() + .name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(name()) + .type(toPrimitive(actualType())) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .update(it -> { + if (configured.decorator() != null) { + it.addContent("new ") + .addContent(configured.decorator()) + .addContent("().decorate(this, ") + .addContent(Optional.class) + .addContent(".of(") + .addContent(name()) + .addContentLine("));"); + } + }) + .addContentLine("this." + name() + " = " + name() + ";") + .addContentLine("return self();"); + classBuilder.addMethod(method); + } + + if (actualType().equals(CHAR_ARRAY)) { + charArraySetter(classBuilder, configured, returnType, blueprintJavadoc); + } + + if (factoryMethod.createTargetType().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.createTargetType().get(); + String optionalSuffix = optionalSuffix(fm.factoryMethodReturnType()); + String argumentName = name() + "Config"; + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(argumentName) + .type(fm.argumentType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("this." + name() + " = ") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "(" + argumentName + ")" + optionalSuffix + ";") + .addContentLine("return self();")); + } + + if (factoryMethod.builder().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.builder().get(); + + TypeName builderType; + String className = fm.factoryMethodReturnType().className(); + if (className.equals("Builder") || className.endsWith(".Builder")) { + builderType = fm.factoryMethodReturnType(); + } else { + builderType = TypeName.create(fm.factoryMethodReturnType().fqName() + ".Builder"); + } + String argumentName = "consumer"; + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build(); + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, blueprintJavadoc.returnDescription()) + .build(); + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .returnType(returnType) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .addContent(Objects.class) + .javadoc(javadoc) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("var builder = ") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "();") + .addContentLine("consumer.accept(builder);") + .addContentLine("this." + name() + "(builder.build());") + .addContentLine("return self();")); + } + } + + private void declaredSetter(InnerClass.Builder classBuilder, + TypeName returnType, + Javadoc blueprintJavadoc) { + // declared setter - optional is package local, field is never optional in builder + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + " = " + name() + + ".map(" + actualType().fqName() + ".class::cast)" + + ".orElse(this." + name() + ");") + .addContentLine("return self();")); + } + + private void clearSetter(InnerClass.Builder classBuilder, + TypeName returnType, + AnnotationDataOption configured) { + // declared setter - optional is package local, field is never optional in builder + classBuilder.addMethod(builder -> builder.name("clear" + capitalize(name())) + .accessModifier(setterAccessModifier(configured)) + .description("Clear existing value of this property.") + .returnType(returnType, "updated builder instance") + .addJavadocTag("see", "#" + getterName() + "()") + .update(it -> { + if (configured.decorator() != null) { + builder.addContent("new ") + .addContent(configured.decorator()) + .addContent("().decorate(this, ") + .addContent(Optional.class) + .addContentLine(".empty());"); + } + }) + .addContentLine("this." + name() + " = null;") + .addContentLine("return self();")); + } + + private String optionalSuffix(TypeName typeName) { + if (OPTIONAL.equals(typeName.genericTypeName())) { + return ".orElse(null)"; + } + return ""; + } + + private TypeName toPrimitive(TypeName typeName) { + return Optional.ofNullable(BOXED_TO_PRIMITIVE.get(typeName)) + .orElse(typeName); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java new file mode 100644 index 00000000000..1e146d5a879 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Optional; + +import io.helidon.common.types.TypeName; + +import static io.helidon.common.types.TypeNames.SET; + +class TypeHandlerSet extends TypeHandlerCollection { + + TypeHandlerSet(String name, String getterName, String setterName, TypeName declaredType) { + super(name, + getterName, + setterName, + declaredType, + SET, + "collect(java.util.stream.Collectors.toSet())", + Optional.of(".map(java.util.Set::copyOf)")); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java new file mode 100644 index 00000000000..4a2ece08837 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSupplier.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.Objects; +import java.util.function.Consumer; + +import io.helidon.codegen.classmodel.Field; +import io.helidon.codegen.classmodel.InnerClass; +import io.helidon.codegen.classmodel.Javadoc; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.builder.codegen.Types.CHAR_ARRAY; +import static io.helidon.common.types.TypeNames.SUPPLIER; + +class TypeHandlerSupplier extends TypeHandler.OneTypeHandler { + + TypeHandlerSupplier(String name, String getterName, String setterName, TypeName declaredType) { + super(name, getterName, setterName, declaredType); + } + + @Override + Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilder, boolean alwaysFinal) { + Field.Builder builder = Field.builder() + .type(declaredType()) + .name(name()) + .isFinal(alwaysFinal || !isBuilder); + + if (isBuilder && configured.hasDefault()) { + builder.addContent("() -> "); + configured.defaultValue().accept(builder); + } + + return builder; + } + + @Override + TypeName argumentTypeName() { + return TypeName.builder(SUPPLIER) + .addTypeArgument(toWildcard(actualType())) + .build(); + } + + @Override + void generateFromConfig(Method.Builder method, AnnotationDataOption configured, FactoryMethods factoryMethods) { + if (configured.provider()) { + return; + } + if (factoryMethods.createFromConfig().isPresent()) { + method.addContentLine(configGet(configured) + + generateFromConfig(factoryMethods) + + ".ifPresent(this::" + setterName() + ");"); + } else if (actualType().isOptional()) { + method.addContent(setterName() + "("); + method.addContent(configGet(configured)); + method.addContent(generateFromConfigOptional(factoryMethods)); + method.addContentLine(".optionalSupplier());"); + } else { + method.addContent(setterName() + "("); + method.addContent(configGet(configured)); + method.addContent(generateFromConfig(factoryMethods)); + method.addContentLine(".supplier());"); + } + } + + String generateFromConfigOptional(FactoryMethods factoryMethods) { + TypeName optionalType = actualType().typeArguments().get(0); + if (optionalType.fqName().equals("char[]")) { + return ".asString().as(String::toCharArray)"; + } + + TypeName boxed = optionalType.boxed(); + return factoryMethods.createFromConfig() + .map(it -> ".map(" + it.typeWithFactoryMethod().genericTypeName().fqName() + "::" + it.createMethodName() + ")") + .orElseGet(() -> ".as(" + boxed.fqName() + ".class)"); + + } + + @Override + void setters(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + FactoryMethods factoryMethod, + TypeName returnType, + Javadoc blueprintJavadoc) { + + declaredSetter(classBuilder, configured, returnType, blueprintJavadoc); + + // and add the setter with the actual type + Method.Builder method = Method.builder() + .name(setterName()) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(name()) + .type(actualType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + " = () -> " + name() + ";") + .addContentLine("return self();"); + classBuilder.addMethod(method); + + if (actualType().equals(CHAR_ARRAY)) { + classBuilder.addMethod(builder -> builder.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(TypeNames.STRING) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + " = () -> " + name() + ".toCharArray();") + .addContentLine("return self();")); + } + + if (factoryMethod.createTargetType().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.createTargetType().get(); + String argumentName = name() + "Config"; + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(argumentName) + .type(fm.argumentType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("this." + name() + " = ") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "(" + argumentName + ");") + .addContentLine("return self();")); + } + + if (factoryMethod.builder().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.builder().get(); + + TypeName builderType; + String className = fm.factoryMethodReturnType().className(); + if (className.equals("Builder") || className.endsWith(".Builder")) { + builderType = fm.factoryMethodReturnType(); + } else { + builderType = TypeName.create(fm.factoryMethodReturnType().fqName() + ".Builder"); + } + String argumentName = "consumer"; + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build(); + + Javadoc javadoc = setterJavadoc(blueprintJavadoc) + .addParameter(argumentName, blueprintJavadoc.returnDescription()) + .build(); + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .returnType(returnType) + .addParameter(param -> param.name(argumentName) + .type(argumentType)) + .javadoc(javadoc) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + argumentName + ");") + .addContent("var builder = ") + .addContent(fm.typeWithFactoryMethod().genericTypeName()) + .addContentLine("." + fm.createMethodName() + "();") + .addContentLine("consumer.accept(builder);") + .addContentLine("this." + name() + "(builder.build());") + .addContentLine("return self();")); + } + } + + protected void declaredSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + classBuilder.addMethod(method -> method.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .addContent(Objects.class) + .addContentLine(".requireNonNull(" + name() + ");") + .addContentLine("this." + name() + " = " + name() + "::get;") + .addContentLine("return self();")); + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java new file mode 100644 index 00000000000..8706b70daf4 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; + +import io.helidon.common.Generated; +import io.helidon.common.types.TypeName; + +final class Types { + static final TypeName COMMON_CONFIG = TypeName.create("io.helidon.common.config.Config"); + static final TypeName GENERATED = TypeName.create(Generated.class); + static final TypeName DEPRECATED = TypeName.create(Deprecated.class); + static final TypeName LINKED_HASH_MAP = TypeName.create(LinkedHashMap.class); + static final TypeName ARRAY_LIST = TypeName.create(ArrayList.class); + static final TypeName LINKED_HASH_SET = TypeName.create(LinkedHashSet.class); + static final TypeName CHAR_ARRAY = TypeName.create(char[].class); + + static final TypeName BUILDER_DESCRIPTION = TypeName.create("io.helidon.builder.api.Description"); + + static final TypeName PROTOTYPE_BLUEPRINT = TypeName.create("io.helidon.builder.api.Prototype.Blueprint"); + static final TypeName PROTOTYPE_IMPLEMENT = TypeName.create("io.helidon.builder.api.Prototype.Implement"); + static final TypeName PROTOTYPE_API = TypeName.create("io.helidon.builder.api.Prototype.Api"); + static final TypeName PROTOTYPE_ANNOTATED = TypeName.create("io.helidon.builder.api.Prototype.Annotated"); + static final TypeName PROTOTYPE_FACTORY = TypeName.create("io.helidon.builder.api.Prototype.Factory"); + static final TypeName PROTOTYPE_CONFIGURED = TypeName.create("io.helidon.builder.api.Prototype.Configured"); + static final TypeName PROTOTYPE_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.Builder"); + static final TypeName PROTOTYPE_CONFIGURED_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.ConfiguredBuilder"); + static final TypeName PROTOTYPE_CUSTOM_METHODS = TypeName.create("io.helidon.builder.api.Prototype.CustomMethods"); + static final TypeName PROTOTYPE_FACTORY_METHOD = TypeName.create("io.helidon.builder.api.Prototype.FactoryMethod"); + static final TypeName PROTOTYPE_BUILDER_METHOD = TypeName.create("io.helidon.builder.api.Prototype.BuilderMethod"); + static final TypeName PROTOTYPE_PROTOTYPE_METHOD = TypeName.create("io.helidon.builder.api.Prototype.PrototypeMethod"); + static final TypeName PROTOTYPE_BUILDER_DECORATOR = TypeName.create("io.helidon.builder.api.Prototype.BuilderDecorator"); + static final TypeName PROTOTYPE_CONSTANT = TypeName.create("io.helidon.builder.api.Prototype.Constant"); + + static final TypeName RUNTIME_PROTOTYPE = TypeName.create("io.helidon.builder.api.RuntimeType.PrototypedBy"); + static final TypeName RUNTIME_PROTOTYPED_BY = TypeName.create("io.helidon.builder.api.RuntimeType.PrototypedBy"); + static final TypeName RUNTIME_API = TypeName.create("io.helidon.builder.api.RuntimeType.Api"); + + static final TypeName OPTION_SAME_GENERIC = TypeName.create("io.helidon.builder.api.Option.SameGeneric"); + static final TypeName OPTION_SINGULAR = TypeName.create("io.helidon.builder.api.Option.Singular"); + static final TypeName OPTION_CONFIDENTIAL = TypeName.create("io.helidon.builder.api.Option.Confidential"); + static final TypeName OPTION_REDUNDANT = TypeName.create("io.helidon.builder.api.Option.Redundant"); + static final TypeName OPTION_CONFIGURED = TypeName.create("io.helidon.builder.api.Option.Configured"); + static final TypeName OPTION_ACCESS = TypeName.create("io.helidon.builder.api.Option.Access"); + static final TypeName OPTION_REQUIRED = TypeName.create("io.helidon.builder.api.Option.Required"); + static final TypeName OPTION_PROVIDER = TypeName.create("io.helidon.builder.api.Option.Provider"); + static final TypeName OPTION_ALLOWED_VALUES = TypeName.create("io.helidon.builder.api.Option.AllowedValues"); + static final TypeName OPTION_ALLOWED_VALUE = TypeName.create("io.helidon.builder.api.Option.AllowedValue"); + static final TypeName OPTION_DEFAULT = TypeName.create("io.helidon.builder.api.Option.Default"); + static final TypeName OPTION_DEFAULT_INT = TypeName.create("io.helidon.builder.api.Option.DefaultInt"); + static final TypeName OPTION_DEFAULT_DOUBLE = TypeName.create("io.helidon.builder.api.Option.DefaultDouble"); + static final TypeName OPTION_DEFAULT_BOOLEAN = TypeName.create("io.helidon.builder.api.Option.DefaultBoolean"); + static final TypeName OPTION_DEFAULT_LONG = TypeName.create("io.helidon.builder.api.Option.DefaultLong"); + static final TypeName OPTION_DEFAULT_METHOD = TypeName.create("io.helidon.builder.api.Option.DefaultMethod"); + static final TypeName OPTION_DEFAULT_CODE = TypeName.create("io.helidon.builder.api.Option.DefaultCode"); + static final TypeName OPTION_DEPRECATED = TypeName.create("io.helidon.builder.api.Option.Deprecated"); + static final TypeName OPTION_TYPE = TypeName.create("io.helidon.builder.api.Option.Type"); + static final TypeName OPTION_DECORATOR = TypeName.create("io.helidon.builder.api.Option.Decorator"); + + private Types() { + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java new file mode 100644 index 00000000000..f0aa71335cb --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/ValidationTask.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.codegen; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.common.Errors; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.builder.codegen.Types.PROTOTYPE_FACTORY; +import static io.helidon.builder.codegen.Types.RUNTIME_API; +import static io.helidon.builder.codegen.Types.RUNTIME_PROTOTYPED_BY; + +abstract class ValidationTask { + abstract void validate(Errors.Collector errors); + + private static void validateImplements(Errors.Collector errors, + TypeInfo validatedType, + TypeName implementedInterface, + String message) { + if (validatedType.interfaceTypeInfo() + .stream() + .noneMatch(it -> it.typeName().equals(implementedInterface))) { + errors.fatal(message); + } + } + + private static void validateFactoryMethod(Errors.Collector errors, + TypeInfo validatedType, + TypeName returnType, + String methodName, + TypeName argument, + String message) { + if (validatedType.elementInfo().stream() + .filter(ElementInfoPredicates::isMethod) + .filter(ElementInfoPredicates::isStatic) + .filter(ElementInfoPredicates.elementName(methodName)) + .filter(it -> returnType.equals(it.typeName())) + .filter(it -> { + List args = it.parameterArguments(); + + if (argument == null) { + return args.isEmpty(); + } + if (args.size() != 1) { + return false; + } + TypedElementInfo typedElementInfo = args.getFirst(); + return argument.equals(typedElementInfo.typeName()); + }) + .findFirst() + .isEmpty()) { + errors.fatal(validatedType.typeName().fqName(), message); + } + } + + /** + * Validate runtime object that is configured by a prototype. + *

+ * If annotated by {@link io.helidon.builder.codegen.Types#RUNTIME_PROTOTYPE} + * - RuntimeType must have "static RuntimeType create(ConfigObject)" + * - RuntimeType must have "static RuntimeType create(Consumer) + * - must implement {@link io.helidon.builder.codegen.Types#RUNTIME_API} + */ + static class ValidateConfiguredType extends ValidationTask { + private final TypeInfo runtimeTypeInfo; + private final List nestedValidators; + + ValidateConfiguredType(TypeInfo runtimeTypeInfo, TypeName configObjectType) { + this.runtimeTypeInfo = runtimeTypeInfo; + + // the type has to have same type parameters as its config bean + TypeName configObjectWithTypeParams = TypeName.builder(configObjectType) + .typeArguments(runtimeTypeInfo.typeName().typeArguments()) + .build(); + + TypeName configuredTypeInterface = TypeName.builder(RUNTIME_API) + .addTypeArgument(configObjectType) + .build(); + + this.nestedValidators = List.of( + new ValidateCreateMethod(configObjectWithTypeParams, runtimeTypeInfo), + new ValidateCreateWithConsumerMethod(configObjectWithTypeParams, runtimeTypeInfo), + new ValidateImplements(runtimeTypeInfo, + configuredTypeInterface, + "Type annotated with @" + + RUNTIME_PROTOTYPED_BY.classNameWithEnclosingNames() + + "(" + configObjectType.className() + + ".class) must implement " + + RUNTIME_API.classNameWithEnclosingNames() + + "<" + + configObjectWithTypeParams.classNameWithTypes() + ">") + ); + } + + @Override + public void validate(Errors.Collector errors) { + for (ValidationTask nestedValidator : nestedValidators) { + nestedValidator.validate(errors); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidateConfiguredType that = (ValidateConfiguredType) o; + return Objects.equals(runtimeTypeInfo, that.runtimeTypeInfo); + } + + @Override + public int hashCode() { + return Objects.hash(runtimeTypeInfo); + } + } + + private static class ValidateImplements extends ValidationTask { + private final TypeInfo typeInfo; + private final TypeName requiredInterface; + private final String message; + + ValidateImplements(TypeInfo typeInfo, TypeName requiredInterface, String message) { + this.typeInfo = typeInfo; + this.requiredInterface = requiredInterface; + this.message = message; + } + + @Override + void validate(Errors.Collector errors) { + ValidationTask.validateImplements(errors, typeInfo, requiredInterface, message); + } + } + + static class ValidateBlueprint extends ValidationTask { + private final TypeInfo blueprint; + + ValidateBlueprint(TypeInfo blueprint) { + this.blueprint = blueprint; + } + + @Override + public void validate(Errors.Collector errors) { + // must be package local + if (blueprint.accessModifier() == AccessModifier.PUBLIC) { + errors.fatal(blueprint.typeName().fqName() + " is defined as public, it must be package local"); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidateBlueprint that = (ValidateBlueprint) o; + return Objects.equals(blueprint.typeName(), that.blueprint.typeName()); + } + + @Override + public int hashCode() { + return Objects.hash(blueprint.typeName()); + } + } + + /** + * Validation for blueprints that extend a factory. + *

+ * If "PrototypeBlueprint" implements a Factory + * - RuntimeType must implement RuntimeType.Api + * - RuntimeType must have "static RuntimeType create(ConfigObject)" + * - RuntimeType must have "static RuntimeType create(Consumer) + * - RuntimeType must have "static ConfigObject.Builder builder() + */ + static class ValidateBlueprintExtendsFactory extends ValidationTask { + private final List nestedValidators; + private final TypeName configObjectType; + private final TypeInfo blueprintInfo; + private final TypeInfo runtimeTypeInfo; + + ValidateBlueprintExtendsFactory(TypeName configObjectType, TypeInfo blueprintInfo, TypeInfo runtimeTypeInfo) { + this.configObjectType = configObjectType; + this.blueprintInfo = blueprintInfo; + this.runtimeTypeInfo = runtimeTypeInfo; + + TypeName configObjectBuilder = TypeName.builder() + .packageName(configObjectType.packageName()) + .enclosingNames(List.of(configObjectType.className())) + .className("Builder") + .build(); + + nestedValidators = List.of( + new ValidateBuilderMethod(configObjectType, runtimeTypeInfo, configObjectBuilder), + new ValidateAnnotatedWith(runtimeTypeInfo, + RUNTIME_PROTOTYPED_BY, + configObjectType.genericTypeName().fqName()) + ); + } + + @Override + public void validate(Errors.Collector errors) { + validateImplements(errors, + runtimeTypeInfo, + TypeName.builder(RUNTIME_API) + .addTypeArgument(configObjectType.boxed()) + .build(), + "As " + blueprintInfo.typeName().fqName() + " implements " + + PROTOTYPE_FACTORY.classNameWithEnclosingNames() + + "<" + + runtimeTypeInfo.typeName().fqName() + ">, the runtime type must implement(or extend) " + + "interface " + RUNTIME_API.fqName() + "<" + configObjectType.className() + ">" + ); + for (ValidationTask nestedValidator : nestedValidators) { + nestedValidator.validate(errors); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidateBlueprintExtendsFactory that = (ValidateBlueprintExtendsFactory) o; + return Objects.equals(blueprintInfo, that.blueprintInfo) && Objects.equals(runtimeTypeInfo, + that.runtimeTypeInfo); + } + + @Override + public int hashCode() { + return Objects.hash(blueprintInfo, runtimeTypeInfo); + } + } + + private static class ValidateAnnotatedWith extends ValidationTask { + + private final TypeInfo typeInfo; + private final TypeName annotation; + private final String expectedValue; + + ValidateAnnotatedWith(TypeInfo typeInfo, TypeName annotation, String expectedValue) { + this.typeInfo = typeInfo; + this.annotation = annotation; + this.expectedValue = expectedValue; + } + + @Override + void validate(Errors.Collector errors) { + if (typeInfo.findAnnotation(annotation) + .stream() + .noneMatch(it -> it.value().map(expectedValue::equals).orElse(false))) { + errors.fatal("Type " + typeInfo.typeName() + .fqName() + " must be annotated with " + annotation.fqName() + "(" + expectedValue + ")"); + } + } + } + + /** + * Validate that runtime object has a factory method to be created from prototype. + *

+     * public static Tls create(TlsConfig tlsConfig) {
+     *     return new TlsImpl(tlsConfig);
+     * }
+     * 
+ */ + private static class ValidateCreateMethod extends ValidationTask { + private final TypeName configObjectType; + private final TypeInfo runtimeTypeInfo; + + ValidateCreateMethod(TypeName configObjectType, TypeInfo runtimeTypeInfo) { + this.configObjectType = configObjectType; + this.runtimeTypeInfo = runtimeTypeInfo; + } + + @Override + public void validate(Errors.Collector errors) { + String fqName = runtimeTypeInfo.typeName().genericTypeName().fqName(); + + validateFactoryMethod(errors, + runtimeTypeInfo, + runtimeTypeInfo.typeName(), + "create", + configObjectType, + "As " + fqName + " is annotated with @" + + RUNTIME_PROTOTYPED_BY.classNameWithEnclosingNames() + + "(" + + configObjectType.className() + + "), the type must implement the following " + + "method:\n" + + "static " + runtimeTypeInfo.typeName().classNameWithTypes() + " create(" + + configObjectType.classNameWithTypes() + ");"); + } + } + + /** + * Validate that runtime object has a factory method with prototype builder consumer. + *
+     * public static Tls create(Consumer consumer) {
+     *     TlsConfig.Builder builder = TlsConfig.builder();
+     *     consumer.accept(builder);
+     *     return builder.build();
+     * }
+     * 
+ */ + private static class ValidateCreateWithConsumerMethod extends ValidationTask { + private final TypeName configObjectType; + private final TypeInfo runtimeTypeInfo; + + ValidateCreateWithConsumerMethod(TypeName configObjectType, + TypeInfo runtimeTypeInfo) { + this.configObjectType = configObjectType; + this.runtimeTypeInfo = runtimeTypeInfo; + } + + @Override + public void validate(Errors.Collector errors) { + TypeName consumerArgument = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(TypeName.create(configObjectType.fqName() + ".Builder")) + .build(); + validateFactoryMethod(errors, + runtimeTypeInfo, + runtimeTypeInfo.typeName(), + "create", + consumerArgument, + "As " + configObjectType.fqName() + " implements " + + PROTOTYPE_FACTORY.classNameWithEnclosingNames() + + "<" + + runtimeTypeInfo.typeName().resolvedName() + ">, the type " + + runtimeTypeInfo.typeName().className() + + " must implement the following " + + "method:\n" + + "static " + + runtimeTypeInfo.typeName().className() + + " create(" + consumerArgument.resolvedName() + " consumer) {\n" + + " return builder().update(consumer).build();" + + "}"); + } + } + + /** + * Validate that a runtime object has static prototype builder method. + *
+     * public static TlsConfig.Builder builder() {
+     *     return TlsConfig.builder();
+     * }
+     * 
+ */ + private static class ValidateBuilderMethod extends ValidationTask { + private final TypeName configObjectType; + private final TypeInfo runtimeTypeInfo; + private final TypeName configObjectBuilder; + + ValidateBuilderMethod(TypeName configObjectType, + TypeInfo runtimeTypeInfo, + TypeName configObjectBuilder) { + this.configObjectType = configObjectType; + this.runtimeTypeInfo = runtimeTypeInfo; + this.configObjectBuilder = configObjectBuilder; + } + + @Override + public void validate(Errors.Collector errors) { + + validateFactoryMethod(errors, + runtimeTypeInfo, + configObjectBuilder, + "builder", + null, + "As " + configObjectType.fqName() + " implements " + + PROTOTYPE_FACTORY.classNameWithEnclosingNames() + + "<" + + runtimeTypeInfo.typeName() + .fqName() + ">, the runtime type must implement the following " + + "method:\n" + + "static " + configObjectType.className() + ".Builder" + + " builder() {\n" + + " return " + configObjectType.className() + ".builder();\n" + + "}"); + } + } +} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/package-info.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/package-info.java new file mode 100644 index 00000000000..fe79c2e5ac7 --- /dev/null +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Code generation for {@code Blueprint} annotated interfaces. + */ +package io.helidon.builder.codegen; diff --git a/builder/codegen/src/main/java/module-info.java b/builder/codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..daf59b4212a --- /dev/null +++ b/builder/codegen/src/main/java/module-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Code generation for builders. + *

+ * Start with {@code Blueprint} annotation from the {@code helidon-builder-api} module. + */ +module io.helidon.builder.codegen { + requires io.helidon.common.types; + requires io.helidon.codegen; + requires io.helidon.codegen.classmodel; + + exports io.helidon.builder.codegen; + + provides io.helidon.codegen.spi.CodegenExtensionProvider + with io.helidon.builder.codegen.BuilderCodegenProvider; +} \ No newline at end of file diff --git a/builder/pom.xml b/builder/pom.xml index ac713d9293d..e217ac420b3 100644 --- a/builder/pom.xml +++ b/builder/pom.xml @@ -38,6 +38,7 @@ api + codegen processor tests diff --git a/builder/processor/pom.xml b/builder/processor/pom.xml index 89d6af0a24f..e12e10614f9 100644 --- a/builder/processor/pom.xml +++ b/builder/processor/pom.xml @@ -30,6 +30,8 @@ helidon-builder-processor Helidon Builder Annotation Processor + This module is deprecated and will be removed, please use helidon-builder-codegen in + combination with helidon-codegen-apt diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/BlueprintProcessor.java b/builder/processor/src/main/java/io/helidon/builder/processor/BlueprintProcessor.java index 745c261ec18..759e334cff2 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/BlueprintProcessor.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/BlueprintProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,11 @@ /** * Annotation processor for prototype blueprints. * Generates prototype implementation from the blueprint. + * + * @deprecated replaced with helidon-builder-codegen in + * combination with helidon-codegen-apt */ +@Deprecated(forRemoval = true, since = "4.1.0") public class BlueprintProcessor extends AbstractProcessor { private static final String SOURCE_SPACING = " "; private static final TypeName GENERATOR = TypeName.create(BlueprintProcessor.class); diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/package-info.java b/builder/processor/src/main/java/io/helidon/builder/processor/package-info.java index cb280d178a7..a7c5138cafa 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/package-info.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,5 +16,9 @@ /** * The Builder annotation processing package. + * + * @deprecated replaced with helidon-builder-codegen in + * combination with helidon-codegen-apt */ +@Deprecated(forRemoval = true, since = "4.1.0") package io.helidon.builder.processor; diff --git a/builder/processor/src/main/java/module-info.java b/builder/processor/src/main/java/module-info.java index 98a90f8ca19..a82f1be1c2b 100644 --- a/builder/processor/src/main/java/module-info.java +++ b/builder/processor/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,11 @@ /** * The Builder annotation processor module. + * + * @deprecated replaced with helidon-builder-codegen in + * combination with helidon-codegen-apt */ +@Deprecated(forRemoval = true, since = "4.1.0") module io.helidon.builder.processor { requires java.compiler; requires jdk.compiler; diff --git a/builder/tests/builder/pom.xml b/builder/tests/builder/pom.xml index 9e65a6d4467..43e8440a93e 100644 --- a/builder/tests/builder/pom.xml +++ b/builder/tests/builder/pom.xml @@ -91,12 +91,17 @@ io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -104,12 +109,17 @@ io.helidon.builder - helidon-builder-processor + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExample.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExample.java new file mode 100644 index 00000000000..28560282847 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExample.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.test.testsubjects; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; + +@RuntimeType.PrototypedBy(RuntimeTypeExampleConfig.class) +public class RuntimeTypeExample implements RuntimeType.Api { + private final RuntimeTypeExampleConfig prototype; + + private RuntimeTypeExample(RuntimeTypeExampleConfig prototype) { + this.prototype = prototype; + } + + static RuntimeTypeExample create(RuntimeTypeExampleConfig prototype) { + return new RuntimeTypeExample(prototype); + } + + static RuntimeTypeExampleConfig.Builder builder() { + return RuntimeTypeExampleConfig.builder(); + } + + static RuntimeTypeExample create(Consumer consumer) { + return builder().update(consumer).build(); + } + + @Override + public RuntimeTypeExampleConfig prototype() { + return prototype; + } +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleConfigBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleConfigBlueprint.java new file mode 100644 index 00000000000..68a2d09f093 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleConfigBlueprint.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.test.testsubjects; + +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +interface RuntimeTypeExampleConfigBlueprint extends Prototype.Factory { + String type(); +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterface.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterface.java new file mode 100644 index 00000000000..7c60c937a71 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterface.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.test.testsubjects; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; + +@RuntimeType.PrototypedBy(RuntimeTypeExampleInterfaceConfig.class) +public interface RuntimeTypeExampleInterface extends RuntimeType.Api { + + static RuntimeTypeExampleInterface create(RuntimeTypeExampleInterfaceConfig prototype) { + return new AnImplementation(prototype); + } + + static RuntimeTypeExampleInterfaceConfig.Builder builder() { + return RuntimeTypeExampleInterfaceConfig.builder(); + } + + static RuntimeTypeExampleInterface create(Consumer consumer) { + return builder().update(consumer).build(); + } + + class AnImplementation implements RuntimeTypeExampleInterface { + private final RuntimeTypeExampleInterfaceConfig prototype; + + private AnImplementation(RuntimeTypeExampleInterfaceConfig prototype) { + this.prototype = prototype; + } + + @Override + public RuntimeTypeExampleInterfaceConfig prototype() { + return prototype; + } + } + +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterfaceConfigBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterfaceConfigBlueprint.java new file mode 100644 index 00000000000..1f7026328f9 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/RuntimeTypeExampleInterfaceConfigBlueprint.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.test.testsubjects; + +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +interface RuntimeTypeExampleInterfaceConfigBlueprint extends Prototype.Factory { + String type(); +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/RuntimeTypeExampleTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/RuntimeTypeExampleTest.java new file mode 100644 index 00000000000..64a53a459a5 --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/RuntimeTypeExampleTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.builder.test; + +import io.helidon.builder.test.testsubjects.RuntimeTypeExample; +import io.helidon.builder.test.testsubjects.RuntimeTypeExampleConfig; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class RuntimeTypeExampleTest { + @Test + void sanityCheck() { + RuntimeTypeExample runtimeType = RuntimeTypeExampleConfig.builder() + .type("type") + .build(); + + assertThat(runtimeType.prototype().type(), is("type")); + } +} diff --git a/builder/tests/common-types/pom.xml b/builder/tests/common-types/pom.xml index ebcbd2cb3e2..a5d35ebf675 100644 --- a/builder/tests/common-types/pom.xml +++ b/builder/tests/common-types/pom.xml @@ -74,27 +74,37 @@ true + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index d673f71f711..8820e70fe50 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,14 @@ interface TypeInfoBlueprint extends Annotated { @Option.Required TypeName typeName(); + /** + * Description, such as javadoc, if available. + * + * @return description of this element + */ + @Option.Redundant + Optional description(); + /** * The type element kind. *

@@ -182,7 +190,7 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn */ @Option.Singular @Option.Redundant - @Option.Deprecated("typeModifiers") + @Option.Deprecated("elementModifiers") @Deprecated(forRemoval = true, since = "4.1.0") Set modifiers(); diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java index c8e93a07c8c..20e860c9093 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; @@ -59,6 +60,10 @@ public final class TypeNames { * Type name for {@link java.util.Collection}. */ public static final TypeName COLLECTION = TypeName.create(Collection.class); + /** + * Type name for {@link java.time.Duration}. + */ + public static final TypeName DURATION = TypeName.create(Duration.class); /* Primitive types and their boxed counterparts */ diff --git a/codegen/README.md b/codegen/README.md new file mode 100644 index 00000000000..d0fa72831ef --- /dev/null +++ b/codegen/README.md @@ -0,0 +1,57 @@ +Codegen +---- + +Code generation and code processing tools of Helidon. + +We see the following three environments that are used for code processing: + +1. Annotation processors +2. Classpath scanning +3. Reflection based in a running VM + +In Helidon, we do as much as possible using annotation processing and source code generation. +For the cases where we need to analyze external libraries, or arbitrary code, we use classpath scanning (such as from a Maven plugin or command line tool). +Reflection can only be used in Helidon Microprofile, or in selected modules that are intentionally reflection based (this should be currently limited to Config object mapping module). + + +# Modules + +This top level module contains the following modules: + +- `helidon-codegen` - API and SPI and utilities that are shared between possible environments +- `helidon-codegen-apt` - implementations specific to annotation processing +- `helidon-codegen-scan` - implementations specific to classpath scanning +- `class-model` - class code generation abstraction, that provides builders to create a new source file +- `compiler` - wrapper around Java compiler that is running within the current VM +- `helidon-copyright` - Helidon specific implementation of copyright handler, used by Helidon project itself to generate sources + +## Codegen abstraction (module `helidon-codegen`) + +Codegen provides types that each code generation implementation can code against, without the need to hard code against +annotation processing or classpath scanning. + +Main entry point is the `CodegenContext` that provides: + +- Current module info (if available) - this is a read-only representation of a module info, to validate `provides` etc. +- `CodegenFiler` - filer abstraction, to generate source files and resources +- `CodegenLogger` - logger abstraction, with implementation for annotation processor `Messager`, Maven `Log` and `System.Logger` +- `CodegenScope` - to provide information on the scope we are processing (expecting production or test) +- possibility to obtain `TypeInfo` (backed by appropriate `TypeInfoFactoryBase`) +- access to `ElementMapper`, `TypeMapper`, and `AnnotationMapper` used in those factories +- `CodegenOptions` - configuration options provided either from Maven plugin, Annotation processing options, or command line arguments + +### Tools + +- `CodegenUtil` - methods useful when generating code (such as capitalization of first letter, constant name from method name etc.) +- `CopyrightHandler` - API and SPI to generate correct copyright statements +- `ElementInfoPredicates` - predicates to filter `TypedElementInfo`, such as only getting public, static etc. elements +- `GeneratedAnnotationHandler` - API and SPI to generate correct `@Generated` annotation +- `ModuleInfoSourceParser` - to parse source code of module-info.java +- `TypesCodeGen` - tool to generate source code to create instances of common types + +## Class model (module `helidon-codegen-class-model`) + +Class model provides APIs to construct a class in memory, and then write it out (using for example `CodegenFiler`) as source file. + +Start with `ClassModel.builder()`. + diff --git a/codegen/apt/pom.xml b/codegen/apt/pom.xml new file mode 100644 index 00000000000..9602ff02081 --- /dev/null +++ b/codegen/apt/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + + + helidon-codegen-apt + Helidon Codegen APT + + Tools for annotation processing + + + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + \ No newline at end of file diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java new file mode 100644 index 00000000000..42f0e363f7b --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +/** + * Factory for annotations. + */ +final class AptAnnotationFactory { + private AptAnnotationFactory() { + } + + /** + * Creates a set of annotations using annotation processor. + * + * @param annoMirrors the annotation type mirrors + * @param elements annotation processing element utils + * @return the annotation value set + */ + public static Set createAnnotations(List annoMirrors, Elements elements) { + return annoMirrors.stream() + .map(it -> createAnnotation(it, elements)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates a set of annotations based using annotation processor. + * + * @param type the enclosing/owing type element + * @param elements annotation processing element utils + * @return the annotation value set + */ + public static Set createAnnotations(Element type, Elements elements) { + return createAnnotations(type.getAnnotationMirrors(), elements); + } + + /** + * Creates an instance from an annotation mirror during annotation processing. + * + * @param am the annotation mirror + * @param elements the elements + * @return the new instance or empty if the annotation mirror passed is invalid + */ + public static Annotation createAnnotation(AnnotationMirror am, + Elements elements) { + TypeName val = AptTypeFactory.createTypeName(am.getAnnotationType()) + .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + + am.getAnnotationType())); + + return Annotation.create(val, extractAnnotationValues(am, elements)); + } + + /** + * Extracts values from the annotation mirror value. + * + * @param am the annotation mirror + * @param elements the elements + * @return the extracted values + */ + private static Map extractAnnotationValues(AnnotationMirror am, + Elements elements) { + return extractAnnotationValues(elements, elements.getElementValuesWithDefaults(am)); + } + + /** + * Extracts values from the annotation element values. + * + * @param values the element values + * @return the extracted values + */ + private static Map + extractAnnotationValues(Elements elements, Map values) { + Map result = new LinkedHashMap<>(); + values.forEach((el, val) -> { + String name = el.getSimpleName().toString(); + Object value = val.accept(new ToAnnotationValueVisitor(elements), null); + if (value != null) { + result.put(name, value); + } + }); + return result; + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java new file mode 100644 index 00000000000..4cd2555d5a7 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.Option; + +/** + * Annotation processing code generation context. + */ +public interface AptContext extends CodegenContext { + /** + * Create context from the processing environment, and a set of additional supported options. + * + * @param env processing environment + * @param options supported options + * @return a new annotation processing context + */ + static AptContext create(ProcessingEnvironment env, Set> options) { + return AptContextImpl.create(env, options); + } + + /** + * Annotation processing environment. + * + * @return environment + */ + ProcessingEnvironment aptEnv(); +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java new file mode 100644 index 00000000000..8ce02a3d4d8 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import io.helidon.codegen.CodegenContextBase; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.CodegenScope; +import io.helidon.codegen.ModuleInfo; +import io.helidon.codegen.ModuleInfoSourceParser; +import io.helidon.codegen.Option; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +class AptContextImpl extends CodegenContextBase implements AptContext { + private static final Pattern SCOPE_PATTERN = Pattern.compile("(\\w+).*classes"); + + private final ProcessingEnvironment env; + private final ModuleInfo moduleInfo; + + AptContextImpl(ProcessingEnvironment env, + CodegenOptions options, + Set> supportedOptions, + AptFiler aptFiler, + AptLogger aptLogger, + CodegenScope scope, + ModuleInfo moduleInfo /* may be null*/) { + super(options, supportedOptions, aptFiler, aptLogger, scope); + + this.env = env; + this.moduleInfo = moduleInfo; + } + + static AptContext create(ProcessingEnvironment env, Set> supportedOptions) { + CodegenOptions options = AptOptions.create(env); + + CodegenScope scope = guessScope(env, options); + Optional module = findModule(env.getFiler()); + + return new AptContextImpl(env, + options, + supportedOptions, + new AptFiler(env, options), + new AptLogger(env, options), + scope, + module.orElse(null)); + } + + @Override + public ProcessingEnvironment aptEnv() { + return env; + } + + @Override + public Optional typeInfo(TypeName typeName) { + return AptTypeInfoFactory.create(this, typeName); + } + + @Override + public Optional typeInfo(TypeName typeName, Predicate elementPredicate) { + return AptTypeInfoFactory.create(this, typeName, elementPredicate); + } + + @Override + public Optional module() { + return Optional.ofNullable(moduleInfo); + } + + private static Optional findModule(Filer filer) { + // expected is source location + try { + FileObject resource = filer.getResource(StandardLocation.SOURCE_PATH, "", "module-info.java"); + try (InputStream in = resource.openInputStream()) { + return Optional.of(ModuleInfoSourceParser.parse(in)); + } + } catch (IOException ignored) { + // it is not in sources, let's see if it got generated + } + // generated + try { + FileObject resource = filer.getResource(StandardLocation.SOURCE_OUTPUT, "", "module-info.java"); + try (InputStream in = resource.openInputStream()) { + return Optional.of(ModuleInfoSourceParser.parse(in)); + } + } catch (IOException ignored) { + // not in generated source either + } + // we do not see a module info + return Optional.empty(); + } + + private static CodegenScope guessScope(ProcessingEnvironment env, CodegenOptions options) { + CodegenScope scopeFromOptions = CodegenOptions.CODEGEN_SCOPE.findValue(options).orElse(null); + + if (scopeFromOptions != null) { + return scopeFromOptions; + } + try { + URI resourceUri = env.getFiler() + .getResource(StandardLocation.CLASS_OUTPUT, "does.not.exist", "DefinitelyDoesNotExist") + .toUri(); + + // should be something like: + // file:///projects/helidon_4/inject/tests/resources-inject/target/test-classes/does/not/exist/DefinitlyDoesNotExist + String resourceUriString = resourceUri.toString(); + if (!resourceUriString.endsWith("/does/not/exist/DefinitelyDoesNotExist")) { + // cannot guess, not ending in expected string, assume production scope + return CodegenScope.PRODUCTION; + } + // full URI + resourceUriString = resourceUriString + .substring(0, resourceUriString.length() - "/does/not/exist/DefinitelyDoesNotExist".length()); + // file:///projects/helidon_4/inject/tests/resources-inject/target/test-classes + int lastSlash = resourceUriString.lastIndexOf('/'); + if (lastSlash < 0) { + // cannot guess, no path, assume production scope + return CodegenScope.PRODUCTION; + } + resourceUriString = resourceUriString.substring(lastSlash + 1); + // test-classes + Matcher matcher = SCOPE_PATTERN.matcher(resourceUriString); + if (matcher.matches()) { + return new CodegenScope(matcher.group(1)); + } + // not matched, either production (just "classes"), or could not match - assume production scope + return CodegenScope.PRODUCTION; + } catch (IOException e) { + // we assume production scope + return CodegenScope.PRODUCTION; + } + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java new file mode 100644 index 00000000000..801cd1baa66 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenFiler; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.IndentType; +import io.helidon.codegen.classmodel.ClassModel; + +class AptFiler implements CodegenFiler { + private final Filer filer; + private final String indent; + + AptFiler(ProcessingEnvironment env, CodegenOptions options) { + this.filer = env.getFiler(); + + IndentType value = CodegenOptions.INDENT_TYPE.value(options); + int codegenRepeat = CodegenOptions.INDENT_COUNT.value(options); + + this.indent = String.valueOf(value.character()).repeat(codegenRepeat); + } + + @Override + public Path writeSourceFile(ClassModel classModel, Object... originatingElements) { + Element[] elements = toElements(originatingElements); + + try { + JavaFileObject sourceFile = filer.createSourceFile(classModel.typeName().fqName(), elements); + try (Writer os = sourceFile.openWriter()) { + classModel.write(os, indent); + } + return Path.of(sourceFile.toUri()); + } catch (IOException e) { + throw new CodegenException("Failed to write source file for type: " + classModel.typeName(), + e, + originatingElement(elements, classModel.typeName())); + } + } + + @Override + public Path writeResource(byte[] resource, String location, Object... originatingElements) { + Element[] elements = toElements(originatingElements); + + try { + FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", location, elements); + try (OutputStream os = fileObject.openOutputStream()) { + os.write(resource); + } + return Path.of(fileObject.toUri()); + } catch (IOException e) { + throw new CodegenException("Failed to write resource file " + location, + e, + originatingElement(elements, location)); + } + } + + private Object originatingElement(Element[] elements, Object alternative) { + if (elements.length == 0) { + return alternative; + } + return elements[0]; + } + + private Element[] toElements(Object[] originatingElements) { + List result = new ArrayList<>(); + for (Object originatingElement : originatingElements) { + if (originatingElement instanceof Element element) { + result.add(element); + } + } + return result.toArray(new Element[0]); + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptLogger.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptLogger.java new file mode 100644 index 00000000000..e2eb281c71c --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptLogger.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.List; + +import javax.annotation.processing.Messager; +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.TypeElement; +import javax.tools.Diagnostic; + +import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenLogger; +import io.helidon.codegen.CodegenOptions; +import io.helidon.common.types.TypeName; + +class AptLogger implements CodegenLogger { + private final System.Logger logger; + private final Messager messager; + private final ProcessingEnvironment env; + + AptLogger(ProcessingEnvironment env, CodegenOptions options) { + this.messager = env.getMessager(); + this.env = env; + this.logger = System.getLogger(AptLogger.class.getName()); + } + + @Override + public void log(CodegenEvent event) { + // we always log to system logger if info or below + // we only log to messager if info or above + switch (event.level()) { + case TRACE, DEBUG -> logSystem(event); + case INFO -> { + logSystem(event); + logApt(event); + } + case WARNING, ERROR -> { + logApt(event); + } + default -> logSystem(event); + } + } + + private void logApt(CodegenEvent event) { + Diagnostic.Kind kind = mapKind(event.level()); + if (kind == Diagnostic.Kind.OTHER) { + // not supported + return; + } + + List objects = event.objects(); + messager.printMessage(kind, + event.message(), + findElement(objects), + findAnnotation(objects), + findAnnotationValue(objects)); + } + + private AnnotationValue findAnnotationValue(List objects) { + for (Object object : objects) { + if (object instanceof AnnotationValue value) { + return value; + } + } + return null; + } + + private AnnotationMirror findAnnotation(List objects) { + for (Object object : objects) { + if (object instanceof AnnotationMirror mirror) { + return mirror; + } + } + return null; + } + + private Element findElement(List objects) { + for (Object object : objects) { + if (object instanceof Element e) { + return e; + } + } + for (Object object : objects) { + if (object instanceof TypeName t) { + TypeElement element = env.getElementUtils().getTypeElement(t.declaredName()); + if (element != null) { + return element; + } + } + } + return null; + } + + private Diagnostic.Kind mapKind(System.Logger.Level level) { + return switch (level) { + case ALL, OFF, DEBUG, TRACE -> Diagnostic.Kind.OTHER; + case INFO -> Diagnostic.Kind.NOTE; + case WARNING -> Diagnostic.Kind.WARNING; + case ERROR -> Diagnostic.Kind.ERROR; + }; + } + + private void logSystem(CodegenEvent event) { + if (logger.isLoggable(event.level())) { + logger.log(event.level(), + event.message(), + event.throwable().orElse(null)); + } + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptOptions.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptOptions.java new file mode 100644 index 00000000000..036d34c4c56 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptOptions.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenOptions; +import io.helidon.codegen.Option; + +class AptOptions implements CodegenOptions { + private final ProcessingEnvironment aptEnv; + + AptOptions(ProcessingEnvironment aptEnv) { + this.aptEnv = aptEnv; + } + + static CodegenOptions create(ProcessingEnvironment env) { + return new AptOptions(env); + } + + @Override + public Optional option(String option) { + return Optional.ofNullable(aptEnv.getOptions().get(option)); + } + + @Override + public void validate(Set> permittedOptions) { + Set helidonOptions = aptEnv.getOptions() + .keySet() + .stream() + .filter(it -> it.startsWith("helidon.")) + .collect(Collectors.toSet()); + + // now remove all expected + permittedOptions.stream() + .map(Option::name) + .forEach(helidonOptions::remove); + + helidonOptions.remove(CODEGEN_SCOPE.name()); + helidonOptions.remove(CODEGEN_MODULE.name()); + helidonOptions.remove(CODEGEN_PACKAGE.name()); + helidonOptions.remove(INDENT_TYPE.name()); + helidonOptions.remove(INDENT_COUNT.name()); + helidonOptions.remove(CREATE_META_INF_SERVICES.name()); + + if (!helidonOptions.isEmpty()) { + throw new CodegenException("Unrecognized/unsupported Helidon option configured: " + helidonOptions); + } + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java new file mode 100644 index 00000000000..0d95a814394 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; + +import io.helidon.codegen.Codegen; +import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.Option; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +import static java.lang.System.Logger.Level.TRACE; +import static java.lang.System.Logger.Level.WARNING; + +/** + * Annotation processor that maps APT types to Helidon types, and invokes {@link io.helidon.codegen.Codegen}. + */ +public final class AptProcessor extends AbstractProcessor { + private static final TypeName GENERATOR = TypeName.create(AptProcessor.class); + + private AptContext ctx; + private Codegen codegen; + + /** + * Only for {@link java.util.ServiceLoader}, to be loaded by compiler. + */ + @Deprecated + public AptProcessor() { + super(); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Stream.concat(codegen.supportedAnnotations() + .stream() + .map(TypeName::fqName), + codegen.supportedAnnotationPackagePrefixes() + .stream() + .map(it -> it + "*")) + .collect(Collectors.toSet()); + } + + @Override + public Set getSupportedOptions() { + return Codegen.supportedOptions() + .stream() + .map(Option::name) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + + this.ctx = AptContext.create(processingEnv, Codegen.supportedOptions()); + this.codegen = Codegen.create(ctx, GENERATOR); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Thread thread = Thread.currentThread(); + ClassLoader previousClassloader = thread.getContextClassLoader(); + thread.setContextClassLoader(AptProcessor.class.getClassLoader()); + + // we want everything to execute in the classloader of this type, so service loaders + // use the classpath of the annotation processor, and not some "random" classloader, such as a maven one + try { + doProcess(annotations, roundEnv); + return true; + } finally { + thread.setContextClassLoader(previousClassloader); + } + } + + private void doProcess(Set annotations, RoundEnvironment roundEnv) { + ctx.logger().log(TRACE, "Process annotations: " + annotations + ", processing over: " + roundEnv.processingOver()); + + if (roundEnv.processingOver()) { + codegen.processingOver(); + return; + } + + if (annotations.isEmpty()) { + // no annotations, no types, still call the codegen, maybe it has something to do + codegen.process(List.of()); + return; + } + + List allTypes = discoverTypes(annotations, roundEnv); + codegen.process(allTypes); + } + + private List discoverTypes(Set annotations, RoundEnvironment roundEnv) { + // we must discover all types that should be handled, create TypeInfo and only then check if these should be processed + // as we may replace annotations, elements, and whole types. + + // first collect all types (group by type name, so we do not have duplicity) + Map types = new HashMap<>(); + + for (TypeElement annotation : annotations) { + Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation); + for (Element element : elementsAnnotatedWith) { + ElementKind kind = element.getKind(); + switch (kind) { + case ENUM, INTERFACE, CLASS, ANNOTATION_TYPE, RECORD -> addType(types, element, element, annotation); + case ENUM_CONSTANT, CONSTRUCTOR, METHOD, FIELD, STATIC_INIT, INSTANCE_INIT, RECORD_COMPONENT -> + addType(types, element.getEnclosingElement(), element, annotation); + case PARAMETER -> addType(types, element.getEnclosingElement().getEnclosingElement(), element, annotation); + default -> ctx.logger().log(TRACE, "Ignoring annotated element, not supported: " + element + ", kind: " + kind); + } + } + } + + return types.values() + .stream() + .flatMap(element -> { + Optional typeInfo = AptTypeInfoFactory.create(ctx, element); + + if (typeInfo.isEmpty()) { + ctx.logger().log(CodegenEvent.builder() + .level(WARNING) + .message("Could not create TypeInfo for annotated type.") + .addObject(element) + .build()); + } + return typeInfo.stream(); + }) + .toList(); + } + + private void addType(Map types, + Element typeElement, + Element processedElement, + TypeElement annotation) { + Optional typeName = AptTypeFactory.createTypeName(typeElement); + if (typeName.isPresent()) { + types.putIfAbsent(typeName.get(), (TypeElement) typeElement); + } else { + processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, + "Could not create TypeName for annotated type." + + " Annotation: " + annotation, + processedElement); + } + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java new file mode 100644 index 00000000000..f752bc8b0c8 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import io.helidon.common.types.TypeName; + +import static io.helidon.common.types.TypeName.createFromGenericDeclaration; + +/** + * Factory for types. + */ +public final class AptTypeFactory { + private AptTypeFactory() { + } + + /** + * Creates a name from a declared type during annotation processing. + * + * @param type the element type + * @return the associated type name instance + */ + public static Optional createTypeName(DeclaredType type) { + return createTypeName(type.asElement()); + } + + /** + * Create type from type mirror. + * + * @param typeMirror annotation processing type mirror + * @return type name + * @throws IllegalArgumentException when the mirror cannot be resolved into a name (such as when it represents + * none or error) + */ + public static Optional createTypeName(TypeMirror typeMirror) { + TypeKind kind = typeMirror.getKind(); + if (kind.isPrimitive()) { + Class type = switch (kind) { + case BOOLEAN -> boolean.class; + case BYTE -> byte.class; + case SHORT -> short.class; + case INT -> int.class; + case LONG -> long.class; + case CHAR -> char.class; + case FLOAT -> float.class; + case DOUBLE -> double.class; + default -> throw new IllegalStateException("Unknown primitive type: " + kind); + }; + + return Optional.of(TypeName.create(type)); + } + + switch (kind) { + case VOID -> { + return Optional.of(TypeName.create(void.class)); + } + case TYPEVAR -> { + return Optional.of(createFromGenericDeclaration(typeMirror.toString())); + } + case WILDCARD, ERROR -> { + return Optional.of(TypeName.create(typeMirror.toString())); + } + // this is most likely a type that is code generated as part of this round, best effort + case NONE -> { + return Optional.empty(); + } + default -> { + } + // fall through + } + + if (typeMirror instanceof ArrayType arrayType) { + return Optional.of(TypeName.builder(createTypeName(arrayType.getComponentType()).orElseThrow()) + .array(true) + .build()); + } + + if (typeMirror instanceof DeclaredType declaredType) { + List typeParams = declaredType.getTypeArguments() + .stream() + .map(AptTypeFactory::createTypeName) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + + TypeName result = createTypeName(declaredType.asElement()).orElse(null); + if (typeParams.isEmpty() || result == null) { + return Optional.ofNullable(result); + } + + return Optional.of(TypeName.builder(result).typeArguments(typeParams).build()); + } + + throw new IllegalStateException("Unknown type mirror: " + typeMirror); + } + + /** + * Create type from type mirror. The element is needed to correctly map + * type arguments to type parameters. + * + * @param element the type element of the type mirror + * @param mirror the type mirror as declared in source code + * @return type for the provided values + */ + public static Optional createTypeName(TypeElement element, TypeMirror mirror) { + Optional result = AptTypeFactory.createTypeName(mirror); + if (result.isEmpty()) { + return result; + } + + TypeName mirrorName = result.get(); + int typeArgumentSize = mirrorName.typeArguments().size(); + + List typeParameters = element.getTypeParameters() + .stream() + .map(TypeParameterElement::toString) + .toList(); + if (typeArgumentSize > typeParameters.size()) { + throw new IllegalStateException("Found " + typeArgumentSize + " type arguments, but only " + typeParameters.size() + + " type parameters on: " + mirror); + } + return Optional.of(TypeName.builder(mirrorName) + .typeParameters(typeParameters) + .build()); + } + + /** + * Creates a name from an element type during annotation processing. + * + * @param type the element type + * @return the associated type name instance + */ + public static Optional createTypeName(Element type) { + if (type instanceof VariableElement) { + return createTypeName(type.asType()); + } + + if (type instanceof ExecutableElement) { + return createTypeName(((ExecutableElement) type).getReturnType()); + } + + List classNames = new ArrayList<>(); + String simpleName = type.getSimpleName().toString(); + + Element enclosing = type.getEnclosingElement(); + while (enclosing != null && ElementKind.PACKAGE != enclosing.getKind()) { + if (enclosing.getKind() == ElementKind.CLASS + || enclosing.getKind() == ElementKind.INTERFACE + || enclosing.getKind() == ElementKind.ANNOTATION_TYPE + || enclosing.getKind() == ElementKind.RECORD) { + classNames.add(enclosing.getSimpleName().toString()); + } + enclosing = enclosing.getEnclosingElement(); + } + Collections.reverse(classNames); + + // try to find the package + while (enclosing != null && enclosing.getKind() != ElementKind.PACKAGE) { + enclosing = enclosing.getEnclosingElement(); + } + String packageName = enclosing == null ? "" : ((PackageElement) enclosing).getQualifiedName().toString(); + + // the package name may be our enclosing type, if the type in question is created as part of annotation processing + // in this round; as we want to support this (e.g. production classes depend on generated classes), we need to resolve it + if (!packageName.isEmpty() && Character.isUpperCase(packageName.charAt(0))) { + classNames = List.of(packageName.split("\\.")); + packageName = ""; + } + + return Optional.of(TypeName.builder() + .packageName(packageName) + .className(simpleName) + .enclosingNames(classNames) + .build()); + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java new file mode 100644 index 00000000000..4203495ee7d --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java @@ -0,0 +1,561 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; +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 javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.TypeInfoFactoryBase; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import static io.helidon.common.types.TypeName.createFromGenericDeclaration; +import static java.util.function.Predicate.not; + +/** + * Factory to analyze processed types and to provide {@link io.helidon.common.types.TypeInfo} for them. + */ +public final class AptTypeInfoFactory extends TypeInfoFactoryBase { + + // we expect that annotations themselves are not code generated, and can be cached + private static final Map> META_ANNOTATION_CACHE = new ConcurrentHashMap<>(); + + private AptTypeInfoFactory() { + } + + /** + * Create type information for a type name, reading all child elements. + * + * @param ctx annotation processor processing context + * @param typeName type name to find + * @return type info for the type element + * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for + * a primitive type) + */ + public static Optional create(AptContext ctx, + TypeName typeName) { + return create(ctx, typeName, ElementInfoPredicates.ALL_PREDICATE); + } + + /** + * Create type information for a type name. + * + * @param ctx annotation processor processing environment + * @param typeName type name to find + * @param elementPredicate predicate for child elements + * @return type info for the type element, or empty if it cannot be resolved + */ + public static Optional create(AptContext ctx, + TypeName typeName, + Predicate elementPredicate) throws IllegalArgumentException { + + TypeElement typeElement = ctx.aptEnv().getElementUtils().getTypeElement(typeName.fqName()); + if (typeElement == null) { + return Optional.empty(); + } + return AptTypeFactory.createTypeName(typeElement.asType()) + .flatMap(it -> create(ctx, typeElement, elementPredicate, it)) + .flatMap(it -> mapType(ctx, it)); + } + + /** + * Create type information from a type element, reading all child elements. + * + * @param ctx annotation processor processing context + * @param typeElement type element of the type we want to analyze + * @return type info for the type element + * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for + * a primitive type) + */ + public static Optional create(AptContext ctx, + TypeElement typeElement) { + + TypeName typeName = AptTypeFactory.createTypeName(typeElement.asType()).orElse(null); + + if (typeName == null) { + return Optional.empty(); + } + + return create(ctx, typeName); + } + + /** + * Create type information from a type element. + * + * @param ctx annotation processor processing context + * @param typeElement type element of the type we want to analyze + * @param elementPredicate predicate for child elements + * @return type info for the type element, or empty if it cannot be resolved + */ + public static Optional create(AptContext ctx, + TypeElement typeElement, + Predicate elementPredicate) throws IllegalArgumentException { + + return AptTypeFactory.createTypeName(typeElement.asType()) + .flatMap(it -> create(ctx, typeElement, elementPredicate, it)); + } + + /** + * Creates an instance of a {@link io.helidon.common.types.TypedElementInfo} given its type and variable element from + * annotation processing. If the passed in element is not a {@link io.helidon.common.types.ElementKind#FIELD}, + * {@link io.helidon.common.types.ElementKind#METHOD}, + * {@link io.helidon.common.types.ElementKind#CONSTRUCTOR}, or {@link io.helidon.common.types.ElementKind#PARAMETER} + * then this method may return empty. + * + * @param ctx annotation processing context + * @param v the element (from annotation processing) + * @param elements the elements + * @return the created instance + */ + public static Optional createTypedElementInfoFromElement(AptContext ctx, + Element v, + Elements elements) { + TypeName type = AptTypeFactory.createTypeName(v).orElse(null); + TypeMirror typeMirror = null; + String defaultValue = null; + List params = List.of(); + List componentTypeNames = List.of(); + List elementTypeAnnotations = List.of(); + Set modifierNames = v.getModifiers() + .stream() + .map(Modifier::toString) + .collect(Collectors.toSet()); + Set thrownChecked = Set.of(); + + if (v instanceof ExecutableElement ee) { + typeMirror = Objects.requireNonNull(ee.getReturnType()); + params = ee.getParameters().stream() + .map(it -> createTypedElementInfoFromElement(ctx, it, elements).orElseThrow(() -> { + return new CodegenException("Failed to create element info for parameter: " + it + ", either it uses " + + "invalid type, or it was removed by an element mapper. This would" + + " result in an invalid TypeInfo model.", + it); + })) + .toList(); + AnnotationValue annotationValue = ee.getDefaultValue(); + defaultValue = (annotationValue == null) ? null + : String.valueOf(annotationValue.accept(new ToAnnotationValueVisitor(elements) + .mapBooleanToNull(true) + .mapVoidToNull(true) + .mapBlankArrayToNull(true) + .mapEmptyStringToNull(true) + .mapToSourceDeclaration(true), null)); + + thrownChecked = ee.getThrownTypes() + .stream() + .filter(it -> isCheckedException(ctx, it)) + .flatMap(it -> AptTypeFactory.createTypeName(it).stream()) + .collect(Collectors.toSet()); + } else if (v instanceof VariableElement ve) { + typeMirror = Objects.requireNonNull(ve.asType()); + } + + if (typeMirror != null) { + if (type == null) { + Element element = ctx.aptEnv().getTypeUtils().asElement(typeMirror); + if (element instanceof TypeElement typeElement) { + type = AptTypeFactory.createTypeName(typeElement, typeMirror) + .orElse(createFromGenericDeclaration(typeMirror.toString())); + } else { + type = AptTypeFactory.createTypeName(typeMirror) + .orElse(createFromGenericDeclaration(typeMirror.toString())); + } + } + if (typeMirror instanceof DeclaredType) { + List args = ((DeclaredType) typeMirror).getTypeArguments(); + componentTypeNames = args.stream() + .map(AptTypeFactory::createTypeName) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .collect(Collectors.toList()); + elementTypeAnnotations = + createAnnotations(ctx, ((DeclaredType) typeMirror).asElement(), elements); + } + } + String javadoc = ctx.aptEnv().getElementUtils().getDocComment(v); + javadoc = javadoc == null || javadoc.isBlank() ? "" : javadoc; + + TypedElementInfo.Builder builder = TypedElementInfo.builder() + .description(javadoc) + .typeName(type) + .componentTypes(componentTypeNames) + .elementName(v.getSimpleName().toString()) + .kind(kind(v.getKind())) + .annotations(createAnnotations(ctx, v, elements)) + .elementTypeAnnotations(elementTypeAnnotations) + .elementModifiers(modifiers(ctx, modifierNames)) + .accessModifier(accessModifier(modifierNames)) + .throwsChecked(thrownChecked) + .parameterArguments(params) + .originatingElement(v); + AptTypeFactory.createTypeName(v.getEnclosingElement()).ifPresent(builder::enclosingType); + Optional.ofNullable(defaultValue).ifPresent(builder::defaultValue); + + return mapElement(ctx, builder.build()); + } + + private static boolean isCheckedException(AptContext ctx, TypeMirror it) { + ProcessingEnvironment aptEnv = ctx.aptEnv(); + Elements elements = aptEnv.getElementUtils(); + Types types = aptEnv.getTypeUtils(); + TypeMirror exception = elements.getTypeElement(Exception.class.getName()).asType(); + TypeMirror runtimeException = elements.getTypeElement(RuntimeException.class.getName()).asType(); + + return types.isAssignable(it, exception) && !types.isAssignable(it, runtimeException); + } + + private static ElementKind kind(javax.lang.model.element.ElementKind kind) { + try { + return ElementKind.valueOf(String.valueOf(kind).toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + // not supported, consider other type + return ElementKind.OTHER; + } + } + + private static Optional create(AptContext ctx, + TypeElement typeElement, + Predicate elementPredicate, + TypeName typeName) { + + Objects.requireNonNull(ctx); + Objects.requireNonNull(typeElement); + Objects.requireNonNull(elementPredicate); + Objects.requireNonNull(typeName); + + if (typeName.resolvedName().equals(Object.class.getName())) { + // Object is not to be analyzed + return Optional.empty(); + } + TypeName genericTypeName = typeName.genericTypeName(); + Set allInterestingTypeNames = new LinkedHashSet<>(); + allInterestingTypeNames.add(genericTypeName); + typeName.typeArguments() + .stream() + .map(TypeName::genericTypeName) + .filter(not(AptTypeInfoFactory::isBuiltInJavaType)) + .filter(not(TypeName::generic)) + .forEach(allInterestingTypeNames::add); + + Elements elementUtils = ctx.aptEnv().getElementUtils(); + try { + List annotations = + List.copyOf(createAnnotations(ctx, + elementUtils.getTypeElement(genericTypeName.resolvedName()), + elementUtils)); + Set annotationsOnTypeOrElements = new HashSet<>(); + annotations.stream() + .map(Annotation::typeName) + .forEach(annotationsOnTypeOrElements::add); + + List elementsWeCareAbout = new ArrayList<>(); + List otherElements = new ArrayList<>(); + typeElement.getEnclosedElements() + .stream() + .flatMap(it -> createTypedElementInfoFromElement(ctx, it, elementUtils).stream()) + .forEach(it -> { + if (elementPredicate.test(it)) { + elementsWeCareAbout.add(it); + } else { + otherElements.add(it); + } + annotationsOnTypeOrElements.addAll(it.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet())); + it.parameterArguments() + .forEach(arg -> annotationsOnTypeOrElements.addAll(arg.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet()))); + }); + + Set modifiers = toModifierNames(typeElement.getModifiers()); + TypeInfo.Builder builder = TypeInfo.builder() + .originatingElement(typeElement) + .typeName(typeName) + .kind(kind(typeElement.getKind())) + .annotations(annotations) + .elementModifiers(modifiers(ctx, modifiers)) + .accessModifier(accessModifier(modifiers)) + .elementInfo(elementsWeCareAbout) + .otherElementInfo(otherElements); + + String javadoc = elementUtils.getDocComment(typeElement); + if (javadoc != null) { + builder.description(javadoc); + } + + // add all of the element's and parameters to the references annotation set + elementsWeCareAbout.forEach(it -> { + if (!isBuiltInJavaType(it.typeName()) && !it.typeName().generic()) { + allInterestingTypeNames.add(it.typeName().genericTypeName()); + } + it.parameterArguments().stream() + .map(TypedElementInfo::typeName) + .map(TypeName::genericTypeName) + .filter(t -> !isBuiltInJavaType(t)) + .filter(t -> !t.generic()) + .forEach(allInterestingTypeNames::add); + }); + + TypeMirror superTypeMirror = typeElement.getSuperclass(); + TypeElement superTypeElement = (TypeElement) ctx.aptEnv().getTypeUtils().asElement(superTypeMirror); + + TypeName fqSuperTypeName; + if (superTypeElement != null) { + fqSuperTypeName = AptTypeFactory.createTypeName(superTypeElement, superTypeMirror) + .orElse(null); + + if (fqSuperTypeName != null && !TypeNames.OBJECT.equals(fqSuperTypeName)) { + + TypeName genericSuperTypeName = fqSuperTypeName.genericTypeName(); + Optional superTypeInfo = + create(ctx, superTypeElement, elementPredicate, fqSuperTypeName); + superTypeInfo.ifPresent(builder::superTypeInfo); + allInterestingTypeNames.add(genericSuperTypeName); + fqSuperTypeName.typeArguments().stream() + .map(TypeName::genericTypeName) + .filter(it -> !isBuiltInJavaType(it)) + .filter(it -> !it.generic()) + .forEach(allInterestingTypeNames::add); + } + } + + typeElement.getInterfaces().forEach(interfaceTypeMirror -> { + TypeName fqInterfaceTypeName = AptTypeFactory.createTypeName(interfaceTypeMirror).orElse(null); + + if (fqInterfaceTypeName != null) { + TypeName genericInterfaceTypeName = fqInterfaceTypeName.genericTypeName(); + allInterestingTypeNames.add(genericInterfaceTypeName); + fqInterfaceTypeName.typeArguments().stream() + .map(TypeName::genericTypeName) + .filter(it -> !isBuiltInJavaType(it)) + .filter(it -> !it.generic()) + .forEach(allInterestingTypeNames::add); + TypeElement interfaceTypeElement = elementUtils.getTypeElement(fqInterfaceTypeName.genericTypeName() + .resolvedName()); + if (interfaceTypeElement != null) { + Optional superTypeInfo = + create(ctx, interfaceTypeElement, elementPredicate, fqInterfaceTypeName); + superTypeInfo.ifPresent(builder::addInterfaceTypeInfo); + } + } + }); + + AtomicReference moduleName = new AtomicReference<>(); + allInterestingTypeNames.forEach(it -> { + TypeElement theTypeElement = elementUtils.getTypeElement(it.name()); + if (theTypeElement == null || !isTypeInThisModule(ctx, theTypeElement, moduleName)) { + if (hasValue(moduleName.get())) { + builder.putReferencedModuleName(it, moduleName.get()); + } + } + }); + ModuleElement module = ctx.aptEnv().getElementUtils().getModuleOf(typeElement); + if (module != null) { + builder.module(module.toString()); + } + + builder.referencedTypeNamesToAnnotations(toMetaAnnotations(ctx, annotationsOnTypeOrElements)); + + return Optional.of(builder.build()); + } catch (Exception e) { + throw new IllegalStateException("Failed to process: " + typeElement, e); + } + } + + private static AccessModifier accessModifier(Set stringModifiers) { + for (String stringModifier : stringModifiers) { + try { + return AccessModifier.valueOf(stringModifier.toUpperCase(Locale.ROOT)); + } catch (Exception ignored) { + // we do not care about modifiers we do not understand - either non-access modifier, or something new + } + } + return AccessModifier.PACKAGE_PRIVATE; + } + + private static List createAnnotations(AptContext ctx, Element element, Elements elements) { + ElementKind elementKind = kind(element.getKind()); + return element.getAnnotationMirrors() + .stream() + .map(it -> AptAnnotationFactory.createAnnotation(it, elements)) + .flatMap(it -> mapAnnotation(ctx, it, elementKind).stream()) + .filter(TypeInfoFactoryBase::annotationFilter) + .toList(); + } + + /** + * Converts the provided modifiers to the corresponding set of modifier names. + * + * @param modifiers the modifiers + * @return the modifier names + */ + private static Set toModifierNames(Set modifiers) { + return modifiers.stream() + .map(Modifier::name) + .collect(Collectors.toSet()); + } + + /** + * Determines if the given type element is defined in the module being processed. If so then the return value is set to + * {@code true} and the moduleName is cleared out. If not then the return value is set to {@code false} and the + * {@code moduleName} is set to the module name if it has a qualified module name, and not from an internal java module system + * type. Note that this method will only return {@code true} when the module info paths are being used in the project. + * + * @param ctx processing context + * @param type the type element to analyze + * @param moduleName the module name to populate if it is determinable + * @return true if the type is definitely defined in this module, false otherwise + */ + private static boolean isTypeInThisModule(AptContext ctx, + TypeElement type, + AtomicReference moduleName) { + moduleName.set(null); + + ModuleElement module = ctx.aptEnv().getElementUtils().getModuleOf(type); + if (!module.isUnnamed()) { + String name = module.getQualifiedName().toString(); + if (hasValue(name)) { + moduleName.set(name); + } + } + + // if there is no module-info in use we need to try to find the type is in our source path and if + // not found then just assume it is external + try { + Trees trees = Trees.instance(ctx.aptEnv()); + TreePath path = trees.getPath(type); + if (path == null) { + return false; + } + JavaFileObject sourceFile = path.getCompilationUnit().getSourceFile(); + return (sourceFile != null); + } catch (Throwable t) { + // assumed external + return false; + } + } + + /** + * Returns the map of meta annotations for the provided collection of annotation values. + * + * @param annotations the annotations + * @return the meta annotations for the provided set of annotations + */ + private static Map> toMetaAnnotations(AptContext ctx, + Set annotations) { + if (annotations.isEmpty()) { + return Map.of(); + } + + Map> result = new HashMap<>(); + + gatherMetaAnnotations(ctx, annotations, result); + + return result; + } + + // gather a single level map of types to their meta annotation + private static void gatherMetaAnnotations(AptContext ctx, + Set annotationTypes, + Map> result) { + if (annotationTypes.isEmpty()) { + return; + } + + Elements elements = ctx.aptEnv().getElementUtils(); + + annotationTypes.stream() + .filter(not(result::containsKey)) // already in the result, no need to add it + .forEach(it -> { + List meta = META_ANNOTATION_CACHE.get(it); + boolean fromCache = true; + if (meta == null) { + fromCache = false; + TypeElement typeElement = elements.getTypeElement(it.fqName()); + if (typeElement != null) { + List metaAnnotations = createAnnotations(ctx, typeElement, elements); + result.put(it, new ArrayList<>(metaAnnotations)); + // now rinse and repeat for the referenced annotations + gatherMetaAnnotations(ctx, + metaAnnotations.stream() + .map(Annotation::typeName) + .collect(Collectors.toSet()), + result); + meta = metaAnnotations; + } else { + meta = List.of(); + } + } + if (!fromCache) { + // we cannot use computeIfAbsent, as that would do a recursive update if nested more than once + META_ANNOTATION_CACHE.putIfAbsent(it, meta); + } + if (!meta.isEmpty()) { + result.put(it, meta); + } + }); + } + + /** + * Simple check to see the passed String value is non-null and non-blank. + * + * @param val the value to check + * @return true if non-null and non-blank + */ + private static boolean hasValue(String val) { + return (val != null && !val.isBlank()); + } +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java new file mode 100644 index 00000000000..58cb031ceec --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/ToAnnotationValueVisitor.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.apt; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.AnnotationValueVisitor; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; + +class ToAnnotationValueVisitor implements AnnotationValueVisitor { + private final Elements elements; + private boolean mapVoidToNull; + private boolean mapFalseToNull; + private boolean mapEmptyStringToNull; + private boolean mapBlankArrayToNull; + private boolean mapToSourceDeclaration; + + ToAnnotationValueVisitor(Elements elements) { + this.elements = elements; + } + + ToAnnotationValueVisitor mapVoidToNull(boolean val) { + this.mapVoidToNull = val; + return this; + } + + ToAnnotationValueVisitor mapBooleanToNull(boolean val) { + this.mapFalseToNull = val; + return this; + } + + ToAnnotationValueVisitor mapEmptyStringToNull(boolean val) { + this.mapEmptyStringToNull = val; + return this; + } + + ToAnnotationValueVisitor mapBlankArrayToNull(boolean val) { + this.mapBlankArrayToNull = val; + return this; + } + + ToAnnotationValueVisitor mapToSourceDeclaration(boolean val) { + this.mapToSourceDeclaration = val; + return this; + } + + @Override + public Object visit(AnnotationValue av, Object o) { + return av.accept(this, o); + } + + @Override + public Object visitBoolean(boolean b, Object o) { + if (!b && mapFalseToNull) { + return null; + } + return b; + } + + @Override + public Object visitByte(byte b, Object o) { + return b; + } + + @Override + public Object visitChar(char c, Object o) { + return c; + } + + @Override + public Object visitDouble(double d, Object o) { + return d; + } + + @Override + public Object visitFloat(float f, Object o) { + return f; + } + + @Override + public Object visitInt(int i, Object o) { + return i; + } + + @Override + public Object visitLong(long i, Object o) { + return i; + } + + @Override + public Object visitShort(short s, Object o) { + return s; + } + + @Override + public Object visitString(String s, Object o) { + if (mapEmptyStringToNull && s != null && s.isBlank()) { + return null; + } + + if (mapToSourceDeclaration) { + return "\"" + s + "\""; + } + + return s; + } + + @Override + public Object visitType(TypeMirror t, Object o) { + String val = t.toString(); + if (mapVoidToNull && ("void".equals(val) || Void.class.getName().equals(val))) { + val = null; + } + return val; + } + + @Override + public Object visitEnumConstant(VariableElement c, Object o) { + return String.valueOf(c.getSimpleName()); + } + + @Override + public Object visitAnnotation(AnnotationMirror a, Object o) { + return AptAnnotationFactory.createAnnotation(a, elements); + } + + @Override + public Object visitArray(List vals, Object o) { + List values = new ArrayList<>(vals.size()); + + for (AnnotationValue val : vals) { + Object elementValue = val.accept(this, null); + if (elementValue != null) { + values.add(elementValue); + } + } + + if (mapBlankArrayToNull && values.isEmpty()) { + return null; + } else if (mapToSourceDeclaration) { + return vals.stream() + .map(v -> v.accept(this, null)) + .filter(Objects::nonNull) + .map(String::valueOf) + .collect(Collectors.joining(", ", "{", "}")); + } + + return values; + } + + @Override + public String visitUnknown(AnnotationValue av, Object o) { + return null; + } + +} diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/package-info.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/package-info.java new file mode 100644 index 00000000000..84344a47e29 --- /dev/null +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Implementation of codegen tools for Java annotation processing. + * + * @see io.helidon.codegen.apt.AptContext + * @see io.helidon.codegen.apt.AptTypeFactory + * @see io.helidon.codegen.apt.AptTypeInfoFactory + */ +package io.helidon.codegen.apt; diff --git a/codegen/apt/src/main/java/module-info.java b/codegen/apt/src/main/java/module-info.java new file mode 100644 index 00000000000..f87694699ea --- /dev/null +++ b/codegen/apt/src/main/java/module-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Implementation of codegen utilities for Java annotation processing. + */ +module io.helidon.codegen.apt { + requires jdk.compiler; + requires transitive io.helidon.codegen; + + requires transitive io.helidon.common.types; + requires transitive java.compiler; + + exports io.helidon.codegen.apt; + + provides javax.annotation.processing.Processor with + io.helidon.codegen.apt.AptProcessor; +} \ No newline at end of file diff --git a/codegen/class-model/pom.xml b/codegen/class-model/pom.xml new file mode 100644 index 00000000000..9779cd5033a --- /dev/null +++ b/codegen/class-model/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + + + helidon-codegen-class-model + Helidon Codegen Class Model + + Class model generator designed to be used by code generating components (annotation processors, maven plugins) + + + + + io.helidon.common + helidon-common-types + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + \ No newline at end of file diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java new file mode 100644 index 00000000000..e13b6783f07 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotatedComponent.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +abstract class AnnotatedComponent extends CommonComponent { + + private final List annotations; + + AnnotatedComponent(Builder builder) { + super(builder); + annotations = List.copyOf(builder.annotations); + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + annotations.forEach(annotation -> annotation.addImports(imports)); + } + + List annotations() { + return annotations; + } + + abstract static class Builder, T extends AnnotatedComponent> extends CommonComponent.Builder { + + private final List annotations = new ArrayList<>(); + + Builder() { + } + + @Override + public B description(String description) { + return super.description(description); + } + + @Override + public B description(List description) { + return super.description(description); + } + + @Override + public B addDescriptionLine(String line) { + return super.description(line); + } + + /** + * Add new annotation to the component. + * + * @param annotation annotation instance + * @return updated builder instance + */ + public B addAnnotation(io.helidon.common.types.Annotation annotation) { + return addAnnotation(newAnnot -> { + newAnnot.type(annotation.typeName()); + annotation.values() + .forEach(newAnnot::addParameter); + }); + } + + /** + * Add new annotation to the component. + * + * @param consumer annotation builder consumer + * @return updated builder instance + */ + public B addAnnotation(Consumer consumer) { + Annotation.Builder builder = Annotation.builder(); + consumer.accept(builder); + return addAnnotation(builder.build()); + } + + /** + * Add new annotation to the component. + * + * @param builder annotation builder + * @return updated builder instance + */ + public B addAnnotation(Annotation.Builder builder) { + return addAnnotation(builder.build()); + } + + /** + * Add new annotation to the component. + * + * @param annotation annotation instance + * @return updated builder instance + */ + public B addAnnotation(Annotation annotation) { + annotations.add(annotation); + return identity(); + } + + @Override + public B name(String name) { + return super.name(name); + } + + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java new file mode 100644 index 00000000000..c52c7a56f1c --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +import io.helidon.common.types.TypeName; + +/** + * Model of the annotation. + */ +public final class Annotation extends CommonComponent { + + private final List parameters; + + private Annotation(Builder builder) { + super(builder); + this.parameters = List.copyOf(builder.parameters.values()); + } + + /** + * New {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * New {@link Annotation} instance based on the type. + * + * @param type class type + * @return new annotation instance + */ + public static Annotation create(Class type) { + return builder().type(type).build(); + } + + /** + * Parse new Annotation object out of the String. + * + * @param annotationDefinition annotation definition + * @return new annotation instance + */ + public static Annotation parse(String annotationDefinition) { + int annotationBodyStart = annotationDefinition.indexOf("("); + int annotationBodyEnd = annotationDefinition.indexOf(")"); + String annotationName = annotationBodyStart > 0 + ? annotationDefinition.substring(0, annotationBodyStart) + : annotationDefinition; + Annotation.Builder builder = Annotation.builder() + .type(annotationName); + if (annotationBodyStart > 0) { + //TODO this needs to be improved in cases where chars , or = are part of the String value + String[] valuePairs = annotationDefinition.substring(annotationBodyStart + 1, annotationBodyEnd).split(","); + for (String valuePair : valuePairs) { + String[] keyValue = valuePair.split("="); + if (keyValue.length == 1 && valuePairs.length != 1) { + throw new IllegalStateException("Invalid custom annotation specified: " + annotationDefinition); + } + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + builder.addParameter(paramBuilder -> paramBuilder.name(key) + .type(value.startsWith("\"") ? String.class : Object.class) + .value(value)); + } + } + return builder.build(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + writer.write("@" + imports.typeName(type(), includeImport())); + if (!parameters.isEmpty()) { + writer.write("("); + if (parameters.size() == 1) { + AnnotationParameter parameter = parameters.get(0); + if (parameter.name().equals("value")) { + writer.write(parameter.value()); + } else { + parameter.writeComponent(writer, declaredTokens, imports, classType); + } + } else { + boolean first = true; + for (AnnotationParameter parameter : parameters) { + if (first) { + first = false; + } else { + writer.write(", "); + } + parameter.writeComponent(writer, declaredTokens, imports, classType); + } + } + writer.write(")"); + } + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + parameters.forEach(parameter -> parameter.addImports(imports)); + } + + /** + * Fluent API builder for {@link Annotation}. + */ + public static final class Builder extends CommonComponent.Builder { + + private final Map parameters = new LinkedHashMap<>(); + + private Builder() { + } + + @Override + public Annotation build() { + if (type() == null) { + throw new ClassModelException("Annotation type needs to be set"); + } + return new Annotation(this); + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + /** + * Adds annotation parameter. + * + * @param name annotation parameter name + * @param value parameter value + * @return updated builder instance + */ + public Builder addParameter(String name, Object value) { + Objects.requireNonNull(value); + + Class paramType = value instanceof TypeName + ? Class.class + : value.getClass(); + + return addParameter(builder -> builder.name(name) + .type(paramType) + .value(value)); + } + + /** + * Adds annotation parameter. + * + * @param consumer annotation parameter builder consumer + * @return updated builder instance + */ + public Builder addParameter(Consumer consumer) { + Objects.requireNonNull(consumer); + AnnotationParameter.Builder builder = AnnotationParameter.builder(); + consumer.accept(builder); + return addParameter(builder.build()); + } + + /** + * Adds annotation parameter. + * + * @param builder annotation parameter builder + * @return updated builder instance + */ + public Builder addParameter(AnnotationParameter.Builder builder) { + Objects.requireNonNull(builder); + return addParameter(builder.build()); + } + + /** + * Adds annotation parameter. + * + * @param parameter annotation parameter + * @return updated builder instance + */ + public Builder addParameter(AnnotationParameter parameter) { + Objects.requireNonNull(parameter); + parameters.put(parameter.name(), parameter); + return this; + } + + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java new file mode 100644 index 00000000000..b4a28580262 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Annotation parameter model. + */ +public final class AnnotationParameter extends CommonComponent { + + private final String value; + + private AnnotationParameter(Builder builder) { + super(builder); + this.value = resolveValueToString(builder.type(), builder.value); + } + + /** + * Create new {@link Builder}. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + writer.write(name() + " = " + value); + } + + private static String resolveValueToString(Type type, Object value) { + Class valueClass = value.getClass(); + if (valueClass.isEnum()) { + return valueClass.getSimpleName() + "." + ((Enum) value).name(); + } else if (type.fqTypeName().equals(String.class.getName())) { + String stringValue = value.toString(); + if (!stringValue.startsWith("\"") && !stringValue.endsWith("\"")) { + return "\"" + stringValue + "\""; + } + } else if (value instanceof TypeName typeName) { + return typeName.fqName() + ".class"; + } + return value.toString(); + } + + String value() { + return value; + } + + /** + * Fluent API builder for {@link AnnotationParameter}. + */ + public static final class Builder extends CommonComponent.Builder { + + private Object value; + + private Builder() { + } + + @Override + public AnnotationParameter build() { + if (value == null || name() == null) { + throw new ClassModelException("Annotation parameter needs to have value and type set"); + } + return new AnnotationParameter(this); + } + + @Override + public Builder name(String name) { + return super.name(name); + } + + /** + * Set annotation parameter value. + * + * @param value annotation parameter value + * @return updated builder instance + */ + public Builder value(Object value) { + this.value = Objects.requireNonNull(value); + return this; + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java new file mode 100644 index 00000000000..611f3826c0f --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassBase.java @@ -0,0 +1,691 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeName; + +/** + * Abstract class type model. Contains common logic for all class related models. + */ +public abstract class ClassBase extends AnnotatedComponent { + + private final boolean isFinal; + private final boolean isAbstract; + private final boolean isStatic; + private final List fields; + private final List staticFields; + private final List methods; + private final List staticMethods; + private final Set interfaces; + private final Set tokenNames; + private final List constructors; + private final List genericParameters; + private final List innerClasses; + private final ClassType classType; + private final Type superType; + + ClassBase(Builder builder) { + super(builder); + this.isFinal = builder.isFinal; + this.isAbstract = builder.isAbstract; + this.isStatic = builder.isStatic; + if (builder.sortFields) { + this.fields = builder.fields.values().stream().sorted(ClassBase::fieldComparator).toList(); + } else { + this.fields = List.copyOf(builder.fields.values()); + } + if (builder.sortStaticFields) { + this.staticFields = builder.staticFields.values().stream().sorted(ClassBase::fieldComparator).toList(); + } else { + this.staticFields = List.copyOf(builder.staticFields.values()); + } + this.methods = builder.methods.stream().sorted(ClassBase::methodCompare).toList(); + this.staticMethods = builder.staticMethods.stream().sorted(ClassBase::methodCompare).toList(); + this.constructors = List.copyOf(builder.constructors); + this.interfaces = Collections.unmodifiableSet(new LinkedHashSet<>(builder.interfaces)); + this.innerClasses = List.copyOf(builder.innerClasses.values()); + this.genericParameters = List.copyOf(builder.genericParameters); + this.tokenNames = this.genericParameters.stream() + .map(TypeArgument::token) + .collect(Collectors.toSet()); + this.classType = builder.classType; + this.superType = builder.superType; + } + + private static int methodCompare(Method method1, Method method2) { + if (method1.accessModifier() == method2.accessModifier()) { + return 0; + } else { + return method1.accessModifier().compareTo(method2.accessModifier()); + } + } + + private static int fieldComparator(Field field1, Field field2) { + //This is here for ordering purposes. + if (field1.accessModifier() == field2.accessModifier()) { + if (field1.isFinal() == field2.isFinal()) { + if (field1.type().simpleTypeName().equals(field2.type().simpleTypeName())) { + if (field1.type().resolvedTypeName().equals(field2.type().resolvedTypeName())) { + return field1.name().compareTo(field2.name()); + } + return field1.type().resolvedTypeName().compareTo(field2.type().resolvedTypeName()); + } else if (field1.type().simpleTypeName().equalsIgnoreCase(field2.type().simpleTypeName())) { + //To ensure that types with the types with the same name, + //but with the different capital letters, will not be mixed + return field1.type().simpleTypeName().compareTo(field2.type().simpleTypeName()); + } + //ignoring case sensitivity to ensure primitive types are properly sorted + return field1.type().simpleTypeName().compareToIgnoreCase(field2.type().simpleTypeName()); + } + //final fields should be before non-final + return Boolean.compare(field2.isFinal(), field1.isFinal()); + } else { + return field1.accessModifier().compareTo(field2.accessModifier()); + } + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws + IOException { + Set combinedTokens = Stream.concat(declaredTokens.stream(), this.tokenNames.stream()).collect(Collectors.toSet()); + if (javadoc().generate()) { + javadoc().writeComponent(writer, combinedTokens, imports, this.classType); + writer.write("\n"); + } + if (!annotations().isEmpty()) { + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, combinedTokens, imports, this.classType); + writer.write("\n"); + } + } + if (AccessModifier.PACKAGE_PRIVATE != accessModifier()) { + writer.write(accessModifier().modifierName() + " "); + } + if (isStatic) { + writer.write("static "); + } + if (isFinal) { + writer.write("final "); + } + if (isAbstract) { + if (isFinal) { + throw new IllegalStateException("Class cannot be abstract and final"); + } + writer.write("abstract "); + } + writer.write(this.classType.typeName() + " " + name()); + if (!genericParameters.isEmpty()) { + writeGenericParameters(writer, combinedTokens, imports); + } + writer.write(" "); + if (superType != null) { + writer.write("extends "); + superType.writeComponent(writer, combinedTokens, imports, this.classType); + writer.write(" "); + } + if (!interfaces.isEmpty()) { + writeClassInterfaces(writer, combinedTokens, imports); + } + writer.write("{"); + writer.writeSeparatorLine(); + if (!staticFields.isEmpty()) { + writeClassFields(staticFields, writer, combinedTokens, imports); + } + if (!fields.isEmpty()) { + writeClassFields(fields, writer, combinedTokens, imports); + } + if (!constructors.isEmpty()) { + writerClassConstructors(writer, combinedTokens, imports); + } + if (!staticMethods.isEmpty()) { + writerClassMethods(staticMethods, writer, combinedTokens, imports); + } + if (!methods.isEmpty()) { + writerClassMethods(methods, writer, combinedTokens, imports); + } + if (!innerClasses.isEmpty()) { + writeInnerClasses(writer, combinedTokens, imports); + } + writer.write("\n"); + writer.write("}"); + } + + private void writeGenericParameters(ModelWriter writer, Set declaredTokens, ImportOrganizer imports) + throws IOException { + writer.write("<"); + boolean first = true; + for (Type parameter : genericParameters) { + if (first) { + first = false; + } else { + writer.write(", "); + } + parameter.writeComponent(writer, declaredTokens, imports, this.classType); + } + writer.write(">"); + } + + private void writeClassInterfaces(ModelWriter writer, Set declaredTokens, ImportOrganizer imports) + throws IOException { + if (classType == ClassType.INTERFACE) { + writer.write("extends "); + } else { + writer.write("implements "); + } + boolean first = true; + for (Type interfaceName : interfaces) { + if (first) { + first = false; + } else { + writer.write(", "); + } + interfaceName.writeComponent(writer, declaredTokens, imports, this.classType); + } + writer.write(" "); + } + + private void writeClassFields(Collection fields, + ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports) throws IOException { + writer.increasePaddingLevel(); + for (Field field : fields) { + writer.write("\n"); + field.writeComponent(writer, declaredTokens, imports, this.classType); + } + writer.decreasePaddingLevel(); + writer.writeSeparatorLine(); + } + + private void writerClassConstructors(ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports) throws IOException { + writer.increasePaddingLevel(); + for (Constructor constructor : constructors) { + writer.write("\n"); + constructor.writeComponent(writer, declaredTokens, imports, this.classType); + writer.writeSeparatorLine(); + } + writer.decreasePaddingLevel(); + } + + private void writerClassMethods(List methods, + ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports) throws IOException { + writer.increasePaddingLevel(); + for (Method method : methods) { + writer.write("\n"); + method.writeComponent(writer, declaredTokens, imports, this.classType); + writer.writeSeparatorLine(); + } + writer.decreasePaddingLevel(); + } + + private void writeInnerClasses(ModelWriter writer, Set declaredTokens, ImportOrganizer imports) throws IOException { + writer.increasePaddingLevel(); + for (InnerClass innerClass : innerClasses) { + writer.write("\n"); + innerClass.writeComponent(writer, declaredTokens, imports, this.classType); + writer.writeSeparatorLine(); + } + writer.decreasePaddingLevel(); + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + fields.forEach(field -> field.addImports(imports)); + staticFields.forEach(field -> field.addImports(imports)); + methods.forEach(method -> method.addImports(imports)); + staticMethods.forEach(method -> method.addImports(imports)); + interfaces.forEach(imp -> imp.addImports(imports)); + constructors.forEach(constructor -> constructor.addImports(imports)); + genericParameters.forEach(param -> param.addImports(imports)); + innerClasses.forEach(innerClass -> { + imports.from(innerClass.imports()); + innerClass.addImports(imports); + }); + if (superType != null) { + superType.addImports(imports); + } + } + + ClassType classType() { + return classType; + } + + /** + * Fluent API builder for {@link ClassBase}. + * + * @param builder type + * @param built object type + */ + public abstract static class Builder, T extends ClassBase> + extends AnnotatedComponent.Builder { + + private final Set methods = new LinkedHashSet<>(); + private final Set staticMethods = new LinkedHashSet<>(); + private final Set interfaces = new LinkedHashSet<>(); + private final Map fields = new LinkedHashMap<>(); + private final Map staticFields = new LinkedHashMap<>(); + private final Map innerClasses = new LinkedHashMap<>(); + private final List constructors = new ArrayList<>(); + private final List genericParameters = new ArrayList<>(); + private final ImportOrganizer.Builder importOrganizer = ImportOrganizer.builder(); + private ClassType classType = ClassType.CLASS; + private Type superType; + private boolean isFinal; + private boolean isAbstract; + private boolean isStatic; + private boolean sortFields = true; + private boolean sortStaticFields = true; + + Builder() { + } + + @Override + public B javadoc(Javadoc javadoc) { + return super.javadoc(javadoc); + } + + @Override + public B addJavadocTag(String tag, String description) { + return super.addJavadocTag(tag, description); + } + + @Override + public B accessModifier(AccessModifier accessModifier) { + return super.accessModifier(accessModifier); + } + + /** + * Whether this type is final. + * + * @param isFinal type is abstract + * @return updated builder instance + */ + public B isFinal(boolean isFinal) { + this.isFinal = isFinal; + return identity(); + } + + /** + * Whether this type is abstract. + * + * @param isAbstract type is abstract + * @return updated builder instance + */ + public B isAbstract(boolean isAbstract) { + this.isAbstract = isAbstract; + return identity(); + } + + /** + * Set new super type of this type. + * + * @param superType super type of this type + * @return updated builder instance + */ + public B superType(Class superType) { + return superType(TypeName.create(superType)); + } + + /** + * Set new fully qualified super type name of this type. + * + * @param superType super type of this type + * @return updated builder instance + */ + public B superType(String superType) { + return superType(TypeName.create(superType)); + } + + /** + * Set new super type of this type. + * + * @param superType super type of this type + * @return updated builder instance + */ + public B superType(TypeName superType) { + this.superType = Type.fromTypeName(superType); + return identity(); + } + + /** + * Add new field to the type. + * + * @param consumer field builder consumer + * @return updated builder instance + */ + public B addField(Consumer consumer) { + Field.Builder builder = Field.builder(); + consumer.accept(builder); + return addField(builder.build()); + } + + /** + * Add new field to the type. + * + * @param builder field builder + * @return updated builder instance + */ + public B addField(Field.Builder builder) { + return addField(builder.build()); + } + + /** + * Add new field to the type. + * + * @param field new field + * @return updated builder instance + */ + public B addField(Field field) { + String fieldName = field.name(); + if (field.isStatic()) { + fields.remove(fieldName); + staticFields.put(fieldName, field); + } else { + staticFields.remove(fieldName); + fields.put(fieldName, field); + } + return identity(); + } + + /** + * Add new method to the type. + * + * @param consumer method builder consumer + * @return updated builder instance + */ + public B addMethod(Consumer consumer) { + Method.Builder methodBuilder = Method.builder(); + consumer.accept(methodBuilder); + return addMethod(methodBuilder); + } + + /** + * Add new method to the type. + * + * @param builder method builder + * @return updated builder instance + */ + public B addMethod(Method.Builder builder) { + return addMethod(builder.build()); + } + + /** + * Add new method to the type. + * + * @param method new method + * @return updated builder instance + */ + public B addMethod(Method method) { + methods.remove(method); + staticMethods.remove(method); + if (method.isStatic()) { + staticMethods.add(method); + } else { + methods.add(method); + } + return identity(); + } + + /** + * Add interface this type should implement. + * + * @param interfaceType interface type to implement + * @return updated builder instance + */ + public B addInterface(Class interfaceType) { + if (interfaceType.isInterface()) { + return addInterface(TypeName.create(interfaceType)); + } else { + throw new IllegalArgumentException("Provided value needs to be interface, but it was not: " + + interfaceType.getName()); + } + } + + /** + * Add interface this type should implement. + * + * @param interfaceName fully qualified interface name to implement + * @return updated builder instance + */ + public B addInterface(String interfaceName) { + return addInterface(TypeName.create(interfaceName)); + } + + /** + * Add interface this type should implement. + * + * @param interfaceType interface to implement + * @return updated builder instance + */ + public B addInterface(TypeName interfaceType) { + interfaces.add(Type.fromTypeName(interfaceType)); + return identity(); + } + + /** + * Add new inner type to this type. + * + * @param consumer inner class builder consumer + * @return updated builder instance + */ + public B addInnerClass(Consumer consumer) { + InnerClass.Builder innerClassBuilder = InnerClass.builder(); + consumer.accept(innerClassBuilder); + return addInnerClass(innerClassBuilder); + } + + /** + * Add new inner type to this type. + * + * @param supplier inner class builder supplier + * @return updated builder instance + */ + public B addInnerClass(Supplier supplier) { + return addInnerClass(supplier.get()); + } + + /** + * Add new inner type to this type. + * + * @param innerClass inner class instance + * @return updated builder instance + */ + public B addInnerClass(InnerClass innerClass) { + this.innerClasses.put(innerClass.name(), innerClass); + return identity(); + } + + /** + * Add new constructor to this class. + * + * @param constructor constructor builder + * @return updated builder instance + */ + public B addConstructor(Constructor.Builder constructor) { + constructors.add(constructor.type(name()).build()); + return identity(); + } + + /** + * Add new constructor to this class. + * + * @param consumer constructor builder consumer + * @return updated builder instance + */ + public B addConstructor(Consumer consumer) { + Constructor.Builder constructorBuilder = Constructor.builder() + .type(name()); + consumer.accept(constructorBuilder); + constructors.add(constructorBuilder.build()); + return identity(); + } + + /** + * Add generic argument type. + * + * @param typeArgument generic argument type + * @return updated builder instance + */ + public B addGenericArgument(TypeArgument typeArgument) { + this.genericParameters.add(typeArgument); + return addGenericToken(typeArgument.token(), typeArgument.description()); + } + + /** + * Add generic argument type. + * + * @param consumer generic argument type builder consumer + * @return updated builder instance + */ + public B addGenericArgument(Consumer consumer) { + TypeArgument.Builder tokenBuilder = TypeArgument.builder(); + consumer.accept(tokenBuilder); + return addGenericArgument(tokenBuilder.build()); + } + + /** + * Add specific class to be imported. + * + * @param typeImport type to be included among imports + * @return updated builder instance + */ + public B addImport(Class typeImport) { + importOrganizer.addImport(typeImport); + return identity(); + } + + /** + * Add specific fully qualified type name to be imported. + * + * @param importName type to be included among imports + * @return updated builder instance + */ + public B addImport(String importName) { + importOrganizer.addImport(importName); + return identity(); + } + + /** + * Add specific fully qualified type name to be imported. + * + * @param typeName type to be included among imports + * @return updated builder instance + */ + public B addImport(TypeName typeName) { + importOrganizer.addImport(typeName); + return identity(); + } + + /** + * Add specific static import. + * + * @param staticImport fully qualified static import name + * @return updated builder instance + */ + public B addStaticImport(String staticImport) { + importOrganizer.addStaticImport(staticImport); + return identity(); + } + + /** + * Type of the Java type we are creating. + * For example: class, interface etc. + * + * @param classType Java type + * @return updated builder instance + */ + public B classType(ClassType classType) { + this.classType = classType; + return identity(); + } + + /** + * Type of the Java type we are creating. + * For example: class, interface etc. + * + * @param kind the element kind, must be a supported top level type + * @return updated builder instance + * @throws java.lang.IllegalArgumentException in case the kind is not supported + */ + public B classType(ElementKind kind) { + return switch (kind) { + case CLASS -> classType(ClassType.CLASS); + case INTERFACE -> classType(ClassType.INTERFACE); + default -> throw new IllegalArgumentException("Top level class is not supported for kind: " + kind); + }; + } + + /** + * Whether to sort non-static fields by type and name (defaults to {@code true}). + * If set to {@code false}, fields are ordered by insertion sequence. + * + * @param sort whether to sort fields + * @return updated builder instance + */ + public B sortFields(boolean sort) { + this.sortFields = sort; + return identity(); + } + + /** + * Whether to sort static fields by type and name (defaults to {@code true}). + * If set to {@code false}, fields are ordered by insertion sequence. + * + * @param sort whether to sort fields + * @return updated builder instance + */ + public B sortStaticFields(boolean sort) { + this.sortStaticFields = sort; + return identity(); + } + + /** + * Whether this type is static. + * + * @param isStatic whether type is static + * @return updated builder instance + */ + B isStatic(boolean isStatic) { + this.isStatic = isStatic; + return identity(); + } + + ImportOrganizer.Builder importOrganizer() { + return importOrganizer; + } + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java new file mode 100644 index 00000000000..4b935235ab7 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModel.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.Writer; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; + +/** + * Entry point to create class model. + * This model contain all needed information for each generated type and handles resulting generation. + */ +public final class ClassModel extends ClassBase { + + /** + * Padding token used for identifying extra padding requirement for content formatting. + */ + public static final String PADDING_TOKEN = "<>"; + /** + * Type token is used to prepend and append to the fully qualified type names to support import handling. + */ + public static final String TYPE_TOKEN = "@"; + /** + * Pattern in which are type names saved in the content templates. + */ + public static final String TYPE_TOKEN_PATTERN = TYPE_TOKEN + "name" + TYPE_TOKEN; + /** + * Default padding used in the generated type. + */ + public static final String DEFAULT_PADDING = " "; + private final String packageName; + private final String copyright; + private ImportOrganizer imports; + + private ClassModel(Builder builder) { + super(builder); + this.copyright = builder.copyright; + this.packageName = builder.packageName; + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static ClassModel.Builder builder() { + return new Builder(); + } + + /** + * Write created type model. + * Default padding {@link #DEFAULT_PADDING} is used. + * + * @param writer writer to be used + * @throws IOException write operation failure + */ + public void write(Writer writer) throws IOException { + write(writer, DEFAULT_PADDING); + } + + /** + * Write created type model. + * + * @param writer writer to be used + * @param padding padding to be used + * @throws IOException write operation failure + */ + public void write(Writer writer, String padding) throws IOException { + ModelWriter innerWriter = new ModelWriter(writer, padding); + writeComponent(innerWriter, Set.of(), imports, classType()); + } + + @Override + void writeComponent(ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports, + ClassType classType) throws IOException { + if (copyright != null) { + String[] lines = copyright.split("\n"); + if (lines.length > 1) { + boolean applyFormatting = !lines[0].startsWith("/*"); + if (applyFormatting) { + writer.write("/*\n"); + } + for (String line : lines) { + if (applyFormatting) { + writer.write(" * " + line + "\n"); + } else { + writer.write(line + "\n"); + } + } + if (applyFormatting) { + writer.write(" */\n\n"); + } + } else { + if (!lines[0].startsWith("//")) { + writer.write("// "); + } + writer.write(lines[0] + "\n"); + } + writer.writeSeparatorLine(); + } + if (packageName != null && !packageName.isEmpty()) { + writer.write("package " + packageName + ";\n\n"); + } + imports.writeImports(writer); + imports.writeStaticImports(writer); + super.writeComponent(writer, declaredTokens, imports, classType); + writer.writeSeparatorLine(); + } + + /** + * Type name of this class. + * + * @return type name + */ + public TypeName typeName() { + return TypeName.create(packageName + "." + name()); + } + + @Override + public String toString() { + return "ClassModel{" + + "packageName='" + packageName + '\'' + + "name='" + name() + '\'' + + '}'; + } + + /** + * Fluent API builder for {@link ClassModel}. + */ + public static final class Builder extends ClassBase.Builder { + + private String packageName = ""; + private String copyright; + + private Builder() { + } + + @Override + public ClassModel build() { + if (name() == null) { + throw new ClassModelException("Class need to have name specified"); + } + ClassModel classModel = new ClassModel(this); + ImportOrganizer.Builder importOrganizer = importOrganizer(); + classModel.addImports(importOrganizer); + classModel.imports = importOrganizer.build(); + return classModel; + } + + @Override + public Builder accessModifier(AccessModifier accessModifier) { + if (accessModifier == AccessModifier.PRIVATE) { + throw new IllegalArgumentException("Outer class cannot be private!"); + } + return super.accessModifier(accessModifier); + } + + /** + * Package name of this type. + * + * @param packageName type package name + * @return updated builder instance + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + importOrganizer().packageName(packageName); + return this; + } + + /** + * Copyright header to be used. + * + * @param copyright copyright header + * @return updated builder instance + */ + public Builder copyright(String copyright) { + this.copyright = copyright; + return this; + } + + @Override + public Builder name(String name) { + importOrganizer().typeName(name); + return super.name(name); + } + + @Override + public Builder type(TypeName type) { + packageName(type.packageName()); + name(type.className()); + return this; + } + + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModelException.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModelException.java new file mode 100644 index 00000000000..424a7b5711c --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassModelException.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +/** + * Exception message which corresponds to the error in class model creation. + */ +public class ClassModelException extends RuntimeException { + + ClassModelException(String message) { + super(message); + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassType.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassType.java new file mode 100644 index 00000000000..6c14eb6b169 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ClassType.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +/** + * Class type. + */ +public enum ClassType { + + /** + * Class type is an interface. + */ + INTERFACE("interface"), + /** + * Class type is a class. + */ + CLASS("class"); + + private final String typeName; + + ClassType(String typeName) { + this.typeName = typeName; + } + + String typeName() { + return typeName; + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java new file mode 100644 index 00000000000..1dc0c9f07c0 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/CommonComponent.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.List; +import java.util.Objects; + +import io.helidon.common.types.AccessModifier; + +abstract class CommonComponent extends DescribableComponent { + + private final String name; + private final Javadoc javadoc; + private final AccessModifier accessModifier; + + CommonComponent(Builder builder) { + super(builder); + this.name = builder.name; + this.accessModifier = builder.accessModifier; + this.javadoc = builder.javadocBuilder.build(builder); + } + + String name() { + return name; + } + + Javadoc javadoc() { + return javadoc; + } + + AccessModifier accessModifier() { + return accessModifier; + } + + abstract static class Builder, T extends CommonComponent> extends DescribableComponent.Builder { + private final Javadoc.Builder javadocBuilder = Javadoc.builder(); + private AccessModifier accessModifier = AccessModifier.PUBLIC; + private String name; + + Builder() { + } + + @Override + B description(String description) { + return description(List.of(description)); + } + + @Override + B description(List description) { + this.javadocBuilder.content(description); + this.javadocBuilder.generate(true); + return identity(); + } + + /** + * Add another line to already existing javadoc. + * + * @param line line to add + * @return updated builder instance + */ + B addDescriptionLine(String line) { + this.javadocBuilder.addLine(line); + return identity(); + } + + /** + * Javadoc of the component. + * Overwrites all previously created content. + * + * @param javadoc component javadoc + * @return updated builder instance + */ + B javadoc(Javadoc javadoc) { + this.javadocBuilder.clear(); + this.javadocBuilder.from(javadoc); + return identity(); + } + + /** + * Set whether to generate javadoc or not. + * Javadoc is automatically generated when description is set. + * + * @param generateJavadoc true if javadoc should be generated + * @return updated builder instance + */ + B generateJavadoc(boolean generateJavadoc) { + this.javadocBuilder.generate(generateJavadoc); + return identity(); + } + + /** + * Add method parameter javadoc. + * + * @param parameter parameter name + * @param description param description + * @return updated builder instance + */ + B addJavadocParameter(String parameter, List description) { + this.javadocBuilder.addParameter(parameter, description); + return identity(); + } + + /** + * Add generic parameter javadoc. + * + * @param parameter parameter name + * @param description param description + * @return updated builder instance + */ + B addGenericToken(String parameter, String description) { + this.javadocBuilder.addGenericArgument(parameter, description); + return identity(); + } + + /** + * Add generic parameter javadoc. + * + * @param parameter parameter name + * @param description param description + * @return updated builder instance + */ + B addGenericToken(String parameter, List description) { + this.javadocBuilder.addGenericArgument(parameter, description); + return identity(); + } + + /** + * Add throws javadoc description. + * + * @param exception exception name + * @param description exception description + * @return updated builder instance + */ + B addJavadocThrows(String exception, List description) { + this.javadocBuilder.addThrows(exception, description); + return identity(); + } + + /** + * Add any javadoc tag. + * + * @param tag tag name + * @param description tag description + * @return updated builder instance + */ + B addJavadocTag(String tag, String description) { + this.javadocBuilder.addTag(tag, description); + return identity(); + } + + /** + * Set javadoc deprecation description. + * + * @param description deprecation description + * @return updated builder instance + */ + B deprecationJavadoc(String description) { + this.javadocBuilder.deprecation(description); + return identity(); + } + + /** + * Method return type javadoc. + * + * @param description return type description + * @return updated builder instance + */ + B returnJavadoc(String description) { + this.javadocBuilder.returnDescription(description); + return identity(); + } + + /** + * Method return type javadoc. + * + * @param description return type description + * @return updated builder instance + */ + B returnJavadoc(List description) { + this.javadocBuilder.returnDescription(description); + return identity(); + } + + /** + * Set new name of this component. + * + * @param name name of this component + * @return updated builder instance + */ + B name(String name) { + this.name = Objects.requireNonNull(name); + return identity(); + } + + /** + * Set new access modifier of this component. + * + * @param accessModifier access modifier + * @return updated builder instance + */ + B accessModifier(AccessModifier accessModifier) { + this.accessModifier = Objects.requireNonNull(accessModifier); + return identity(); + } + + String name() { + return name; + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java new file mode 100644 index 00000000000..bded3b90bc0 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ConcreteType.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Concrete type representation. + */ +class ConcreteType extends Type { + + private final TypeName typeName; + private final Type declaringType; + private final List typeParams; + + ConcreteType(Builder builder) { + super(builder); + this.typeName = builder.typeName; + if (typeName.enclosingNames().isEmpty()) { + this.declaringType = null; + } else { + String parents = String.join(".", typeName.enclosingNames()); + TypeName parent; + if (typeName.packageName().isEmpty()) { + parent = TypeName.create(parents); + } else { + parent = TypeName.create(typeName.packageName() + "." + parents); + } + + this.declaringType = Type.fromTypeName(parent); + } + this.typeParams = List.copyOf(builder.typeParams); + } + + static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws + IOException { + String typeName = imports.typeName(this, includeImport()); + writer.write(typeName); + if (!typeParams.isEmpty()) { + writer.write("<"); + boolean first = true; + for (Type parameter : typeParams) { + if (first) { + first = false; + } else { + writer.write(", "); + } + parameter.writeComponent(writer, declaredTokens, imports, classType); + } + writer.write(">"); + } + if (isArray()) { + writer.write("[]"); + } + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + if (includeImport()) { + imports.addImport(this); + } + typeParams.forEach(type -> type.addImports(imports)); + } + + @Override + String fqTypeName() { + if (innerClass()) { + return typeName.classNameWithEnclosingNames(); + } else { + return typeName.name(); + } + } + + @Override + String resolvedTypeName() { + return typeName.resolvedName(); + } + + @Override + String simpleTypeName() { + return typeName.className(); + } + + @Override + boolean isArray() { + return typeName.array(); + } + + @Override + boolean innerClass() { + return !typeName.enclosingNames().isEmpty(); + } + + @Override + Optional declaringClass() { + return Optional.ofNullable(declaringType); + } + + @Override + TypeName genericTypeName() { + return typeName; + } + + String packageName() { + return typeName.packageName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConcreteType that = (ConcreteType) o; + return isArray() == that.isArray() + && Objects.equals(typeName.resolvedName(), that.typeName.resolvedName()); + } + + @Override + public String toString() { + return typeName.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(isArray(), typeName.resolvedName()); + } + + static final class Builder extends ModelComponent.Builder { + private final List typeParams = new ArrayList<>(); + private TypeName typeName; + + private Builder() { + } + + @Override + public ConcreteType build() { + if (typeName == null) { + throw new ClassModelException("Type value needs to be set"); + } + return new ConcreteType(this); + } + + Builder type(String typeName) { + return type(TypeName.create(typeName)); + } + + Builder type(Class typeName) { + return type(TypeName.create(typeName)); + } + + Builder type(TypeName typeName) { + this.typeName = typeName; + return this; + } + + Builder addParam(TypeName typeName) { + this.typeParams.add(Type.fromTypeName(typeName)); + return this; + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Constructor.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Constructor.java new file mode 100644 index 00000000000..77614354640 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Constructor.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; + +/** + * Constructor model. + */ +public final class Constructor extends Executable { + + private Constructor(Builder builder) { + super(builder); + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + if (javadoc().generate()) { + javadoc().writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + if (AccessModifier.PACKAGE_PRIVATE != accessModifier()) { + writer.write(accessModifier().modifierName() + " "); + } + String typeName = type().simpleTypeName(); + writer.write(typeName + "("); + boolean first = true; + for (Parameter parameter : parameters()) { + if (first) { + first = false; + } else { + writer.write(", "); + } + parameter.writeComponent(writer, declaredTokens, imports, classType); + } + writer.write(")"); + writeThrows(writer, declaredTokens, imports, classType); + writer.write(" {"); + if (hasBody()) { + writeBody(writer, imports); + } else { + writer.write("\n"); + } + writer.write("}"); + } + + /** + * Fluent API builder for {@link Constructor}. + */ + public static final class Builder extends Executable.Builder { + + private Builder() { + } + + @Override + public Constructor build() { + return new Constructor(this); + } + + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Content.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Content.java new file mode 100644 index 00000000000..eddc73d47e7 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Content.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.common.types.TypeName; + +import static io.helidon.codegen.classmodel.ClassModel.PADDING_TOKEN; +import static io.helidon.codegen.classmodel.ClassModel.TYPE_TOKEN; + +class Content { + + private final StringBuilder content; + private final Set toImport; + private final List tokenPositions; + + private Content(Builder builder) { + this.content = new StringBuilder(builder.content); + this.toImport = Set.copyOf(builder.toImport); + this.tokenPositions = List.copyOf(builder.tokenPositions); + } + + static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return content.toString(); + } + + boolean hasBody() { + return !content.isEmpty(); + } + + void writeBody(ModelWriter writer, ImportOrganizer imports) throws IOException { + int offset = 0; + Map replacements = new HashMap<>(); + for (Position position : tokenPositions) { + String replacement = replacements.computeIfAbsent(position.type, key -> { + TypeName typeName = TypeName.create(key); + return imports.typeName(Type.fromTypeName(typeName), true); + }); + content.replace(position.start - offset, position.end - offset, replacement); + //Since we are replacing values in the StringBuilder, previously obtained position indexes for class name tokens + //will differ and because fo that, these changes need to be reflected via calculating overall offset + offset += (position.end - position.start) - replacement.length(); + } + String[] lines = content.toString().split("\n"); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + writer.write(line); + if (i + 1 != lines.length) { + writer.write("\n"); + } + } + } + + void addImports(ImportOrganizer.Builder builder) { + toImport.forEach(builder::addImport); + } + + /** + * Fluent API builder for {@link Content}. + */ + static final class Builder implements ContentBuilder, io.helidon.common.Builder { + + private static final Pattern TYPE_NAME_PATTERN = Pattern.compile(TYPE_TOKEN + "(.*?)" + TYPE_TOKEN); + private static final Pattern TYPE_IDENTIFICATION_PATTERN = Pattern.compile("[.a-zA-Z0-9_]+"); + + private final StringBuilder content = new StringBuilder(); + private final Set toImport = new HashSet<>(); + private final List tokenPositions = new ArrayList<>(); + private String extraPadding = ""; + private int extraPaddingLevel = 0; + private boolean newLine = false; + + private Builder() { + } + + @Override + public Content build() { + toImport.clear(); + tokenPositions.clear(); + identifyClassTokens(); + return new Content(this); + } + + @Override + public Builder content(List content) { + this.content.setLength(0); + content.forEach(this::addContentLine); + return this; + } + + @Override + public Builder addContent(String line) { + String trimmed = line.trim(); + if (trimmed.equals("}")) { + decreaseContentPadding(); + } + if (newLine) { + this.content.append(extraPadding); + } + this.newLine = line.endsWith("\n"); + String replacedLine; + //we need to ensure proper extra padding if multiline String is received + if (newLine) { + //newly added line ends with \n. This \n must not be replaced + replacedLine = line.substring(0, line.lastIndexOf("\n")) + .replaceAll("\n", "\n" + extraPadding) + "\n"; + } else { + replacedLine = line.replaceAll("\n", "\n" + extraPadding); + } + this.content.append(replacedLine); + if (trimmed.endsWith("{")) { + increaseContentPadding(); + } + return this; + } + + @Override + public Builder padContent() { + this.content.append(PADDING_TOKEN); + return identity(); + } + + @Override + public Builder padContent(int repetition) { + this.content.append(PADDING_TOKEN.repeat(repetition)); + return identity(); + } + + @Override + public Builder addTypeToContent(String typeName) { + String processedFqName = TYPE_IDENTIFICATION_PATTERN.matcher(typeName) + .replaceAll(className -> ClassModel.TYPE_TOKEN_PATTERN.replace("name", className.group())); + return addContent(processedFqName); + } + + @Override + public Builder increaseContentPadding() { + this.extraPaddingLevel++; + this.extraPadding = PADDING_TOKEN.repeat(this.extraPaddingLevel); + return identity(); + } + + @Override + public Builder decreaseContentPadding() { + this.extraPaddingLevel--; + if (this.extraPaddingLevel < 0) { + throw new ClassModelException("Content padding cannot be negative"); + } + this.extraPadding = PADDING_TOKEN.repeat(this.extraPaddingLevel); + return identity(); + } + + @Override + public Builder clearContent() { + this.content.setLength(0); + return identity(); + } + + /** + * Method which identifies all the type names in the generated content. + * These names are later replaced with fully qualified names or just simple class names. + */ + private void identifyClassTokens() { + Matcher matcher = TYPE_NAME_PATTERN.matcher(content); + while (matcher.find()) { + String className = matcher.group(1); + toImport.add(className); + tokenPositions.add(new Position(matcher.start(), matcher.end(), className)); + } + } + } + + /** + * Position of the type name token, which should later be replaced. + * + * @param start starting index of the token + * @param end end index of the token + * @param type name of the type + */ + private record Position(int start, int end, String type) { + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java new file mode 100644 index 00000000000..4237f75f8ce --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentBuilder.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.List; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * A component capable of holding content. + * + * @param type of the component, to support fluent API + * @see io.helidon.codegen.classmodel.Method + * @see io.helidon.codegen.classmodel.Constructor + * @see io.helidon.codegen.classmodel.Field + */ +public interface ContentBuilder> { + /** + * Set new content. + * This method replaces previously created content in this builder. + * + * @param content content to be set + * @return updated builder instance + */ + default T content(String content) { + return content(List.of(content)); + } + + /** + * Set new content. + * This method replaces previously created content in this builder. + * + * @param content content to be set + * @return updated builder instance + */ + T content(List content); + + /** + * Add text line to the content. + * New line character is added after this line. + * + * @param line line to add + * @return updated builder instance + */ + default T addContentLine(String line) { + return addContent(line).addContent("\n"); + } + + /** + * Add text line to the content. + * New line character is not added after this line, so all newly added text will be appended to the same line. + * + * @param line line to add + * @return updated builder instance + */ + T addContent(String line); + + /** + * Add type name to content, correctly handling imports. + * In case the type should not contain any type parameters, use {@link io.helidon.common.types.TypeName#genericTypeName()}. + * + * @param typeName type name to add + * @return updated component builder + */ + default T addContent(TypeName typeName) { + return addTypeToContent(typeName.resolvedName()); + } + + /** + * Obtained type is enclosed between {@link ClassModel#TYPE_TOKEN} tokens. + * Class names in such a format are later recognized as class names for import handling. + * + * @param type type to import + * @return updated builder instance + */ + default T addContent(Class type) { + return addTypeToContent(type.getCanonicalName()); + } + + /** + * Add content that creates a new {@link io.helidon.common.types.TypeName} in the generated code that is the same as the + * type name provided. + *

+ * To create a type name without type arguments (such as when used with {@code .class}), use + * {@link io.helidon.common.types.TypeName#genericTypeName()}. + *

+ * The generated content will be similar to: {@code TypeName.create("some.type.Name")} + * + * @param typeName type name to code generate + * @return updated builder instance + */ + default T addContentCreate(TypeName typeName) { + ContentSupport.addCreateTypeName(this, typeName); + return addContent(""); + } + + /** + * Add content that creates a new {@link io.helidon.common.types.Annotation} in the generated code that is the same as the + * annotation provided. + * + * @param annotation annotation to code generate + * @return updated builder instance + */ + default T addContentCreate(Annotation annotation) { + ContentSupport.addCreateAnnotation(this, annotation); + return addContent(""); + } + + /** + * Add content that creates a new {@link io.helidon.common.types.TypedElementInfo} in the generated code that is + * the same as the element provided. + * + * @param element element to code generate + * @return updated builder instance + */ + default T addContentCreate(TypedElementInfo element) { + ContentSupport.addCreateElement(this, element); + return addContent(""); + } + + /** + * Obtained fully qualified type name is enclosed between {@link ClassModel#TYPE_TOKEN} tokens. + * Class names in such a format are later recognized as class names for import handling. + * + * @param typeName fully qualified class name to import + * @return updated builder instance + */ + T addTypeToContent(String typeName); + + /** + * Adds single padding. + * This extra padding is added only once. If more permanent padding increment is needed use + * {{@link #increaseContentPadding()}}. + * + * @return updated builder instance + */ + T padContent(); + + /** + * Adds padding with number of repetitions. + * This extra padding is added only once. If more permanent padding increment is needed use + * {{@link #increaseContentPadding()}}. + * + * @param repetition number of padding repetitions + * @return updated builder instance + */ + T padContent(int repetition); + + /** + * Method for manual padding increment. + * This method will affect padding of the later added content. + * + * @return updated builder instance + */ + T increaseContentPadding(); + + /** + * Method for manual padding decrement. + * This method will affect padding of the later added content. + * + * @return updated builder instance + */ + T decreaseContentPadding(); + + /** + * Clears created content. + * + * @return updated builder instance + */ + T clearContent(); +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java new file mode 100644 index 00000000000..d8725fab180 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +final class ContentSupport { + private static final TypeName ANNOTATION = TypeName.create(Annotation.class); + private static final TypeName ELEMENT = TypeName.create(TypedElementInfo.class); + private static final TypeName ELEMENT_KIND = TypeName.create(ElementKind.class); + private static final TypeName MODIFIER = TypeName.create(Modifier.class); + private static final TypeName ACCESS_MODIFIER = TypeName.create(AccessModifier.class); + + private ContentSupport() { + } + + static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo element) { + contentBuilder.addContent(ELEMENT) + .addContentLine(".builder()") + .increaseContentPadding() + .increaseContentPadding(); + + contentBuilder.addContent(".kind(") + .addContent(ELEMENT_KIND) + .addContent(".") + .addContent(element.kind().name()) + .addContentLine(")"); + + contentBuilder.addContent(".typeName(") + .addContentCreate(element.typeName()) + .addContentLine(")"); + + if (element.kind() != ElementKind.CONSTRUCTOR) { + contentBuilder.addContent(".elementName(\"") + .addContent(element.elementName()) + .addContentLine("\")"); + } + + for (Annotation annotation : element.annotations()) { + contentBuilder.addContent(".addAnnotation(") + .addContentCreate(annotation) + .addContentLine(")"); + } + + AccessModifier accessModifier = element.accessModifier(); + if (accessModifier != AccessModifier.PACKAGE_PRIVATE) { + contentBuilder.addContent(".accessModifier(") + .addContent(ACCESS_MODIFIER) + .addContent(".") + .addContent(accessModifier.name()) + .addContentLine(")"); + + } + + Set modifiers = element.elementModifiers(); + for (Modifier modifier : modifiers) { + contentBuilder.addContent(".addModifier(") + .addContent(MODIFIER) + .addContent(".") + .addContent(modifier.name()) + .addContentLine(")"); + } + + for (TypedElementInfo parameterArgument : element.parameterArguments()) { + contentBuilder.addContent(".addParameterArgument(") + .addContentCreate(parameterArgument) + .addContentLine(")"); + } + + contentBuilder.addContentLine(".build()") + .decreaseContentPadding() + .decreaseContentPadding(); + } + + static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation annotation) { + + Map values = annotation.values(); + if (values.isEmpty()) { + // Annotation.create(TypeName.create("my.type.AnnotationType")) + contentBuilder.addContent(ANNOTATION) + .addContent(".create(") + .addContentCreate(annotation.typeName()) + .addContent(")"); + return; + } + + // Annotation.builder() + // .typeName(TypeName.create("my.type.AnnotationType")) + contentBuilder.addContent(ANNOTATION) + .addContentLine(".builder()") + .increaseContentPadding() + .increaseContentPadding() + .addContent(".typeName(") + .addContentCreate(annotation.typeName()) + .addContentLine(")"); + + // .putValue("key", 14) + annotation.values() + .keySet() + .forEach(propertyName -> { + contentBuilder.addContent(".putValue(\"") + .addContent(propertyName) + .addContent("\", "); + addAnnotationValue(contentBuilder, annotation.objectValue(propertyName).get()); + contentBuilder.addContentLine(")"); + }); + + // .build() + contentBuilder.addContentLine(".build()") + .decreaseContentPadding() + .decreaseContentPadding(); + + } + + static void addCreateTypeName(ContentBuilder builder, TypeName typeName) { + // TypeName.create("my.type.Name") + builder.addContent(TypeNames.TYPE_NAME) + .addContent(".create(\"") + .addContent(typeName.resolvedName()) + .addContent("\")"); + } + + private static void addAnnotationValue(ContentBuilder contentBuilder, Object objectValue) { + switch (objectValue) { + case String value -> contentBuilder.addContent("\"" + value + "\""); + case Boolean value -> contentBuilder.addContent(String.valueOf(value)); + case Long value -> contentBuilder.addContent(String.valueOf(value) + 'L'); + case Double value -> contentBuilder.addContent(String.valueOf(value) + 'D'); + case Integer value -> contentBuilder.addContent(String.valueOf(value)); + case Byte value -> contentBuilder.addContent("(byte)" + value); + case Character value -> contentBuilder.addContent("'" + value + "'"); + case Short value -> contentBuilder.addContent("(short)" + value); + case Float value -> contentBuilder.addContent(String.valueOf(value) + 'F'); + case Class value -> contentBuilder.addContentCreate(TypeName.create(value)); + case TypeName value -> contentBuilder.addContentCreate(value); + case Annotation value -> contentBuilder.addContentCreate(value); + case Enum value -> toEnumValue(contentBuilder, value); + case List values -> toListValues(contentBuilder, values); + default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass() + .getName() + ": " + objectValue); + } + } + + private static void toListValues(ContentBuilder contentBuilder, List values) { + contentBuilder.addContent(List.class) + .addContent(".of("); + int size = values.size(); + for (int i = 0; i < size; i++) { + Object value = values.get(i); + addAnnotationValue(contentBuilder, value); + if (i != size - 1) { + contentBuilder.addContent(","); + } + } + contentBuilder.addContent(")"); + } + + private static void toEnumValue(ContentBuilder contentBuilder, Enum enumValue) { + contentBuilder.addContent(enumValue.getDeclaringClass()) + .addContent(".") + .addContent(enumValue.name()); + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java new file mode 100644 index 00000000000..956a8f6efcb --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/DescribableComponent.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.helidon.common.types.TypeName; + +abstract class DescribableComponent extends ModelComponent { + + private final Type type; + private final List description; + + DescribableComponent(Builder builder) { + super(builder); + this.type = builder.type; + this.description = builder.description; + } + + Type type() { + return type; + } + + List description() { + return description; + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + if (includeImport() && type != null) { + type.addImports(imports); + } + } + + abstract static class Builder, T extends DescribableComponent> + extends ModelComponent.Builder { + + private final List description = new ArrayList<>(); + private Type type; + + /** + * Set type of the component. + * This should be fully qualified type name. + * + * @param type fully qualified type name + * @return updated builder instance + */ + B type(String type) { + Objects.requireNonNull(type); + return type(TypeName.create(type)); + } + + /** + * Set type of the component. + * + * @param type type of the component + * @return updated builder instance + */ + B type(Class type) { + Objects.requireNonNull(type); + return type(TypeName.create(type)); + } + + /** + * Set type of the component. + * + * @param type type of the component + * @return updated builder instance + */ + B type(TypeName type) { + Objects.requireNonNull(type); + return type(Type.fromTypeName(type)); + } + + B type(Type type) { + Objects.requireNonNull(type); + this.type = type; + return identity(); + } + + /** + * Set description of the component. + * It overwrites previously set description. + * + * @param description component description + * @return updated builder instance + */ + B description(String description) { + Objects.requireNonNull(description); + this.description.clear(); + this.description.add(description); + return identity(); + } + + /** + * Set description of the component. + * It overwrites previously set description. + * + * @param description component description + * @return updated builder instance + */ + B description(List description) { + Objects.requireNonNull(description); + this.description.clear(); + this.description.addAll(description); + return identity(); + } + + Type type() { + return type; + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java new file mode 100644 index 00000000000..3e1866d86a8 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Executable.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; + +/** + * Executable base, used by method and constructor. + */ +public abstract class Executable extends AnnotatedComponent { + + private final Content content; + private final List parameters; + private final List exceptions; + + Executable(Builder builder) { + super(builder); + this.content = builder.contentBuilder.build(); + this.parameters = List.copyOf(builder.parameters.values()); + this.exceptions = List.copyOf(builder.exceptions); + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + parameters.forEach(parameter -> parameter.addImports(imports)); + content.addImports(imports); + exceptions.forEach(exc -> exc.addImports(imports)); + } + + void writeThrows(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + if (!exceptions().isEmpty()) { + writer.write(" throws "); + boolean first = true; + for (Type exception : exceptions()) { + if (first) { + first = false; + } else { + writer.write(", "); + } + exception.writeComponent(writer, declaredTokens, imports, classType); + } + } + } + + void writeBody(ModelWriter writer, ImportOrganizer imports) throws IOException { + writer.increasePaddingLevel(); + writer.write("\n"); + content.writeBody(writer, imports); + writer.decreasePaddingLevel(); + writer.write("\n"); + } + + List parameters() { + return parameters; + } + + List exceptions() { + return exceptions; + } + + boolean hasBody() { + return content.hasBody(); + } + + /** + * Base builder from executable components (method an constructor). + * + * @param type of the builder + * @param type of the built instance + */ + public abstract static class Builder, T extends Executable> + extends AnnotatedComponent.Builder + implements ContentBuilder { + + private final Map parameters = new LinkedHashMap<>(); + private final Set exceptions = new LinkedHashSet<>(); + private final Content.Builder contentBuilder = Content.builder(); + + Builder() { + } + + @Override + public B javadoc(Javadoc javadoc) { + return super.javadoc(javadoc); + } + + @Override + public B addJavadocTag(String tag, String description) { + return super.addJavadocTag(tag, description); + } + + @Override + public B accessModifier(AccessModifier accessModifier) { + return super.accessModifier(accessModifier); + } + + @Override + public B content(List content) { + contentBuilder.content(content); + return identity(); + } + + @Override + public B addContent(String line) { + contentBuilder.addContent(line); + return identity(); + } + + @Override + public B addContent(TypeName typeName) { + contentBuilder.addContent(typeName); + return identity(); + } + + @Override + public B addTypeToContent(String typeName) { + contentBuilder.addTypeToContent(typeName); + return identity(); + } + + @Override + public B padContent() { + contentBuilder.padContent(); + return identity(); + } + + @Override + public B padContent(int repetition) { + contentBuilder.padContent(repetition); + return identity(); + } + + @Override + public B increaseContentPadding() { + contentBuilder.increaseContentPadding(); + return identity(); + } + + @Override + public B decreaseContentPadding() { + contentBuilder.decreaseContentPadding(); + return identity(); + } + + @Override + public B clearContent() { + contentBuilder.clearContent(); + return identity(); + } + + /** + * Add new method parameter. + * + * @param consumer method builder consumer + * @return updated builder instance + */ + public B addParameter(Consumer consumer) { + Parameter.Builder builder = Parameter.builder(); + consumer.accept(builder); + return addParameter(builder.build()); + } + + /** + * Add new method parameter. + * + * @param parameter method parameter + * @return updated builder instance + */ + public B addParameter(Parameter parameter) { + this.parameters.put(parameter.name(), parameter); + return this.addJavadocParameter(parameter.name(), parameter.description()); + } + + /** + * Add new method parameter. + * + * @param supplier method parameter supplier + * @return updated builder instance + */ + public B addParameter(Supplier supplier) { + Parameter parameter = supplier.get(); + this.parameters.put(parameter.name(), parameter); + return this.addJavadocParameter(parameter.name(), parameter.description()); + } + + /** + * Add a declared throws definition. + * + * @param exception exception declaration + * @param description description to add to javadoc + * @return updated builder instance + */ + public B addThrows(TypeName exception, String description) { + Objects.requireNonNull(exception); + Objects.requireNonNull(description); + return addThrows(ex -> ex.type(exception) + .description(description)); + } + + /** + * Add a declared throws definition. + * + * @param consumer exception declaration builder consumer + * @return updated builder instance + */ + public B addThrows(Consumer consumer) { + Objects.requireNonNull(consumer); + Throws.Builder builder = Throws.builder(); + consumer.accept(builder); + return addThrows(builder); + } + + /** + * Add a declared throws definition. + * + * @param supplier exception declaration supplier + * @return updated builder instance + */ + public B addThrows(Supplier supplier) { + Objects.requireNonNull(supplier); + return addThrows(supplier.get()); + } + + /** + * Add a declared throws definition. + * + * @param exception exception declaration + * @return updated builder instance + */ + public B addThrows(Throws exception) { + Objects.requireNonNull(exception); + this.exceptions.add(exception.type()); + return addJavadocThrows(exception.type().fqTypeName(), exception.description()); + } + + @Override + public B generateJavadoc(boolean generateJavadoc) { + return super.generateJavadoc(generateJavadoc); + } + + Map parameters() { + return parameters; + } + } + +} + diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java new file mode 100644 index 00000000000..611ce2b5c02 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +/** + * Field model representation. + */ +public final class Field extends AnnotatedComponent { + + private final Content defaultValue; + private final boolean isFinal; + private final boolean isStatic; + + private Field(Builder builder) { + super(builder); + this.defaultValue = builder.defaultValueBuilder.build(); + this.isFinal = builder.isFinal; + this.isStatic = builder.isStatic; + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder().accessModifier(AccessModifier.PRIVATE); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + if (javadoc().generate()) { + javadoc().writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + if (classType != ClassType.INTERFACE) { + if (AccessModifier.PACKAGE_PRIVATE != accessModifier()) { + writer.write(accessModifier().modifierName()); + writer.write(" "); + } + if (isStatic) { + writer.write("static "); + } + if (isFinal) { + writer.write("final "); + } + } + type().writeComponent(writer, declaredTokens, imports, classType); + writer.write(" "); + writer.write(name()); + if (defaultValue.hasBody()) { + writer.write(" = "); + defaultValue.writeBody(writer, imports); + writer.write(";"); + } else { + writer.write(";"); + } + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + type().addImports(imports); + defaultValue.addImports(imports); + } + + boolean isStatic() { + return isStatic; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Field field = (Field) o; + return name().equals(field.name()) + && type().equals(field.type()) + && isStatic == field.isStatic + && isFinal == field.isFinal + && accessModifier().equals(field.accessModifier()); + } + + @Override + public int hashCode() { + return Objects.hash(name(), type(), isFinal, isStatic, accessModifier()); + } + + @Override + public String toString() { + if (defaultValue.hasBody()) { + return accessModifier().modifierName() + " " + type().fqTypeName() + " " + name() + " = " + defaultValue; + } + return accessModifier().modifierName() + " " + type().fqTypeName() + " " + name(); + } + + boolean isFinal() { + return isFinal; + } + + /** + * Fluent API builder for {@link Field}. + */ + public static final class Builder extends AnnotatedComponent.Builder implements ContentBuilder { + + private final Content.Builder defaultValueBuilder = Content.builder(); + private boolean isFinal = false; + private boolean isStatic = false; + + private Builder() { + } + + @Override + public Field build() { + return new Field(this); + } + + /** + * Set default value this field should be initialized with, wrapping the value in double quotes + * if the field type is String. + * + * @param defaultValue default value + * @return updated builder instance + */ + public Builder defaultValue(String defaultValue) { + if (defaultValue != null + && type().equals(TypeNames.STRING) + && !type().isArray() + && !defaultValue.startsWith("\"") + && !defaultValue.endsWith("\"")) { + defaultValueBuilder.content("\"" + defaultValue + "\""); + } else { + defaultValueBuilder.content(defaultValue); + } + return this; + } + + /** + * Configure a default value for this field as a string that will be copied verbatim to the generated sources. + * + * @param defaultValue default value + * @return updated builder instance + */ + public Builder defaultValueContent(String defaultValue) { + defaultValueBuilder.content(defaultValue); + return this; + } + + @Override + public Builder content(List content) { + defaultValueBuilder.content(content); + return this; + } + + @Override + public Builder addContent(String line) { + defaultValueBuilder.addContent(line); + return this; + } + + @Override + public Builder addContent(TypeName typeName) { + defaultValueBuilder.addContent(typeName); + return this; + } + + @Override + public Builder padContent() { + defaultValueBuilder.padContent(); + return this; + } + + @Override + public Builder padContent(int repetition) { + defaultValueBuilder.padContent(repetition); + return this; + } + + @Override + public Builder increaseContentPadding() { + defaultValueBuilder.increaseContentPadding(); + return this; + } + + @Override + public Builder decreaseContentPadding() { + defaultValueBuilder.decreaseContentPadding(); + return this; + } + + @Override + public Builder clearContent() { + defaultValueBuilder.clearContent(); + return this; + } + + @Override + public Builder addTypeToContent(String typeName) { + defaultValueBuilder.addTypeToContent(typeName); + return this; + } + + /** + * Whether this field is final. + * + * @param isFinal final field + * @return updated builder instance + */ + public Builder isFinal(boolean isFinal) { + this.isFinal = isFinal; + return this; + } + + /** + * Whether this field is static. + * + * @param isStatic static field + * @return updated builder instance + */ + public Builder isStatic(boolean isStatic) { + this.isStatic = isStatic; + return this; + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + @Override + public Builder accessModifier(AccessModifier accessModifier) { + return super.accessModifier(accessModifier); + } + + @Override + public Builder javadoc(Javadoc javadoc) { + return super.javadoc(javadoc); + } + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java new file mode 100644 index 00000000000..812325d396f --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; + +class ImportOrganizer { + + private final List> importsToWrite; + private final List> staticImportsToWrite; + //Set of all imports to make it easier to go through when checking what import name should be used. + private final Set imports; + private final Set noImport; + private final Set forcedFullImports; + private final Map identifiedInnerClasses; + + private ImportOrganizer(Builder builder) { + this.importsToWrite = ImportSorter.sortImports(builder.finalImports.values()); + this.staticImportsToWrite = ImportSorter.sortImports(builder.staticImports.stream() + .map(Type::fqTypeName) + .toList()); + this.imports = Set.copyOf(builder.finalImports.values()); + this.noImport = builder.noImports.values() + .stream() + .map(Type::fqTypeName) + .collect(Collectors.toSet()); + this.forcedFullImports = Set.copyOf(builder.forcedFullImports); + this.identifiedInnerClasses = Map.copyOf(builder.identifiedInnerClasses); + } + + static Builder builder() { + return new Builder(); + } + + String typeName(Type type, boolean includedImport) { + if (type instanceof TypeArgument) { + return type.fqTypeName(); + } + Type checkedType = type.declaringClass().orElse(type); + String fullTypeName = checkedType.fqTypeName(); + String simpleTypeName = checkedType.simpleTypeName(); + + if (!includedImport) { + return fullTypeName; + } + if (forcedFullImports.contains(fullTypeName)) { + return type.fqTypeName(); + } else if (noImport.contains(fullTypeName) || imports.contains(fullTypeName)) { + return identifiedInnerClasses.getOrDefault(type.fqTypeName(), simpleTypeName); + } + return identifiedInnerClasses.getOrDefault(type.fqTypeName(), type.fqTypeName()); + } + + void writeImports(ModelWriter writer) throws IOException { + if (!importsToWrite.isEmpty()) { + for (List importGroup : importsToWrite) { + for (String importName : importGroup) { + writer.writeLine("import " + importName + ";"); + } + if (!importGroup.isEmpty()) { + writer.writeSeparatorLine(); + } + } + } + } + + void writeStaticImports(ModelWriter writer) throws IOException { + if (!staticImportsToWrite.isEmpty()) { + for (List importGroup : staticImportsToWrite) { + for (String importName : importGroup) { + writer.writeLine("import static " + importName + ";"); + } + if (!importGroup.isEmpty()) { + writer.writeSeparatorLine(); + } + } + } + } + + List imports() { + return importsToWrite.stream() + .flatMap(List::stream) + .toList(); + } + + static final class Builder implements io.helidon.common.Builder { + + private final Set imports = new HashSet<>(); + private final Set staticImports = new HashSet<>(); + + /** + * Class imports. + */ + private final Map finalImports = new HashMap<>(); + + /** + * Imports from "java.lang" package or classes within the same package. + * They should be monitored for name collisions, but not included in class imports. + */ + private final Map noImports = new HashMap<>(); + + /** + * Collection for class names with colliding simple names. + * The first registered will be used as import. The later ones have to be used as full names. + */ + private final Set forcedFullImports = new HashSet<>(); + + /** + * Map of known inner classes. + */ + private final Map identifiedInnerClasses = new HashMap<>(); + + private String packageName = ""; + private String typeName; + + private Builder() { + } + + Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + Builder typeName(String typeName) { + this.typeName = typeName; + return this; + } + + Builder type(TypeName type) { + this.typeName = type.className(); + this.packageName = type.packageName(); + return this; + } + + Builder addImport(String type) { + return addImport(TypeName.create(type)); + } + + Builder addImport(Class type) { + return addImport(TypeName.create(type)); + } + + Builder addImport(TypeName type) { + return addImport(Type.fromTypeName(type.genericTypeName())); + } + + Builder addImport(Type type) { + imports.add(type); + return this; + } + + Builder addStaticImport(String type) { + return addStaticImport(TypeName.create(type)); + } + + Builder addStaticImport(Class type) { + return addStaticImport(TypeName.create(type)); + } + + Builder addStaticImport(TypeName type) { + staticImports.add(Type.fromTypeName(type)); + return this; + } + + Builder from(ImportOrganizer.Builder builder) { + this.imports.addAll(builder.imports); + this.staticImports.addAll(builder.staticImports); + return this; + } + + @Override + public ImportOrganizer build() { + if (typeName == null) { + throw new ClassModelException("Import organizer requires to have built type name specified."); + } + finalImports.clear(); + forcedFullImports.clear(); + noImports.clear(); + resolveFinalImports(); + return new ImportOrganizer(this); + } + + private void resolveFinalImports() { + for (Type type : imports) { + //If processed type is inner class, we will be importing parent class + Type typeToProcess = type.declaringClass().orElse(type); + String fqTypeName = typeToProcess.fqTypeName(); + String typePackage = typeToProcess.packageName(); + String typeSimpleName = typeToProcess.simpleTypeName(); + + if (type.innerClass()) { + if (typeToProcess.innerClass()) { + identifiedInnerClasses.put(type.fqTypeName(), fqTypeName + "." + type.simpleTypeName()); + } else { + identifiedInnerClasses.put(type.fqTypeName(), typeSimpleName + "." + type.simpleTypeName()); + } + } + + if (typePackage.equals("java.lang")) { + //imported class is from java.lang package -> automatically imported + processImportJavaLang(type, fqTypeName, typeSimpleName); + } else if (this.packageName.equals(typePackage)) { + processImportSamePackage(type, fqTypeName, typeSimpleName); + } else if (finalImports.containsKey(typeSimpleName) + && !finalImports.get(typeSimpleName).equals(fqTypeName)) { + //If there is imported class with this simple name already, but it is not in the same package as this one + //add this newly added among the forced full names + forcedFullImports.add(fqTypeName); + } else if (noImports.containsKey(typeSimpleName)) { + //There is already class with the same name present in the package we are generating to + //or imported from java.lang + forcedFullImports.add(fqTypeName); + } else if (typeName.equals(typeSimpleName)) { + //If the processed class name is the same as the one currently built. + forcedFullImports.add(fqTypeName); + } else if (!typePackage.isEmpty()) { + finalImports.put(typeSimpleName, fqTypeName); + } + } + } + + private void processImportJavaLang(Type type, String typeName, String typeSimpleName) { + //new class is from java.lang package + if (finalImports.containsKey(typeSimpleName)) { + //some other class with the same name is already being imported (but with the different package) + //remove that previously added class from imports and place it to the list of forced full class names + forcedFullImports.add(finalImports.remove(typeSimpleName)); + } else if (noImports.containsKey(typeSimpleName) + && !noImports.get(typeSimpleName).fqTypeName().equals(typeName)) { + //if there is already class with the same name, but different package, added among the imports, + // and it does not need import specified (java.lang and the same package), remove it from the exception + // list and add it among forced imports. + forcedFullImports.add(typeName); + return; + } + noImports.put(typeSimpleName, type); + } + + private void processImportSamePackage(Type type, String typeName, String typeSimpleName) { + String simpleName = typeSimpleName; + if (this.typeName.equals(simpleName)) { + simpleName = type.simpleTypeName(); + if (noImports.containsKey(simpleName) + && !noImports.get(simpleName).fqTypeName().equals(type.fqTypeName())) { + forcedFullImports.add(noImports.remove(simpleName).fqTypeName()); + } + } + if (finalImports.containsKey(simpleName)) { + //There is a class among general imports which match the currently added class name. + forcedFullImports.add(finalImports.remove(simpleName)); + noImports.put(simpleName, type); + } else if (noImports.containsKey(simpleName)) { + //There is already specialized handling of a class with this name + if (!noImports.get(simpleName).fqTypeName().equals(typeName)) { + forcedFullImports.add(noImports.remove(simpleName).fqTypeName()); + noImports.put(simpleName, type); + } + } else { + noImports.put(simpleName, type); + } + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportSorter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportSorter.java new file mode 100644 index 00000000000..4a38bca523c --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportSorter.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class ImportSorter { + + private ImportSorter() { + } + + static List> sortImports(Collection imports) { + if (imports.isEmpty()) { + return List.of(); + } + List sorted = imports.stream().sorted().toList(); + List javaImports = new ArrayList<>(); + List javaxImports = new ArrayList<>(); + List helidonImports = new ArrayList<>(); + List everythingElse = new ArrayList<>(); + for (String val : sorted) { + if (val.startsWith("java.")) { + javaImports.add(val); + } else if (val.startsWith("javax.")) { + javaxImports.add(val); + } else if (val.startsWith("io.helidon.")) { + helidonImports.add(val); + } else { + everythingElse.add(val); + } + } + return List.of(javaImports, javaxImports, helidonImports, everythingElse); + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/InnerClass.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/InnerClass.java new file mode 100644 index 00000000000..23d3bc0c540 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/InnerClass.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +/** + * Inner class model. + */ +public final class InnerClass extends ClassBase { + + //Collected directly specified imports when building this class + private final ImportOrganizer.Builder imports; + + private InnerClass(Builder builder) { + super(builder); + imports = ImportOrganizer.builder().from(builder.importOrganizer()); + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + ImportOrganizer.Builder imports() { + return imports; + } + + /** + * Fluent API builder for {@link InnerClass}. + */ + public static final class Builder extends ClassBase.Builder { + + private Builder() { + } + + @Override + public InnerClass build() { + if (name() == null) { + throw new ClassModelException("Class need to have name specified"); + } + return new InnerClass(this); + } + + @Override + public Builder isStatic(boolean isStatic) { + return super.isStatic(isStatic); + } + + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Javadoc.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Javadoc.java new file mode 100644 index 00000000000..69770dae22c --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Javadoc.java @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Javadoc model representation. + *
+ * Javadoc tags are printed out in the ordering of: + *

    + *
  • parameters
  • + *
  • generic arguments
  • + *
  • return
  • + *
  • throws
  • + *
  • deprecated
  • + *
  • everything else
  • + *
+ */ +public final class Javadoc extends ModelComponent { + + private final List content; + private final Map> parameters; + private final Map> genericsTokens; + private final Map> throwsDesc; + private final Map>> otherTags; + private final List returnDescription; + private final List deprecation; + private final Boolean generate; + + private Javadoc(Builder builder) { + super(builder); + this.content = List.of(builder.contentBuilder.toString().split("\n")); + this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(builder.filteredParameters)); + this.genericsTokens = Collections.unmodifiableMap(new LinkedHashMap<>(builder.genericArguments)); + this.throwsDesc = Collections.unmodifiableMap(new LinkedHashMap<>(builder.throwsDesc)); + this.otherTags = createCopyOfTagMap(builder.otherTags); + this.returnDescription = List.copyOf(builder.finalReturnDescription); + this.deprecation = List.copyOf(builder.deprecation); + this.generate = builder.generate; + } + + /** + * Parse Javadoc model object from the String. + * + * @param fullJavadocString javadoc string + * @return new javadoc instance + */ + public static Javadoc parse(String fullJavadocString) { + return builder().parse(fullJavadocString).build(); + } + + /** + * Parse Javadoc model object from the list of strings. + * + * @param fullJavadocLines javadoc string lines + * @return new javadoc instance + */ + public static Javadoc parse(List fullJavadocLines) { + return builder().parse(fullJavadocLines).build(); + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + + /** + * Create new {@link Builder} instance. + * + * @param javadoc existing javadoc to copy + * @return new builder instance + */ + public static Builder builder(Javadoc javadoc) { + return new Builder() + .from(javadoc); + } + + private static Map>> createCopyOfTagMap(Map>> otherTags) { + Map>> newTags = new HashMap<>(); + for (Map.Entry>> entry : otherTags.entrySet()) { + newTags.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return Map.copyOf(newTags); + } + + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + writer.write("/**\n"); + for (String line : content) { + if (!line.isEmpty() && Character.isWhitespace(line.charAt(0))) { + writer.writeLine(" *" + line); + } else if (line.isBlank()) { + writer.writeLine(" *"); + } else { + writer.writeLine(" * " + line); + } + } + if (hasAnyOtherParts()) { + writer.write(" *\n"); + } + for (Map.Entry> entry : parameters.entrySet()) { + writeTagInformation(writer, "param", entry.getKey(), entry.getValue()); + } + for (Map.Entry> entry : genericsTokens.entrySet()) { + String key = entry.getKey(); + if (key.startsWith("<") && key.endsWith(">")) { + writeTagInformation(writer, "param", key, entry.getValue()); + } else { + writeTagInformation(writer, "param", "<" + key + ">", entry.getValue()); + } + } + if (!returnDescription.isEmpty()) { + writeTagInformation(writer, "return", null, returnDescription); + } + for (Map.Entry> entry : throwsDesc.entrySet()) { + writeTagInformation(writer, "throws", entry.getKey(), entry.getValue()); + } + if (!deprecation.isEmpty()) { + writeTagInformation(writer, "deprecated", null, deprecation); + } + for (Map.Entry>> entry : otherTags.entrySet()) { + for (List description : entry.getValue()) { + writeTagInformation(writer, entry.getKey(), null, description); + } + } + writer.write(" */"); + } + + private void writeTagInformation(ModelWriter writer, String paramName, String name, List description) + throws IOException { + if (description.isEmpty()) { + if (name != null) { + writer.writeLine(" * @" + paramName + " " + name); + } else { + writer.writeLine(" * @" + paramName); + } + } else { + boolean first = true; + String padding; + if (name != null) { + //If there is specific name, we want this to be included into smart padding + //Example: @param myParam first line + // second line + padding = " ".repeat(1 + paramName.length() + 1 + name.length() + 1); + } else { + //There is no specific for this tag + //Example: @return first line + // second line + padding = " ".repeat(1 + paramName.length() + 1); + } + for (String line : description) { + if (first) { + if (name != null) { + writer.write(" * @" + paramName + " " + name); + } else { + writer.write(" * @" + paramName); + } + if (line.isBlank()) { + writer.writeLine(""); + } else { + writer.writeLine(" " + line); + } + first = false; + } else { + writer.writeLine(" * " + padding + line); + } + } + } + } + + /** + * Content of this javadoc. + * + * @return content + */ + public List content() { + return content; + } + + /** + * Parameter tags names and descriptions. + * + * @return parameter tags + */ + public Map> parameters() { + return parameters; + } + + /** + * Generic parameter tags names and descriptions. + * + * @return generic parameter tags + */ + public Map> genericsTokens() { + return genericsTokens; + } + + /** + * Return type description. + * + * @return return type description + */ + public List returnDescription() { + return returnDescription; + } + + /** + * Throws tags names and descriptions. + * + * @return throws tags + */ + public Map> throwsDesc() { + return throwsDesc; + } + + /** + * Deprecation description. + * + * @return deprecation description + */ + public List deprecation() { + return deprecation; + } + + /** + * Other created tags with descriptions. + * + * @return other tags + */ + public Map>> otherTags() { + return otherTags; + } + + boolean generate() { + return generate; + } + + private boolean hasAnyOtherParts() { + return !parameters.isEmpty() + || !throwsDesc.isEmpty() + || !genericsTokens.isEmpty() + || !returnDescription.isEmpty() + || !deprecation.isEmpty() + || !otherTags.isEmpty(); + } + + /** + * Fluent API builder for {@link Javadoc}. + */ + public static final class Builder extends ModelComponent.Builder { + + private final StringBuilder contentBuilder = new StringBuilder(); + private final Map> parameters = new LinkedHashMap<>(); + private final Map> genericArguments = new LinkedHashMap<>(); + private final Map> throwsDesc = new LinkedHashMap<>(); + private final Map>> otherTags = new LinkedHashMap<>(); + private final List returnDescription = new ArrayList<>(); + private final List deprecation = new ArrayList<>(); + private Map> filteredParameters = parameters; + private List finalReturnDescription = returnDescription; + private boolean generate = false; + + private Builder() { + } + + @Override + public Javadoc build() { + return new Javadoc(this); + } + + /** + * Add text line to the content. + * New line character is added after this line. + * + * @param line line to add + * @return updated builder instance + */ + public Builder addLine(String line) { + this.contentBuilder.append(line).append("\n"); + return this; + } + + /** + * Add text line to the content. + * New line character is not added after this line, so all newly added text will be appended to the same line. + * + * @param line line to add + * @return updated builder instance + */ + public Builder add(String line) { + this.contentBuilder.append(line); + return this; + } + + /** + * Set new content. + * This method replaces previously created content in this builder. + * + * @param content content to be set + * @return updated builder instance + */ + public Builder content(List content) { + this.contentBuilder.setLength(0); + content.forEach(this::addLine); + return this; + } + + /** + * Add parameter tag name and description. + * + * @param paramName parameter name + * @param description parameter description + * @return updated builder instance + */ + public Builder addParameter(String paramName, String description) { + return addParameter(paramName, List.of(description.split("\n"))); + } + + /** + * Add parameter tag name and description. + * + * @param paramName parameter name + * @param description parameter description + * @return updated builder instance + */ + public Builder addParameter(String paramName, List description) { + if (parameters.containsKey(paramName) && description.isEmpty()) { + //Do nothing, since there is already some description of this parameter, + // and we are rewriting it with empty list + return this; + } + this.parameters.put(paramName, List.copyOf(description)); + return this; + } + + /** + * Add throws tag name and description. + * + * @param exception exception name + * @param description exception description + * @return updated builder instance + */ + public Builder addThrows(String exception, List description) { + this.throwsDesc.put(exception, description); + return this; + } + + /** + * Add throws tag name and description. + * + * @param tag tag name + * @param description tag description + * @return updated builder instance + */ + public Builder addTag(String tag, String description) { + this.otherTags.computeIfAbsent(tag, k -> new ArrayList<>()) + .add(List.of(description.split("\n"))); + return this; + } + + /** + * Add throws tag name and description. + * + * @param tag tag name + * @param description tag description + * @return updated builder instance + */ + public Builder addTag(String tag, List description) { + this.otherTags.computeIfAbsent(tag, k -> new ArrayList<>()) + .add(List.copyOf(description)); + return this; + } + + /** + * Add return type description. + * + * @param returnDescription return type description + * @return updated builder instance + */ + public Builder returnDescription(String returnDescription) { + return returnDescription(List.of(returnDescription)); + } + + /** + * Add return type description. + * + * @param returnDescription return type description + * @return updated builder instance + */ + public Builder returnDescription(List returnDescription) { + Objects.requireNonNull(returnDescription); + if (returnDescription.isEmpty()) { + //This is here to prevent overwriting of the previously set value with empty description + return this; + } + this.returnDescription.clear(); + this.returnDescription.addAll(returnDescription); + return this; + } + + /** + * Add generic argument tag name and description. + * + * @param argument parameter name + * @param description parameter description + * @return updated builder instance + */ + public Builder addGenericArgument(String argument, List description) { + this.genericArguments.put(argument, List.copyOf(description)); + return this; + } + + /** + * Add generic argument tag name and description. + * + * @param argument parameter name + * @param description parameter description + * @return updated builder instance + */ + public Builder addGenericArgument(String argument, String description) { + this.genericArguments.put(argument, List.of(description.split("\n"))); + return this; + } + + /** + * Deprecation description. + * + * @param deprecation deprecation description + * @return updated builder instance + */ + public Builder deprecation(String deprecation) { + this.deprecation.clear(); + this.deprecation.add(deprecation); + return this; + } + + /** + * Deprecation description, multiple lines. + * + * @param deprecation deprecation description + * @return updated builder instance + */ + public Builder deprecation(List deprecation) { + this.deprecation.clear(); + this.deprecation.addAll(deprecation); + return this; + } + + /** + * Whether to generate this javadoc. + * + * @param generate generate javadoc + * @return updated builder instance + */ + public Builder generate(boolean generate) { + this.generate = generate; + return this; + } + + /** + * Populate this builder with content of the already created Javadoc instance. + * + * @param javadoc already created javadoc instance + * @return updated builder instance + */ + public Builder from(Javadoc javadoc) { + this.generate = true; + this.deprecation.addAll(javadoc.deprecation()); + this.returnDescription.addAll(javadoc.returnDescription()); + this.contentBuilder.append(String.join("\n", javadoc.content())); + this.parameters.putAll(javadoc.parameters()); + this.genericArguments.putAll(javadoc.genericsTokens()); + this.throwsDesc.putAll(javadoc.throwsDesc()); + this.otherTags.putAll(javadoc.otherTags()); + return this; + } + + /** + * Remove everything from this builder. + * + * @return updated builder instance + */ + public Builder clear() { + this.generate = false; + this.deprecation.clear(); + this.returnDescription.clear(); + this.contentBuilder.delete(0, contentBuilder.length()); + this.parameters.clear(); + this.genericArguments.clear(); + this.throwsDesc.clear(); + this.otherTags.clear(); + return this; + } + + /** + * Populates this builder with the parsed javadoc data. + * + * @param fullJavadocString string format javadoc + * @return updated builder instance + */ + public Builder parse(String fullJavadocString) { + return JavadocParser.parse(this, fullJavadocString); + } + + /** + * Populates this builder with the parsed javadoc data. + * + * @param fullJavadocLines string list format javadoc + * @return updated builder instance + */ + public Builder parse(List fullJavadocLines) { + return JavadocParser.parse(this, fullJavadocLines); + } + + Javadoc build(CommonComponent.Builder componentBuilder) { + //This build method serves as configuration method based on the component this javadoc is generated for + if (componentBuilder instanceof Method.Builder methodBuilder) { + return build(methodBuilder); + } + return build(); + } + + Javadoc build(Method.Builder methodBuilder) { + this.filteredParameters = new LinkedHashMap<>(); + for (String paramName : methodBuilder.parameters().keySet()) { + //generate only really present parameters + if (parameters.containsKey(paramName)) { + this.filteredParameters.put(paramName, parameters.get(paramName)); + } + } + if (methodBuilder.returnType().fqTypeName().equals(void.class.getName())) { + //Do not add return tag if method does not return anything + finalReturnDescription = new ArrayList<>(); + } + return build(); + } + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/JavadocParser.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/JavadocParser.java new file mode 100644 index 00000000000..74a24ae3636 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/JavadocParser.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.ArrayList; +import java.util.List; + +class JavadocParser { + + private JavadocParser() { + } + + static Javadoc.Builder parse(Javadoc.Builder javadocBuilder, String docString) { + return parse(javadocBuilder, List.of(docString.split("\n"))); + } + + static Javadoc.Builder parse(Javadoc.Builder javadocBuilder, List documentation) { + + ParserState state = ParserState.LINES; + + String currentTagName = null; + List currentTag = new ArrayList<>(); + + for (String raw : documentation) { + String line = raw.trim(); + if (line.startsWith("@")) { + // this is a new tag, finish previous, change state + addTag(javadocBuilder, state, currentTagName, currentTag); + currentTagName = null; + currentTag.clear(); + // and now parse the current tag line + if (line.startsWith("@param")) { + // param doc + state = ParserState.PARAM; + int space = line.indexOf(' '); + if (space < 0) { + // should be @param paramName documentation + // there is no param name defined, this is bad + // TODO add location! + throw new IllegalStateException("Failed to parse javadoc, @param without param name: " + line); + } + int secondSpace = line.indexOf(' ', space + 2); + if (secondSpace < 0) { + throw new IllegalStateException("Failed to parse javadoc, @param without param name or docs: " + line); + } + currentTagName = line.substring(space + 1, secondSpace); + currentTag.add(line.substring(secondSpace + 1)); + if (currentTagName.startsWith("<")) { + currentTagName = currentTagName.substring(1, currentTagName.indexOf(">")); + state = ParserState.GENERIC_PARAM; + } + } else if (line.startsWith("@return")) { + // return doc + state = ParserState.RETURNS; + currentTag.add(line.substring("@return".length()).trim()); // trim to remove whitespace after @returns + } else if (line.startsWith("@throws")) { + // throw doc + state = ParserState.THROWS; + int space = line.indexOf(' '); + if (space < 0) { + // should be @throws exception documentation + // there is no exception name defined, this is bad + throw new IllegalStateException("Failed to parse javadoc, @throws without exception name: " + line); + } + int secondSpace = line.indexOf(' ', space + 2); + if (secondSpace < 0) { + throw new IllegalStateException("Failed to parse javadoc, @throws without exception name or docs: " + + line); + } + currentTagName = line.substring(space + 1, secondSpace); + currentTag.add(line.substring(secondSpace + 1)); + } else { + // other tag + state = ParserState.TAG; + // @see some link + int space = line.indexOf(' '); + if (space < 0) { + // should be @tag documentation + // TODO add location! + throw new IllegalStateException("Failed to parse javadoc, @tag without space: " + line); + } + currentTagName = line.substring(1, space); // without @ + currentTag.add(line.substring(space + 1)); + } + } else { + // continuation of previous state + if (state == ParserState.LINES) { + javadocBuilder.addLine(raw); + } else { + currentTag.add(line); + } + } + } + + addTag(javadocBuilder, state, currentTagName, currentTag); + + return javadocBuilder; + } + + private static void addTag(Javadoc.Builder javadocBuilder, + ParserState state, + String currentTagName, + List currentTag) { + if (state == ParserState.PARAM) { + javadocBuilder.addParameter(currentTagName, currentTag); + } else if (state == ParserState.GENERIC_PARAM) { + javadocBuilder.addGenericArgument(currentTagName, currentTag); + } else if (state == ParserState.TAG) { + if ("deprecated".equals(currentTagName)) { + javadocBuilder.deprecation(currentTag); + } else { + javadocBuilder.addTag(currentTagName, currentTag); + } + } else if (state == ParserState.RETURNS) { + javadocBuilder.returnDescription(currentTag); + } else if (state == ParserState.THROWS) { + javadocBuilder.addThrows(currentTagName, currentTag); + } + } + + private enum ParserState { + LINES, + PARAM, + GENERIC_PARAM, + THROWS, + RETURNS, + TAG + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java new file mode 100644 index 00000000000..f02d54fdfb1 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Method.java @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; + +/** + * Model of the method which should be created in the specific type. + */ +public final class Method extends Executable { + + private final Map declaredTokens; + private final boolean isDefault; + private final boolean isFinal; + private final boolean isStatic; + private final boolean isAbstract; + + private Method(Builder builder) { + super(builder); + this.isDefault = builder.isDefault; + this.isFinal = builder.isFinal; + this.isStatic = builder.isStatic; + this.isAbstract = builder.isAbstract; + this.declaredTokens = Collections.unmodifiableMap(new LinkedHashMap<>(builder.declaredTokens)); + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder() + .returnType(builder -> builder.type(void.class)); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + if (javadoc().generate()) { + javadoc().writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write("\n"); + } + if (classType == ClassType.INTERFACE) { + if (isDefault) { + writer.write("default "); + } else if (isStatic) { + writer.write("static "); + } + } else { + if (AccessModifier.PACKAGE_PRIVATE != accessModifier()) { + writer.write(accessModifier().modifierName() + " "); + } + if (isStatic) { + writer.write("static "); + } + if (isFinal) { + writer.write("final "); + } + if (isAbstract) { + writer.write("abstract "); + } + } + appendTokenDeclaration(writer, declaredTokens, imports, classType); + type().writeComponent(writer, declaredTokens, imports, classType); //write return type + writer.write(" " + name() + "("); + boolean first = true; + for (Parameter parameter : parameters()) { + if (first) { + first = false; + } else { + writer.write(", "); + } + parameter.writeComponent(writer, declaredTokens, imports, classType); + } + writer.write(")"); + writeThrows(writer, declaredTokens, imports, classType); + if (classType == ClassType.INTERFACE) { + if (!isDefault && !isStatic) { + writer.write(";"); + return; + } + } else { + if (isAbstract) { + writer.write(";"); + return; + } + } + writer.write(" {"); + if (hasBody()) { + writeBody(writer, imports); + } else { + writer.write("\n"); + } + writer.write("}"); + } + + private void appendTokenDeclaration(ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports, + ClassType classType) + throws IOException { + Set tokensToDeclare = new LinkedHashSet<>(); + if (isStatic) { + for (Parameter parameter : parameters()) { + if (parameter.type() instanceof TypeArgument typeArgument) { + String tokenName = typeArgument.token(); + if (!tokenName.equals("?")) { + tokensToDeclare.add(tokenName); + } + } + } + } else { + for (Parameter parameter : parameters()) { + if (parameter.type() instanceof TypeArgument typeArgument) { + String tokenName = typeArgument.token(); + if (!declaredTokens.contains(tokenName) && !tokenName.equals("?")) { + tokensToDeclare.add(tokenName); + } + } + } + } + if (!tokensToDeclare.isEmpty()) { + writer.write("<"); + boolean first = true; + for (String token : tokensToDeclare) { + if (first) { + first = false; + } else { + writer.write(", "); + } + if (this.declaredTokens.containsKey(token)) { + this.declaredTokens.get(token).writeComponent(writer, declaredTokens, imports, classType); + } else { + writer.write(token); + } + } + for (Map.Entry entry : this.declaredTokens.entrySet()) { + if (!tokensToDeclare.contains(entry.getKey())) { + entry.getValue().writeComponent(writer, declaredTokens, imports, classType); + } + } + writer.write("> "); + } else if (!this.declaredTokens.isEmpty()) { + writer.write("<"); + boolean first = true; + for (Map.Entry entry : this.declaredTokens.entrySet()) { + if (first) { + first = false; + } else { + writer.write(", "); + } + entry.getValue().writeComponent(writer, declaredTokens, imports, classType); + } + writer.write("> "); + } + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + super.addImports(imports); + type().addImports(imports); + } + + boolean isStatic() { + return isStatic; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Method method = (Method) o; + return Objects.equals(type(), method.type()) + && Objects.equals(name(), method.name()) + && parameters().size() == method.parameters().size() + && parameters().equals(method.parameters()); + } + + @Override + public int hashCode() { + return Objects.hash(type(), name(), parameters()); + } + + @Override + public String toString() { + return "Method{" + + "name=" + name() + + ", isFinal=" + isFinal + + ", isStatic=" + isStatic + + ", isAbstract=" + isAbstract + + ", returnType=" + type().fqTypeName() + + '}'; + } + + /** + * Fluent API builder for {@link Method}. + */ + public static final class Builder extends Executable.Builder { + + private final Map declaredTokens = new LinkedHashMap<>(); + private boolean isDefault = false; + private boolean isFinal = false; + private boolean isStatic = false; + private boolean isAbstract = false; + + Builder() { + } + + @Override + public Method build() { + if (name() == null) { + throw new ClassModelException("Method needs to have name specified"); + } + if (isStatic && isAbstract) { + throw new IllegalStateException("Method cannot be static and abstract at the same time"); + } + if (isFinal && isAbstract) { + throw new IllegalStateException("Method cannot be final and abstract at the same time"); + } + return new Method(this); + } + + @Override + public Builder content(List content) { + declaredTokens.clear(); + return super.content(content); + } + + /** + * Whether this method is final. + * + * @param isFinal method is final + * @return updated builder instance + */ + public Builder isFinal(boolean isFinal) { + this.isFinal = isFinal; + return this; + } + + /** + * Whether this method is static. + * + * @param isStatic method is static + * @return updated builder instance + */ + public Builder isStatic(boolean isStatic) { + this.isStatic = isStatic; + return this; + } + + /** + * Whether this method is abstract. + * + * @param isAbstract method is abstract + * @return updated builder instance + */ + public Builder isAbstract(boolean isAbstract) { + this.isAbstract = isAbstract; + return this; + } + + /** + * Whether this method is default. + * + * @param isDefault method is default + * @return updated builder instance + */ + public Builder isDefault(boolean isDefault) { + this.isDefault = isDefault; + return this; + } + + /** + * Set return type of the method. + * Default is {@code void}. + * + * @param type return type + * @return updated builder instance + */ + public Builder returnType(TypeName type) { + return type(type); + } + + /** + * Set return type of the method. + * Default is {@code void}. + * + * @param type return type + * @param description return type description + * @return updated builder instance + */ + public Builder returnType(TypeName type, String description) { + return type(type).returnJavadoc(description); + } + + /** + * Set return type of the method. + * Default is {@code void}. + * + * @param consumer return type builder consumer + * @return updated builder instance + */ + public Builder returnType(Consumer consumer) { + Objects.requireNonNull(consumer); + Returns.Builder builder = Returns.builder(); + consumer.accept(builder); + return returnType(builder); + } + + /** + * Set return type of the method. + * Default is {@code void}. + * + * @param supplier return type supplier + * @return updated builder instance + */ + public Builder returnType(Supplier supplier) { + Objects.requireNonNull(supplier); + return returnType(supplier.get()); + } + + /** + * Set return type of the method. + * Default is {@code void}. + * + * @param returnType return type + * @return updated builder instance + */ + public Builder returnType(Returns returnType) { + return type(returnType.type()) + .returnJavadoc(returnType.description()); + } + + /** + * Add generic argument to be declared by this method. + * + * @param typeArgument argument to be declared + * @return updated builder instance + */ + public Builder addGenericArgument(TypeArgument typeArgument) { + declaredTokens.put(typeArgument.token(), typeArgument); + addGenericToken(typeArgument.token(), typeArgument.description()); + return this; + } + + Type returnType() { + return type(); + } + + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelComponent.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelComponent.java new file mode 100644 index 00000000000..b54b9b70c4d --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelComponent.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.Set; + +abstract class ModelComponent { + + private final boolean includeImport; + + ModelComponent(Builder builder) { + this.includeImport = builder.includeImport; + } + + abstract void writeComponent(ModelWriter writer, + Set declaredTokens, + ImportOrganizer imports, + ClassType classType) throws IOException; + + void addImports(ImportOrganizer.Builder imports) { + } + + boolean includeImport() { + return includeImport; + } + + abstract static class Builder, T extends ModelComponent> + implements io.helidon.common.Builder { + + private boolean includeImport = true; + + Builder() { + } + + /** + * Whether to include import type information among the imports. + * + * @param includeImport + * @return + */ + public B includeImport(boolean includeImport) { + this.includeImport = includeImport; + return identity(); + } + + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelWriter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelWriter.java new file mode 100644 index 00000000000..1c045befbc9 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ModelWriter.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.Writer; + +import static io.helidon.codegen.classmodel.ClassModel.PADDING_TOKEN; + +class ModelWriter extends Writer { + + private final Writer delegate; + private final String padding; + private String currentPadding = ""; //no padding + private int paddingLevel = 0; + private boolean firstWrite = true; + + ModelWriter(Writer delegate, String padding) { + this.delegate = delegate; + this.padding = padding; + } + + void increasePaddingLevel() { + paddingLevel++; + currentPadding = padding.repeat(paddingLevel); + } + + void decreasePaddingLevel() { + paddingLevel--; + currentPadding = padding.repeat(paddingLevel); + } + + void writeLine(String str) throws IOException { + write(str); + write("\n"); + } + + /** + * Separator line is line which is completely empty and with no padding. + * + * @throws IOException If an I/O error occurs + */ + void writeSeparatorLine() throws IOException { + delegate.write("\n"); + } + + @Override + public void write(String str) throws IOException { + if (firstWrite) { + delegate.write(currentPadding); + firstWrite = false; + } + String padded = str.replaceAll("\n", "\n" + currentPadding); + padded = padded.replaceAll(PADDING_TOKEN, padding); + delegate.write(padded); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + delegate.write(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java new file mode 100644 index 00000000000..876eb3691b4 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Parameter.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Method parameter model. + */ +public final class Parameter extends AnnotatedComponent { + + private final boolean optional; + private final List description; + + private Parameter(Builder builder) { + super(builder); + this.optional = builder.optional; + this.description = List.copyOf(builder.description); + } + + /** + * Create new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + for (Annotation annotation : annotations()) { + annotation.writeComponent(writer, declaredTokens, imports, classType); + writer.write(" "); + } + type().writeComponent(writer, declaredTokens, imports, classType); + if (optional) { + writer.write("..."); + } + writer.write(" " + name()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Parameter parameter = (Parameter) o; + return optional == parameter.optional + && type().equals(parameter.type()); + } + + @Override + public int hashCode() { + return Objects.hash(optional); + } + + @Override + public String toString() { + return "Parameter{type=" + type().fqTypeName() + ", simpleType=" + type().simpleTypeName() + ", name=" + name() + "}"; + } + + List description() { + return description; + } + + /** + * Fluent API builder for {@link Parameter}. + */ + public static final class Builder extends AnnotatedComponent.Builder { + + private boolean optional = false; + private final List description = new ArrayList<>(); + + private Builder() { + } + + @Override + public Parameter build() { + if (type() == null || name() == null) { + throw new ClassModelException("Annotation parameter must have name and type set"); + } + return new Parameter(this); + } + + /** + * Whether this parameter is optional. + * + * @param optional optional parameter + * @return updated builder instance + */ + public Builder optional(boolean optional) { + this.optional = optional; + return this; + } + + @Override + public Builder description(List description) { + this.description.clear(); + this.description.addAll(description); + return this; + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Returns.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Returns.java new file mode 100644 index 00000000000..198bbfeaf75 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Returns.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Objects which describes return type configuration. + */ +public final class Returns extends DescribableComponent { + + private Returns(Builder builder) { + super(builder); + } + + /** + * Return new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + String typeName = imports.typeName(type(), includeImport()); + writer.write(typeName); + } + + /** + * Fluent API builder for {@link Returns}. + */ + public static final class Builder extends DescribableComponent.Builder { + + private Builder() { + } + + @Override + public Returns build() { + return new Returns(this); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder description(String description) { + return super.description(description); + } + + @Override + public Builder description(List description) { + return super.description(description); + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Throws.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Throws.java new file mode 100644 index 00000000000..a63ef8ec8fb --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Throws.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Objects which describes exception throws configuration. + */ +public class Throws extends DescribableComponent { + + private Throws(Builder builder) { + super(builder); + } + + /** + * Return new {@link Builder} instance. + * + * @return new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + String typeName = imports.typeName(type(), includeImport()); + writer.write(typeName); + } + + /** + * Fluent API builder for {@link Throws}. + */ + public static final class Builder extends DescribableComponent.Builder { + + private Builder() { + } + + @Override + public Throws build() { + return new Throws(this); + } + + @Override + public Builder type(String type) { + return super.type(type); + } + + @Override + public Builder type(Class type) { + return super.type(type); + } + + @Override + public Builder type(TypeName type) { + return super.type(type); + } + + @Override + public Builder description(String description) { + return super.description(description); + } + + @Override + public Builder description(List description) { + return super.description(description); + } + } + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java new file mode 100644 index 00000000000..45e24c4100f --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.Optional; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; + +abstract class Type extends ModelComponent { + + Type(Builder builder) { + super(builder); + } + + static Type fromTypeName(TypeName typeName) { + if (typeName instanceof TypeArgument argument) { + return argument; + } + if (typeName.typeArguments().isEmpty()) { + if (typeName.array() + || Optional.class.getName().equals(typeName.declaredName())) { + return ConcreteType.builder() + .type(typeName) + .build(); + } else if (typeName.wildcard()) { + boolean isObject = typeName.name().equals("?") || Object.class.getName().equals(typeName.name()); + if (isObject) { + return TypeArgument.create("?"); + } else { + return TypeArgument.builder() + .token("?") + .bound(extractBoundTypeName(typeName.genericTypeName())) + .build(); + } + } + return ConcreteType.builder() + .type(typeName) + .build(); + } + ConcreteType.Builder typeBuilder = ConcreteType.builder() + .type(typeName); + typeName.typeArguments() + .forEach(typeBuilder::addParam); + return typeBuilder.build(); + } + + private static String extractBoundTypeName(TypeName instance) { + String name = calcName(instance); + StringBuilder nameBuilder = new StringBuilder(name); + + if (!instance.typeArguments().isEmpty()) { + nameBuilder.append('<') + .append(instance.typeArguments() + .stream() + .map(TypeName::resolvedName) + .collect(Collectors.joining(", "))) + .append('>'); + } + + if (instance.array()) { + nameBuilder.append("[]"); + } + + return nameBuilder.toString(); + } + + private static String calcName(TypeName instance) { + String className; + if (instance.enclosingNames().isEmpty()) { + className = instance.className(); + } else { + className = String.join(".", instance.enclosingNames()) + "." + instance.className(); + } + + return (instance.primitive() || instance.packageName().isEmpty()) + ? className : instance.packageName() + "." + className; + } + + abstract String fqTypeName(); + abstract String resolvedTypeName(); + + abstract String packageName(); + + abstract String simpleTypeName(); + + abstract boolean isArray(); + + abstract boolean innerClass(); + + abstract Optional declaringClass(); + + abstract TypeName genericTypeName(); + +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java new file mode 100644 index 00000000000..e44e09c6c06 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +/** + * Generic type argument model. + */ +public final class TypeArgument extends Type implements TypeName { + + private final TypeName token; + private final Type bound; + private final List description; + + private TypeArgument(Builder builder) { + super(builder); + this.token = builder.tokenBuilder.build(); + this.bound = builder.bound; + this.description = builder.description; + } + + /** + * Creates new {@link TypeArgument} instance based on the provided token. + * + * @param token argument token + * @return new argument instance + */ + public static TypeArgument create(String token) { + return builder().token(token).build(); + } + + /** + * Return new {@link Builder} instance. + * + * @return new builder instance + */ + public static TypeArgument.Builder builder() { + return new TypeArgument.Builder(); + } + + @Override + public TypeName boxed() { + return this; + } + + @Override + public TypeName genericTypeName() { + if (bound == null) { + return null; + } + return bound.genericTypeName(); + } + + @Override + void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) + throws IOException { + writer.write(token.className()); + if (bound != null) { + writer.write(" extends "); + bound.writeComponent(writer, declaredTokens, imports, classType); + } + } + + @Override + void addImports(ImportOrganizer.Builder imports) { + if (bound != null) { + bound.addImports(imports); + } + } + + /** + * Type argument token. + * + * @return token value + */ + public String token() { + return token.className(); + } + + @Override + public String packageName() { + return ""; + } + + List description() { + return description; + } + + @Override + String fqTypeName() { + return token.className(); + } + + @Override + String resolvedTypeName() { + return token.resolvedName(); + } + + @Override + String simpleTypeName() { + return token.className(); + } + + @Override + boolean isArray() { + return false; + } + + @Override + boolean innerClass() { + return false; + } + + @Override + Optional declaringClass() { + return Optional.empty(); + } + + @Override + public String className() { + return token.className(); + } + + @Override + public List enclosingNames() { + return List.of(); + } + + @Override + public boolean primitive() { + return false; + } + + @Override + public boolean array() { + return token.array(); + } + + @Override + public boolean generic() { + return token.generic(); + } + + @Override + public boolean wildcard() { + return token.wildcard(); + } + + @Override + public List typeArguments() { + return List.of(); + } + + @Override + public List typeParameters() { + return List.of(); + } + + @Override + public String toString() { + if (bound == null) { + return "Token: " + token.className(); + } + return "Token: " + token.className() + " Bound: " + bound; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TypeArgument typeArgument1 = (TypeArgument) o; + return Objects.equals(token, typeArgument1.token) + && Objects.equals(bound, typeArgument1.bound); + } + + @Override + public int hashCode() { + return Objects.hash(token, bound); + } + + @Override + public int compareTo(TypeName o) { + return token.compareTo(o); + } + + /** + * Fluent API builder for {@link TypeArgument}. + */ + public static final class Builder extends Type.Builder { + + private final TypeName.Builder tokenBuilder = TypeName.builder() + .generic(true); + private Type bound; + private List description = List.of(); + + private Builder() { + } + + /** + * Token name of this argument. + * + * @param token token name + * @return updated builder instance + */ + public Builder token(String token) { + tokenBuilder.className(Objects.requireNonNull(token)) + .wildcard(token.startsWith("?")); + return this; + } + + /** + * Type this argument is bound to. + * + * @param bound argument bound + * @return updated builder instance + */ + public Builder bound(String bound) { + return bound(TypeName.create(bound)); + } + + /** + * Type this argument is bound to. + * + * @param bound argument bound + * @return updated builder instance + */ + public Builder bound(Class bound) { + return bound(TypeName.create(bound)); + } + + /** + * Type this argument is bound to. + * + * @param bound argument bound + * @return updated builder instance + */ + public Builder bound(TypeName bound) { + this.bound = Type.fromTypeName(bound); + return this; + } + + /** + * Set description of the component. + * It overwrites previously set description. + * + * @param description component description + * @return updated builder instance + */ + public Builder description(String description) { + this.description = List.of(description.split("\n")); + return this; + } + + /** + * Set description of the component. + * It overwrites previously set description. + * + * @param description component description + * @return updated builder instance + */ + public Builder description(List description) { + this.description = List.copyOf(description); + return this; + } + + @Override + public TypeArgument build() { + if (tokenBuilder.className().isEmpty()) { + throw new ClassModelException("Token name needs to be specified."); + } + return new TypeArgument(this); + } + + } +} diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/package-info.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/package-info.java new file mode 100644 index 00000000000..15c2dc94561 --- /dev/null +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Class model generator for annotation processors. + */ +package io.helidon.codegen.classmodel; diff --git a/codegen/class-model/src/main/java/module-info.java b/codegen/class-model/src/main/java/module-info.java new file mode 100644 index 00000000000..386bae5d716 --- /dev/null +++ b/codegen/class-model/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * The class model code generator. + */ +module io.helidon.codegen.classmodel { + requires transitive io.helidon.common.types; + + exports io.helidon.codegen.classmodel; + +} diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/ImportOrganizerTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/ImportOrganizerTest.java new file mode 100644 index 00000000000..52f20550795 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/ImportOrganizerTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.TypeName; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +class ImportOrganizerTest { + @Test + void testImportSystemLoggerLevel() throws IOException { + TypeName typeNameLevel = TypeName.create(System.Logger.Level.class); + assertThat(typeNameLevel.className(), is("Level")); + assertThat(typeNameLevel.enclosingNames(), hasItems("System", "Logger")); + assertThat(typeNameLevel.packageName(), is("java.lang")); + + Type type = Type.fromTypeName(typeNameLevel); + assertThat(type.packageName(), is("java.lang")); + assertThat(type.declaringClass(), is(Optional.of(Type.fromTypeName(TypeName.create(System.Logger.class))))); + assertThat(type.innerClass(), is(true)); + + ImportOrganizer io = ImportOrganizer.builder() + .typeName("io.helidon.NotImportant") + .packageName("io.helidon") + .addImport(type) + .build(); + StringWriter writer = new StringWriter(); + ModelWriter modelWriter = new ModelWriter(writer, ""); + type.writeComponent(modelWriter, Set.of(), io, ClassType.CLASS); + + String written = writer.toString(); + assertThat(written, is("System.Logger.Level")); + + List imports = io.imports(); + assertThat(imports, empty()); + } +} \ No newline at end of file diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TestContentBuilder.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TestContentBuilder.java new file mode 100644 index 00000000000..c4e7f453945 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TestContentBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.util.ArrayList; +import java.util.List; + +class TestContentBuilder implements ContentBuilder { + private final List content = new ArrayList<>(); + private final StringBuilder currentLine = new StringBuilder(); + private final String padding = " "; + + int currentPadding = 0; + + @Override + public TestContentBuilder addContentLine(String line) { + addContent(line); + content.add(currentLine.toString()); + currentLine.delete(0, currentLine.length()); + return this; + } + + @Override + public TestContentBuilder content(List content) { + this.content.clear(); + this.content.addAll(content); + return this; + } + + @Override + public TestContentBuilder addContent(String line) { + if (currentLine.isEmpty()) { + currentLine.append(padding.repeat(currentPadding + 1)); + } + currentLine.append(line); + return this; + } + + @Override + public TestContentBuilder addTypeToContent(String typeName) { + addContent("@" + typeName + "@"); + return this; + } + + @Override + public TestContentBuilder padContent() { + addContent(padding); + return this; + } + + @Override + public TestContentBuilder padContent(int repetition) { + addContent(padding.repeat(repetition)); + return this; + } + + @Override + public TestContentBuilder increaseContentPadding() { + currentPadding++; + return this; + } + + @Override + public TestContentBuilder decreaseContentPadding() { + currentPadding--; + return this; + } + + @Override + public TestContentBuilder clearContent() { + content.clear(); + currentLine.delete(0, currentLine.length()); + currentPadding = 0; + return this; + } + + String generatedString() { + if (!this.currentLine.isEmpty()) { + this.content.add(this.currentLine.toString()); + this.currentLine.delete(0, this.currentLine.length()); + } + return String.join("\n", this.content); + } +} diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypeTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypeTest.java new file mode 100644 index 00000000000..ec01e4dd912 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypeTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Set; + +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class TypeTest { + @Test + void testPlainType() throws IOException { + assertThat(write(TypeNames.STRING), is("java.lang.String")); + } + + @Test + void testGenericType() throws IOException { + assertThat(write(TypeName.builder(TypeNames.LIST) + .addTypeArgument(TypeNames.STRING) + .build()), is("java.util.List")); + } + + @Test + void testNestedGenericType() throws IOException { + assertThat(write(TypeName.builder(TypeNames.LIST) + .addTypeArgument(TypeName.builder(TypeNames.SUPPLIER) + .addTypeArgument(TypeNames.STRING) + .build()) + .build()), is("java.util.List>")); + } + + @Test + void testWildcardType() throws IOException { + assertThat(write(TypeName.builder(TypeNames.SUPPLIER) + .addTypeArgument(TypeName.builder(TypeName.create( + "io.helidon.inject.api.InjectionPointInfo")) + .wildcard(true) + .build()) + .build()), + is("java.util.function.Supplier")); + } + + private String write(TypeName typeName) throws IOException { + Type classModelType = Type.fromTypeName(typeName); + StringWriter stringWriter = new StringWriter(); + ModelWriter modelWriter = new ModelWriter(stringWriter, ""); + classModelType.writeComponent(modelWriter, Set.of(), ImportOrganizer.builder() + .packageName("io.helidon.tests") + .typeName("MyType") + .build(), ClassType.CLASS); + + return stringWriter.toString(); + } +} diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java new file mode 100644 index 00000000000..e9f763a40a1 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.classmodel; + +import java.lang.annotation.ElementType; +import java.util.List; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class TypesCodegenTest { + @Test + void testIt() { + Annotation annotation = Annotation.builder() + .typeName(TypeName.create("io.helidon.RandomAnnotation")) + .putValue("string", "value1") + .putValue("boolean", true) + .putValue("long", 49L) + .putValue("double", 49.0D) + .putValue("integer", 49) + .putValue("byte", (byte) 49) + .putValue("char", 'x') + .putValue("short", (short) 49) + .putValue("float", 49.0F) + .putValue("class", TypesCodegenTest.class) + .putValue("type", TypeName.create(TypesCodegenTest.class)) + .putValue("enum", ElementType.FIELD) + .putValue("lstring", List.of("value1", "value2")) + .putValue("lboolean", List.of(true, false)) + .putValue("llong", List.of(49L, 50L)) + .putValue("ldouble", List.of(49.0, 50.0)) + .putValue("linteger", List.of(49, 50)) + .putValue("lbyte", List.of((byte) 49, (byte) 50)) + .putValue("lchar", List.of('x', 'y')) + .putValue("lshort", List.of((short) 49, (short) 50)) + .putValue("lfloat", List.of(49.0F, 50.0F)) + .putValue("lclass", List.of(TypesCodegenTest.class, TypesCodegenTest.class)) + .putValue("ltype", + List.of(TypeName.create(TypesCodegenTest.class), TypeName.create(TypesCodegenTest.class))) + .putValue("lenum", List.of(ElementType.FIELD, ElementType.MODULE)) + .build(); + + TestContentBuilder contentBuilder = new TestContentBuilder(); + ContentSupport.addCreateAnnotation(contentBuilder, annotation); + String createString = contentBuilder.generatedString(); + + assertThat(createString.replaceAll(" {4}", ""), + is(""" + @io.helidon.common.types.Annotation@.builder() + .typeName(@io.helidon.common.types.TypeName@.create("io.helidon.RandomAnnotation")) + .putValue("string", "value1") + .putValue("boolean", true) + .putValue("long", 49L) + .putValue("double", 49.0D) + .putValue("integer", 49) + .putValue("byte", (byte)49) + .putValue("char", 'x') + .putValue("short", (short)49) + .putValue("float", 49.0F) + .putValue("class", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) + .putValue("type", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) + .putValue("enum", @java.lang.annotation.ElementType@.FIELD) + .putValue("lstring", @java.util.List@.of("value1","value2")) + .putValue("lboolean", @java.util.List@.of(true,false)) + .putValue("llong", @java.util.List@.of(49L,50L)) + .putValue("ldouble", @java.util.List@.of(49.0D,50.0D)) + .putValue("linteger", @java.util.List@.of(49,50)) + .putValue("lbyte", @java.util.List@.of((byte)49,(byte)50)) + .putValue("lchar", @java.util.List@.of('x','y')) + .putValue("lshort", @java.util.List@.of((short)49,(short)50)) + .putValue("lfloat", @java.util.List@.of(49.0F,50.0F)) + .putValue("lclass", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) + .putValue("ltype", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) + .putValue("lenum", @java.util.List@.of(@java.lang.annotation.ElementType@.FIELD,@java.lang.annotation.ElementType@.MODULE)) + .build()""")); + } +} diff --git a/codegen/codegen/pom.xml b/codegen/codegen/pom.xml new file mode 100644 index 00000000000..7bba6e30cd0 --- /dev/null +++ b/codegen/codegen/pom.xml @@ -0,0 +1,58 @@ + + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + + + helidon-codegen + Helidon Codegen + Code generation common utilities + + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen-class-model + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ClassCode.java b/codegen/codegen/src/main/java/io/helidon/codegen/ClassCode.java new file mode 100644 index 00000000000..18f0688c912 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ClassCode.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; + +/** + * A code generated type. + * + * @param newType the type that is to be created + * @param classModel class code + * @param mainTrigger main type responsible for this code generation + * @param originatingElements to map to source types that triggered this code generation + */ +public record ClassCode(TypeName newType, ClassModel.Builder classModel, TypeName mainTrigger, Object... originatingElements) { +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java new file mode 100644 index 00000000000..844149801b6 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/Codegen.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Central piece of code processing and generation. + * This type loads {@link io.helidon.codegen.spi.CodegenExtensionProvider extension providers}, and invokes + * each {@link io.helidon.codegen.spi.CodegenExtension} with appropriate types and annotations. + */ +public class Codegen { + private static final List EXTENSIONS = + HelidonServiceLoader.create(ServiceLoader.load(CodegenExtensionProvider.class, + Codegen.class.getClassLoader())) + .asList(); + private static final Set> SUPPORTED_APT_OPTIONS; + + static { + Set> supportedOptions = EXTENSIONS.stream() + .flatMap(it -> it.supportedOptions().stream()) + .collect(Collectors.toSet()); + supportedOptions.add(CodegenOptions.CODEGEN_SCOPE); + supportedOptions.add(CodegenOptions.INDENT_TYPE); + supportedOptions.add(CodegenOptions.INDENT_COUNT); + + SUPPORTED_APT_OPTIONS = Set.copyOf(supportedOptions); + } + + private final Map> typeToExtensions = new HashMap<>(); + private final Map> extensionPredicates = new IdentityHashMap<>(); + private final CodegenContext ctx; + private final List extensions; + private final Set supportedAnnotations; + private final Set supportedPackagePrefixes; + + private Codegen(CodegenContext ctx, TypeName generator) { + this.ctx = ctx; + + this.extensions = EXTENSIONS.stream() + .map(it -> { + CodegenExtension extension = it.create(this.ctx, generator); + + for (TypeName typeName : it.supportedAnnotations()) { + typeToExtensions.computeIfAbsent(typeName, key -> new ArrayList<>()) + .add(extension); + } + Collection packages = it.supportedAnnotationPackages(); + if (!packages.isEmpty()) { + extensionPredicates.put(extension, discoveryPredicate(packages)); + } + + return extension; + }) + .toList(); + + // handle supported annotations and package prefixes + Set packagePrefixes = new HashSet<>(); + Set annotations = new HashSet<>(ctx.mapperSupportedAnnotations()); + + for (CodegenExtensionProvider extension : EXTENSIONS) { + annotations.addAll(extension.supportedAnnotations()); + + ctx.mapperSupportedAnnotationPackages() + .stream() + .map(Codegen::toPackagePrefix) + .forEach(packagePrefixes::add); + } + ctx.mapperSupportedAnnotationPackages() + .stream() + .map(Codegen::toPackagePrefix) + .forEach(packagePrefixes::add); + + this.supportedAnnotations = Set.copyOf(annotations); + this.supportedPackagePrefixes = Set.copyOf(packagePrefixes); + } + + /** + * Create a new instance of the top level Codegen. + * This type discovers all {@link io.helidon.codegen.spi.CodegenExtensionProvider CodegenExtensionProviders} + * and invokes the provided {@link io.helidon.codegen.spi.CodegenExtension CodegenExtensions} as needed. + * + * @param ctx code processing and generation context + * @param generator type name of the invoking generator (such as maven plugin, annotation procesor, command line tool) + * @return a new codegen instance + */ + public static Codegen create(CodegenContext ctx, TypeName generator) { + Codegen codegen = new Codegen(ctx, generator); + Set> allOptions = new HashSet<>(SUPPORTED_APT_OPTIONS); + allOptions.addAll(ctx.supportedOptions()); + ctx.options().validate(allOptions); + return codegen; + } + + /** + * Set of supported options by all extensions. + * + * @return supported options + */ + public static Set> supportedOptions() { + return SUPPORTED_APT_OPTIONS; + } + + /** + * Process all types discovered. + * This method analyzes the types and invokes each extension with the correct subset. + * + * @param allTypes all types for this processing round + */ + public void process(List allTypes) { + List toWrite = new ArrayList<>(); + + // type info list will contain all mapped annotations, so this is the state we can do annotation processing on + List annotatedTypes = annotatedTypes(allTypes); + + for (CodegenExtension extension : extensions) { + // and now for each extension, we discover types that contain annotations supported by that extension + // and create a new round context for each extension + + RoundContextImpl roundCtx = createRoundContext(annotatedTypes, extension); + extension.process(roundCtx); + toWrite.addAll(roundCtx.newTypes()); + } + + writeNewTypes(toWrite); + } + + /** + * Finish processing. No additional rounds will be done. + */ + public void processingOver() { + List toWrite = new ArrayList<>(); + + // do processing over in each extension + for (CodegenExtension extension : extensions) { + RoundContextImpl roundCtx = createRoundContext(List.of(), extension); + extension.processingOver(roundCtx); + toWrite.addAll(roundCtx.newTypes()); + } + + // if there was any type generated, write it out (will not trigger next round) + writeNewTypes(toWrite); + } + + /** + * A set of annotation types. + * + * @return set of annotations that should be processed + */ + public Set supportedAnnotations() { + return supportedAnnotations; + } + + /** + * A set of package prefixes (expected to end with a {@code .}). + * + * @return set of package prefixes of annotations that should be processed + */ + public Set supportedAnnotationPackagePrefixes() { + return supportedPackagePrefixes; + } + + private static Predicate discoveryPredicate(Collection packages) { + List prefixes = packages.stream() + .map(it -> it.endsWith(".*") ? it.substring(0, it.length() - 2) : it) + .toList(); + return typeName -> { + String packageName = typeName.packageName(); + for (String prefix : prefixes) { + if (packageName.startsWith(prefix)) { + return true; + } + } + return false; + }; + } + + private static String toPackagePrefix(String configured) { + if (configured.endsWith(".*")) { + return configured.substring(0, configured.length() - 1); + } + if (configured.endsWith(".")) { + return configured; + } + return configured + "."; + } + + private List annotatedTypes(List allTypes) { + List result = new ArrayList<>(); + + for (TypeInfo typeInfo : allTypes) { + result.add(new TypeInfoAndAnnotations(typeInfo, annotations(typeInfo))); + } + return result; + } + + private void writeNewTypes(List toWrite) { + // after each round, write all generated types + CodegenFiler filer = ctx.filer(); + + // generate all code + for (var classCode : toWrite) { + ClassModel classModel = classCode.classModel().build(); + filer.writeSourceFile(classModel, classCode.originatingElements()); + } + } + + private RoundContextImpl createRoundContext(List annotatedTypes, CodegenExtension extension) { + Set extAnnots = new HashSet<>(); + Map> extAnnotToType = new HashMap<>(); + Map extTypes = new HashMap<>(); + + for (TypeInfoAndAnnotations annotatedType : annotatedTypes) { + for (TypeName typeName : annotatedType.annotations()) { + boolean added = false; + List validExts = this.typeToExtensions.get(typeName); + if (validExts != null) { + for (CodegenExtension validExt : validExts) { + if (validExt == extension) { + extAnnots.add(typeName); + extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) + .add(annotatedType.typeInfo()); + extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); + added = true; + } + } + } + if (!added) { + Predicate predicate = this.extensionPredicates.get(extension); + if (predicate != null && predicate.test(typeName)) { + extAnnots.add(typeName); + extAnnotToType.computeIfAbsent(typeName, key -> new ArrayList<>()) + .add(annotatedType.typeInfo()); + extTypes.put(annotatedType.typeInfo().typeName(), annotatedType.typeInfo); + } + } + } + } + + return new RoundContextImpl( + Set.copyOf(extAnnots), + Map.copyOf(extAnnotToType), + List.copyOf(extTypes.values())); + } + + private Set annotations(TypeInfo theTypeInfo) { + Set result = new HashSet<>(); + + // on type + theTypeInfo.annotations() + .stream() + .map(Annotation::typeName) + .forEach(result::add); + + // on fields, methods etc. + theTypeInfo.elementInfo() + .stream() + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + // on parameters + theTypeInfo.elementInfo() + .stream() + .map(TypedElementInfo::parameterArguments) + .flatMap(List::stream) + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + return result; + } + + private record TypeInfoAndAnnotations(TypeInfo typeInfo, Set annotations) { + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java new file mode 100644 index 00000000000..5a4b2c80579 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContext.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.codegen.spi.AnnotationMapper; +import io.helidon.codegen.spi.ElementMapper; +import io.helidon.codegen.spi.TypeMapper; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Code processing and generation context. + */ +public interface CodegenContext { + + /** + * Module that is being processed. + * + * @return module info if defined, for modules without {@code module-info.java} returns empty optional + */ + Optional module(); + + /** + * Configured module name using {@link io.helidon.codegen.CodegenOptions#CODEGEN_MODULE}, or name of the + * module if defined from {@link #module()}, or empty if not identified. + * + * @return name of the module + */ + default Optional moduleName() { + return CodegenOptions.CODEGEN_MODULE.findValue(options()) + .or(() -> module().map(ModuleInfo::name)); + } + + /** + * Filer to generate sources and resources. + * + * @return a filer abstraction + */ + CodegenFiler filer(); + + /** + * Logger to log messages according to the environment we run in (Annotation processor, Maven plugin, command line). + * + * @return a logger abstraction + */ + CodegenLogger logger(); + + /** + * Current code generation scope. Usually guessed from the environment, can be overridden using {@link CodegenOptions#CODEGEN_SCOPE} + * + * @return scope + */ + CodegenScope scope(); + + /** + * Code generation options. + * + * @return options of the current environment + */ + CodegenOptions options(); + + /** + * Discover information about the provided type. + * + * @param typeName type name to discover + * @return discovered type information, or empty if the type cannot be discovered + */ + Optional typeInfo(TypeName typeName); + + /** + * Discover information about the provided type, with a predicate for child elements. + * + * @param typeName type name to discover + * @param elementPredicate predicate for child elements + * @return discovered type information, or empty if the type cannot be discovered + */ + Optional typeInfo(TypeName typeName, Predicate elementPredicate); + + /** + * List of available element mappers in this environment. + * Used for example when discovering {@link #typeInfo(io.helidon.common.types.TypeName)}. + * + * @return list of mapper + */ + List elementMappers(); + + /** + * List of available type mappers in this environment. + * Used for example when discovering {@link #typeInfo(io.helidon.common.types.TypeName)}. + * + * @return list of mapper + */ + List typeMappers(); + + /** + * List of available annotation mappers in this environment. + * Used for example when discovering {@link #typeInfo(io.helidon.common.types.TypeName)}. + * + * @return list of mapper + */ + List annotationMappers(); + + /** + * Annotations supported by the mappers. This is augmented by the annotations supported by all extensions and used + * to discover types. + * + * @return set of annotation types supported by the mapper + */ + Set mapperSupportedAnnotations(); + + /** + * Annotation packages supported by the mappers. + * This is augmented by the annotation packages supported by all extensions and used + * to discover types. + * + * @return set of annotation packages + */ + Set mapperSupportedAnnotationPackages(); + + /** + * Codegen options supported by the mappers. + * This is augmented by the options supported by all extensions. + * + * @return set of supported options + */ + Set> supportedOptions(); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java new file mode 100644 index 00000000000..711644ca5a8 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextBase.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; + +import io.helidon.codegen.spi.AnnotationMapper; +import io.helidon.codegen.spi.AnnotationMapperProvider; +import io.helidon.codegen.spi.CodegenProvider; +import io.helidon.codegen.spi.ElementMapper; +import io.helidon.codegen.spi.ElementMapperProvider; +import io.helidon.codegen.spi.TypeMapper; +import io.helidon.codegen.spi.TypeMapperProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.TypeName; + +/** + * Base of codegen context implementation taking care of the common parts of the API. + */ +public abstract class CodegenContextBase implements CodegenContext { + private final List elementMappers; + private final List typeMappers; + private final List annotationMappers; + private final Set> supportedOptions; + private final Set supportedPackages; + private final Set supportedAnnotations; + private final CodegenOptions options; + private final CodegenFiler filer; + private final CodegenLogger logger; + private final CodegenScope scope; + + /** + * Create a new instance with the common parts of the API. + * + * @param options codegen options for the current environment + * @param additionalOptions additional options to add to the list of supported options + * @param filer filer abstraction for the current environment + * @param logger logger abstraction for the current environment + * @param scope scope of the current environment + */ + protected CodegenContextBase(CodegenOptions options, + Set> additionalOptions, + CodegenFiler filer, + CodegenLogger logger, + CodegenScope scope) { + this.options = options; + this.filer = filer; + this.logger = logger; + this.scope = scope; + Set> supportedOptions = new HashSet<>(additionalOptions); + Set supportedPackages = new HashSet<>(); + Set supportedAnnotations = new HashSet<>(); + + this.annotationMappers = HelidonServiceLoader.create( + ServiceLoader.load(AnnotationMapperProvider.class, + CodegenContextBase.class.getClassLoader())) + .stream() + .peek(it -> addSupported(it, supportedOptions, supportedPackages, supportedAnnotations)) + .map(it -> it.create(options)) + .toList(); + + this.elementMappers = HelidonServiceLoader.create( + ServiceLoader.load(ElementMapperProvider.class, + CodegenContextBase.class.getClassLoader())) + .stream() + .peek(it -> addSupported(it, supportedOptions, supportedPackages, supportedAnnotations)) + .map(it -> it.create(options)) + .toList(); + + this.typeMappers = HelidonServiceLoader.create( + ServiceLoader.load(TypeMapperProvider.class, + CodegenContextBase.class.getClassLoader())) + .stream() + .peek(it -> addSupported(it, supportedOptions, supportedPackages, supportedAnnotations)) + .map(it -> it.create(options)) + .toList(); + + this.supportedOptions = Set.copyOf(supportedOptions); + this.supportedPackages = Set.copyOf(supportedPackages); + this.supportedAnnotations = Set.copyOf(supportedAnnotations); + + supportedOptions.forEach(it -> it.findValue(options)); + } + + @Override + public List elementMappers() { + return elementMappers; + } + + @Override + public List typeMappers() { + return typeMappers; + } + + @Override + public List annotationMappers() { + return annotationMappers; + } + + @Override + public Set mapperSupportedAnnotations() { + return supportedAnnotations; + } + + @Override + public Set mapperSupportedAnnotationPackages() { + return supportedPackages; + } + + @Override + public Set> supportedOptions() { + return supportedOptions; + } + + @Override + public CodegenFiler filer() { + return filer; + } + + @Override + public CodegenLogger logger() { + return logger; + } + + @Override + public CodegenScope scope() { + return scope; + } + + @Override + public CodegenOptions options() { + return options; + } + + private static void addSupported(CodegenProvider provider, + Set> supportedOptions, + Set supportedPackages, + Set supportedAnnotations) { + supportedOptions.addAll(provider.supportedOptions()); + supportedAnnotations.addAll(provider.supportedAnnotations()); + provider.supportedAnnotationPackages() + .stream() + .map(it -> it.endsWith(".*") ? it : it + ".*") + .forEach(supportedPackages::add); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java new file mode 100644 index 00000000000..e2055d1e816 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenContextDelegate.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.codegen.spi.AnnotationMapper; +import io.helidon.codegen.spi.ElementMapper; +import io.helidon.codegen.spi.TypeMapper; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Base of codegen context implementation that delegates common parts of the API to an existing instance. + */ +public abstract class CodegenContextDelegate implements CodegenContext { + private final CodegenContext delegate; + + /** + * Create a new instance delegating all calls to the delegate. + * + * @param delegate to use for all methods + */ + protected CodegenContextDelegate(CodegenContext delegate) { + this.delegate = delegate; + } + + @Override + public Optional module() { + return delegate.module(); + } + + @Override + public CodegenFiler filer() { + return delegate.filer(); + } + + @Override + public CodegenLogger logger() { + return delegate.logger(); + } + + @Override + public CodegenScope scope() { + return delegate.scope(); + } + + @Override + public CodegenOptions options() { + return delegate.options(); + } + + @Override + public Optional typeInfo(TypeName typeName) { + return delegate.typeInfo(typeName); + } + + @Override + public Optional typeInfo(TypeName typeName, Predicate elementPredicate) { + return delegate.typeInfo(typeName, elementPredicate); + } + + @Override + public List elementMappers() { + return delegate.elementMappers(); + } + + @Override + public List typeMappers() { + return delegate.typeMappers(); + } + + @Override + public List annotationMappers() { + return delegate.annotationMappers(); + } + + @Override + public Set mapperSupportedAnnotations() { + return delegate.mapperSupportedAnnotations(); + } + + @Override + public Set mapperSupportedAnnotationPackages() { + return delegate.mapperSupportedAnnotationPackages(); + } + + @Override + public Set> supportedOptions() { + return delegate.supportedOptions(); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEvent.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEvent.java new file mode 100644 index 00000000000..163b4bbf877 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEvent.java @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.Errors; + +/** + * An event happening during code gen to be logged with {@link io.helidon.codegen.CodegenLogger#log(CodegenEvent)}. + * This is not a fast solution, it is only to be used when processing code, where + * we can have a bit of an overhead! + * + * @see #builder() + */ +public interface CodegenEvent extends CodegenEventBlueprint { + + /** + * Create a new fluent API builder to customize configuration. + * + * @return a new builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Create a new fluent API builder from an existing instance. + * + * @param instance an existing instance used as a base for the builder + * @return a builder based on an instance + */ + static Builder builder(CodegenEvent instance) { + return CodegenEvent.builder().from(instance); + } + + /** + * Fluent API builder base for {@link io.helidon.codegen.CodegenEvent}. + * + * @param type of the builder extending this abstract builder + */ + abstract class BuilderBase> implements io.helidon.common.Builder { + + private final List objects = new ArrayList<>(); + private System.Logger.Level level = System.Logger.Level.INFO; + private String message; + private Throwable throwable; + + /** + * Protected to support extensibility. + */ + protected BuilderBase() { + } + + /** + * Update this builder from an existing prototype instance. + * + * @param prototype existing prototype to update this builder from + * @return updated builder instance + */ + public BUILDER from(CodegenEvent prototype) { + level(prototype.level()); + message(prototype.message()); + throwable(prototype.throwable()); + addObjects(prototype.objects()); + return identity(); + } + + /** + * Update this builder from an existing prototype builder instance. + * + * @param builder existing builder prototype to update this builder from + * @return updated builder instance + */ + public BUILDER from(BuilderBase builder) { + level(builder.level()); + builder.message().ifPresent(this::message); + builder.throwable().ifPresent(this::throwable); + addObjects(builder.objects()); + return identity(); + } + + /** + * Level can be used directly (command line tools), mapped to Maven level (maven plugins), + * or mapped to diagnostics kind (annotation processing). + *

+ * Mapping table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Level mappings
LevelMaven log levelAPT Diagnostic.Kind
ERRORerrorERROR
WARNINGwarnWARNING
INFOinfoNOTE
DEBUG, TRACEdebugN/A - only logged to logger
+ * + * @param level level to use, defaults to INFO + * @return updated builder instance + * @see #level() + */ + public BUILDER level(System.Logger.Level level) { + Objects.requireNonNull(level); + this.level = level; + return identity(); + } + + /** + * Message to be delivered to the user. + * + * @param message the message + * @return updated builder instance + * @see #message() + */ + public BUILDER message(String message) { + Objects.requireNonNull(message); + this.message = message; + return identity(); + } + + /** + * Clear existing value of this property. + * + * @return updated builder instance + * @see #throwable() + */ + public BUILDER clearThrowable() { + this.throwable = null; + return identity(); + } + + /** + * Throwable if available. + * + * @param throwable throwable + * @return updated builder instance + * @see #throwable() + */ + public BUILDER throwable(Throwable throwable) { + Objects.requireNonNull(throwable); + this.throwable = throwable; + return identity(); + } + + /** + * Additional information, such as source elements. + * These may or may not be ignored by the final log destination. + *

+ * Expected supported types: + *

    + *
  • APT: {@code Element}, {@code AnnotationMirror}, {@code AnnotationValue}
  • + *
  • Classpath scanning: {@code ClassInfo}, {@code MethodInfo} etc.
  • + *
  • Any environment: {@link io.helidon.common.types.TypeName}, + * {@link io.helidon.common.types.TypeInfo}, + * or {@link io.helidon.common.types.TypedElementInfo}
  • + *
+ * + * @param objects list of objects causing this event to happen + * @return updated builder instance + * @see #objects() + */ + public BUILDER objects(List objects) { + Objects.requireNonNull(objects); + this.objects.clear(); + this.objects.addAll(objects); + return identity(); + } + + /** + * Additional information, such as source elements. + * These may or may not be ignored by the final log destination. + *

+ * Expected supported types: + *

    + *
  • APT: {@code Element}, {@code AnnotationMirror}, {@code AnnotationValue}
  • + *
  • Classpath scanning: {@code ClassInfo}, {@code MethodInfo} etc.
  • + *
  • Any environment: {@link io.helidon.common.types.TypeName}, + * {@link io.helidon.common.types.TypeInfo}, + * or {@link io.helidon.common.types.TypedElementInfo}
  • + *
+ * + * @param objects list of objects causing this event to happen + * @return updated builder instance + * @see #objects() + */ + public BUILDER addObjects(List objects) { + Objects.requireNonNull(objects); + this.objects.addAll(objects); + return identity(); + } + + /** + * Additional information, such as source elements. + * These may or may not be ignored by the final log destination. + *

+ * Expected supported types: + *

    + *
  • APT: {@code Element}, {@code AnnotationMirror}, {@code AnnotationValue}
  • + *
  • Classpath scanning: {@code ClassInfo}, {@code MethodInfo} etc.
  • + *
  • Any environment: {@link io.helidon.common.types.TypeName}, + * {@link io.helidon.common.types.TypeInfo}, + * or {@link io.helidon.common.types.TypedElementInfo}
  • + *
+ * + * @param object list of objects causing this event to happen + * @return updated builder instance + * @see #objects() + */ + public BUILDER addObject(Object object) { + Objects.requireNonNull(object); + this.objects.add(object); + return identity(); + } + + /** + * Level can be used directly (command line tools), mapped to Maven level (maven plugins), + * or mapped to diagnostics kind (annotation processing). + *

+ * Mapping table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Level mappings
LevelMaven log levelAPT Diagnostic.Kind
ERRORerrorERROR
WARNINGwarnWARNING
INFOinfoNOTE
DEBUG, TRACEdebugN/A - only logged to logger
+ * + * @return the level + */ + public System.Logger.Level level() { + return level; + } + + /** + * Message to be delivered to the user. + * + * @return the message + */ + public Optional message() { + return Optional.ofNullable(message); + } + + /** + * Throwable if available. + * + * @return the throwable + */ + public Optional throwable() { + return Optional.ofNullable(throwable); + } + + /** + * Additional information, such as source elements. + * These may or may not be ignored by the final log destination. + *

+ * Expected supported types: + *

    + *
  • APT: {@code Element}, {@code AnnotationMirror}, {@code AnnotationValue}
  • + *
  • Classpath scanning: {@code ClassInfo}, {@code MethodInfo} etc.
  • + *
  • Any environment: {@link io.helidon.common.types.TypeName}, + * {@link io.helidon.common.types.TypeInfo}, + * or {@link io.helidon.common.types.TypedElementInfo}
  • + *
+ * + * @return the objects + */ + public List objects() { + return objects; + } + + @Override + public String toString() { + return "CodegenEventBuilder{" + + "level=" + level + "," + + "message=" + message + "," + + "throwable=" + throwable + "," + + "objects=" + objects + + "}"; + } + + /** + * Handles providers and decorators. + */ + protected void preBuildPrototype() { + } + + /** + * Validates required properties. + */ + protected void validatePrototype() { + Errors.Collector collector = Errors.collector(); + if (message == null) { + collector.fatal(getClass(), "Property \"message\" must not be null, but not set"); + } + collector.collect().checkValid(); + } + + /** + * Throwable if available. + * + * @param throwable throwable + * @return updated builder instance + * @see #throwable() + */ + BUILDER throwable(Optional throwable) { + Objects.requireNonNull(throwable); + this.throwable = throwable.map(Throwable.class::cast).orElse(this.throwable); + return identity(); + } + + /** + * Generated implementation of the prototype, can be extended by descendant prototype implementations. + */ + protected static class CodegenEventImpl implements CodegenEvent { + + private final System.Logger.Level level; + private final List objects; + private final Optional throwable; + private final String message; + + /** + * Create an instance providing a builder. + * + * @param builder extending builder base of this prototype + */ + protected CodegenEventImpl(BuilderBase builder) { + this.level = builder.level(); + this.message = builder.message().get(); + this.throwable = builder.throwable(); + this.objects = List.copyOf(builder.objects()); + } + + @Override + public System.Logger.Level level() { + return level; + } + + @Override + public String message() { + return message; + } + + @Override + public Optional throwable() { + return throwable; + } + + @Override + public List objects() { + return objects; + } + + @Override + public String toString() { + return "CodegenEvent{" + + "level=" + level + "," + + "message=" + message + "," + + "throwable=" + throwable + "," + + "objects=" + objects + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof CodegenEvent other)) { + return false; + } + return Objects.equals(level, other.level()) + && Objects.equals(message, other.message()) + && Objects.equals(throwable, other.throwable()) + && Objects.equals(objects, other.objects()); + } + + @Override + public int hashCode() { + return Objects.hash(level, message, throwable, objects); + } + + } + + } + + /** + * Fluent API builder for {@link io.helidon.codegen.CodegenEvent}. + */ + class Builder extends BuilderBase { + + private Builder() { + } + + @Override + public CodegenEvent build() { + preBuildPrototype(); + validatePrototype(); + return new CodegenEventImpl(this); + } + + } + +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEventBlueprint.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEventBlueprint.java new file mode 100644 index 00000000000..0bbe9fcefb6 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenEventBlueprint.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.Optional; + +/** + * An event happening during code gen. This is not a fast solution, it is only to be used when processing code, where + * we can have a bit of an overhead! + */ +interface CodegenEventBlueprint { + /** + * Level can be used directly (command line tools), mapped to Maven level (maven plugins), + * or mapped to diagnostics kind (annotation processing). + *

+ * Mapping table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Level mappings
LevelMaven log levelAPT Diagnostic.Kind
ERRORerrorERROR
WARNINGwarnWARNING
INFOinfoNOTE
DEBUG, TRACEdebugN/A - only logged to logger
+ * + * @return level to use, defaults to INFO + */ + System.Logger.Level level(); + + /** + * Message to be delivered to the user. + * + * @return the message + */ + String message(); + + /** + * Throwable if available. + * + * @return throwable + */ + Optional throwable(); + + /** + * Additional information, such as source elements. + * These may or may not be ignored by the final log destination. + *

+ * Expected supported types: + *

    + *
  • APT: {@code Element}, {@code AnnotationMirror}, {@code AnnotationValue}
  • + *
  • Classpath scanning: {@code ClassInfo}, {@code MethodInfo} etc.
  • + *
  • Any environment: {@link io.helidon.common.types.TypeName}, + * {@link io.helidon.common.types.TypeInfo}, + * or {@link io.helidon.common.types.TypedElementInfo}
  • + *
+ * @return list of objects causing this event to happen + */ + List objects(); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenException.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenException.java new file mode 100644 index 00000000000..0b65f5e891a --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenException.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Optional; + +/** + * An exception for any code processing and generation tools. + * This exception can hold {@link #originatingElement()} that may be used to provide more information to the user. + */ +public class CodegenException extends RuntimeException { + private final Object originatingElement; + + /** + * Constructor with a message. + * + * @param message descriptive message + */ + public CodegenException(String message) { + super(message); + this.originatingElement = null; + } + + /** + * Constructor with a message and a cause. + * + * @param message descriptive message + * @param cause throwable triggering this exception + */ + public CodegenException(String message, Throwable cause) { + super(message, cause); + this.originatingElement = null; + } + + /** + * Constructor with a message and an originating element. + * + * @param message descriptive message + * @param originatingElement element that caused this exception + */ + public CodegenException(String message, Object originatingElement) { + super(message); + this.originatingElement = originatingElement; + } + + /** + * Constructor with a message, cause, and an originating element. + * + * @param message descriptive message + * @param cause throwable triggering this exception + * @param originatingElement element that caused this exception + */ + public CodegenException(String message, Throwable cause, Object originatingElement) { + super(message, cause); + this.originatingElement = originatingElement; + } + + /** + * Originating element. + * This may be an annotation processing element, a classpath scanning {@code ClassInfo}, or a + * {@link io.helidon.common.types.TypeName}. + * Not type will cause an exception, each environment may check the instance and use it or not. + * + * @return originating element of this exception + */ + public Optional originatingElement() { + return Optional.ofNullable(originatingElement); + } + + /** + * Create a codegen event to log with {@link io.helidon.codegen.CodegenLogger#log(CodegenEvent)}. + * + * @param level log level to use + * @param message additional message describing the location + * @return a new codegen event that can be directly logged + */ + public CodegenEvent toEvent(System.Logger.Level level, String message) { + return CodegenEvent.builder() + .level(level) + .message(message) + .throwable(this) + .update(it -> originatingElement().ifPresent(it::addObject)) + .build(); + } + + /** + * Create a codegen event to log with {@link io.helidon.codegen.CodegenLogger#log(CodegenEvent)}. + * + * @param level log level to use + * @return a new codegen event that can be directly logged + */ + public CodegenEvent toEvent(System.Logger.Level level) { + return CodegenEvent.builder() + .level(level) + .message(getMessage()) + .throwable(this) + .update(it -> originatingElement().ifPresent(it::addObject)) + .build(); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java new file mode 100644 index 00000000000..a54669f0c83 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; + +/** + * An abstraction for writing out source files and resource files. + * Always attempts to create a new file and replace its content (as it is impossible to update files in annotation processing). + */ +public interface CodegenFiler { + /** + * Write a source file from its {@link io.helidon.codegen.classmodel.ClassModel}. + * + * @param classModel class model to write out + * @param originatingElements elements that caused this type to be generated + * (you can use {@link io.helidon.common.types.TypeInfo#originatingElement()} for example + * @return written path, we expect to always run on local file system + */ + Path writeSourceFile(ClassModel classModel, Object... originatingElements); + + /** + * Write a resource file. + * + * @param resource bytes of the resource file + * @param location location to write to in the classes output directory + * @param originatingElements elements that caused this type to be generated + * @return written path, we expect to always run on local file system + */ + Path writeResource(byte[] resource, String location, Object... originatingElements); + + /** + * Write a {@code META-INF/services} file for a specific provider interface and implementation(s). + * + * @param generator type of the generator (to mention in the generated code) + * @param providerInterface type of the provider interface (and also name of the file to be generated) + * @param providers list of provider implementations to add to the file + * @param originatingElements elements that caused this type to be generated + */ + default void services(TypeName generator, + TypeName providerInterface, + List providers, + Object... originatingElements) { + Objects.requireNonNull(generator); + Objects.requireNonNull(providerInterface); + Objects.requireNonNull(providers); + + String location = "META-INF/services/" + providerInterface.fqName(); + if (providers.isEmpty()) { + throw new CodegenException("List of providers is empty, cannot generate " + location); + } + byte[] resourceBytes = ( + "# " + GeneratedAnnotationHandler.create(generator, + providers.getFirst(), + TypeName.create( + "MetaInfServicesModuleComponent"), + "1", + "") + + "\n" + + providers.stream() + .map(TypeName::declaredName) + .collect(Collectors.joining("\n"))) + .getBytes(StandardCharsets.UTF_8); + + writeResource(resourceBytes, + location, + originatingElements); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenLogger.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenLogger.java new file mode 100644 index 00000000000..54ad9f30930 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenLogger.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +/** + * An abstraction for logging code processing and generation events. + */ +public interface CodegenLogger { + /** + * Create a new logger backed by {@link java.lang.System.Logger}. + * + * @param logger delegate to log all events to + * @return a new {@link io.helidon.codegen.CodegenLogger} backed by the system logger + */ + static CodegenLogger create(System.Logger logger) { + return new SystemLogger(logger); + } + + /** + * Log a new codegen event. + * See {@link io.helidon.codegen.CodegenEvent} for log level mappings. + * + * @param event to log + */ + void log(CodegenEvent event); + + /** + * Log a new codegen (simple) event. + * See {@link io.helidon.codegen.CodegenEvent} for log level mappings. + * + * @param level log level to use + * @param message message to log + */ + default void log(System.Logger.Level level, String message) { + log(CodegenEvent.builder() + .level(level) + .message(message) + .build()); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenOptions.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenOptions.java new file mode 100644 index 00000000000..20626efc9cf --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenOptions.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import io.helidon.common.GenericType; + +/** + * Configuration options. + */ +public interface CodegenOptions { + /** + * Tag to define custom module name. + */ + String TAG_CODEGEN_MODULE = "helidon.codegen.module-name"; + /** + * Tag to define custom package name. + */ + String TAG_CODEGEN_PACKAGE = "helidon.codegen.package-name"; + + /** + * Codegen option to configure codegen scope. + */ + Option CODEGEN_SCOPE = Option.create("helidon.codegen.scope", + "Override scope that is \"guessed\" from the " + + "environment. By default we support " + + "production and test scopes", + CodegenScope.PRODUCTION, + CodegenScope::new, + GenericType.create(CodegenScope.class)); + /** + * Codegen option to configure module name of the module being processed. + */ + Option CODEGEN_MODULE = Option.create(TAG_CODEGEN_MODULE, + "Override name of the module that is being processed, or provide it" + + " if this module does not have a module-info.java", + ""); + + /** + * Codegen option to configure module name of the module being processed. + */ + Option CODEGEN_PACKAGE = Option.create(TAG_CODEGEN_PACKAGE, + "Define package to use for generated types.", + ""); + /** + * Codegen option to configure which indent type to use (a space character, or a tab character). + */ + Option INDENT_TYPE = Option.create("helidon.codegen.indent.type", + "Type of indentation, either of " + Arrays.toString(IndentType.values()), + IndentType.SPACE, + IndentType::valueOf, + GenericType.create(IndentType.class)); + /** + * Codegen option to configure how many times to repeat the {@link #INDENT_TYPE} for indentation. + *

+ * Defaults to {@code 4}. + */ + Option INDENT_COUNT = Option.create("helidon.codegen.indent.count", + "Number of indents to use (such as 4, if combined with SPACE will indent by 4 " + + "spaces", + 4); + + /** + * Codegen option to configure creation of META-INF services when module-info.java is present. + * Defaults to {@code true}, so the file is generated. + */ + Option CREATE_META_INF_SERVICES = Option.create("helidon.codegen.meta-inf.services", + "Whether to create META-INF/services for generated services even " + + "if module-info.java is present", + true); + + /** + * Find an option. + * + * @param option option name + * @return option value if configured + */ + Optional option(String option); + + /** + * Enumeration option. + * + * @param option option name + * @param defaultValue default value + * @param enumType type of the enum + * @param type of the enum + * @return option value, or default value if not defined + * @throws IllegalArgumentException in case the enum value is not valid for the provided enum type + */ + default > T option(String option, T defaultValue, Class enumType) { + return option(option) + .map(it -> Enum.valueOf(enumType, it)) + .orElse(defaultValue); + } + + /** + * Boolean option that defaults to false. + * + * @param option option to check + * @return whether the option is enabled (e.g. its value is explicitly configured to {@code true}) + */ + default boolean enabled(Option option) { + return option.value(this); + } + + /** + * List of string options. + * + * @param option option name + * @return list of values, or an empty list if not defined + */ + default List asList(String option) { + return option(option) + .stream()// stream of string (option value) + .map(it -> it.split(",")) // split to array + .flatMap(Stream::of) // stream from array + .map(String::trim) // trim each element + .toList(); + } + + /** + * Set of string options. + * + * @param option option name + * @return set of values, or an empty list if not defined + */ + default Set asSet(String option) { + return Set.copyOf(asList(option)); + } + + /** + * Validate options against the permitted options. The implementations in Helidon only validate + * {@code helidon.} prefixed options. + * + * @param permittedOptions options permitted by the codegen in progress + */ + default void validate(Set> permittedOptions) { + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenScope.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenScope.java new file mode 100644 index 00000000000..a7a9db1a976 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenScope.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import static io.helidon.codegen.CodegenUtil.capitalize; + +/** + * Scope of the current code generation session. + * + * @param name name of the scope, use empty string for production scope, see {@link #PRODUCTION}, use lower case names + */ +public record CodegenScope(String name) { + /** + * Production scope. + */ + public static final CodegenScope PRODUCTION = new CodegenScope(""); + + /** + * Whether this is a production scope. + * + * @return if production + */ + public boolean isProduction() { + return name.isBlank(); + } + + /** + * Type prefix for this scope. + * + * @return the scope name with first letter as capital + */ + public String prefix() { + if (isProduction()) { + return name; + } + return capitalize(name); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenUtil.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenUtil.java new file mode 100644 index 00000000000..eb6926bcaa0 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Locale; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +/** + * Tools for generating code. + */ +public final class CodegenUtil { + private CodegenUtil() { + } + + /** + * Capitalize the first letter of the provided string. + * + * @param name string to capitalize + * @return name with the first character as capital letter + */ + public static String capitalize(String name) { + if (name.isBlank() || name.isEmpty()) { + return name; + } + char first = name.charAt(0); + first = Character.toUpperCase(first); + return first + name.substring(1); + } + + /** + * Create a constant field for a name of an element. + *

+ * For example for {@code maxInitialLineLength} we would get + * {@code MAX_INITIAL_LINE_LENGTH}. + * + * @param elementName name of the element + * @return name of a constant + */ + public static String toConstantName(String elementName) { + /* + Method name is camel case (such as maxInitialLineLength) + result is constant like (such as MAX_INITIAL_LINE_LENGTH). + */ + StringBuilder result = new StringBuilder(); + + char[] chars = elementName.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + char aChar = chars[i]; + + if (Character.isUpperCase(aChar)) { + if (!result.isEmpty() && Character.isLowerCase(chars[i - 1])) { + result.append('_') + .append(Character.toLowerCase(aChar)); + } else { + result.append(aChar); + } + } else if (Character.isLowerCase(aChar)) { + result.append(aChar); + } else if (Character.isDigit(aChar)) { + result.append(aChar); + } else { + // not a character, replace with underscore + result.append('_'); + } + } + + return result.toString().toUpperCase(Locale.ROOT); + } + + /** + * Provides copyright header to be added before package declaration. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @return copyright string (can be multiline) + */ + public static String copyright(TypeName generator, TypeName trigger, TypeName generatedType) { + return CopyrightHandler.copyright(generator, trigger, generatedType); + } + + /** + * Create a generated annotation. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @param versionId version of the generator + * @param comments additional comments, never use null (use empty string so they do not appear in annotation) + * @return a new annotation to add to the generated type + */ + public static Annotation generatedAnnotation(TypeName generator, + TypeName trigger, + TypeName generatedType, + String versionId, + String comments) { + return GeneratedAnnotationHandler.create(generator, trigger, generatedType, versionId, comments); + } + +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CopyrightHandler.java b/codegen/codegen/src/main/java/io/helidon/codegen/CopyrightHandler.java new file mode 100644 index 00000000000..6824b0f934c --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CopyrightHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ServiceLoader; + +import io.helidon.codegen.spi.CopyrightProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.TypeName; + +/** + * Handle copyright for generated types. + */ +final class CopyrightHandler { + private static final CopyrightProvider PROVIDER = + HelidonServiceLoader.builder(ServiceLoader.load(CopyrightProvider.class, + CopyrightHandler.class.getClassLoader())) + .addService(new DefaultProvider(), 0) + .build() + .iterator() + .next(); + + private CopyrightHandler() { + } + + /** + * Provides copyright header to be added before package declaration. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @return copyright string (can be multiline) + */ + static String copyright(TypeName generator, TypeName trigger, TypeName generatedType) { + return PROVIDER.copyright(generator, trigger, generatedType); + } + + private static final class DefaultProvider implements CopyrightProvider { + @Override + public String copyright(TypeName generator, TypeName trigger, TypeName generatedType) { + return "// This is a generated file (powered by Helidon). " + "Do not edit or extend from this artifact as it is " + + "subject to change at any time!"; + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java new file mode 100644 index 00000000000..007f272f325 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ElementInfoPredicates.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.function.Predicate; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +/** + * Commonly used predicates to filter typed element info. + * + * @see io.helidon.common.types.TypedElementInfo + * @see io.helidon.common.types.TypeInfo#elementInfo() + */ +public final class ElementInfoPredicates { + /** + * A predicate that accepts all. + */ + public static final Predicate ALL_PREDICATE = new AllPredicate(); + + /** + * Predicate for method element kind. + * + * @param element typed element info to test + * @return whether the element represents a method + */ + public static boolean isMethod(TypedElementInfo element) { + return ElementKind.METHOD == element.kind(); + } + + /** + * Predicate for field element kind. + * + * @param element typed element info to test + * @return whether the element represents a field + */ + public static boolean isField(TypedElementInfo element) { + return ElementKind.FIELD == element.kind(); + } + + /** + * Predicate for static modifier. + * + * @param element typed element info to test + * @return whether the element has static modifier + */ + public static boolean isStatic(TypedElementInfo element) { + return element.elementModifiers().contains(Modifier.STATIC); + } + + /** + * Predicate for private modifier. + * + * @param element typed element info to test + * @return whether the element has private modifier + */ + public static boolean isPrivate(TypedElementInfo element) { + return AccessModifier.PRIVATE == element.accessModifier(); + } + + /** + * Predicate for public modifier. + * + * @param element typed element info to test + * @return whether the element has public modifier + */ + public static boolean isPublic(TypedElementInfo element) { + return AccessModifier.PUBLIC == element.accessModifier(); + } + + + /** + * Predicate for default modifier (default methods on interfaces). + * + * @param element typed element info to test + * @return whether the element has default modifier + */ + public static boolean isDefault(TypedElementInfo element) { + return element.elementModifiers().contains(Modifier.DEFAULT); + } + + /** + * Predicate for void methods. + * + * @param element typed element info to test + * @return whether the element has void return type (both primitive and boxed) + */ + public static boolean isVoid(TypedElementInfo element) { + TypeName typeName = element.typeName(); + return TypeNames.PRIMITIVE_VOID.equals(typeName) || TypeNames.BOXED_VOID.equals(typeName); + } + + + /** + * Predicate for element with no arguments (suitable for methods). + * + * @param element typed element info to test + * @return whether the element has no arguments + */ + public static boolean hasNoArgs(TypedElementInfo element) { + return element.parameterArguments().isEmpty(); + } + + /** + * Predicate for an existence of an annotation. + * + * @param annotation Annotation to check for + * @return a new predicate for the provided annotation + */ + public static Predicate hasAnnotation(TypeName annotation) { + return element -> element.hasAnnotation(annotation); + } + + /** + * Predicate for element name (such as method name, or field name). + * @param name name of the element to check for + * @return a new predicate for the provided element name + */ + public static Predicate elementName(String name) { + return element -> name.equals(element.elementName()); + } + + /** + * Predicate for element with the specified parameters types (suitable for methods). + * The method must have exactly the same number and types of parameters. + * + * @param paramTypes expected parameter types + * @return a new predicate for the provided parameter types + */ + public static Predicate hasParams(TypeName... paramTypes) { + return element -> { + List arguments = element.parameterArguments(); + if (paramTypes.length != arguments.size()) { + return false; + } + for (int i = 0; i < paramTypes.length; i++) { + TypeName paramType = paramTypes[i]; + if (!paramType.equals(arguments.get(i).typeName())) { + return false; + } + } + return true; + }; + } + + /** + * Predicate for element with the specified parameters types (suitable for methods). + * The method must have exactly the same number and types of parameters. + * + * @param paramTypes expected parameter types + * @return a new predicate for the provided parameter types + */ + public static Predicate hasParams(List paramTypes) { + return element -> { + List arguments = element.parameterArguments(); + if (paramTypes.size() != arguments.size()) { + return false; + } + for (int i = 0; i < paramTypes.size(); i++) { + TypeName paramType = paramTypes.get(i); + if (!paramType.equals(arguments.get(i).typeName())) { + return false; + } + } + return true; + }; + } + + + private ElementInfoPredicates() { + } + + private static final class AllPredicate implements Predicate { + @Override + public boolean test(TypedElementInfo typedElementName) { + return true; + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/GeneratedAnnotationHandler.java b/codegen/codegen/src/main/java/io/helidon/codegen/GeneratedAnnotationHandler.java new file mode 100644 index 00000000000..f55abceff12 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/GeneratedAnnotationHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ServiceLoader; + +import io.helidon.codegen.spi.GeneratedAnnotationProvider; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +/** + * Support for generated annotation. + */ +final class GeneratedAnnotationHandler { + private static final GeneratedAnnotationProvider PROVIDER = HelidonServiceLoader.builder(ServiceLoader.load( + GeneratedAnnotationProvider.class)) + .addService(new DefaultProvider(), 0) + .build() + .iterator() + .next(); + + private GeneratedAnnotationHandler() { + } + + /** + * Create a generated annotation. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @param versionId version of the generator + * @param comments additional comments, never use null (use empty string so they do not appear in annotation) + * @return a new annotation to add to the generated type + */ + static Annotation create(TypeName generator, + TypeName trigger, + TypeName generatedType, + String versionId, + String comments) { + return PROVIDER.create(generator, trigger, generatedType, versionId, comments); + } + + // @Generated(value = "io.helidon.inject.tools.ActivatorCreatorDefault", comments = "version=1") + private static class DefaultProvider implements GeneratedAnnotationProvider { + @Override + public Annotation create(TypeName generator, + TypeName trigger, + TypeName generatedType, + String versionId, + String comments) { + return Annotation.builder() + .typeName(TypeNames.GENERATED) + .putValue("value", generator.resolvedName()) + .putValue("trigger", trigger.resolvedName()) + .update(it -> { + if (!"1".equals(versionId)) { + it.putValue("version", versionId); + } + }) + .update(it -> { + if (!comments.isBlank()) { + it.putValue("comments", comments); + } + }) + .build(); + } + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/IndentType.java b/codegen/codegen/src/main/java/io/helidon/codegen/IndentType.java new file mode 100644 index 00000000000..6dd957dc774 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/IndentType.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +/** + * Indentation kind. + */ +public enum IndentType { + /** + * Use spaces to indent generated source code. + */ + SPACE(' '), + /** + * Use tabulators to indent generated source code. + */ + TAB('\t'); + + private final char character; + + IndentType(char character) { + this.character = character; + } + + /** + * Indentation character to use. + * + * @return the character + */ + public char character() { + return character; + } +} + diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ListOptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/ListOptionImpl.java new file mode 100644 index 00000000000..704221b8583 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ListOptionImpl.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import io.helidon.common.GenericType; + +final class ListOptionImpl implements Option> { + private final String name; + private final String description; + private final List defaultValue; + private final Function mapFunction; + private final GenericType> type; + + ListOptionImpl(String name, + String description, + List defaultValue, + Function mapFunction, + GenericType> type) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + this.mapFunction = mapFunction; + this.type = type; + } + + @Override + public GenericType> type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return description; + } + + @Override + public List defaultValue() { + return defaultValue; + } + + @Override + public Optional> findValue(CodegenOptions options) { + return options.option(name) + .map(it -> it.split(",")) + .map(this::toList); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ListOptionImpl option)) { + return false; + } + return Objects.equals(name, option.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + private List toList(String[] strings) { + return Stream.of(strings) + .map(String::trim) + .map(mapFunction) + .toList(); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfo.java b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfo.java new file mode 100644 index 00000000000..9b44fbde186 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfo.java @@ -0,0 +1,735 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import io.helidon.common.Errors; +import io.helidon.common.types.TypeName; + +/** + * Module info. + * + * @see #builder() + */ +public interface ModuleInfo { + /** + * The default module name (i.e., "unnamed"). + */ + String DEFAULT_MODULE_NAME = "unnamed"; + /** + * The file name in sources ({@value}). + */ + String FILE_NAME = "module-info.java"; + + /** + * Create a new fluent API builder to customize configuration. + * + * @return a new builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Create a new fluent API builder from an existing instance. + * + * @param instance an existing instance used as a base for the builder + * @return a builder based on an instance + */ + static Builder builder(ModuleInfo instance) { + return ModuleInfo.builder().from(instance); + } + + /** + * Name of the module. + * + * @return module name + */ + String name(); + + /** + * Whether this module is declared as open module. + * + * @return whether this module is open + */ + boolean isOpen(); + + /** + * Declared dependencies of the module. + * + * @return list of requires + */ + List requires(); + + /** + * Exports of the module. + * + * @return map of exported packages (exports x.y.z to a.b.cSomeModule). + */ + Map> exports(); + + /** + * Used service loader providers. + * + * @return list of used provider interfaces + */ + List uses(); + + /** + * Map of provider interfaces to provider implementations provided by this module. + * + * @return map of interface to implementations + */ + Map> provides(); + + /** + * Map of opened packages to modules (if any). + * + * @return map of package to modules + */ + Map> opens(); + + /** + * first export that does not export to a specific module (if present). + * + * @return package that is exported + */ + default Optional firstUnqualifiedExport() { + return exports().entrySet() + .stream() + .filter(it -> it.getValue().isEmpty()) + .map(Map.Entry::getKey) + .findFirst(); + } + + /** + * Fluent API builder base for {@link io.helidon.codegen.ModuleInfo}. + * + * @param type of the builder extending this abstract builder + */ + abstract class BuilderBase> implements io.helidon.common.Builder { + + private final List uses = new ArrayList<>(); + private final List requires = new ArrayList<>(); + private final Map> exports = new LinkedHashMap<>(); + private final Map> provides = new LinkedHashMap<>(); + private final Map> opens = new LinkedHashMap<>(); + private boolean isOpen = false; + private String name; + + /** + * Protected to support extensibility. + */ + protected BuilderBase() { + } + + /** + * Update this builder from an existing prototype instance. + * + * @param prototype existing prototype to update this builder from + * @return updated builder instance + */ + public BUILDER from(ModuleInfo prototype) { + name(prototype.name()); + isOpen(prototype.isOpen()); + addRequires(prototype.requires()); + addExports(prototype.exports()); + addUses(prototype.uses()); + addProvides(prototype.provides()); + addOpens(prototype.opens()); + return identity(); + } + + /** + * Update this builder from an existing prototype builder instance. + * + * @param builder existing builder prototype to update this builder from + * @return updated builder instance + */ + public BUILDER from(BuilderBase builder) { + builder.name().ifPresent(this::name); + isOpen(builder.isOpen()); + addRequires(builder.requires()); + addExports(builder.exports()); + addUses(builder.uses()); + addProvides(builder.provides()); + addOpens(builder.opens()); + return identity(); + } + + /** + * Name of the module. + * + * @param name module name + * @return updated builder instance + * @see #name() + */ + public BUILDER name(String name) { + Objects.requireNonNull(name); + this.name = name; + return identity(); + } + + /** + * Whether this module is declared as open module. + * + * @param isOpen whether this module is open + * @return updated builder instance + * @see #isOpen() + */ + public BUILDER isOpen(boolean isOpen) { + this.isOpen = isOpen; + return identity(); + } + + /** + * Declared dependencies of the module. + * + * @param requires list of requires + * @return updated builder instance + * @see #requires() + */ + public BUILDER requires(List requires) { + Objects.requireNonNull(requires); + this.requires.clear(); + this.requires.addAll(requires); + return identity(); + } + + /** + * Declared dependencies of the module. + * + * @param requires list of requires + * @return updated builder instance + * @see #requires() + */ + public BUILDER addRequires(List requires) { + Objects.requireNonNull(requires); + this.requires.addAll(requires); + return identity(); + } + + /** + * Declared dependencies of the module. + * + * @param require list of requires + * @return updated builder instance + * @see #requires() + */ + public BUILDER addRequire(ModuleInfoRequires require) { + Objects.requireNonNull(require); + this.requires.add(require); + return identity(); + } + + /** + * Exports of the module. + * + * @param exports list of exported packages + * @return updated builder instance + * @see #exports() + */ + public BUILDER exports(Map> exports) { + Objects.requireNonNull(exports); + this.exports.clear(); + this.exports.putAll(exports); + return identity(); + } + + /** + * Exports of the module. + * + * @param exports list of exported packages + * @return updated builder instance + * @see #exports() + */ + public BUILDER addExports(Map> exports) { + Objects.requireNonNull(exports); + this.exports.putAll(exports); + return identity(); + } + + /** + * This method adds a new value to the map, or replaces it if the key already exists. + * + * @param packageName key to add or replace + * @param moduleNames new value for the key + * @return updated builder instance + * @see #opens() + */ + public BUILDER putExports(String packageName, List moduleNames) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(moduleNames); + this.exports.put(packageName, List.copyOf(moduleNames)); + return identity(); + } + + /** + * Exports of the module. + * + * @param export package to export + * @param to exported to a module + * @return updated builder instance + * @see #exports() + */ + public BUILDER addExport(String export, String to) { + Objects.requireNonNull(export); + Objects.requireNonNull(to); + this.opens.compute(export, (k, v) -> { + v = v == null ? new ArrayList<>() : new ArrayList<>(v); + v.add(to); + return v; + }); + return identity(); + } + + /** + * Used service loader providers. + * + * @param uses list of used provider interfaces + * @return updated builder instance + * @see #uses() + */ + public BUILDER uses(List uses) { + Objects.requireNonNull(uses); + this.uses.clear(); + this.uses.addAll(uses); + return identity(); + } + + /** + * Used service loader providers. + * + * @param uses list of used provider interfaces + * @return updated builder instance + * @see #uses() + */ + public BUILDER addUses(List uses) { + Objects.requireNonNull(uses); + this.uses.addAll(uses); + return identity(); + } + + /** + * Used service loader providers. + * + * @param use list of used provider interfaces + * @return updated builder instance + * @see #uses() + */ + public BUILDER addUse(TypeName use) { + Objects.requireNonNull(use); + this.uses.add(use); + return identity(); + } + + /** + * Used service loader providers. + * + * @param consumer list of used provider interfaces + * @return updated builder instance + * @see #uses() + */ + public BUILDER addUse(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = TypeName.builder(); + consumer.accept(builder); + this.uses.add(builder.build()); + return identity(); + } + + /** + * This method replaces all values with the new ones. + * + * @param provides map of interface to implementations + * @return updated builder instance + * @see #provides() + */ + public BUILDER provides(Map> provides) { + Objects.requireNonNull(provides); + this.provides.clear(); + this.provides.putAll(provides); + return identity(); + } + + /** + * This method keeps existing values, then puts all new values into the map. + * + * @param provides map of interface to implementations + * @return updated builder instance + * @see #provides() + */ + public BUILDER addProvides(Map> provides) { + Objects.requireNonNull(provides); + this.provides.putAll(provides); + return identity(); + } + + /** + * This method adds a new value to the map value, or creates a new value. + * + * @param key key to add to + * @param provide additional value for the key + * @return updated builder instance + * @see #provides() + */ + public BUILDER addProvide(TypeName key, TypeName provide) { + Objects.requireNonNull(key); + Objects.requireNonNull(provide); + this.provides.compute(key, (k, v) -> { + v = v == null ? new ArrayList<>() : new ArrayList<>(v); + v.add(provide); + return v; + }); + return identity(); + } + + /** + * This method adds a new value to the map value, or creates a new value. + * + * @param key key to add to + * @param provides additional values for the key + * @return updated builder instance + * @see #provides() + */ + public BUILDER addProvides(TypeName key, List provides) { + Objects.requireNonNull(key); + Objects.requireNonNull(provides); + this.provides.compute(key, (k, v) -> { + v = v == null ? new ArrayList<>() : new ArrayList<>(v); + v.addAll(provides); + return v; + }); + return identity(); + } + + /** + * This method adds a new value to the map, or replaces it if the key already exists. + * + * @param key key to add or replace + * @param provide new value for the key + * @return updated builder instance + * @see #provides() + */ + public BUILDER putProvide(TypeName key, List provide) { + Objects.requireNonNull(key); + Objects.requireNonNull(provide); + this.provides.put(key, List.copyOf(provide)); + return identity(); + } + + /** + * This method replaces all values with the new ones. + * + * @param opens map of package to modules + * @return updated builder instance + * @see #opens() + */ + public BUILDER opens(Map> opens) { + Objects.requireNonNull(opens); + this.opens.clear(); + this.opens.putAll(opens); + return identity(); + } + + /** + * This method keeps existing values, then puts all new values into the map. + * + * @param opens map of package to modules + * @return updated builder instance + * @see #opens() + */ + public BUILDER addOpens(Map> opens) { + Objects.requireNonNull(opens); + this.opens.putAll(opens); + return identity(); + } + + /** + * This method adds a new value to the map value, or creates a new value. + * + * @param key key to add to + * @param open additional value for the key + * @return updated builder instance + * @see #opens() + */ + public BUILDER addOpen(String key, String open) { + Objects.requireNonNull(key); + Objects.requireNonNull(open); + this.opens.compute(key, (k, v) -> { + v = v == null ? new ArrayList<>() : new ArrayList<>(v); + v.add(open); + return v; + }); + return identity(); + } + + /** + * This method adds a new value to the map value, or creates a new value. + * + * @param key key to add to + * @param opens additional values for the key + * @return updated builder instance + * @see #opens() + */ + public BUILDER addOpens(String key, List opens) { + Objects.requireNonNull(key); + Objects.requireNonNull(opens); + this.opens.compute(key, (k, v) -> { + v = v == null ? new ArrayList<>() : new ArrayList<>(v); + v.addAll(opens); + return v; + }); + return identity(); + } + + /** + * This method adds a new value to the map, or replaces it if the key already exists. + * + * @param key key to add or replace + * @param open new value for the key + * @return updated builder instance + * @see #opens() + */ + public BUILDER putOpen(String key, List open) { + Objects.requireNonNull(key); + Objects.requireNonNull(open); + this.opens.put(key, List.copyOf(open)); + return identity(); + } + + /** + * Name of the module. + * + * @return the name + */ + public Optional name() { + return Optional.ofNullable(name); + } + + /** + * Whether this module is declared as open module. + * + * @return the is open + */ + public boolean isOpen() { + return isOpen; + } + + /** + * Declared dependencies of the module. + * + * @return the requires + */ + public List requires() { + return requires; + } + + /** + * Exports of the module. + * + * @return the exports + */ + public Map> exports() { + return exports; + } + + /** + * Used service loader providers. + * + * @return the uses + */ + public List uses() { + return uses; + } + + /** + * Map of provider interfaces to provider implementations provided by this module. + * + * @return the provides + */ + public Map> provides() { + return provides; + } + + /** + * Map of opened packages to modules (if any). + * + * @return the opens + */ + public Map> opens() { + return opens; + } + + @Override + public String toString() { + return "ModuleInfoBuilder{" + + "name=" + name + "," + + "isOpen=" + isOpen + "," + + "requires=" + requires + "," + + "exports=" + exports + "," + + "uses=" + uses + "," + + "provides=" + provides + "," + + "opens=" + opens + + "}"; + } + + /** + * Handles providers and decorators. + */ + protected void preBuildPrototype() { + } + + /** + * Validates required properties. + */ + protected void validatePrototype() { + Errors.Collector collector = Errors.collector(); + if (name == null) { + collector.fatal(getClass(), "Property \"name\" must not be null, but not set"); + } + collector.collect().checkValid(); + } + + /** + * Generated implementation of the prototype, can be extended by descendant prototype implementations. + */ + protected static class ModuleInfoImpl implements ModuleInfo { + + private final boolean isOpen; + private final List uses; + private final List requires; + private final Map> exports; + private final Map> provides; + private final Map> opens; + private final String name; + + /** + * Create an instance providing a builder. + * + * @param builder extending builder base of this prototype + */ + protected ModuleInfoImpl(ModuleInfo.BuilderBase builder) { + this.name = builder.name().get(); + this.isOpen = builder.isOpen(); + this.requires = List.copyOf(builder.requires()); + this.exports = Map.copyOf(builder.exports()); + this.uses = List.copyOf(builder.uses()); + this.provides = Collections.unmodifiableMap(new LinkedHashMap<>(builder.provides())); + this.opens = Collections.unmodifiableMap(new LinkedHashMap<>(builder.opens())); + } + + @Override + public String name() { + return name; + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public List requires() { + return requires; + } + + @Override + public Map> exports() { + return exports; + } + + @Override + public List uses() { + return uses; + } + + @Override + public Map> provides() { + return provides; + } + + @Override + public Map> opens() { + return opens; + } + + @Override + public String toString() { + return "ModuleInfo{" + + "name=" + name + "," + + "isOpen=" + isOpen + "," + + "requires=" + requires + "," + + "exports=" + exports + "," + + "uses=" + uses + "," + + "provides=" + provides + "," + + "opens=" + opens + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ModuleInfo other)) { + return false; + } + return Objects.equals(name, other.name()) + && isOpen == other.isOpen() + && Objects.equals(requires, other.requires()) + && Objects.equals(exports, other.exports()) + && Objects.equals(uses, other.uses()) + && Objects.equals(provides, other.provides()) + && Objects.equals(opens, other.opens()); + } + + @Override + public int hashCode() { + return Objects.hash(name, isOpen, requires, exports, uses, provides, opens); + } + + } + + } + + /** + * Fluent API builder for {@link io.helidon.codegen.ModuleInfo}. + */ + class Builder extends BuilderBase implements io.helidon.common.Builder { + + private Builder() { + } + + @Override + public ModuleInfo build() { + preBuildPrototype(); + validatePrototype(); + return new ModuleInfoImpl(this); + } + + } + +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoRequires.java b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoRequires.java new file mode 100644 index 00000000000..0faac5113f4 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoRequires.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +/** + * A requires definition of a module-info.java. + * + * @param module module that is required + * @param isTransitive whether the requires is defined as {@code transitive} + * @param isStatic whether the requires is defined as {@code static} + */ +public record ModuleInfoRequires(String module, boolean isTransitive, boolean isStatic) { +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoSourceParser.java b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoSourceParser.java new file mode 100644 index 00000000000..1b4c41ceca8 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/ModuleInfoSourceParser.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.common.types.TypeName; + +/** + * Support for parsing module-info.java sources. + */ +public final class ModuleInfoSourceParser { + private static final String PROVIDES = "provides"; + private static final String OPENS = "opens"; + private static final Pattern ANNOTATION = Pattern.compile("(@\\w+)(.*)"); + private static final Pattern OPENS_PATTERN = Pattern.compile("opens (.*?)(?:\\s+to\\s+(.*?))?"); + private static final Pattern EXPORTS_PATTERN = Pattern.compile("exports (.*?)(?:\\s+to\\s+(.*?))?"); + + private final Map importAliases = new LinkedHashMap<>(); + private final List currentComments = new ArrayList<>(); + + // state of the parser + private State state = State.PRE_IMPORTS; + // state of the parser outside of current comments + private State outState = State.PRE_IMPORTS; + // in progress string + private String current; + // opened brackets counter (used in annotation parsing) + private int bracketsOpened; + + private ModuleInfoSourceParser() { + } + + /** + * Parse the module info from its input stream. + * + * @param inputStream input stream to parse + * @return module info + */ + public static ModuleInfo parse(InputStream inputStream) { + return parse(new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))); + } + + /** + * Parse the module info from its source file. + * + * @param path path to parse + * @return module info + */ + public static ModuleInfo parse(Path path) { + try (InputStream inputStream = Files.newInputStream(path)) { + return parse(inputStream); + } catch (IOException e) { + throw new CodegenException("Failed to read module info from " + path.toAbsolutePath(), e); + } + + } + + static ModuleInfo parse(BufferedReader reader) { + ModuleInfoSourceParser parser = new ModuleInfoSourceParser(); + try { + return parser.doParse(reader); + } catch (IOException e) { + throw new CodegenException("Failed to parse module info", e, ModuleInfoSourceParser.class); + } + } + + private ModuleInfo doParse(BufferedReader reader) throws IOException { + ModuleInfo.Builder builder = ModuleInfo.builder(); + + String line; + while ((line = reader.readLine()) != null && state != State.DONE) { + String inProgress = line; + while (!inProgress.isEmpty() && state != State.DONE) { + inProgress = + switch (state) { + case PRE_IMPORTS, POST_IMPORTS, MODULE_CONTENT, UNKNOWN -> nextState(builder, inProgress); + case M_COMMENTS -> mComments(inProgress); + case IMPORTS -> imports(inProgress); + case ANNOTATION -> annotation(builder, inProgress); + case MODULE_NAME -> moduleName(builder, inProgress); + case REQUIRES -> contentToSemi(builder, inProgress, this::parseRequires); + case EXPORTS -> contentToSemi(builder, inProgress, this::parseExports); + case USES -> contentToSemi(builder, inProgress, this::parseUses); + case PROVIDES -> contentToSemi(builder, inProgress, this::parseProvides); + case OPENS -> contentToSemi(builder, inProgress, this::parseOpens); + default -> throw new CodegenException("Unexpected parsing state: " + state); + }; + } + } + + return builder.build(); + } + + private String annotation(ModuleInfo.Builder builder, String inProgress) { + // we have processed @Something + if (inProgress.isBlank()) { + // next line + return ""; + } + if (inProgress.startsWith("(")) { + bracketsOpened++; + } + int lastIndex = -1; + while (bracketsOpened > 0) { + int index = inProgress.indexOf(')', lastIndex + 1); + if (index == -1) { + break; + } + lastIndex = index; + bracketsOpened--; + } + if (bracketsOpened > 0) { + current = current + " " + inProgress; + return ""; + } + if (lastIndex >= 0) { + return newState(State.POST_IMPORTS, inProgress.substring(lastIndex + 1)); + } + return newState(State.POST_IMPORTS, inProgress); + } + + private String moduleName(ModuleInfo.Builder builder, String inProgress) { + int index = inProgress.indexOf('{'); + if (index > -1) { + parseModuleName(builder, current + " " + inProgress.substring(0, index)); + + return newState(State.MODULE_CONTENT, inProgress.substring(index + 1)); + } + current = current + " " + inProgress; + return ""; + } + + private void parseModuleName(ModuleInfo.Builder builder, String moduleNameString) { + // such as `open module io.helidon.config` + String[] split = moduleNameString.split("\\s+"); + + boolean isOpen = false; + String name = null; + for (String nameElement : split) { + if ("open".equals(nameElement)) { + isOpen = true; + continue; + } + if ("module".equals(nameElement)) { + continue; + } + // last element is expected to be the module name + name = nameElement; + } + if (name == null) { + throw new CodegenException("Cannot discover module name from: " + moduleNameString); + } + builder.name(name); + builder.isOpen(isOpen); + } + + private String contentToSemi(ModuleInfo.Builder builder, + String inProgress, + BiConsumer parseMethod) { + int index = inProgress.indexOf(';'); + if (index > -1) { + parseMethod.accept(builder, current + " " + inProgress.substring(0, index)); + + return newState(State.MODULE_CONTENT, inProgress.substring(index + 1)); + } + current = current + " " + inProgress; + return ""; + } + + private String imports(String inProgress) { + int index = inProgress.indexOf(';'); + if (index > -1) { + parseImport(current + " " + inProgress.substring(0, index)); + + return newState(State.POST_IMPORTS, inProgress.substring(index + 1)); + } + current = current + " " + inProgress; + return ""; + } + + private String mComments(String inProgress) { + int index = inProgress.indexOf("*/"); + if (index > 0) { + state = outState; + String comment = inProgress.substring(0, index).trim(); + if (!comment.isEmpty()) { + currentComments.add(comment); + } + return inProgress.substring(index + 2); + } + currentComments.add(inProgress); + return ""; + } + + private String nextState(ModuleInfo.Builder builder, String inProgress) { + String trimmed = inProgress.trim(); + if (trimmed.isEmpty()) { + return ""; + } + // now we know we have something, let's handle it + if (trimmed.startsWith("/*")) { + // M_COMMENTS + if (trimmed.startsWith("/**/")) { + // empty comment + return trimmed.substring(4); + } + + int begin = 2; + + if (trimmed.startsWith("/**")) { + // javadoc + begin = 3; + } + + int endOfComments = trimmed.indexOf("*/"); + if (endOfComments > 0) { + // end on the same line + String comment = trimmed.substring(begin, endOfComments); + currentComments.add(comment); + return trimmed.substring(endOfComments + 2); + } + String comment = trimmed.substring(begin); + if (!comment.isEmpty()) { + currentComments.add(comment); + } + outState = state; + state = State.M_COMMENTS; + return ""; + } + if (trimmed.startsWith("//")) { + currentComments.add(trimmed.substring(2)); + return ""; + } + if (trimmed.startsWith("import") && (state == State.PRE_IMPORTS || state == State.POST_IMPORTS)) { + if (state == State.PRE_IMPORTS) { + // builder.headerComment(String.join("\n", currentComments)); + currentComments.clear(); + } + + int index = trimmed.indexOf(';'); + if (index > 0) { + // single line import statement + parseImport(trimmed.substring(0, index).trim()); + return newState(State.POST_IMPORTS, trimmed.substring(index + 1)); + } + // beginning of multiline import + return stateContinuation(State.IMPORTS, trimmed); + } + + if (trimmed.startsWith("@")) { + return analyzeAnnotation(builder, trimmed); + } + + if (state == State.PRE_IMPORTS || state == State.POST_IMPORTS) { + // whatever we have, it must be module declaration + // we handle multiline comments, comments, imports, and annotations above this section + if (!currentComments.isEmpty()) { + // builder.descriptionComment(String.join("\n", currentComments)); + currentComments.clear(); + } + int index = trimmed.indexOf('{'); + if (index > 0) { + parseModule(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + return stateContinuation(State.MODULE_NAME, trimmed); + } + + if (trimmed.startsWith("requires") && state == State.MODULE_CONTENT) { + int index = trimmed.indexOf(';'); + if (index > 0) { + parseRequires(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + + return stateContinuation(State.REQUIRES, trimmed); + } + if (trimmed.startsWith("exports") && state == State.MODULE_CONTENT) { + int index = trimmed.indexOf(';'); + if (index > 0) { + parseExports(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + + return stateContinuation(State.EXPORTS, trimmed); + } + if (trimmed.startsWith("uses") && state == State.MODULE_CONTENT) { + int index = trimmed.indexOf(';'); + if (index > 0) { + parseUses(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + + return stateContinuation(State.USES, trimmed); + } + if (trimmed.startsWith("provides") && state == State.MODULE_CONTENT) { + int index = trimmed.indexOf(';'); + if (index > 0) { + parseProvides(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + + return stateContinuation(State.PROVIDES, trimmed); + } + if (trimmed.startsWith("opens") && state == State.MODULE_CONTENT) { + int index = trimmed.indexOf(';'); + if (index > 0) { + parseOpens(builder, trimmed.substring(0, index).trim()); + return trimmed.substring(index + 1); + } + + return stateContinuation(State.OPENS, trimmed); + } + + if (trimmed.startsWith("}")) { + // we are done + return stateContinuation(State.DONE, ""); + } + + // builder.addUnhandledLine(trimmed); + return ""; + } + + /** + * Set new current state, and use the string as the new current value. + * + * @param newState new state + * @param newCurrent value to continue with parsing + * @return empty string + */ + private String stateContinuation(State newState, String newCurrent) { + state = newState; + outState = newState; + current = newCurrent; + return ""; + } + + /** + * Set new current state, and return the remaining line. + * + * @param newState new state + * @param inProgress remainder of the line after parsing current value + * @return inProgress + */ + private String newState(State newState, String inProgress) { + state = newState; + outState = newState; + current = ""; + return inProgress; + } + + private String analyzeAnnotation(ModuleInfo.Builder builder, String annotationString) { + Matcher matcher = ANNOTATION.matcher(annotationString); + if (matcher.matches()) { + current = matcher.group(1); + return newState(State.ANNOTATION, matcher.group(2)); + } else { + throw new CodegenException("Invalid annotation in module-info.java: " + annotationString); + } + } + + private void parseOpens(ModuleInfo.Builder builder, String opensString) { + // opens X to Y, Z + + Matcher m = OPENS_PATTERN.matcher(opensString); + if (m.matches()) { + String first = m.group(1); + String second = m.group(2); + if (second == null) { + builder.putOpen(first, List.of()); + } else { + builder.putOpen(first, Arrays.stream(second.split(",")) + .map(String::trim) + .toList()); + } + } else { + String inProgress = opensString.substring(OPENS.length()).trim(); + int toIndex = inProgress.indexOf("to"); + if (toIndex < 0) { + throw new CodegenException("Cannot parse opens in module-info.java: " + opensString); + } + + String what = inProgress.substring(0, toIndex).trim(); + String to = inProgress.substring(toIndex + 2).trim(); + List toList = Arrays.stream(to.split(",")) + .map(String::trim) + .toList(); + + builder.putOpen(what, toList); + } + } + + private void parseProvides(ModuleInfo.Builder builder, String providesString) { + // provides X with Y, Z + + String inProgress = providesString.substring(PROVIDES.length()).trim(); + int withIndex = inProgress.indexOf("with"); + if (withIndex < 0) { + throw new CodegenException("Cannot parse provides in module-info.java: " + providesString); + } + + String what = checkImports(inProgress.substring(0, withIndex).trim()); + String with = inProgress.substring(withIndex + 5).trim(); + List withList = Arrays.stream(with.split(",")) + .map(String::trim) + .map(this::checkImports) + .map(TypeName::create) + .toList(); + + builder.putProvide(TypeName.create(what), withList); + } + + private void parseUses(ModuleInfo.Builder builder, String usesString) { + String usedType = checkImports(usesString.substring(4).trim()); + + builder.addUse(TypeName.create(usedType)); + } + + private String checkImports(String typeName) { + TypeName imported = importAliases.get(typeName); + return imported == null ? typeName : imported.fqName(); + } + + private void parseExports(ModuleInfo.Builder builder, String exportsString) { + // either "exports package" + Matcher m = EXPORTS_PATTERN.matcher(exportsString); + if (m.matches()) { + String first = m.group(1); + String second = m.group(2); + if (second == null) { + builder.putExports(first, List.of()); + } else { + builder.putExports(first, Arrays.stream(second.split(",")) + .map(String::trim) + .toList()); + } + } else { + // or "exports package to module name (fallback if does not match) + builder.putExports(exportsString.substring(7).trim(), List.of()); + } + } + + private void parseRequires(ModuleInfo.Builder builder, String requiresString) { + boolean isStatic = false; + boolean isTransitive = false; + String target = null; + String[] split = requiresString.split("\\s+"); // split by one or more whitespaces + + for (String element : split) { + if (element.equals("static")) { + isStatic = true; + continue; + } + if (element.equals("transitive")) { + isTransitive = true; + continue; + } + target = element; + } + + if (target == null) { + throw new CodegenException("Failed to parse module-info.java line " + requiresString); + } + + // requires static/transitive something; + builder.addRequire(new ModuleInfoRequires(target, isTransitive, isStatic)); + } + + private void parseModule(ModuleInfo.Builder builder, String moduleString) { + // module some.name + builder.name(moduleString.substring(6).trim()); + state = State.MODULE_CONTENT; + outState = State.MODULE_CONTENT; + } + + private void parseImport(String importStatement) { + // expects import a.b.c + + String importString = importStatement.substring(6).trim(); + String importedType = importString.substring(importString.lastIndexOf('.') + 1); + importAliases.put(importedType, TypeName.create(importString)); + } + + enum State { + PRE_IMPORTS, + POST_IMPORTS, + M_COMMENTS, + IMPORTS, + ANNOTATION, + MODULE_NAME, + MODULE_CONTENT, + REQUIRES, + EXPORTS, + USES, + PROVIDES, + OPENS, + DONE, + UNKNOWN + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/Option.java b/codegen/codegen/src/main/java/io/helidon/codegen/Option.java new file mode 100644 index 00000000000..f1fd7ea06cf --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/Option.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import io.helidon.common.GenericType; + +/** + * Option definition. + * When implementing your own, hashCode and equals should return value of {@link #name()}, to correctly match against other + * option instances. + * + * @param option type, as options are always loaded from String, the type has to map from a String or list of strings + */ +public interface Option { + /** + * Create a new String option. + * + * @param name name of the option + * @param description human readable description + * @param defaultValue default value to use if not found + * @return a new option + */ + static Option create(String name, String description, String defaultValue) { + return new OptionImpl<>(name, description, defaultValue, Function.identity(), GenericType.STRING); + } + + /** + * Create a new boolean option. + * + * @param name name of the option + * @param description human readable description + * @param defaultValue default value to use if not found + * @return a new option + */ + static Option create(String name, String description, boolean defaultValue) { + return new OptionImpl<>(name, description, defaultValue, Boolean::parseBoolean, GenericType.create(Boolean.class)); + } + + /** + * Create a new int option. + * + * @param name name of the option + * @param description human readable description + * @param defaultValue default value to use if not found + * @return a new option + */ + static Option create(String name, String description, int defaultValue) { + return new OptionImpl<>(name, description, defaultValue, Integer::parseInt, GenericType.create(Integer.class)); + } + + /** + * Create a new option with a custom mapper. + * + * @param name name of the option + * @param description description of the option + * @param defaultValue default value + * @param mapper mapper from string + * @param type type of the option + * @param type of the option + * @return a new option that can be used to load value from {@link io.helidon.codegen.CodegenOptions} + */ + static Option create(String name, + String description, + T defaultValue, + Function mapper, + GenericType type) { + return new OptionImpl<>(name, description, defaultValue, mapper, type); + } + + /** + * Create a new option that has a set of values, with a custom mapper. + * + * @param name name of the option + * @param description description of the option + * @param defaultValue default value + * @param mapper mapper from string + * @param type type of the option + * @param type of the option + * @return a new option that can be used to load value from {@link io.helidon.codegen.CodegenOptions} + */ + static Option> createSet(String name, + String description, + Set defaultValue, + Function mapper, + GenericType> type) { + return new SetOptionImpl<>(name, description, defaultValue, mapper, type); + } + + /** + * Create a new option that has a list of values, with a custom mapper. + * + * @param name name of the option + * @param description description of the option + * @param defaultValue default value + * @param mapper mapper from string + * @param type type of the option + * @param type of the option + * @return a new option that can be used to load value from {@link io.helidon.codegen.CodegenOptions} + */ + static Option> createList(String name, + String description, + List defaultValue, + Function mapper, + GenericType> type) { + return new ListOptionImpl<>(name, description, defaultValue, mapper, type); + } + + /** + * Type of the option, metadata that can be used to list available options and their types. + * + * @return type of this option + */ + GenericType type(); + + /** + * Name of the option. The name can be configured in Maven plugin, through command line arguments, + * or through {@code -A} prefixed annotation processing arguments to compiler. + * + * @return name of the option + */ + String name(); + + /** + * Option description, metadata that can be used to list available options and their description. + * + * @return option description + */ + String description(); + + /** + * Default to use if the option is not defined. + * + * @return default value + */ + T defaultValue(); + + /** + * Find an option value from the codegen options. + * + * @param options as obtained from the caller + * @return value of this option, or empty if not configured + */ + Optional findValue(CodegenOptions options); + + /** + * Obtain an option value from the codegen options using {@link #defaultValue()} if none configured. + * + * @param options as obtained from the caller + * @return value of this option + */ + default T value(CodegenOptions options) { + return findValue(options).orElseGet(this::defaultValue); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java new file mode 100644 index 00000000000..1cf876ec26e --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/OptionImpl.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import io.helidon.common.GenericType; + +final class OptionImpl implements Option { + private final String name; + private final String description; + private final T defaultValue; + private final Function mapFunction; + private final GenericType type; + + OptionImpl(String name, String description, T defaultValue, Function mapFunction, GenericType type) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + this.mapFunction = mapFunction; + this.type = type; + } + + @Override + public GenericType type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return description; + } + + @Override + public T defaultValue() { + return defaultValue; + } + + @Override + public Optional findValue(CodegenOptions options) { + return options.option(name).map(mapFunction); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OptionImpl option)) { + return false; + } + return Objects.equals(name, option.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java new file mode 100644 index 00000000000..89b240254d0 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Collection; +import java.util.Optional; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Context of a single round of code generation. + * For example the first round may generate types, that require additional code generation. + */ +public interface RoundContext { + /** + * Annotations available in this round, the collection contains only annotations valid for the extension being invoked. + * + * @return available annotations + */ + Collection availableAnnotations(); + + /** + * All types that are processed in this round. Only contains types that are valid for processing by this extension. + * + * @return matching types + */ + Collection types(); + + /** + * All types annotated with a specific annotation. + * + * @param annotationType annotation to check + * @return types that contain the annotation + */ + Collection annotatedTypes(TypeName annotationType); + + /** + * All elements annotated with a specific annotation. + * + * @param annotationType annotation to check + * @return elements that contain the annotation + */ + Collection annotatedElements(TypeName annotationType); + + /** + * Add a new class to be code generated. + *

+ * Actual code generation will be done once, at the end of this round. + * Note that you can always force immediate generation through {@link io.helidon.codegen.CodegenContext#filer()}. In such + * a case do not add the type through this method. + * If you call this method with a type that was already registered, you will replace that instance. + * + * @param type type of the new class + * @param newClass builder of the new class + * @param mainTrigger a type that caused this, may be the processor itself, if not bound to any type + * @param originatingElements possible originating elements (such as Element in APT, or ClassInfo in classpath scanning) + */ + void addGeneratedType(TypeName type, ClassModel.Builder newClass, TypeName mainTrigger, Object... originatingElements); + + /** + * Class model builder for a type that is to be code generated. + * This method provides access to all types that are to be generated, even from other extensions that do not match + * annotations. + * Whether another extension was already called depends on its {@link io.helidon.codegen.spi.CodegenExtensionProvider} + * weight. + * + * @param type type of the generated type + * @return class model of the new type if any + */ + Optional generatedType(TypeName type); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java new file mode 100644 index 00000000000..e19ca8a735e --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +class RoundContextImpl implements RoundContext { + private final Map newTypes = new HashMap<>(); + private final Map> annotationToTypes; + private final List types; + private final Collection annotations; + + RoundContextImpl(Set annotations, + Map> annotationToTypes, + List types) { + + this.annotations = annotations; + this.annotationToTypes = annotationToTypes; + this.types = types; + } + + @Override + public Collection availableAnnotations() { + return annotations; + } + + @Override + public Collection types() { + return types; + } + + @Override + public Collection annotatedElements(TypeName annotationType) { + List typeInfos = annotationToTypes.get(annotationType); + if (typeInfos == null) { + return Set.of(); + } + + List result = new ArrayList<>(); + + for (TypeInfo typeInfo : typeInfos) { + typeInfo.elementInfo() + .stream() + .filter(it -> it.hasAnnotation(annotationType)) + .forEach(result::add); + } + + return result; + } + + @Override + public Collection annotatedTypes(TypeName annotationType) { + List typeInfos = annotationToTypes.get(annotationType); + if (typeInfos == null) { + return Set.of(); + } + + List result = new ArrayList<>(); + + for (TypeInfo typeInfo : typeInfos) { + if (typeInfo.hasAnnotation(annotationType)) { + result.add(typeInfo); + } + } + + return result; + } + + @Override + public void addGeneratedType(TypeName type, + ClassModel.Builder newClass, + TypeName mainTrigger, + Object... originatingElements) { + this.newTypes.put(type, new ClassCode(type, newClass, mainTrigger, originatingElements)); + } + + @Override + public Optional generatedType(TypeName type) { + return Optional.ofNullable(newTypes.get(type)).map(ClassCode::classModel); + } + + Collection newTypes() { + return newTypes.values(); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java new file mode 100644 index 00000000000..67cedf9f803 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/SetOptionImpl.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.common.GenericType; + +final class SetOptionImpl implements Option> { + private final String name; + private final String description; + private final Set defaultValue; + private final Function mapFunction; + private final GenericType> type; + + SetOptionImpl(String name, + String description, + Set defaultValue, + Function mapFunction, + GenericType> type) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + this.mapFunction = mapFunction; + this.type = type; + } + + @Override + public GenericType> type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return description; + } + + @Override + public Set defaultValue() { + return defaultValue; + } + + @Override + public Optional> findValue(CodegenOptions options) { + return options.option(name) + .map(it -> it.split(",")) + .map(this::toSet); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SetOptionImpl option)) { + return false; + } + return Objects.equals(name, option.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + private Set toSet(String[] strings) { + return Stream.of(strings) + .map(String::trim) + .map(mapFunction) + .collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/SystemLogger.java b/codegen/codegen/src/main/java/io/helidon/codegen/SystemLogger.java new file mode 100644 index 00000000000..6c3d52bf66b --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/SystemLogger.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +class SystemLogger implements CodegenLogger { + private final System.Logger logger; + + SystemLogger(System.Logger logger) { + this.logger = logger; + } + + @Override + public void log(CodegenEvent event) { + logger.log(event.level(), event.message(), event.throwable()); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java b/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java new file mode 100644 index 00000000000..e0a233f1d7d --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/TypeInfoFactoryBase.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.spi.AnnotationMapper; +import io.helidon.codegen.spi.ElementMapper; +import io.helidon.codegen.spi.TypeMapper; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +/** + * Common code for type info factories. + */ +public abstract class TypeInfoFactoryBase { + private static final Set IGNORED_ANNOTATIONS = Set.of(TypeName.create(SuppressWarnings.class), + TypeName.create(Override.class), + TypeName.create(Target.class), + TypeName.create(Retention.class), + TypeName.create(Repeatable.class)); + + /** + * There are no side effects of this constructor. + * All provided methods are static. + */ + protected TypeInfoFactoryBase() { + } + + /** + * Map a type using context type mappers. + * + * @param ctx code generation context + * @param type type to map + * @return result of mapping + */ + protected static Optional mapType(CodegenContext ctx, TypeInfo type) { + TypeInfo toReturn = type; + for (TypeMapper typeMapper : ctx.typeMappers()) { + if (typeMapper.supportsType(toReturn)) { + Optional mapped = typeMapper.map(ctx, toReturn); + if (mapped.isEmpty()) { + // type was removed + return mapped; + } + toReturn = mapped.get(); + } + } + return Optional.of(toReturn); + } + + /** + * Map an element using context type mappers. + * + * @param ctx code generation context + * @param element element to map + * @return result of mapping + */ + protected static Optional mapElement(CodegenContext ctx, TypedElementInfo element) { + TypedElementInfo toReturn = element; + for (ElementMapper elementMapper : ctx.elementMappers()) { + if (elementMapper.supportsElement(toReturn)) { + Optional mapped = elementMapper.mapElement(ctx, toReturn); + if (mapped.isEmpty()) { + return mapped; + } + toReturn = mapped.get(); + } + } + return Optional.of(toReturn); + } + + /** + * Map an annotation using context type mappers. + * + * @param ctx code generation context + * @param annotation annotation to map + * @param kind element kind of the annotated element + * @return result of mapping + */ + protected static List mapAnnotation(CodegenContext ctx, Annotation annotation, ElementKind kind) { + List toProcess = new ArrayList<>(); + toProcess.add(annotation); + + for (AnnotationMapper annotationMapper : ctx.annotationMappers()) { + List nextToProcess = new ArrayList<>(); + for (Annotation annot : toProcess) { + if (annotationMapper.supportsAnnotation(annot)) { + nextToProcess.addAll(annotationMapper.mapAnnotation(ctx, annot, kind)); + } else { + nextToProcess.add(annot); + } + } + toProcess = nextToProcess; + } + return toProcess; + } + + /** + * A filter for annotations to exclude ones we are not interested in ({@link java.lang.SuppressWarnings}, + * {@link java.lang.Override}, {@link java.lang.annotation.Target}, {@link java.lang.annotation.Retention}, + * {@link java.lang.annotation.Repeatable}. + * + * @param annotation annotation to check + * @return whether the annotation should be included + */ + protected static boolean annotationFilter(Annotation annotation) { + return !IGNORED_ANNOTATIONS.contains(annotation.typeName()); + } + + /** + * Map a string representation of a modifier to its Helidon counterpart. + * + * @param ctx code generation context + * @param stringModifiers set of modifiers + * @return set of Helidon modifiers (without visibility modifiers) + */ + protected static Set modifiers(CodegenContext ctx, Set stringModifiers) { + Set result = new HashSet<>(); + + for (String stringModifier : stringModifiers) { + try { + result.add(io.helidon.common.types.Modifier.valueOf(stringModifier.toUpperCase(Locale.ROOT))); + } catch (Exception ignored) { + // we do not care about modifiers we do not understand - either access modifier, or something new + ctx.logger().log(System.Logger.Level.TRACE, + "Modifier " + stringModifier + " not understood by type info factory."); + } + } + + return result; + } + + /** + * Check if the provided type is either a primitive type, or is from the {@code java} package namespace. + * + * @param type type to check + * @return {@code true} if the type is a primitive type, or its package starts with {@code java.} + */ + protected static boolean isBuiltInJavaType(TypeName type) { + return type.primitive() || type.packageName().startsWith("java."); + } + +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/package-info.java b/codegen/codegen/src/main/java/io/helidon/codegen/package-info.java new file mode 100644 index 00000000000..5e3a93e551f --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Code generation and processing support. + *

+ * The main type to start with is {@link io.helidon.codegen.Codegen}, that is responsible for discovering all extensions on the + * classpath, to understand what annotations they are interested in, and then invoking them as needed. + * This type is expected to be called from an annotation processor, Maven plugin, or a command line tool (or any other tool + * capable of analyzing sources or byte code and/or generating new types. + * + * @see io.helidon.codegen.CodegenUtil + */ +package io.helidon.codegen; diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapper.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapper.java new file mode 100644 index 00000000000..44e8ab00108 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import java.util.Collection; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; + +/** + * Maps annotation from a single annotation instance to zero or more annotation instances. + */ +public interface AnnotationMapper { + /** + * Check if the annotation is supported. + * + * @param annotation annotation to check + * @return {@code true} if this mapper is interested in the annotation. + */ + boolean supportsAnnotation(Annotation annotation); + + /** + * Map an annotation to a set of new annotations. + * The original annotation is not retained, unless part of the result of this method. + * + * @param ctx code generation context + * @param original original annotation that matches {@link #supportsAnnotation(io.helidon.common.types.Annotation)} + * @param elementKind kind of element the annotation is on + * @return list of annotations to add instead of the provided annotation (may be empty to remove it), + * this result is used to process other mappers (except for this one) + */ + Collection mapAnnotation(CodegenContext ctx, Annotation original, ElementKind elementKind); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapperProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapperProvider.java new file mode 100644 index 00000000000..92fd8631f69 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/AnnotationMapperProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.codegen.CodegenOptions; + +/** + * {@link java.util.ServiceLoader} provider interface for annotation mapping. + * This provider is used to load all mappers accessible through {@link io.helidon.codegen.CodegenContext#annotationMappers()}. + */ +public interface AnnotationMapperProvider extends CodegenProvider { + /** + * Create an annotation mapper based on provided options. + * + * @param options as obtained from annotation processing environment, Maven plugin, or command line arguments + * @return a new annotation mapper + */ + AnnotationMapper create(CodegenOptions options); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtension.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtension.java new file mode 100644 index 00000000000..cfd9aa5299c --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtension.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.codegen.RoundContext; + +/** + * Code processing and generation extension. + */ +public interface CodegenExtension { + /** + * Process a round of code analysis and generation. + * There may be more than one round of processing (such as when a type gets generated that has supported annotations that + * need to be processed). + * + * @param roundContext context of the current round, used to get types to process, and to provide types for code generation + */ + void process(RoundContext roundContext); + + /** + * Processing has finished, any finalization can be done. + * + * @param roundContext context with no available types for processing, still can add types to code generate + */ + default void processingOver(RoundContext roundContext) { + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtensionProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtensionProvider.java new file mode 100644 index 00000000000..3edd2c5e5aa --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenExtensionProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.TypeName; + +/** + * Java {@link java.util.ServiceLoader} provider interface for extensions used to process and code generate. + * Each implementation will be called with types that match its declared {@link #supportedAnnotations()} and + * {@link #supportedAnnotationPackages()}. + */ +public interface CodegenExtensionProvider extends CodegenProvider { + /** + * Create a new instance of the extension provider. + * + * @param ctx codegen context for the current environment + * @param generatorType type of the generator (annotation processor, maven plugin etc.), for reporting purposes + * @return a new codegen extension + */ + CodegenExtension create(CodegenContext ctx, TypeName generatorType); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java new file mode 100644 index 00000000000..674997b5f6e --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CodegenProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import java.util.Set; + +import io.helidon.codegen.Option; +import io.helidon.common.types.TypeName; + +/** + * A provider that is capable of processing types. + * The results of methods defined on this interface can be used to expose this information to the environment, + * such as annotation mapper. + * + * @see io.helidon.codegen.spi.AnnotationMapperProvider + * @see io.helidon.codegen.spi.ElementMapperProvider + * @see io.helidon.codegen.spi.TypeMapperProvider + */ +public interface CodegenProvider { + /** + * Configuration options that are supported. + * + * @return set of configuration options + */ + default Set> supportedOptions() { + return Set.of(); + } + + /** + * Annotations that are supported. + * + * @return set of annotation types + */ + default Set supportedAnnotations() { + return Set.of(); + } + + /** + * Supported packages of annotations. + * + * @return set of annotation packages + */ + default Set supportedAnnotationPackages() { + return Set.of(); + } +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/CopyrightProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CopyrightProvider.java new file mode 100644 index 00000000000..8300566337f --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/CopyrightProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.common.types.TypeName; + +/** + * Extension point to customize copyright headers for generated types. + */ +public interface CopyrightProvider { + /** + * Create a copyright header, including comment begin/end, or line comments. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @return copyright string (can be multiline) + */ + String copyright(TypeName generator, TypeName trigger, TypeName generatedType); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapper.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapper.java new file mode 100644 index 00000000000..bba537319f4 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import java.util.Optional; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.TypedElementInfo; + +/** + * Maps (or removes) elements. + */ +public interface ElementMapper { + /** + * Check if the element is supported. + * + * @param element element to check + * @return {@code true} if this mapper is interested in the element + */ + boolean supportsElement(TypedElementInfo element); + + /** + * Map an element to a different element (changing any of its properties), or remove the element. + * + * @param ctx code generation context + * @param element element to map + * @return mapped element, or empty optional to remove the element + */ + Optional mapElement(CodegenContext ctx, TypedElementInfo element); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapperProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapperProvider.java new file mode 100644 index 00000000000..967ea6fe958 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/ElementMapperProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.codegen.CodegenOptions; + + +/** + * {@link java.util.ServiceLoader} provider interface for element mapping. + * This provider is used to load all mappers accessible through {@link io.helidon.codegen.CodegenContext#elementMappers()}. + */ +public interface ElementMapperProvider extends CodegenProvider { + /** + * Create an element mapper based on provided options. + * + * @param options as obtained from annotation processing environment, Maven plugin, or command line arguments + * @return a new element mapper + */ + ElementMapper create(CodegenOptions options); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/GeneratedAnnotationProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/GeneratedAnnotationProvider.java new file mode 100644 index 00000000000..3c01d54a717 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/GeneratedAnnotationProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +/** + * Service provider interface to provide customization of generated annotation. + */ +public interface GeneratedAnnotationProvider { + /** + * Create a generated annotation. + * + * @param generator type of the generator (annotation processor) + * @param trigger type of the class that caused this type to be generated + * @param generatedType type that is going to be generated + * @param versionId version of the generator + * @param comments additional comments, never use null (use empty string so they do not appear in annotation) + * @return a new annotation to add to the generated type + */ + Annotation create(TypeName generator, + TypeName trigger, + TypeName generatedType, + String versionId, + String comments); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapper.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapper.java new file mode 100644 index 00000000000..1d33e8d4eba --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import java.util.Optional; + +import io.helidon.codegen.CodegenContext; +import io.helidon.common.types.TypeInfo; + +/** + * Maps {@link io.helidon.common.types.TypeInfo} to another {@link io.helidon.common.types.TypeInfo}. + * This mapper can be used to handle complex changes to a definition of a type, such as combining + * multiple annotations into a single one. + */ +public interface TypeMapper { + /** + * Check if the type is supported. + * + * @param type type to check + * @return {@code true} if this mapper is interested in the element + */ + boolean supportsType(TypeInfo type); + + /** + * Map the original type to a different type, or remove it from processing. + * + * @param ctx code generation context + * @param typeInfo type info to map + * @return mapped type info, or empty optional to remove the type info + */ + Optional map(CodegenContext ctx, TypeInfo typeInfo); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapperProvider.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapperProvider.java new file mode 100644 index 00000000000..cddd80ee091 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/TypeMapperProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.spi; + +import io.helidon.codegen.CodegenOptions; + +/** + * {@link java.util.ServiceLoader} provider interface for type mapping. + * This provider is used to load all mappers accessible through {@link io.helidon.codegen.CodegenContext#typeMappers()}. + */ +public interface TypeMapperProvider extends CodegenProvider { + /** + * Create a type mapper based on provided options. + * + * @param options as obtained from annotation processing environment, Maven plugin, or command line arguments + * @return a new type mapper + */ + TypeMapper create(CodegenOptions options); +} diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/spi/package-info.java b/codegen/codegen/src/main/java/io/helidon/codegen/spi/package-info.java new file mode 100644 index 00000000000..1f9f6f1d9e9 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/spi/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Service provider interfaces to extend code generation support. + * + * @see io.helidon.codegen.spi.AnnotationMapperProvider + * @see io.helidon.codegen.spi.CodegenProvider + * @see io.helidon.codegen.spi.CopyrightProvider + * @see io.helidon.codegen.spi.ElementMapperProvider + * @see io.helidon.codegen.spi.GeneratedAnnotationProvider + * @see io.helidon.codegen.spi.TypeMapperProvider + */ +package io.helidon.codegen.spi; diff --git a/codegen/codegen/src/main/java/module-info.java b/codegen/codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..4638152e07e --- /dev/null +++ b/codegen/codegen/src/main/java/module-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Utilities for code generation. + */ +module io.helidon.codegen { + requires transitive io.helidon.common; + requires transitive io.helidon.common.types; + requires transitive io.helidon.codegen.classmodel; + + exports io.helidon.codegen; + exports io.helidon.codegen.spi; + + uses io.helidon.codegen.spi.CopyrightProvider; + uses io.helidon.codegen.spi.GeneratedAnnotationProvider; + uses io.helidon.codegen.spi.AnnotationMapperProvider; + uses io.helidon.codegen.spi.TypeMapperProvider; + uses io.helidon.codegen.spi.ElementMapperProvider; + uses io.helidon.codegen.spi.CodegenExtensionProvider; +} \ No newline at end of file diff --git a/codegen/codegen/src/test/java/io/helidon/codegen/CodegenUtilTest.java b/codegen/codegen/src/test/java/io/helidon/codegen/CodegenUtilTest.java new file mode 100644 index 00000000000..a4e64b6b1bf --- /dev/null +++ b/codegen/codegen/src/test/java/io/helidon/codegen/CodegenUtilTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class CodegenUtilTest { + @ParameterizedTest + @CsvSource(textBlock = """ + myMethod MyMethod + MY_METHOD MY_METHOD + """, delimiter = ' ') + void testCapitalize(String source, String expected) { + String actual = CodegenUtil.capitalize(source); + assertThat(actual, is(expected)); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + myMethod MY_METHOD + MY_METHOD MY_METHOD + some-value SOME_VALUE + methodIA2 METHOD_IA2 + """, delimiter = ' ') + void testConstantName(String source, String expected) { + String actual = CodegenUtil.toConstantName(source); + assertThat(actual, is(expected)); + } +} \ No newline at end of file diff --git a/codegen/compiler/pom.xml b/codegen/compiler/pom.xml new file mode 100644 index 00000000000..d3fff4c3607 --- /dev/null +++ b/codegen/compiler/pom.xml @@ -0,0 +1,99 @@ + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + + + helidon-codegen-compiler + Helidon Codegen Compiler + + Tools for compilation of Java + + + + + io.helidon.codegen + helidon-codegen + + + io.helidon.builder + helidon-builder-api + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + \ No newline at end of file diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/Compiler.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/Compiler.java new file mode 100644 index 00000000000..ac5c2f7fde5 --- /dev/null +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/Compiler.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.compiler; + +import java.nio.file.Path; + +import io.helidon.codegen.CodegenException; + +/** + * A javac based compiler for in-process compilation. + */ +public final class Compiler { + private Compiler() { + } + + /** + * Compile the provided source files. + * + * @param options compilation options + * @param sourceFiles source files to compile + * @throws CodegenException in case the compilation fails + */ + public static void compile(CompilerOptions options, Path... sourceFiles) throws CodegenException { + JavaC.create(options) + .compile(sourceFiles) + .maybeThrowError(); + } +} diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java new file mode 100644 index 00000000000..37c17191603 --- /dev/null +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.compiler; + +import java.nio.file.Path; +import java.util.List; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.codegen.CodegenLogger; + +/** + * Provides configuration to the javac compiler. + */ +@Prototype.Blueprint +interface CompilerOptionsBlueprint { + + /** + * The classpath to pass to the compiler. + * + * @return classpath + */ + @Option.Singular + List classpath(); + + /** + * The modulepath to pass to the compiler. + * + * @return the module path + */ + @Option.Singular + List modulepath(); + + /** + * The source path to pass to the compiler. + * + * @return the source path + */ + @Option.Singular + List sourcepath(); + + /** + * The command line arguments to pass to the compiler. + * + * @return arguments + */ + @Option.Singular + List commandLineArguments(); + + /** + * The compiler source version. + * + * @return source version + */ + @Option.Default("21") + String source(); + + /** + * The compiler target version. + * + * @return target version + */ + @Option.Default("21") + String target(); + + /** + * Target directory to generate class files to. + * + * @return output directory + */ + Path outputDirectory(); + + /** + * Logger to use, falls back to system logger. + * + * @return logger + */ + @Option.DefaultCode("@io.helidon.codegen.CodegenLogger@.create(System.getLogger(\"io.helidon.codegen.compiler.Compiler\"))") + CodegenLogger logger(); +} diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java new file mode 100644 index 00000000000..ac7f67127c8 --- /dev/null +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.compiler; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenLogger; +import io.helidon.codegen.ModuleInfo; + +class JavaC { + private final List classpath; + private final List sourcepath; + private final List modulepath; + private final List commandLineArgs; + private final String source; + private final String target; + private final Path outputDirectory; + private final CodegenLogger logger; + + private JavaC(CompilerOptions options) { + this.classpath = options.classpath(); + this.sourcepath = options.sourcepath(); + this.modulepath = options.modulepath(); + this.commandLineArgs = options.commandLineArguments(); + this.source = options.source(); + this.target = options.target(); + this.outputDirectory = options.outputDirectory(); + this.logger = options.logger(); + } + + static JavaC create(CompilerOptions options) { + return new JavaC(options); + } + + /** + * Terminates the builder by triggering compilation. + * + * @param sourceFiles the java file(s) to compile + * @return the result of the compilation + */ + Result compile(Path... sourceFiles) { + Result result = new Result(); + doCompile(result, sourceFiles); + return result; + } + + String toClasspath() { + if (!classpath.isEmpty()) { + return classpath.stream() + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + } + return null; + } + + String toSourcepath() { + if (!sourcepath.isEmpty()) { + return sourcepath.stream() + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + } + return null; + } + + String toModulePath() { + if (!modulepath.isEmpty()) { + return modulepath.stream() + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + } + return null; + } + + private void doCompile(Result result, Path[] sourceFilesToCompile) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(result, + null, + StandardCharsets.UTF_8); + + List optionList = new ArrayList<>(); + if (!classpath.isEmpty()) { + optionList.add("-classpath"); + optionList.add(toClasspath()); + } + if (!modulepath.isEmpty()) { + optionList.add("--module-path"); + optionList.add(toModulePath()); + } + if (!sourcepath.isEmpty()) { + optionList.add("--source-path"); + optionList.add(toSourcepath()); + } + if (source != null) { + optionList.add("--source"); + optionList.add(source); + } + if (target != null) { + optionList.add("--target"); + optionList.add(target); + } + optionList.addAll(commandLineArgs); + if (outputDirectory != null) { + optionList.add("-d"); + optionList.add(outputDirectory.toAbsolutePath().toString()); + } + + List filesToCompile = new ArrayList<>(Arrays.asList(sourceFilesToCompile)); + + if (!modulepath.isEmpty()) { + modulepath.forEach(path -> { + Path pathToPossibleModuleInfo = path.resolve(ModuleInfo.FILE_NAME); + if (Files.exists(pathToPossibleModuleInfo)) { + filesToCompile.add(pathToPossibleModuleInfo); + } + }); + } + + Iterable compilationUnit = fileManager + .getJavaFileObjectsFromPaths(filesToCompile); + JavaCompiler.CompilationTask task = compiler + .getTask(null, fileManager, result, optionList, null, compilationUnit); + + if (logger != null) { + logger.log(System.Logger.Level.DEBUG, + "javac " + + String.join(" ", optionList) + + " " + + Stream.of(sourceFilesToCompile).map(Path::toString).collect(Collectors.joining(" "))); + } + + Boolean taskResult = task.call(); + // we do it like this to allow for warnings to be treated as errors + if (taskResult != null && !taskResult) { + result.isSuccessful = false; + } + } + + class Result implements DiagnosticListener { + private final List> diagList = new ArrayList<>(); + private boolean isSuccessful = true; + private boolean hasWarnings = false; + + private Result() { + } + + public boolean isSuccessful() { + return isSuccessful; + } + + @SuppressWarnings("unused") + public boolean hasWarnings() { + return hasWarnings; + } + + public void maybeThrowError() { + if (!isSuccessful()) { + throw new CodegenException("Compilation error encountered:\n" + + diagList.stream() + .map(Object::toString) + .collect(Collectors.joining("\n"))); + } + } + + @Override + public void report(Diagnostic diagnostic) { + System.Logger.Level level; + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + level = System.Logger.Level.ERROR; + isSuccessful = false; + } else if (Diagnostic.Kind.MANDATORY_WARNING == diagnostic.getKind() + || Diagnostic.Kind.WARNING == diagnostic.getKind()) { + level = System.Logger.Level.WARNING; + hasWarnings = true; + } else { + level = System.Logger.Level.INFO; + } + diagList.add(diagnostic); + + logger.log(level, diagnostic.toString()); + } + } + +} diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/package-info.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/package-info.java new file mode 100644 index 00000000000..78dcbc9553f --- /dev/null +++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Java in-process compiler. + * + * @see io.helidon.codegen.compiler.Compiler + * @see io.helidon.codegen.compiler.CompilerOptions + */ +package io.helidon.codegen.compiler; diff --git a/codegen/compiler/src/main/java/module-info.java b/codegen/compiler/src/main/java/module-info.java new file mode 100644 index 00000000000..a1719894678 --- /dev/null +++ b/codegen/compiler/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Java in-process compiler. + */ +module io.helidon.codegen.compiler { + requires java.compiler; + requires transitive io.helidon.codegen; + requires transitive io.helidon.builder.api; + + exports io.helidon.codegen.compiler; +} \ No newline at end of file diff --git a/codegen/helidon-copyright/pom.xml b/codegen/helidon-copyright/pom.xml new file mode 100644 index 00000000000..f0b8dc09309 --- /dev/null +++ b/codegen/helidon-copyright/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + ../pom.xml + + + helidon-codegen-helidon-copyright + Helidon Codegen Helidon Copyright + + Implementation of Helidon copyrights + + + + + io.helidon.codegen + helidon-codegen + + + + diff --git a/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/HelidonCopyrightProvider.java b/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/HelidonCopyrightProvider.java new file mode 100644 index 00000000000..8b882e54804 --- /dev/null +++ b/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/HelidonCopyrightProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.helidon.copyright; + +import java.time.LocalDate; + +import io.helidon.codegen.spi.CopyrightProvider; +import io.helidon.common.Weight; +import io.helidon.common.types.TypeName; + +/** + * Java {@link java.util.ServiceLoader} provider implementation that generates copyright as used by the Helidon project. + */ +@Weight(100) +public class HelidonCopyrightProvider implements CopyrightProvider { + private static final String COPYRIGHT_TEMPLATE = """ + /* + * Copyright (c) {{year}} Oracle and/or its affiliates. + * + * 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. + */ + """; + + @Override + public String copyright(TypeName generator, TypeName trigger, TypeName generatedType) { + return COPYRIGHT_TEMPLATE.replace("{{year}}", year()); + } + + private String year() { + return String.valueOf(LocalDate.now().getYear()); + } +} diff --git a/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/package-info.java b/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/package-info.java new file mode 100644 index 00000000000..f8dca2120d8 --- /dev/null +++ b/codegen/helidon-copyright/src/main/java/io/helidon/codegen/helidon/copyright/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Custom copyright provider that generates Helidon copyright headers. + */ +package io.helidon.codegen.helidon.copyright; diff --git a/codegen/helidon-copyright/src/main/java/module-info.java b/codegen/helidon-copyright/src/main/java/module-info.java new file mode 100644 index 00000000000..5fee7c0ffb5 --- /dev/null +++ b/codegen/helidon-copyright/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Helidon copyright implementation. + */ +module io.helidon.codegen.helidon.copyright { + + requires io.helidon.codegen; + + exports io.helidon.codegen.helidon.copyright; + + provides io.helidon.codegen.spi.CopyrightProvider + with io.helidon.codegen.helidon.copyright.HelidonCopyrightProvider; + +} \ No newline at end of file diff --git a/codegen/pom.xml b/codegen/pom.xml new file mode 100644 index 00000000000..e7dbc690ede --- /dev/null +++ b/codegen/pom.xml @@ -0,0 +1,44 @@ + + + + + 4.0.0 + + io.helidon + helidon-project + 4.0.0-SNAPSHOT + + + io.helidon.codegen + helidon-codegen-project + Helidon Codegen Project + Code generation and code processing + + pom + + + codegen + class-model + helidon-copyright + apt + scan + compiler + + diff --git a/codegen/scan/pom.xml b/codegen/scan/pom.xml new file mode 100644 index 00000000000..b07b81135d5 --- /dev/null +++ b/codegen/scan/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + io.helidon.codegen + helidon-codegen-project + 4.0.0-SNAPSHOT + + + helidon-codegen-scan + Helidon Codegen Scan + + Tools for classpath scanning + + + + + io.helidon.common + helidon-common-types + + + io.helidon.codegen + helidon-codegen + + + io.github.classgraph + classgraph + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + \ No newline at end of file diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java new file mode 100644 index 00000000000..f7fd864644c --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.scan; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; + +import io.github.classgraph.AnnotationClassRef; +import io.github.classgraph.AnnotationEnumValue; +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.AnnotationParameterValue; +import io.github.classgraph.AnnotationParameterValueList; + +/** + * Factory for annotations. + */ +final class ScanAnnotationFactory { + private ScanAnnotationFactory() { + } + + /** + * Creates an instance from an annotation mirror during annotation processing. + * + * @param ctx processing context + * @param am the annotation mirror + * @return the new instance or empty if the annotation mirror passed is invalid + */ + public static Annotation createAnnotation(ScanContext ctx, + AnnotationInfo am) { + TypeName typeName = ScanTypeFactory.create(am.getClassInfo()); + + return Annotation.create(typeName, extractAnnotationValues(ctx, am)); + } + + /** + * Extracts values from the annotation mirror value. + * + * @param ctx the processing context + * @param am the annotation mirror + * @return the extracted values + */ + private static Map extractAnnotationValues(ScanContext ctx, + AnnotationInfo am) { + + Map result = new LinkedHashMap<>(); + AnnotationParameterValueList parameterValues = am.getParameterValues(); + for (AnnotationParameterValue parameterValue : parameterValues) { + String name = parameterValue.getName(); + Object value = parameterValue.getValue(); + if (value != null) { + result.put(name, toAnnotationValue(ctx, value)); + } + } + + return result; + } + + private static Object toAnnotationValue(ScanContext ctx, Object scanAnnotationValue) { + if (scanAnnotationValue.getClass().isArray()) { + List result = new ArrayList<>(); + int length = Array.getLength(scanAnnotationValue); + for (int i = 0; i < length; i++) { + result.add(toAnnotationValue(ctx, Array.get(scanAnnotationValue, i))); + } + return result; + } + + if (scanAnnotationValue instanceof AnnotationEnumValue anEnum) { + return anEnum.getValueName(); + } else if (scanAnnotationValue instanceof AnnotationClassRef aClass) { + return TypeName.create(aClass.getName()); + } else if (scanAnnotationValue instanceof AnnotationInfo annotation) { + return createAnnotation(ctx, annotation); + } + + // supported type + return scanAnnotationValue; + } +} diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanContext.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanContext.java new file mode 100644 index 00000000000..ab77815861d --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanContext.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.scan; + +import io.helidon.codegen.CodegenContext; + +import io.github.classgraph.ScanResult; + +/** + * Classpath scanning code generation context. + */ +public interface ScanContext extends CodegenContext { + /** + * Scan result that should have types from the whole classpath. + * + * @return scan result + */ + ScanResult scanResult(); +} diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanModuleInfo.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanModuleInfo.java new file mode 100644 index 00000000000..e546cb2fcb4 --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanModuleInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.scan; + +import java.lang.module.ModuleDescriptor; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.codegen.ModuleInfo; +import io.helidon.codegen.ModuleInfoRequires; +import io.helidon.common.types.TypeName; + +import io.github.classgraph.ModuleRef; + +/** + * Module info created from classpath scanning. + */ +public final class ScanModuleInfo { + private ScanModuleInfo() { + } + + /** + * Map a module reference to module descriptor. + * + * @param scanModuleInfo module info from classpath scanning + * @return module info if it is possible to parse it from the module ref + */ + public static Optional map(ModuleRef scanModuleInfo) { + Object descriptor = scanModuleInfo.getDescriptor(); + if (!(descriptor instanceof ModuleDescriptor javaDescriptor)) { + return Optional.empty(); + } + ModuleInfo.Builder builder = ModuleInfo.builder() + .name(javaDescriptor.name()) + .isOpen(javaDescriptor.isOpen()); + + javaDescriptor.exports() + .forEach(it -> builder.putExports(it.source(), List.copyOf(it.targets()))); + + javaDescriptor.opens() + .forEach(opens -> builder.putOpen(opens.source(), List.copyOf(opens.targets()))); + + javaDescriptor.uses() + .forEach(uses -> builder.addUse(TypeName.create(uses))); + + javaDescriptor.requires() + .stream() + .map(it -> new ModuleInfoRequires(it.name(), + isTransitive(it.modifiers()), + isStatic(it.modifiers()))) + .forEach(builder::addRequire); + + javaDescriptor.provides() + .forEach(it -> builder.putProvide(TypeName.create(it.service()), + it.providers() + .stream() + .map(TypeName::create) + .toList())); + + return Optional.of(builder.build()); + } + + private static boolean isTransitive(Set modifiers) { + return modifiers.contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE); + } + + private static boolean isStatic(Set modifiers) { + return modifiers.contains(ModuleDescriptor.Requires.Modifier.STATIC); + } +} diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeFactory.java new file mode 100644 index 00000000000..e311e13a383 --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.scan; + +import java.util.Objects; + +import io.helidon.common.types.TypeName; + +import io.github.classgraph.ClassInfo; +import io.github.classgraph.HierarchicalTypeSignature; + +/** + * Factory for types based on classpath scanning. + */ +public final class ScanTypeFactory { + private ScanTypeFactory() { + } + + /** + * Creates a name from a class info from classpath scanning. + * + * @param classInfo the element type + * @return the associated type name instance + */ + public static TypeName create(ClassInfo classInfo) { + Objects.requireNonNull(classInfo); + return TypeName.create(classInfo.getName().replace('$', '.')); + } + + /** + * Creates a type name for a classpath scanning type with possible generic declaration. + * + * @param signature signature to use + * @return type name for the provided signature + */ + public static TypeName create(HierarchicalTypeSignature signature) { + return TypeName.create(signature.toString()); + } +} diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java new file mode 100644 index 00000000000..6a9a0ae7053 --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanTypeInfoFactory.java @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.codegen.scan; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import io.helidon.codegen.CodegenException; +import io.helidon.codegen.ElementInfoPredicates; +import io.helidon.codegen.TypeInfoFactoryBase; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.Modifier; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; +import io.helidon.common.types.TypedElementInfo; + +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassMemberInfo; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodParameterInfo; + +import static java.util.function.Predicate.not; + +/** + * Factory to analyze processed types and to provide {@link io.helidon.common.types.TypeInfo} for them. + */ +public final class ScanTypeInfoFactory extends TypeInfoFactoryBase { + // we expect that annotations themselves are not code generated, and can be cached + private static final Map> META_ANNOTATION_CACHE = new ConcurrentHashMap<>(); + + private ScanTypeInfoFactory() { + } + + /** + * Create type information for a type name, reading all child elements. + * + * @param ctx annotation processor processing context + * @param typeName type name to find + * @return type info for the type element + * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for + * a primitive type) + */ + public static Optional create(ScanContext ctx, + TypeName typeName) { + return create(ctx, typeName, ElementInfoPredicates.ALL_PREDICATE); + } + + /** + * Create type information for a type name. + * + * @param ctx annotation processor processing environment + * @param typeName type name to find + * @param elementPredicate predicate for child elements + * @return type info for the type element, or empty if it cannot be resolved + */ + public static Optional create(ScanContext ctx, + TypeName typeName, + Predicate elementPredicate) throws IllegalArgumentException { + + ClassInfo classInfo = ctx.scanResult().getClassInfo(typeName.fqName()); + if (classInfo == null) { + // this class is not part of the scan + return Optional.empty(); + } + + return create(ctx, typeName, elementPredicate, classInfo) + .flatMap(it -> mapType(ctx, it)); + } + + /** + * Create type information from a type element, reading all child elements. + * + * @param ctx annotation processor processing context + * @param classInfo type element of the type we want to analyze + * @return type info for the type element + * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for + * a primitive type) + */ + public static Optional create(ScanContext ctx, + ClassInfo classInfo) { + return create(ctx, classInfo, ElementInfoPredicates.ALL_PREDICATE); + } + + /** + * Create type information from a type element. + * + * @param ctx annotation processor processing context + * @param classInfo type element of the type we want to analyze + * @param elementPredicate predicate for child elements + * @return type info for the type element, or empty if it cannot be resolved + */ + public static Optional create(ScanContext ctx, + ClassInfo classInfo, + Predicate elementPredicate) throws IllegalArgumentException { + + TypeName typeName = ScanTypeFactory.create(classInfo); + + return create(ctx, typeName, elementPredicate, classInfo); + } + + private static Optional create(ScanContext ctx, + TypeName typeName, + Predicate elementPredicate, + ClassInfo classInfo) { + + if (typeName.fqName().equals(Object.class.getName())) { + // Object or object array is not to be analyzed + return Optional.empty(); + } + TypeName genericTypeName = typeName.genericTypeName(); + Set allInterestingTypeNames = new LinkedHashSet<>(); + allInterestingTypeNames.add(genericTypeName); + typeName.typeArguments() + .stream() + .map(TypeName::genericTypeName) + .filter(not(ScanTypeInfoFactory::isBuiltInJavaType)) + .filter(not(TypeName::generic)) + .forEach(allInterestingTypeNames::add); + + try { + List annotations = createAnnotations(ctx, classInfo.getAnnotationInfo(), kind(classInfo)); + Set annotationsOnTypeOrElements = new HashSet<>(); + annotations.stream() + .map(Annotation::typeName) + .forEach(annotationsOnTypeOrElements::add); + + List elementsWeCareAbout = new ArrayList<>(); + List otherElements = new ArrayList<>(); + + classInfo.getDeclaredFieldInfo() + .forEach(it -> processField(ctx, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it)); + classInfo.getDeclaredConstructorInfo() + .forEach(it -> processMethod(ctx, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it, + true)); + classInfo.getDeclaredMethodInfo() + .forEach(it -> processMethod(ctx, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it, + false)); + + classInfo.getInnerClasses() + .forEach(it -> processInnerClass(ctx, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it)); + + Set modifiers = toModifiers(classInfo); + TypeInfo.Builder builder = TypeInfo.builder() + .originatingElement(classInfo) + .typeName(typeName) + .kind(kind(classInfo)) + .annotations(annotations) + .elementModifiers(modifiers) + .accessModifier(toAccessModifier(classInfo)) + .elementInfo(elementsWeCareAbout) + .otherElementInfo(otherElements); + + // add all of the element's and parameters to the references annotation set + elementsWeCareAbout.forEach(it -> { + if (!isBuiltInJavaType(it.typeName()) && !it.typeName().generic()) { + allInterestingTypeNames.add(it.typeName().genericTypeName()); + } + it.parameterArguments().stream() + .map(TypedElementInfo::typeName) + .map(TypeName::genericTypeName) + .filter(t -> !isBuiltInJavaType(t)) + .filter(t -> !t.generic()) + .forEach(allInterestingTypeNames::add); + }); + + ClassInfo superclass = classInfo.getSuperclass(); + + TypeName fqSuperTypeName; + if (superclass != null) { + fqSuperTypeName = ScanTypeFactory.create(superclass); + + if (fqSuperTypeName != null && !TypeNames.OBJECT.equals(fqSuperTypeName)) { + + TypeName genericSuperTypeName = fqSuperTypeName.genericTypeName(); + Optional superTypeInfo = + create(ctx, fqSuperTypeName, elementPredicate, superclass); + superTypeInfo.ifPresent(builder::superTypeInfo); + allInterestingTypeNames.add(genericSuperTypeName); + fqSuperTypeName.typeArguments().stream() + .map(TypeName::genericTypeName) + .filter(it -> !isBuiltInJavaType(it)) + .filter(it -> !it.generic()) + .forEach(allInterestingTypeNames::add); + } + } + + classInfo.getInterfaces().forEach(ifaceClassInfo -> { + TypeName fqInterfaceTypeName = ScanTypeFactory.create(ifaceClassInfo); + + TypeName genericInterfaceTypeName = fqInterfaceTypeName.genericTypeName(); + allInterestingTypeNames.add(genericInterfaceTypeName); + fqInterfaceTypeName.typeArguments().stream() + .map(TypeName::genericTypeName) + .filter(it -> !isBuiltInJavaType(it)) + .filter(it -> !it.generic()) + .forEach(allInterestingTypeNames::add); + + create(ctx, fqInterfaceTypeName, elementPredicate, ifaceClassInfo) + .ifPresent(builder::addInterfaceTypeInfo); + }); + + var moduleInfo = classInfo.getModuleInfo(); + String moduleName; + if (moduleInfo == null) { + moduleName = null; + } else { + moduleName = moduleInfo.getName(); + builder.module(moduleInfo.getName()); + } + + allInterestingTypeNames.forEach(it -> { + ClassInfo referencedType = ctx.scanResult().getClassInfo(it.fqName()); + if (referencedType != null + && referencedType.getModuleInfo() != null) { + if (moduleName == null || !referencedType.getModuleInfo().getName().equals(moduleName)) { + builder.putReferencedModuleName(it, referencedType.getModuleInfo().getName()); + } + } + }); + + builder.referencedTypeNamesToAnnotations(toMetaAnnotations(ctx, annotationsOnTypeOrElements)); + + return Optional.of(builder.build()); + } catch (Exception e) { + throw new IllegalStateException("Failed to process: " + classInfo, e); + } + } + + private static void processMethod(ScanContext ctx, + Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements, + MethodInfo methodInfo, + boolean isConstructor) { + + ElementKind kind = isConstructor ? ElementKind.CONSTRUCTOR : ElementKind.METHOD; + + TypedElementInfo.Builder builder = TypedElementInfo.builder() + .typeName(ScanTypeFactory.create(methodInfo.getTypeSignatureOrTypeDescriptor())) + .elementModifiers(toModifiers(methodInfo)) + .accessModifier(toAccessModifier(methodInfo)) + .elementName(methodInfo.getName()) + .kind(kind) + .annotations(createAnnotations(ctx, methodInfo.getAnnotationInfo(), kind)) + .originatingElement(methodInfo); + + int index = 0; + for (MethodParameterInfo methodParameterInfo : methodInfo.getParameterInfo()) { + String paramName = methodParameterInfo.getName(); + if (paramName == null) { + paramName = "param_" + index; + index++; + } + processMethodParameter(ctx, + annotationsOnTypeOrElements, + methodParameterInfo, + builder, + paramName); + } + + Set checkedExceptions = methodInfo.getThrownExceptions() + .stream() + .filter(ScanTypeInfoFactory::isCheckedException) + .map(ScanTypeFactory::create) + .collect(Collectors.toSet()); + + builder.addThrowsChecked(checkedExceptions); + + processElement(ctx, + builder.build(), + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements); + } + + private static void processMethodParameter(ScanContext ctx, + Set annotationsOnTypeOrElements, + MethodParameterInfo methodParameterInfo, + TypedElementInfo.Builder methodBuilder, + String paramName) { + + TypedElementInfo paramInfo = TypedElementInfo.builder() + .typeName(ScanTypeFactory.create(methodParameterInfo.getTypeSignatureOrTypeDescriptor())) + .elementName(paramName) + .kind(ElementKind.PARAMETER) + .annotations(createAnnotations(ctx, methodParameterInfo.getAnnotationInfo(), ElementKind.PARAMETER)) + .build(); + paramInfo = processElement(ctx, + paramInfo, + ElementInfoPredicates.ALL_PREDICATE, + new ArrayList<>(), + new ArrayList<>(), + annotationsOnTypeOrElements) + .orElseThrow(() -> new CodegenException("Failed to process a parameter element, as a mapper removed it. " + + "Mappers must not remove parameters, as this would result " + + "in a broken type info model", + methodParameterInfo)); + + methodBuilder.addParameterArgument(paramInfo); + } + + private static void processInnerClass(ScanContext ctx, + Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements, + ClassInfo classInfo) { + + ElementKind kind = kind(classInfo); + + TypedElementInfo elementInfo = TypedElementInfo.builder() + .typeName(ScanTypeFactory.create(classInfo)) + .elementModifiers(toModifiers(classInfo)) + .accessModifier(toAccessModifier(classInfo)) + .elementName(classInfo.getName()) + .kind(kind) + .annotations(createAnnotations(ctx, classInfo.getAnnotationInfo(), kind)) + .originatingElement(classInfo) + .build(); + + processElement(ctx, + elementInfo, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements); + } + + private static void processField(ScanContext ctx, + Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements, + FieldInfo fieldInfo) { + + TypedElementInfo elementInfo = TypedElementInfo.builder() + .typeName(ScanTypeFactory.create(fieldInfo.getTypeSignatureOrTypeDescriptor())) + .elementModifiers(toModifiers(fieldInfo)) + .accessModifier(toAccessModifier(fieldInfo)) + .elementName(fieldInfo.getName()) + .kind(ElementKind.FIELD) + .annotations(createAnnotations(ctx, fieldInfo.getAnnotationInfo(), ElementKind.FIELD)) + .originatingElement(fieldInfo) + .build(); + + processElement(ctx, + elementInfo, + elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements); + } + + private static Optional processElement(ScanContext ctx, TypedElementInfo element, + Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements) { + Optional mapped = mapElement(ctx, element); + if (mapped.isEmpty()) { + return mapped; + } + + TypedElementInfo elementInfo = mapped.get(); + if (elementPredicate.test(elementInfo)) { + elementsWeCareAbout.add(elementInfo); + } else { + otherElements.add(elementInfo); + } + annotationsOnTypeOrElements.addAll(elementInfo.annotations() + .stream() + .map(Annotation::typeName) + .toList()); + return Optional.of(elementInfo); + } + + private static Set toModifiers(ClassInfo classInfo) { + Set result = EnumSet.noneOf(Modifier.class); + + if (classInfo.isFinal()) { + result.add(Modifier.FINAL); + } + if (classInfo.isStatic()) { + result.add(Modifier.STATIC); + } + if (classInfo.isAbstract()) { + result.add(Modifier.ABSTRACT); + } + + return result; + } + + private static Set toModifiers(ClassMemberInfo memberInfo) { + Set result = EnumSet.noneOf(Modifier.class); + + if (memberInfo.isFinal()) { + result.add(Modifier.FINAL); + } + if (memberInfo.isStatic()) { + result.add(Modifier.STATIC); + } + + if (memberInfo instanceof MethodInfo mi) { + if (mi.isDefault()) { + result.add(Modifier.DEFAULT); + } + if (mi.isAbstract()) { + result.add(Modifier.ABSTRACT); + } + } + + return result; + } + + private static AccessModifier toAccessModifier(ClassInfo classInfo) { + if (classInfo.isPrivate()) { + return AccessModifier.PRIVATE; + } + if (classInfo.isProtected()) { + return AccessModifier.PROTECTED; + } + if (classInfo.isPublic()) { + return AccessModifier.PUBLIC; + } + return AccessModifier.PACKAGE_PRIVATE; + } + + private static AccessModifier toAccessModifier(ClassMemberInfo memberInfo) { + if (memberInfo.isPrivate()) { + return AccessModifier.PRIVATE; + } + if (memberInfo.isProtected()) { + return AccessModifier.PROTECTED; + } + if (memberInfo.isPublic()) { + return AccessModifier.PUBLIC; + } + return AccessModifier.PACKAGE_PRIVATE; + } + + private static boolean isCheckedException(ClassInfo exception) { + return exception.extendsSuperclass(Exception.class) && !exception.extendsSuperclass(RuntimeException.class); + } + + private static ElementKind kind(ClassInfo info) { + if (info.isInterface()) { + return ElementKind.INTERFACE; + } + if (info.isEnum()) { + return ElementKind.ENUM; + } + if (info.isRecord()) { + return ElementKind.RECORD; + } + if (info.isAnnotation()) { + return ElementKind.ANNOTATION_TYPE; + } + + if (info.isStandardClass()) { + return ElementKind.CLASS; + } + + return ElementKind.OTHER; + } + + private static List createAnnotations(ScanContext ctx, List annotations, ElementKind kind) { + return annotations + .stream() + .map(it -> ScanAnnotationFactory.createAnnotation(ctx, it)) + .flatMap(it -> mapAnnotation(ctx, it, kind).stream()) + .filter(TypeInfoFactoryBase::annotationFilter) + .toList(); + } + + /** + * Returns the map of meta annotations for the provided collection of annotation values. + * + * @param annotations the annotations + * @return the meta annotations for the provided set of annotations + */ + private static Map> toMetaAnnotations(ScanContext ctx, + Set annotations) { + if (annotations.isEmpty()) { + return Map.of(); + } + + Map> result = new HashMap<>(); + + gatherMetaAnnotations(ctx, annotations, result); + + return result; + } + + // gather a single level map of types to their meta annotation + private static void gatherMetaAnnotations(ScanContext ctx, + Set annotationTypes, + Map> result) { + if (annotationTypes.isEmpty()) { + return; + } + + annotationTypes.stream() + .filter(not(result::containsKey)) // already in the result, no need to add it + .forEach(it -> { + List meta = META_ANNOTATION_CACHE.get(it); + boolean fromCache = true; + if (meta == null) { + fromCache = false; + ClassInfo classInfo = ctx.scanResult().getClassInfo(it.name()); + if (classInfo != null) { + List metaAnnotations = createAnnotations(ctx, + classInfo.getAnnotationInfo(), + ElementKind.ANNOTATION_TYPE); + result.put(it, new ArrayList<>(metaAnnotations)); + // now rinse and repeat for the referenced annotations + gatherMetaAnnotations(ctx, + metaAnnotations.stream() + .map(Annotation::typeName) + .collect(Collectors.toSet()), + result); + meta = metaAnnotations; + } else { + meta = List.of(); + } + } + if (!fromCache) { + // we cannot use computeIfAbsent, as that would do a recursive update if nested more than once + META_ANNOTATION_CACHE.putIfAbsent(it, meta); + } + if (!meta.isEmpty()) { + result.put(it, meta); + } + }); + } +} diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/package-info.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/package-info.java new file mode 100644 index 00000000000..f031772f61f --- /dev/null +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Implementation of codegen utilities for classpath scanning. + * + * @see io.helidon.codegen.scan.ScanContext + * @see io.helidon.codegen.scan.ScanTypeFactory + * @see io.helidon.codegen.scan.ScanModuleInfo + */ +package io.helidon.codegen.scan; diff --git a/codegen/scan/src/main/java/module-info.java b/codegen/scan/src/main/java/module-info.java new file mode 100644 index 00000000000..6ba1329fe97 --- /dev/null +++ b/codegen/scan/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * 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. + */ + +/** + * Implementation of codegen utilities for classpath scanning. + */ +module io.helidon.codegen.scan { + requires transitive io.helidon.common.types; + requires transitive io.helidon.codegen; + requires transitive io.github.classgraph; + + exports io.helidon.codegen.scan; +} \ No newline at end of file diff --git a/common/common/src/main/java/io/helidon/common/GenericType.java b/common/common/src/main/java/io/helidon/common/GenericType.java index 6f2596d2a33..32f179437af 100644 --- a/common/common/src/main/java/io/helidon/common/GenericType.java +++ b/common/common/src/main/java/io/helidon/common/GenericType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,10 @@ public class GenericType implements Type { * Generic type for String. */ public static final GenericType STRING = GenericType.create(String.class); + /** + * Generic type for Object. + */ + public static final GenericType OBJECT = GenericType.create(Object.class); private final Type type; private final Class rawType; diff --git a/common/config/src/main/java/io/helidon/common/config/Config.java b/common/config/src/main/java/io/helidon/common/config/Config.java index 612c84f62b3..5089bcf0368 100644 --- a/common/config/src/main/java/io/helidon/common/config/Config.java +++ b/common/config/src/main/java/io/helidon/common/config/Config.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ /** * Immutable tree-structured configuration. - * + *

* See {@link ConfigValue}. */ public interface Config { diff --git a/common/configurable/pom.xml b/common/configurable/pom.xml index a6ab881de5c..6a20cc389c2 100644 --- a/common/configurable/pom.xml +++ b/common/configurable/pom.xml @@ -94,32 +94,42 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/common/key-util/pom.xml b/common/key-util/pom.xml index 76c127f2a79..252ea929b3b 100644 --- a/common/key-util/pom.xml +++ b/common/key-util/pom.xml @@ -34,20 +34,10 @@ io.helidon.common helidon-common-configurable - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api - - io.helidon.config - helidon-config-metadata-processor - true - io.helidon.config helidon-config-yaml @@ -82,32 +72,42 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/common/key-util/src/main/java/io/helidon/common/pki/KeysBlueprint.java b/common/key-util/src/main/java/io/helidon/common/pki/KeysBlueprint.java index 5a66d16149b..3d826b497e5 100644 --- a/common/key-util/src/main/java/io/helidon/common/pki/KeysBlueprint.java +++ b/common/key-util/src/main/java/io/helidon/common/pki/KeysBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * Configuration of keys. If a key is defined in multiple places (keystore, pem, or explicit), the order of preference is: @@ -36,8 +34,8 @@ * * So if a Private key is defined both explicitly and through PEM, the explicitly defined key would be used. */ -@Configured @Prototype.Blueprint(decorator = KeysBuilderDecorator.class) +@Prototype.Configured interface KeysBlueprint { /** * Configure keys from a keystore. @@ -46,7 +44,7 @@ interface KeysBlueprint { * * @return keystore configuration */ - @ConfiguredOption + @Option.Configured Optional keystore(); /** @@ -56,7 +54,7 @@ interface KeysBlueprint { * * @return pem based definition */ - @ConfiguredOption + @Option.Configured Optional pem(); /** diff --git a/common/key-util/src/main/java/io/helidon/common/pki/KeystoreKeysBlueprint.java b/common/key-util/src/main/java/io/helidon/common/pki/KeystoreKeysBlueprint.java index 75f86987ae5..65d968ccf7b 100644 --- a/common/key-util/src/main/java/io/helidon/common/pki/KeystoreKeysBlueprint.java +++ b/common/key-util/src/main/java/io/helidon/common/pki/KeystoreKeysBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,13 +22,11 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; import io.helidon.common.configurable.Resource; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * Resources from a java keystore (PKCS12, JKS etc.). */ -@Configured +@Prototype.Configured @Prototype.Blueprint @Prototype.CustomMethods(KeystoreKeysBlueprint.CustomMethods.class) interface KeystoreKeysBlueprint { @@ -46,7 +44,8 @@ interface KeystoreKeysBlueprint { * * @return keystore resource, from file path, classpath, URL etc. */ - @ConfiguredOption(required = true, key = "resource") + @Option.Required + @Option.Configured("resource") Resource keystore(); /** @@ -56,7 +55,8 @@ interface KeystoreKeysBlueprint { * * @return keystore type to load the key */ - @ConfiguredOption(DEFAULT_KEYSTORE_TYPE) + @Option.Configured + @Option.Default(DEFAULT_KEYSTORE_TYPE) String type(); /** @@ -65,7 +65,7 @@ interface KeystoreKeysBlueprint { * @return keystore password to use */ @Option.Confidential - @ConfiguredOption + @Option.Configured Optional passphrase(); /** @@ -73,7 +73,7 @@ interface KeystoreKeysBlueprint { * * @return alias of the key in the keystore */ - @ConfiguredOption(key = "key.alias") + @Option.Configured("key.alias") Optional keyAlias(); /** @@ -83,7 +83,7 @@ interface KeystoreKeysBlueprint { * * @return pass-phrase of the key */ - @ConfiguredOption(key = "key.passphrase") + @Option.Configured("key.passphrase") @Option.Confidential Optional keyPassphrase(); @@ -93,7 +93,7 @@ interface KeystoreKeysBlueprint { * * @return alias under which the certificate is stored in the keystore */ - @ConfiguredOption(key = "cert.alias") + @Option.Configured("cert.alias") Optional certAlias(); /** @@ -101,7 +101,7 @@ interface KeystoreKeysBlueprint { * * @return alias of certificate chain in the keystore */ - @ConfiguredOption(key = "cert-chain.alias") + @Option.Configured("cert-chain.alias") Optional certChainAlias(); /** @@ -118,7 +118,8 @@ interface KeystoreKeysBlueprint { * * @return whether this is a trust store */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean trustStore(); final class CustomMethods { diff --git a/common/key-util/src/main/java/io/helidon/common/pki/PemKeysBlueprint.java b/common/key-util/src/main/java/io/helidon/common/pki/PemKeysBlueprint.java index f0f31074b94..e3eb8e5e811 100644 --- a/common/key-util/src/main/java/io/helidon/common/pki/PemKeysBlueprint.java +++ b/common/key-util/src/main/java/io/helidon/common/pki/PemKeysBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; import io.helidon.common.configurable.Resource; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * PEM files based keys - accepts private key and certificate chain. @@ -34,7 +32,7 @@ * The only supported format is PKCS#8. If you have a different format, you must transform it to PKCS8 PEM format (to * use this builder), or to PKCS#12 keystore format (and use {@link io.helidon.common.pki.KeystoreKeys.Builder}). */ -@Configured +@Prototype.Configured @Prototype.Blueprint interface PemKeysBlueprint { /** @@ -42,7 +40,7 @@ interface PemKeysBlueprint { * * @return key resource (file, classpath, URL etc.) */ - @ConfiguredOption(key = "key.resource") + @Option.Configured("key.resource") Optional key(); /** @@ -51,7 +49,7 @@ interface PemKeysBlueprint { * * @return passphrase used to encrypt the private key */ - @ConfiguredOption(key = "key.passphrase") + @Option.Configured("key.passphrase") @Option.Confidential Optional keyPassphrase(); @@ -60,7 +58,7 @@ interface PemKeysBlueprint { * * @return public key resource (file, classpath, URL etc.) */ - @ConfiguredOption(key = "public-key.resource") + @Option.Configured("public-key.resource") Optional publicKey(); /** @@ -68,7 +66,7 @@ interface PemKeysBlueprint { * * @return resource (e.g. classpath, file path, URL etc.) */ - @ConfiguredOption(key = "cert-chain.resource") + @Option.Configured("cert-chain.resource") Optional certChain(); /** @@ -76,6 +74,6 @@ interface PemKeysBlueprint { * * @return key resource (file, classpath, URL etc.) */ - @ConfiguredOption(key = "certificates.resource") + @Option.Configured("certificates.resource") Optional certificates(); } diff --git a/common/key-util/src/main/java/module-info.java b/common/key-util/src/main/java/module-info.java index 7e2a39d1590..ab494f4c833 100644 --- a/common/key-util/src/main/java/module-info.java +++ b/common/key-util/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ requires io.helidon.builder.api; - requires static io.helidon.config.metadata; - requires transitive io.helidon.common.config; requires transitive io.helidon.common.configurable; requires transitive io.helidon.common; diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java index d7eb1f85a63..bb07fabb0fb 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -287,6 +287,7 @@ public ProviderResponse mapper(GenericType source, GenericType target, Str /** * Whether to use built-in mappers. * + * @param useBuiltIn whether to use built in mappers (such as String to Integer) * @return updated builder */ public Builder useBuiltIn(boolean useBuiltIn) { diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Annotation.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Annotation.java index b491d398c15..8b7b247900e 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Annotation.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Annotation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,10 @@ /** * Model of the annotation. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Annotation extends CommonComponent { private final List parameters; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/AnnotationParameter.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/AnnotationParameter.java index 32e158ec5a3..0f34c889599 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/AnnotationParameter.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/AnnotationParameter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,10 @@ /** * Annotation parameter model. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class AnnotationParameter extends CommonComponent { private final String value; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassBase.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassBase.java index ff6c7e04cdd..fced02ad1a4 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassBase.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,10 @@ /** * Abstract class type model. Contains common logic for all class related models. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public abstract class ClassBase extends AnnotatedComponent { private final boolean isFinal; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModel.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModel.java index 26a8527ed86..b1c33671875 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModel.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,10 @@ /** * Entry point to create class model. * This model contain all needed information for each generated type and handles resulting generation. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class ClassModel extends ClassBase { /** diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModelException.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModelException.java index e1ffef41852..3837a3ce51f 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModelException.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassModelException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ /** * Exception message which corresponds to the error in class model creation. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public class ClassModelException extends RuntimeException { ClassModelException(String message) { diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassType.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassType.java index 02766b977de..b0a0bb827c5 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassType.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/ClassType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ /** * Class type. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public enum ClassType { /** diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Constructor.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Constructor.java index bc9e937d70e..c119ca29c70 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Constructor.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Constructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,10 @@ /** * Constructor model. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Constructor extends Executable { private Constructor(Builder builder) { diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Field.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Field.java index 73fd65d75c8..3861354fb11 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Field.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Field.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,10 @@ /** * Field model representation. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Field extends AnnotatedComponent { private final Content defaultValue; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/InnerClass.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/InnerClass.java index 17c26694621..9d50835109d 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/InnerClass.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/InnerClass.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ /** * Inner class model. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class InnerClass extends ClassBase { //Collected directly specified imports when building this class diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Javadoc.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Javadoc.java index 09cf09622ab..f8df6a7c251 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Javadoc.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Javadoc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,10 @@ *

  • deprecated
  • *
  • everything else
  • * + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Javadoc extends ModelComponent { private final List content; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Method.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Method.java index 0b07130fd9c..794d6e578f9 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Method.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Method.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,10 @@ /** * Model of the method which should be created in the specific type. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Method extends Executable { private final Map declaredTokens; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Parameter.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Parameter.java index efda0578d8d..9552a66046b 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Parameter.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,10 @@ /** * Method parameter model. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Parameter extends AnnotatedComponent { private final boolean optional; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Returns.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Returns.java index 487fa3158bf..8fdb1f3e9c8 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Returns.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Returns.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,10 @@ /** * Objects which describes return type configuration. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class Returns extends DescribableComponent { private Returns(Builder builder) { diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Throws.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Throws.java index f372783efb5..9d02a84dad1 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Throws.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Throws.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,10 @@ /** * Objects which describes exception throws configuration. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public class Throws extends DescribableComponent { private Throws(Builder builder) { diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java index 8501c83e152..5a08fac818d 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/TypeArgument.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,10 @@ /** * Generic type argument model. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class TypeArgument extends Type implements TypeName { private final TypeName token; diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/package-info.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/package-info.java index d70ef11fe52..1730f554609 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/package-info.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,5 +16,8 @@ /** * Class model generator for annotation processors. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") package io.helidon.common.processor.classmodel; diff --git a/common/processor/class-model/src/main/java/module-info.java b/common/processor/class-model/src/main/java/module-info.java index 7822f1c17d1..266110ff893 100644 --- a/common/processor/class-model/src/main/java/module-info.java +++ b/common/processor/class-model/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ /** * The class model generator. + * + * @deprecated use {@code helidon-codegen-class-model} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") module io.helidon.common.processor.classmodel { requires io.helidon.common.types; diff --git a/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/HelidonCopyrightProvider.java b/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/HelidonCopyrightProvider.java index f52029fa107..0d48a0586c9 100644 --- a/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/HelidonCopyrightProvider.java +++ b/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/HelidonCopyrightProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,10 @@ /** * Java {@link java.util.ServiceLoader} provider implementation that generates copyright as used by the Helidon project. + * + * @deprecated use {@code helidon-codegen-helidon-copyright} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") @Weight(100) public class HelidonCopyrightProvider implements CopyrightProvider { private static final String COPYRIGHT_TEMPLATE = """ diff --git a/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/package-info.java b/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/package-info.java index 42291a1c8fd..d01476efe6f 100644 --- a/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/package-info.java +++ b/common/processor/helidon-copyright/src/main/java/io/helidon/common/processor/helidon/copyright/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,5 +16,8 @@ /** * Custom copyright provider that generates Helidon copyright headers. + * + * @deprecated use {@code helidon-codegen-helidon-copyright} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") package io.helidon.common.processor.helidon.copyright; diff --git a/common/processor/helidon-copyright/src/main/java/module-info.java b/common/processor/helidon-copyright/src/main/java/module-info.java index 439be6b1e89..e4513c7d725 100644 --- a/common/processor/helidon-copyright/src/main/java/module-info.java +++ b/common/processor/helidon-copyright/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ /** * Helidon copyright implementation. + * + * @deprecated use {@code helidon-codegen-helidon-copyright} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") module io.helidon.common.processor.helidon.copyright { requires io.helidon.common.processor; diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/ElementInfoPredicates.java b/common/processor/processor/src/main/java/io/helidon/common/processor/ElementInfoPredicates.java index a6d3ec59ec8..a57cc569a38 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/ElementInfoPredicates.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/ElementInfoPredicates.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,9 @@ * * @see io.helidon.common.types.TypedElementInfo * @see io.helidon.common.types.TypeInfo#elementInfo() + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class ElementInfoPredicates { /** * Predicate for method element kind. diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratedAnnotationHandler.java b/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratedAnnotationHandler.java index 256e14f7dc5..4715cc72025 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratedAnnotationHandler.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratedAnnotationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,10 @@ /** * Support for generated annotation. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class GeneratedAnnotationHandler { private static final GeneratedAnnotationProvider PROVIDER = HelidonServiceLoader.builder(ServiceLoader.load( GeneratedAnnotationProvider.class)) diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratorTools.java b/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratorTools.java index e20b4fbc037..01a5eeb9251 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratorTools.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/GeneratorTools.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,10 @@ /** * Tools for generating code. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class GeneratorTools { private GeneratorTools() { diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/TypeFactory.java b/common/processor/processor/src/main/java/io/helidon/common/processor/TypeFactory.java index 3048ac0cde8..5eefbf0bde0 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/TypeFactory.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/TypeFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,10 @@ /** * Factory for types. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class TypeFactory { private TypeFactory() { } diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/TypeInfoFactory.java b/common/processor/processor/src/main/java/io/helidon/common/processor/TypeInfoFactory.java index cd252809c4c..c9a239924b2 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/TypeInfoFactory.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/TypeInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationValue; @@ -63,7 +62,10 @@ /** * Factory to analyze processed types and to provide {@link io.helidon.common.types.TypeInfo} for them. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") public final class TypeInfoFactory { private static final AllPredicate ALL_PREDICATE = new AllPredicate(); @@ -183,16 +185,9 @@ public static Optional createTypedElementInfoFromElement(Proce thrownChecked = ee.getThrownTypes() .stream() - .flatMap(it -> { - if (isCheckedException(env, it)) { - TypeName typeName = TypeFactory.createTypeName(it).orElse(null); - if (typeName == null) { - return Stream.of(); - } - return Stream.of(typeName); - } - return Stream.of(); - }) + .filter(it -> isCheckedException(env, it)) + .map(TypeFactory::createTypeName) + .flatMap(Optional::stream) .collect(Collectors.toSet()); } else if (v instanceof VariableElement ve) { typeMirror = Objects.requireNonNull(ve.asType()); diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/package-info.java b/common/processor/processor/src/main/java/io/helidon/common/processor/package-info.java index 84891b95ad3..7f1539c7d07 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/package-info.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,5 +16,8 @@ /** * Tools for annotation processing. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") package io.helidon.common.processor; diff --git a/common/processor/processor/src/main/java/io/helidon/common/processor/spi/package-info.java b/common/processor/processor/src/main/java/io/helidon/common/processor/spi/package-info.java index 1219013de31..877780778a2 100644 --- a/common/processor/processor/src/main/java/io/helidon/common/processor/spi/package-info.java +++ b/common/processor/processor/src/main/java/io/helidon/common/processor/spi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,5 +19,7 @@ * * @see io.helidon.common.processor.spi.CopyrightProvider * @see io.helidon.common.processor.spi.GeneratedAnnotationProvider + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") package io.helidon.common.processor.spi; diff --git a/common/processor/processor/src/main/java/module-info.java b/common/processor/processor/src/main/java/module-info.java index afaf6336b51..5fa40b354f8 100644 --- a/common/processor/processor/src/main/java/module-info.java +++ b/common/processor/processor/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ /** * Utilities for annotation processors. + * + * @deprecated use {@code helidon-codegen} instead. */ +@Deprecated(forRemoval = true, since = "4.1.0") module io.helidon.common.processor { requires io.helidon.common.processor.classmodel; diff --git a/common/socket/pom.xml b/common/socket/pom.xml index 9721a6aa83b..f0aa72304ce 100644 --- a/common/socket/pom.xml +++ b/common/socket/pom.xml @@ -37,11 +37,6 @@ io.helidon.common helidon-common-config - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api @@ -71,32 +66,42 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java index 4fc8d864ebe..a03b8565941 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,13 +27,11 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * Socket options. */ -@Configured +@Prototype.Configured @Prototype.Blueprint(decorator = SocketOptionsBlueprint.BuilderDecorator.class) interface SocketOptionsBlueprint { /** @@ -51,7 +49,8 @@ interface SocketOptionsBlueprint { * * @return connect timeout duration */ - @ConfiguredOption("PT10S") + @Option.Configured + @Option.Default("PT10S") Duration connectTimeout(); /** @@ -59,7 +58,8 @@ interface SocketOptionsBlueprint { * * @return read timeout duration */ - @ConfiguredOption("PT30S") + @Option.Configured + @Option.Default("PT30S") Duration readTimeout(); /** @@ -68,7 +68,7 @@ interface SocketOptionsBlueprint { * @return buffer size, in bytes * @see java.net.StandardSocketOptions#SO_RCVBUF */ - @ConfiguredOption + @Option.Configured Optional socketReceiveBufferSize(); /** @@ -77,7 +77,7 @@ interface SocketOptionsBlueprint { * @return buffer size, in bytes * @see java.net.StandardSocketOptions#SO_SNDBUF */ - @ConfiguredOption + @Option.Configured Optional socketSendBufferSize(); /** @@ -87,7 +87,8 @@ interface SocketOptionsBlueprint { * @return whether to reuse address * @see java.net.StandardSocketOptions#SO_REUSEADDR */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean socketReuseAddress(); /** @@ -97,7 +98,8 @@ interface SocketOptionsBlueprint { * @return keep alive * @see java.net.StandardSocketOptions#SO_KEEPALIVE */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean socketKeepAlive(); /** @@ -107,7 +109,8 @@ interface SocketOptionsBlueprint { * @return whether to use TCP_NODELAY, defaults to {@code false} * @see java.net.StandardSocketOptions#TCP_NODELAY */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean tcpNoDelay(); /** diff --git a/common/socket/src/main/java/module-info.java b/common/socket/src/main/java/module-info.java index d950607519e..3489763e567 100644 --- a/common/socket/src/main/java/module-info.java +++ b/common/socket/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,6 @@ // as unless your module uses config, the API is not useful requires io.helidon.common.config; - requires static io.helidon.config.metadata; - requires transitive io.helidon.common.buffers; requires transitive io.helidon.builder.api; diff --git a/common/tls/pom.xml b/common/tls/pom.xml index 5967e42d122..ece9dfa662c 100644 --- a/common/tls/pom.xml +++ b/common/tls/pom.xml @@ -51,11 +51,6 @@ io.helidon.common helidon-common-config - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api @@ -86,36 +81,46 @@ - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java b/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java index 6082b454669..8e820ee3801 100644 --- a/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java +++ b/common/tls/src/main/java/io/helidon/common/tls/TlsConfigBlueprint.java @@ -30,11 +30,9 @@ import io.helidon.builder.api.Prototype; import io.helidon.common.pki.Keys; import io.helidon.common.tls.spi.TlsManagerProvider; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; @Prototype.Blueprint(decorator = TlsConfigDecorator.class) -@Configured +@Prototype.Configured interface TlsConfigBlueprint extends Prototype.Factory { /** * The default protocol is set to {@value}. @@ -78,7 +76,7 @@ static List createTrust(Keys config) { * * @return private key to use */ - @ConfiguredOption + @Option.Configured Optional privateKey(); /** @@ -87,7 +85,7 @@ static List createTrust(Keys config) { * @return private key certificate chain, only used when private key is configured */ @Option.Singular - @ConfiguredOption(key = "private-key") + @Option.Configured("private-key") // same config node as privateKey List privateKeyCertChain(); @@ -97,7 +95,7 @@ static List createTrust(Keys config) { * @return certificates to be trusted */ @Option.Singular - @ConfiguredOption + @Option.Configured List trust(); /** @@ -106,7 +104,8 @@ static List createTrust(Keys config) { * @return the tls manager of the tls instance * @see ConfiguredTlsManager */ - @ConfiguredOption(provider = true, providerType = TlsManagerProvider.class, providerDiscoverServices = false) + @Option.Configured + @Option.Provider(value = TlsManagerProvider.class, discoverServices = false) TlsManager manager(); /** @@ -130,7 +129,7 @@ static List createTrust(Keys config) { * * @return provider to use, by default no provider is specified */ - @ConfiguredOption + @Option.Configured Optional secureRandomProvider(); /** @@ -138,7 +137,7 @@ static List createTrust(Keys config) { * * @return algorithm to use, by default uses {@link java.security.SecureRandom} constructor */ - @ConfiguredOption + @Option.Configured Optional secureRandomAlgorithm(); /** @@ -147,7 +146,7 @@ static List createTrust(Keys config) { * * @return algorithm to use */ - @ConfiguredOption + @Option.Configured Optional keyManagerFactoryAlgorithm(); /** @@ -162,7 +161,7 @@ static List createTrust(Keys config) { * * @return algorithm to use */ - @ConfiguredOption + @Option.Configured Optional trustManagerFactoryAlgorithm(); /** @@ -187,7 +186,8 @@ static List createTrust(Keys config) { * to disable endpoint identification (equivalent to hostname verification). * Defaults to {@value Tls#ENDPOINT_IDENTIFICATION_HTTPS} */ - @ConfiguredOption(Tls.ENDPOINT_IDENTIFICATION_HTTPS) + @Option.Configured + @Option.Default(Tls.ENDPOINT_IDENTIFICATION_HTTPS) String endpointIdentificationAlgorithm(); /** @@ -195,7 +195,8 @@ static List createTrust(Keys config) { * * @return enabled flag */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean enabled(); /** @@ -206,7 +207,8 @@ static List createTrust(Keys config) { * * @return whether to trust all certificates, do not use in production */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean trustAll(); /** @@ -214,7 +216,8 @@ static List createTrust(Keys config) { * * @return what type of mutual TLS to use, defaults to {@link TlsClientAuth#NONE} */ - @ConfiguredOption(value = "NONE") + @Option.Configured + @Option.Default("NONE") TlsClientAuth clientAuth(); /** @@ -222,7 +225,8 @@ static List createTrust(Keys config) { * * @return protocol to use, defaults to {@value DEFAULT_PROTOCOL} */ - @ConfiguredOption(DEFAULT_PROTOCOL) + @Option.Configured + @Option.Default(DEFAULT_PROTOCOL) String protocol(); /** @@ -230,7 +234,7 @@ static List createTrust(Keys config) { * * @return provider to use, defaults to none (only {@link #protocol()} is used by default) */ - @ConfiguredOption + @Option.Configured Optional provider(); /** @@ -239,7 +243,7 @@ static List createTrust(Keys config) { * @return cipher suits to enable, by default (or if list is empty), all available cipher suites * are enabled */ - @ConfiguredOption(key = "cipher-suite") + @Option.Configured("cipher-suite") @Option.Singular("enabledCipherSuite") List enabledCipherSuites(); @@ -249,7 +253,7 @@ static List createTrust(Keys config) { * * @return protocols to enable, by default (or if list is empty), all available protocols are enabled */ - @ConfiguredOption(key = "protocols") + @Option.Configured("protocols") @Option.Singular List enabledProtocols(); @@ -258,6 +262,7 @@ static List createTrust(Keys config) { * * @return session cache size, defaults to {@value DEFAULT_SESSION_CACHE_SIZE}. */ + @Option.Configured @Option.DefaultInt(DEFAULT_SESSION_CACHE_SIZE) int sessionCacheSize(); @@ -266,6 +271,7 @@ static List createTrust(Keys config) { * * @return session timeout, defaults to {@value DEFAULT_SESSION_TIMEOUT}. */ + @Option.Configured @Option.Default(DEFAULT_SESSION_TIMEOUT) Duration sessionTimeout(); @@ -274,7 +280,7 @@ static List createTrust(Keys config) { * * @return keystore type, defaults to {@link java.security.KeyStore#getDefaultType()} */ - @ConfiguredOption + @Option.Configured Optional internalKeystoreType(); /** @@ -282,7 +288,7 @@ static List createTrust(Keys config) { * * @return keystore provider, if not defined, provider is not specified */ - @ConfiguredOption + @Option.Configured Optional internalKeystoreProvider(); } diff --git a/common/tls/src/main/java/module-info.java b/common/tls/src/main/java/module-info.java index 441fb8ed0b7..b48537cc15b 100644 --- a/common/tls/src/main/java/module-info.java +++ b/common/tls/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ * TLS configuration for client and server. */ module io.helidon.common.tls { - requires static io.helidon.config.metadata; requires static io.helidon.inject.api; requires io.helidon.builder.api; diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java index 1bbfb1a900c..2a7c36c4667 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,7 @@ abstract class BuilderBase builder) { builder.typeName().ifPresent(this::typeName); + builder.description().ifPresent(this::description); builder.typeKind().ifPresent(this::typeKind); builder.kind().ifPresent(this::kind); addElementInfo(builder.elementInfo()); @@ -180,6 +183,30 @@ public BUILDER typeName(Supplier supplier) { return self(); } + /** + * Clear existing value of this property. + * + * @return updated builder instance + * @see #description() + */ + public BUILDER clearDescription() { + this.description = null; + return self(); + } + + /** + * Description, such as javadoc, if available. + * + * @param description description of this element + * @return updated builder instance + * @see #description() + */ + public BUILDER description(String description) { + Objects.requireNonNull(description); + this.description = description; + return self(); + } + /** * The type element kind. *

    @@ -771,6 +798,15 @@ public Optional typeName() { return Optional.ofNullable(typeName); } + /** + * Description, such as javadoc, if available. + * + * @return the description + */ + public Optional description() { + return Optional.ofNullable(description); + } + /** * The type element kind. *

    @@ -969,6 +1005,19 @@ protected void validatePrototype() { collector.collect().checkValid(); } + /** + * Description, such as javadoc, if available. + * + * @param description description of this element + * @return updated builder instance + * @see #description() + */ + BUILDER description(Optional description) { + Objects.requireNonNull(description); + this.description = description.map(java.lang.String.class::cast).orElse(this.description); + return self(); + } + /** * The parent/super class for this type info. * @@ -1025,6 +1074,7 @@ protected static class TypeInfoImpl implements TypeInfo { private final Map> referencedTypeNamesToAnnotations; private final Optional superTypeInfo; private final Optional originatingElement; + private final Optional description; private final Optional module; private final Set elementModifiers; private final Set modifiers; @@ -1038,6 +1088,7 @@ protected static class TypeInfoImpl implements TypeInfo { */ protected TypeInfoImpl(TypeInfo.BuilderBase builder) { this.typeName = builder.typeName().get(); + this.description = builder.description(); this.typeKind = builder.typeKind().get(); this.kind = builder.kind().get(); this.elementInfo = List.copyOf(builder.elementInfo()); @@ -1060,6 +1111,11 @@ public TypeName typeName() { return typeName; } + @Override + public Optional description() { + return description; + } + @Override public String typeKind() { return typeKind; diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 63ff525c1fb..8820e70fe50 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,14 @@ interface TypeInfoBlueprint extends Annotated { @Option.Required TypeName typeName(); + /** + * Description, such as javadoc, if available. + * + * @return description of this element + */ + @Option.Redundant + Optional description(); + /** * The type element kind. *

    diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java index 0db490e65c4..1d9d5fa7118 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -178,6 +178,15 @@ default boolean isOptional() { return TypeNames.OPTIONAL.name().equals(name()); } + /** + * Indicates whether this type is a {@link java.util.function.Supplier}. + * + * @return if this is a supplier + */ + default boolean isSupplier() { + return TypeNames.SUPPLIER.fqName().equals(fqName()); + } + /** * Simple class name with generic declaration (if part of this name). * diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java index 72ee4546018..e8a9d38809b 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNameSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,58 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import io.helidon.builder.api.Prototype; -import static io.helidon.common.types.TypeNames.PRIMITIVES; - final class TypeNameSupport { + private static final TypeName PRIMITIVE_BOOLEAN = TypeName.create(boolean.class); + private static final TypeName PRIMITIVE_BYTE = TypeName.create(byte.class); + private static final TypeName PRIMITIVE_SHORT = TypeName.create(short.class); + private static final TypeName PRIMITIVE_INT = TypeName.create(int.class); + private static final TypeName PRIMITIVE_LONG = TypeName.create(long.class); + private static final TypeName PRIMITIVE_CHAR = TypeName.create(char.class); + private static final TypeName PRIMITIVE_FLOAT = TypeName.create(float.class); + private static final TypeName PRIMITIVE_DOUBLE = TypeName.create(double.class); + private static final TypeName PRIMITIVE_VOID = TypeName.create(void.class); + private static final TypeName BOXED_BOOLEAN = TypeName.create(Boolean.class); + private static final TypeName BOXED_BYTE = TypeName.create(Byte.class); + private static final TypeName BOXED_SHORT = TypeName.create(Short.class); + private static final TypeName BOXED_INT = TypeName.create(Integer.class); + private static final TypeName BOXED_LONG = TypeName.create(Long.class); + private static final TypeName BOXED_CHAR = TypeName.create(Character.class); + private static final TypeName BOXED_FLOAT = TypeName.create(Float.class); + private static final TypeName BOXED_DOUBLE = TypeName.create(Double.class); + private static final TypeName BOXED_VOID = TypeName.create(Void.class); + + // as type names need this class to be initialized, let's have a copy of these + private static final Map PRIMITIVES = Map.of( + "boolean", PRIMITIVE_BOOLEAN, + "byte", PRIMITIVE_BYTE, + "short", PRIMITIVE_SHORT, + "int", PRIMITIVE_INT, + "long", PRIMITIVE_LONG, + "char", PRIMITIVE_CHAR, + "float", PRIMITIVE_FLOAT, + "double", PRIMITIVE_DOUBLE, + "void", PRIMITIVE_VOID + ); + + private static final Map BOXED_TYPES = Map.of( + PRIMITIVE_BOOLEAN, BOXED_BOOLEAN, + PRIMITIVE_BYTE, BOXED_BYTE, + PRIMITIVE_SHORT, BOXED_SHORT, + PRIMITIVE_INT, BOXED_INT, + PRIMITIVE_LONG, BOXED_LONG, + PRIMITIVE_CHAR, BOXED_CHAR, + PRIMITIVE_FLOAT, BOXED_FLOAT, + PRIMITIVE_DOUBLE, BOXED_DOUBLE, + PRIMITIVE_VOID, BOXED_VOID + ); + private TypeNameSupport() { } @@ -55,7 +99,8 @@ static int compareTo(TypeName typeName, TypeName o) { */ @Prototype.PrototypeMethod static TypeName boxed(TypeName original) { - return TypeNames.boxed(original); + return Optional.ofNullable(BOXED_TYPES.get(original)) + .orElse(original); } @Prototype.PrototypeMethod diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNames.java b/common/types/src/main/java/io/helidon/common/types/TypeNames.java index c8e93a07c8c..f4599281b45 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package io.helidon.common.types; +import java.lang.annotation.Retention; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; @@ -23,6 +25,9 @@ import java.util.Set; import java.util.function.Supplier; +import io.helidon.common.Generated; +import io.helidon.common.GenericType; + /** * Commonly used type names. */ @@ -59,6 +64,15 @@ public final class TypeNames { * Type name for {@link java.util.Collection}. */ public static final TypeName COLLECTION = TypeName.create(Collection.class); + /** + * Type name for {@link java.time.Duration}. + */ + public static final TypeName DURATION = TypeName.create(Duration.class); + /** + * Type name for {@link java.lang.annotation.Retention}. + */ + public static final TypeName RETENTION = TypeName.create(Retention.class); + /* Primitive types and their boxed counterparts */ @@ -146,36 +160,27 @@ public final class TypeNames { * Type name of typed element info. */ public static final TypeName TYPED_ELEMENT_INFO = TypeName.create(TypedElementInfo.class); - - static final Map PRIMITIVES = Map.of( - "boolean", PRIMITIVE_BOOLEAN, - "byte", PRIMITIVE_BYTE, - "short", PRIMITIVE_SHORT, - "int", PRIMITIVE_INT, - "long", PRIMITIVE_LONG, - "char", PRIMITIVE_CHAR, - "float", PRIMITIVE_FLOAT, - "double", PRIMITIVE_DOUBLE, - "void", PRIMITIVE_VOID - ); - - private static final Map BOXED_TYPES = Map.of( - PRIMITIVE_BOOLEAN, BOXED_BOOLEAN, - PRIMITIVE_BYTE, BOXED_BYTE, - PRIMITIVE_SHORT, BOXED_SHORT, - PRIMITIVE_INT, BOXED_INT, - PRIMITIVE_LONG, BOXED_LONG, - PRIMITIVE_CHAR, BOXED_CHAR, - PRIMITIVE_FLOAT, BOXED_FLOAT, - PRIMITIVE_DOUBLE, BOXED_DOUBLE, - PRIMITIVE_VOID, BOXED_VOID - ); + /** + * Helidon annotation type. + */ + public static final TypeName ANNOTATION = TypeName.create(Annotation.class); + /** + * Helidon element kind (enum). + */ + public static final TypeName ELEMENT_KIND = TypeName.create(ElementKind.class); + /** + * Helidon access modifier (enum). + */ + public static final TypeName ACCESS_MODIFIER = TypeName.create(AccessModifier.class); + /** + * Helidon Generated annotation type. + */ + public static final TypeName GENERATED = TypeName.create(Generated.class); + /** + * Helidon {@link io.helidon.common.GenericType}. + */ + public static final TypeName GENERIC_TYPE = TypeName.create(GenericType.class); private TypeNames() { } - - static TypeName boxed(TypeName original) { - return Optional.ofNullable(BOXED_TYPES.get(original)) - .orElse(original); - } } diff --git a/common/types/src/test/java/io/helidon/common/types/TypedElementInfoTest.java b/common/types/src/test/java/io/helidon/common/types/TypedElementInfoTest.java index 9036467b039..c847f3f8e8a 100644 --- a/common/types/src/test/java/io/helidon/common/types/TypedElementInfoTest.java +++ b/common/types/src/test/java/io/helidon/common/types/TypedElementInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,14 +23,91 @@ import static org.hamcrest.MatcherAssert.assertThat; class TypedElementInfoTest { - @Test void declarations() { assertThat(TypedElementInfo.builder() .elementName("arg") - .elementTypeKind(TypeValues.KIND_PARAMETER) + .kind(ElementKind.PARAMETER) .typeName(create(boolean.class)) + .build() + .toString(), + is("boolean arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(byte.class)) + .build().toString(), + is("byte arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(short.class)) + .build().toString(), + is("short arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(int.class)) + .build().toString(), + is("int arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(long.class)) + .build().toString(), + is("long arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(char.class)) + .build().toString(), + is("char arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(float.class)) + .build().toString(), + is("float arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(double.class)) + .build().toString(), + is("double arg")); + assertThat(TypedElementInfo.builder() + .elementName("arg") + .kind(ElementKind.PARAMETER) + .typeName(create(void.class)) + .build().toString(), + is("void arg")); + + assertThat(TypedElementInfo.builder() + .enclosingType(create("MyClass")) + .elementName("hello") + .typeName(create(void.class)) + .kind(ElementKind.METHOD) + .addParameterArgument(TypedElementInfo.builder() + .elementName("arg1") + .typeName(create(String.class)) + .kind(ElementKind.PARAMETER) + .build()) + .addParameterArgument(TypedElementInfo.builder() + .elementName("arg2") + .typeName(create(int.class)) + .kind(ElementKind.PARAMETER) + .build()) .build().toString(), + is("MyClass::void hello(java.lang.String arg1, int arg2)")); + } + + @Test + void declarationsToBeRemoved() { + assertThat(TypedElementInfo.builder() + .elementName("arg") + .elementTypeKind(TypeValues.KIND_PARAMETER) + .typeName(create(boolean.class)) + .build() + .toString(), is("boolean arg")); assertThat(TypedElementInfo.builder() .elementName("arg") diff --git a/common/uri/pom.xml b/common/uri/pom.xml index c37bcb2c4ff..d0d10148405 100644 --- a/common/uri/pom.xml +++ b/common/uri/pom.xml @@ -37,11 +37,6 @@ io.helidon.builder helidon-builder-api - - io.helidon.config - helidon-config-metadata - true - org.junit.jupiter junit-jupiter-api @@ -66,27 +61,37 @@ maven-compiler-plugin + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriInfoBlueprint.java b/common/uri/src/main/java/io/helidon/common/uri/UriInfoBlueprint.java index 64565d8896a..9fa5e9e5467 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriInfoBlueprint.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriInfoBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ import java.net.URI; import java.net.URISyntaxException; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.ConfiguredOption; /** * Information about URI, that can be used to invoke a specific request over the network. @@ -34,7 +34,7 @@ interface UriInfoBlueprint { * * @return the scheme, defaults to {@code http} */ - @ConfiguredOption("http") + @Option.Default("http") String scheme(); /** @@ -42,7 +42,7 @@ interface UriInfoBlueprint { * * @return host, defaults to {@code localhost} */ - @ConfiguredOption("localhost") + @Option.Default("localhost") String host(); /** @@ -70,7 +70,7 @@ default String authority() { * * @return path */ - @ConfiguredOption("root()") + @Option.Default("root()") UriPath path(); /** @@ -78,7 +78,7 @@ default String authority() { * * @return query, may be {@link io.helidon.common.uri.UriQuery#isEmpty() empty} */ - @ConfiguredOption("empty()") + @Option.Default("empty()") UriQuery query(); /** @@ -86,7 +86,7 @@ default String authority() { * * @return fragment, may be {@link io.helidon.common.uri.UriFragment#empty() empty} */ - @ConfiguredOption("empty()") + @Option.Default("empty()") UriFragment fragment(); /** diff --git a/common/uri/src/main/java/module-info.java b/common/uri/src/main/java/module-info.java index 10b9f34235c..84d7d47ecde 100644 --- a/common/uri/src/main/java/module-info.java +++ b/common/uri/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ requires io.helidon.builder.api; - requires static io.helidon.config.metadata; - // Parameters used in public API requires transitive io.helidon.common.parameters; diff --git a/dbclient/hikari/pom.xml b/dbclient/hikari/pom.xml index f2027c3acdc..a6f1ebff45e 100644 --- a/dbclient/hikari/pom.xml +++ b/dbclient/hikari/pom.xml @@ -95,27 +95,47 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.inject.configdriven - helidon-inject-configdriven-processor + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/dbclient/jdbc/pom.xml b/dbclient/jdbc/pom.xml index b1f6f7dadcc..bc63ae4343e 100644 --- a/dbclient/jdbc/pom.xml +++ b/dbclient/jdbc/pom.xml @@ -94,27 +94,48 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} + - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.inject.configdriven - helidon-inject-configdriven-processor + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java index 698a9be1b1f..94d39135c5a 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,14 @@ */ package io.helidon.dbclient.jdbc; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * JDBC parameters setter configuration. */ @Prototype.Blueprint -@Configured(prefix = "parameters") +@Prototype.Configured(value = "parameters", root = false) interface JdbcParametersConfigBlueprint { /** @@ -33,7 +32,8 @@ interface JdbcParametersConfigBlueprint { * * @return whether N{@link String} conversion is used */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean useNString(); /** @@ -43,7 +43,8 @@ interface JdbcParametersConfigBlueprint { * * @return whether to use {@link java.io.CharArrayReader} binding */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean useStringBinding(); /** @@ -54,7 +55,8 @@ interface JdbcParametersConfigBlueprint { * * @return {@link String} values length limit for {@link java.io.CharArrayReader} binding */ - @ConfiguredOption("1024") + @Option.Configured + @Option.DefaultInt(1024) int stringBindingSize(); /** @@ -64,7 +66,8 @@ interface JdbcParametersConfigBlueprint { * * @return whether to use {@link java.io.ByteArrayInputStream} binding */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean useByteArrayBinding(); /** @@ -79,7 +82,8 @@ interface JdbcParametersConfigBlueprint { * @return whether to use {@link java.sql.Timestamp} instead of {@link java.sql.Time} * for {@link java.time.LocalTime} values */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean timestampForLocalTime(); /** @@ -89,7 +93,8 @@ interface JdbcParametersConfigBlueprint { * * @return whether to use {@link java.sql.PreparedStatement#setObject(int, Object)} for {@code java.time} Date/Time values */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean setObjectForJavaTime(); } diff --git a/examples/integrations/oci/metrics/pom.xml b/examples/integrations/oci/metrics/pom.xml index 28f5a9fd80a..496394f868e 100644 --- a/examples/integrations/oci/metrics/pom.xml +++ b/examples/integrations/oci/metrics/pom.xml @@ -42,6 +42,10 @@ io.helidon.config helidon-config-yaml + + io.helidon.logging + helidon-logging-jul + com.oracle.oci.sdk oci-java-sdk-monitoring diff --git a/fault-tolerance/fault-tolerance/pom.xml b/fault-tolerance/fault-tolerance/pom.xml index e6f7ed02926..e86c5474f24 100644 --- a/fault-tolerance/fault-tolerance/pom.xml +++ b/fault-tolerance/fault-tolerance/pom.xml @@ -73,14 +73,6 @@ helidon-common-features-api true - - - io.helidon.config - helidon-config-metadata - true - 1.7.0.Final 9.5 - 9.1 + 10.12.5 2.11.0 2.4.14 10.0.2 @@ -91,7 +91,7 @@ 4.3.8 2.26.3 3.10 - 4.8.154 + 4.8.165 @@ -101,7 +101,7 @@ 3.1.2 3.1.0 3.4.0 - 3.1.2 + 3.3.1 3.11.0 3.6.0 1.0 @@ -213,6 +213,7 @@ webclient webserver websocket + codegen diff --git a/tracing/tracing/src/main/java/io/helidon/tracing/Span.java b/tracing/tracing/src/main/java/io/helidon/tracing/Span.java index 5f88960da15..0d0d4f2f16b 100644 --- a/tracing/tracing/src/main/java/io/helidon/tracing/Span.java +++ b/tracing/tracing/src/main/java/io/helidon/tracing/Span.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -218,6 +218,7 @@ interface Builder> extends io.helidon.common.Builderhelidon-common-features-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + io.helidon.inject.configdriven helidon-inject-configdriven-processor @@ -144,11 +155,6 @@ helidon-inject-configdriven-processor ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.common.features helidon-common-features-processor @@ -159,6 +165,21 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientConfigBlueprint.java b/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientConfigBlueprint.java index cc9553b2587..746dc7f9284 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientConfigBlueprint.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/HttpClientConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,6 @@ import io.helidon.common.socket.SocketOptions; import io.helidon.common.uri.UriFragment; import io.helidon.common.uri.UriQuery; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.http.ClientRequestHeaders; import io.helidon.http.Header; import io.helidon.http.WritableHeaders; @@ -46,7 +44,7 @@ /** * This can be used by any HTTP client version, and does not act as a factory, for easy extensibility. */ -@Configured +@Prototype.Configured @Prototype.Blueprint(decorator = HttpClientConfigSupport.HttpBuilderDecorator.class) @Prototype.CustomMethods(HttpClientConfigSupport.HttpCustomMethods.class) interface HttpClientConfigBlueprint extends HttpConfigBaseBlueprint { @@ -66,7 +64,7 @@ static ClientUri createBaseUri(Config config) { * * @return base uri of the client requests */ - @ConfiguredOption(type = String.class) + @Option.Configured Optional baseUri(); /** @@ -95,7 +93,7 @@ static ClientUri createBaseUri(Config config) { * * @return socket options */ - @ConfiguredOption + @Option.Configured SocketOptions socketOptions(); /** @@ -118,7 +116,7 @@ static ClientUri createBaseUri(Config config) { * * @return default headers */ - @ConfiguredOption(key = "default-headers", builderMethod = false) + @Option.Configured("default-headers") Map defaultHeadersMap(); /** @@ -146,7 +144,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return media type parsing mode */ - @ConfiguredOption("STRICT") + @Option.Configured + @Option.Default("STRICT") ParserMode mediaTypeParserMode(); /** @@ -156,7 +155,7 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return content encoding context */ - @ConfiguredOption + @Option.Configured ContentEncodingContext contentEncoding(); /** @@ -166,7 +165,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return media context */ - @ConfiguredOption("create()") + @Option.Configured + @Option.Default("create()") MediaContext mediaContext(); /** @@ -184,7 +184,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * @return services to use with this web client */ @Option.Singular - @ConfiguredOption(provider = true, providerType = WebClientServiceProvider.class) + @Option.Configured + @Option.Provider(WebClientServiceProvider.class) List services(); /** @@ -193,7 +194,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return relative URIs flag */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean relativeUris(); /** @@ -211,7 +213,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return whether Expect:100-Continue header should be sent on streamed transfers */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean sendExpectContinue(); /** @@ -220,7 +223,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * This option limits the size. Setting this number lower than the "usual" number of target services will cause connections * to be closed and reopened frequently. */ - @ConfiguredOption("256") + @Option.Configured + @Option.DefaultInt(256) int connectionCacheSize(); /** @@ -228,7 +232,7 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return cookie manager to use */ - @ConfiguredOption + @Option.Configured Optional cookieManager(); /** @@ -237,7 +241,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return read 100-Continue timeout duration */ - @ConfiguredOption("PT1S") + @Option.Configured + @Option.Default("PT1S") Duration readContinueTimeout(); /** @@ -245,7 +250,8 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return true if connection cache is shared */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean shareConnectionCache(); /** @@ -257,6 +263,7 @@ default ClientRequestHeaders defaultRequestHeaders() { * * @return maximal number of bytes to buffer in memory for supported writers */ - @ConfiguredOption("131072") + @Option.Configured + @Option.DefaultInt(131072) int maxInMemoryEntity(); } diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/HttpConfigBaseBlueprint.java b/webclient/api/src/main/java/io/helidon/webclient/api/HttpConfigBaseBlueprint.java index 4dacbe753d6..05999650d1b 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/HttpConfigBaseBlueprint.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/HttpConfigBaseBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,11 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; import io.helidon.common.tls.Tls; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; /** * Common configuration for HTTP protocols. */ -@Configured +@Prototype.Configured @Prototype.Blueprint(builderPublic = false) interface HttpConfigBaseBlueprint { /** @@ -37,7 +35,8 @@ interface HttpConfigBaseBlueprint { * * @return whether to follow redirects */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean followRedirects(); /** @@ -46,7 +45,8 @@ interface HttpConfigBaseBlueprint { * * @return max number of followed redirects */ - @ConfiguredOption("10") + @Option.Configured + @Option.DefaultInt(10) int maxRedirects(); /** @@ -56,7 +56,7 @@ interface HttpConfigBaseBlueprint { * * @return TLS configuration to use */ - @ConfiguredOption + @Option.Configured Tls tls(); @@ -66,7 +66,7 @@ interface HttpConfigBaseBlueprint { * @return read timeout * @see io.helidon.common.socket.SocketOptions#readTimeout() */ - @ConfiguredOption + @Option.Configured Optional readTimeout(); /** @@ -75,7 +75,7 @@ interface HttpConfigBaseBlueprint { * @return connect timeout * @see io.helidon.common.socket.SocketOptions#connectTimeout() */ - @ConfiguredOption + @Option.Configured Optional connectTimeout(); /** @@ -85,7 +85,8 @@ interface HttpConfigBaseBlueprint { * @return keep alive for this connection * @see io.helidon.common.socket.SocketOptions#socketKeepAlive() */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean keepAlive(); /** @@ -93,7 +94,7 @@ interface HttpConfigBaseBlueprint { * * @return proxy to use, defaults to {@link Proxy#noProxy()} */ - @ConfiguredOption + @Option.Configured Proxy proxy(); /** @@ -102,7 +103,7 @@ interface HttpConfigBaseBlueprint { * * @return map of client properties */ - @ConfiguredOption + @Option.Configured @Option.Singular("property") Map properties(); } diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/WebClientConfigBlueprint.java b/webclient/api/src/main/java/io/helidon/webclient/api/WebClientConfigBlueprint.java index dfc841e6052..f2019e26532 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/WebClientConfigBlueprint.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/WebClientConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.inject.configdriven.api.ConfigBean; import io.helidon.webclient.spi.ProtocolConfig; import io.helidon.webclient.spi.ProtocolConfigProvider; @@ -29,16 +27,17 @@ /** * WebClient configuration. */ -@Configured(root = true, prefix = "clients") @ConfigBean(repeatable = true, wantDefault = true) @Prototype.Blueprint +@Prototype.Configured("clients") interface WebClientConfigBlueprint extends HttpClientConfigBlueprint, Prototype.Factory { /** * Configuration of client protocols. * * @return client protocol configurations */ - @ConfiguredOption(provider = true, providerType = ProtocolConfigProvider.class) + @Option.Configured + @Option.Provider(ProtocolConfigProvider.class) @Option.Singular List protocolConfigs(); diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/WebClientCookieManagerConfigBlueprint.java b/webclient/api/src/main/java/io/helidon/webclient/api/WebClientCookieManagerConfigBlueprint.java index 28710e20325..194c9d8e4b2 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/WebClientCookieManagerConfigBlueprint.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/WebClientCookieManagerConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,18 +23,17 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; @Prototype.Blueprint -@Configured +@Prototype.Configured interface WebClientCookieManagerConfigBlueprint extends Prototype.Factory { /** * Whether automatic cookie store is enabled or not. * * @return status of cookie store */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean automaticStoreEnabled(); /** @@ -42,7 +41,8 @@ interface WebClientCookieManagerConfigBlueprint extends Prototype.Factory defaultCookies(); diff --git a/webclient/api/src/main/java/module-info.java b/webclient/api/src/main/java/module-info.java index 4818e43c5ea..9962fc5e408 100644 --- a/webclient/api/src/main/java/module-info.java +++ b/webclient/api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; - /** * Helidon WebClient API. */ diff --git a/webclient/http1/pom.xml b/webclient/http1/pom.xml index 2d8b48ab398..5c20441e312 100644 --- a/webclient/http1/pom.xml +++ b/webclient/http1/pom.xml @@ -73,11 +73,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api @@ -92,6 +87,11 @@ helidon-inject-configdriven-runtime true + + io.helidon.logging + helidon-logging-jul + test + org.junit.jupiter junit-jupiter-api @@ -126,11 +126,6 @@ helidon-common-features-processor ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.inject.configdriven helidon-inject-configdriven-processor @@ -141,6 +136,21 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + @@ -149,11 +159,6 @@ helidon-inject-configdriven-processor ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.common.features helidon-common-features-processor @@ -164,6 +169,21 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientConfigBlueprint.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientConfigBlueprint.java index c55e2f85f08..0338e389eab 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientConfigBlueprint.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package io.helidon.webclient.http1; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.api.HttpClientConfig; /** @@ -30,6 +30,6 @@ interface Http1ClientConfigBlueprint extends HttpClientConfig, Prototype.Factory * * @return protocol specific configuration */ - @ConfiguredOption("create()") + @Option.Default("create()") Http1ClientProtocolConfig protocolConfig(); } diff --git a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientProtocolConfigBlueprint.java b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientProtocolConfigBlueprint.java index 4583876ef99..7e86fba1cb8 100644 --- a/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientProtocolConfigBlueprint.java +++ b/webclient/http1/src/main/java/io/helidon/webclient/http1/Http1ClientProtocolConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,23 @@ package io.helidon.webclient.http1; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.spi.ProtocolConfig; /** * Configuration of an HTTP/1.1 client. */ @Prototype.Blueprint -@Configured +@Prototype.Configured interface Http1ClientProtocolConfigBlueprint extends ProtocolConfig { @Override default String type() { return Http1ProtocolProvider.CONFIG_KEY; } - @ConfiguredOption(Http1ProtocolProvider.CONFIG_KEY) + @Option.Configured + @Option.Default(Http1ProtocolProvider.CONFIG_KEY) @Override String name(); @@ -42,7 +42,8 @@ default String type() { * @return {@code true} for keeping connections alive and re-using them for multiple requests (default), {@code false} * to create a new connection for each request */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean defaultKeepAlive(); /** @@ -50,7 +51,8 @@ default String type() { * * @return maximum header size */ - @ConfiguredOption("16384") + @Option.Configured + @Option.DefaultInt(16384) int maxHeaderSize(); /** @@ -58,7 +60,8 @@ default String type() { * * @return maximum status line length */ - @ConfiguredOption("256") + @Option.Configured + @Option.DefaultInt(256) int maxStatusLineLength(); /** @@ -69,7 +72,8 @@ default String type() { * * @return whether request header validation should be enabled */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean validateRequestHeaders(); /** @@ -80,6 +84,7 @@ default String type() { * * @return whether response header validation should be enabled */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean validateResponseHeaders(); } diff --git a/webclient/http1/src/main/java/module-info.java b/webclient/http1/src/main/java/module-info.java index 85e4e79cd47..87802215cd8 100644 --- a/webclient/http1/src/main/java/module-info.java +++ b/webclient/http1/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ requires io.helidon.builder.api; // @Builder - interfaces are a runtime dependency requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; // @ConfiguredOption etc requires transitive io.helidon.webclient.api; diff --git a/webclient/http2/pom.xml b/webclient/http2/pom.xml index 13740706c0a..3c32b6859a1 100644 --- a/webclient/http2/pom.xml +++ b/webclient/http2/pom.xml @@ -49,11 +49,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - org.junit.jupiter junit-jupiter-api @@ -98,14 +93,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -121,14 +121,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConfigBlueprint.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConfigBlueprint.java index 7eea6b8c8b4..2386990506e 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConfigBlueprint.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package io.helidon.webclient.http2; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.api.HttpClientConfig; /** @@ -30,6 +30,6 @@ interface Http2ClientConfigBlueprint extends HttpClientConfig, Prototype.Factory * * @return protocol specific configuration */ - @ConfiguredOption("create()") + @Option.Default("create()") Http2ClientProtocolConfig protocolConfig(); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientProtocolConfigBlueprint.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientProtocolConfigBlueprint.java index 69fefce0bd7..76cd212ed35 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientProtocolConfigBlueprint.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientProtocolConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,20 @@ import java.time.Duration; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.spi.ProtocolConfig; @Prototype.Blueprint(decorator = Http2ClientConfigSupport.ProtocolConfigDecorator.class) -@Configured +@Prototype.Configured interface Http2ClientProtocolConfigBlueprint extends ProtocolConfig { @Override default String type() { return Http2ProtocolProvider.CONFIG_KEY; } - @ConfiguredOption(Http2ProtocolProvider.CONFIG_KEY) + @Option.Configured + @Option.Default(Http2ProtocolProvider.CONFIG_KEY) @Override String name(); @@ -51,7 +51,8 @@ default String type() { * * @return whether to use prior knowledge of HTTP/2 */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean priorKnowledge(); /** @@ -61,7 +62,8 @@ default String type() { * * @return data frame size in bytes between 2^14(16_384) and 2^24-1(16_777_215) */ - @ConfiguredOption("16384") + @Option.Configured + @Option.DefaultInt(16384) int maxFrameSize(); /** @@ -71,7 +73,8 @@ default String type() { * * @return units of octets */ - @ConfiguredOption("-1") + @Option.Configured + @Option.DefaultLong(-1L) long maxHeaderListSize(); /** @@ -81,7 +84,8 @@ default String type() { * * @return units of octets */ - @ConfiguredOption("65535") + @Option.Configured + @Option.DefaultInt(65535) int initialWindowSize(); /** @@ -89,7 +93,8 @@ default String type() { * * @return timeout */ - @ConfiguredOption("PT0.1S") + @Option.Configured + @Option.Default("PT0.1S") Duration flowControlBlockTimeout(); /** @@ -98,7 +103,8 @@ default String type() { * * @return use ping if true */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean ping(); /** @@ -107,6 +113,7 @@ default String type() { * * @return timeout */ - @ConfiguredOption("PT0.5S") + @Option.Configured + @Option.Default("PT0.5S") Duration pingTimeout(); } diff --git a/webclient/http2/src/main/java/module-info.java b/webclient/http2/src/main/java/module-info.java index 1f8cfa74294..8211a4f40f9 100644 --- a/webclient/http2/src/main/java/module-info.java +++ b/webclient/http2/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ module io.helidon.webclient.http2 { requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive io.helidon.builder.api; requires transitive io.helidon.common.pki; diff --git a/webclient/websocket/pom.xml b/webclient/websocket/pom.xml index 1f3aff4ca69..08dfc77778e 100644 --- a/webclient/websocket/pom.xml +++ b/webclient/websocket/pom.xml @@ -44,11 +44,6 @@ io.helidon.websocket helidon-websocket - - io.helidon.config - helidon-config-metadata - true - io.helidon.common.features helidon-common-features-api @@ -78,24 +73,29 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.common.features helidon-common-features-processor @@ -106,6 +106,21 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + diff --git a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientConfigBlueprint.java b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientConfigBlueprint.java index 55237b6fb7c..955325ba061 100644 --- a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientConfigBlueprint.java +++ b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ package io.helidon.webclient.websocket; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.api.HttpClientConfig; /** @@ -29,13 +28,14 @@ * io.helidon.webclient.spi.ProtocolConfig) */ @Prototype.Blueprint -@Configured +@Prototype.Configured interface WsClientConfigBlueprint extends HttpClientConfig, Prototype.Factory { /** * WebSocket specific configuration. * * @return protocol specific configuration */ - @ConfiguredOption("create()") + @Option.Configured + @Option.Default("create()") WsClientProtocolConfig protocolConfig(); } diff --git a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientProtocolConfigBlueprint.java b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientProtocolConfigBlueprint.java index 1d61613b0ad..8ebd8c6b96b 100644 --- a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientProtocolConfigBlueprint.java +++ b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClientProtocolConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,26 +20,25 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webclient.spi.ProtocolConfig; /** * Configuration of an HTTP/1.1 client. */ @Prototype.Blueprint -@Configured +@Prototype.Configured interface WsClientProtocolConfigBlueprint extends ProtocolConfig { @Override default String type() { return WsProtocolProvider.CONFIG_KEY; } - @ConfiguredOption(WsProtocolProvider.CONFIG_KEY) + @Option.Configured + @Option.Default(WsProtocolProvider.CONFIG_KEY) @Override String name(); - @ConfiguredOption + @Option.Configured @Option.Singular List subProtocols(); } diff --git a/webclient/websocket/src/main/java/module-info.java b/webclient/websocket/src/main/java/module-info.java index 5a5f998ea57..e5f1fc0f99d 100644 --- a/webclient/websocket/src/main/java/module-info.java +++ b/webclient/websocket/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ requires io.helidon.websocket; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; exports io.helidon.webclient.websocket; diff --git a/webserver/access-log/pom.xml b/webserver/access-log/pom.xml index ce715f462b9..1e6ce1f5699 100644 --- a/webserver/access-log/pom.xml +++ b/webserver/access-log/pom.xml @@ -71,41 +71,51 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.common.features + helidon-common-features-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.common.features - helidon-common-features-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/context/pom.xml b/webserver/context/pom.xml index 90b3f46344d..1af266f3640 100644 --- a/webserver/context/pom.xml +++ b/webserver/context/pom.xml @@ -66,36 +66,46 @@ - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/cors/pom.xml b/webserver/cors/pom.xml index dfe0ebdd9ff..51cee0e4671 100644 --- a/webserver/cors/pom.xml +++ b/webserver/cors/pom.xml @@ -86,18 +86,23 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -109,18 +114,23 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/grpc/pom.xml b/webserver/grpc/pom.xml index 0394fb715d9..caa0014e6c0 100644 --- a/webserver/grpc/pom.xml +++ b/webserver/grpc/pom.xml @@ -60,11 +60,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api @@ -98,24 +93,24 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.config helidon-config-metadata-processor @@ -127,8 +122,18 @@ ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java index f05ffefdfc3..286a8111bee 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ package io.helidon.webserver.grpc; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; import io.helidon.webserver.spi.ProtocolConfig; @Prototype.Blueprint -@Configured(provides = ProtocolConfig.class) +@Prototype.Configured +@Prototype.Provides(ProtocolConfig.class) interface GrpcConfigBlueprint extends ProtocolConfig { /** * Protocol configuration type. diff --git a/webserver/grpc/src/main/java/module-info.java b/webserver/grpc/src/main/java/module-info.java index 24128f2178c..152d3b7d38f 100644 --- a/webserver/grpc/src/main/java/module-info.java +++ b/webserver/grpc/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,6 @@ requires java.logging; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive com.google.protobuf; requires transitive io.grpc; diff --git a/webserver/http2/pom.xml b/webserver/http2/pom.xml index 6bb31942c9e..8dc1227b453 100644 --- a/webserver/http2/pom.xml +++ b/webserver/http2/pom.xml @@ -49,11 +49,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - io.helidon.config helidon-config-yaml @@ -89,28 +84,28 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.config helidon-config-metadata-processor @@ -122,8 +117,18 @@ ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java index 4cda86eed25..b06301dbbdc 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,8 @@ import java.time.Duration; +import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.http.RequestedUriDiscoveryContext; import io.helidon.webserver.spi.ProtocolConfig; @@ -28,7 +27,8 @@ * HTTP/2 server configuration. */ @Prototype.Blueprint(decorator = Http2ConfigBlueprint.Http2ConfigDecorator.class) -@Configured(provides = ProtocolConfig.class) +@Prototype.Configured +@Prototype.Provides(ProtocolConfig.class) interface Http2ConfigBlueprint extends ProtocolConfig { /** * The size of the largest frame payload that the sender is willing to receive in bytes. @@ -37,7 +37,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return maximal frame size */ - @ConfiguredOption("16384") + @Option.Configured + @Option.DefaultInt(16384) int maxFrameSize(); /** @@ -47,7 +48,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return maximal header list size in bytes */ - @ConfiguredOption("8192") + @Option.Configured + @Option.DefaultInt(8192) long maxHeaderListSize(); /** @@ -59,7 +61,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return maximal number of concurrent streams */ - @ConfiguredOption("8192") + @Option.Configured + @Option.DefaultLong(8192) long maxConcurrentStreams(); /** @@ -71,7 +74,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return maximum window size in bytes */ - @ConfiguredOption("1048576") + @Option.Configured + @Option.DefaultInt(1048576) int initialWindowSize(); /** @@ -91,7 +95,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * @return duration * @see ISO_8601 Durations */ - @ConfiguredOption("PT0.1S") + @Option.Configured + @Option.Default("PT0.1S") Duration flowControlTimeout(); /** @@ -102,7 +107,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return whether to send error messages over the network */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean sendErrorDetails(); /** @@ -113,7 +119,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * @see CVE-2023-44487 * @see ISO_8601 Durations */ - @ConfiguredOption("PT10S") + @Option.Configured + @Option.Default("PT10S") Duration rapidResetCheckPeriod(); /** @@ -124,7 +131,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * @return maximum number of rapid resets * @see CVE-2023-44487 */ - @ConfiguredOption("100") + @Option.Configured + @Option.DefaultInt(100) int maxRapidResets(); /** @@ -132,7 +140,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return max number of consecutive empty frames */ - @ConfiguredOption("10") + @Option.Configured + @Option.DefaultInt(10) int maxEmptyFrames(); /** @@ -140,7 +149,8 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return whether to validate path */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean validatePath(); /** @@ -148,7 +158,7 @@ interface Http2ConfigBlueprint extends ProtocolConfig { * * @return settings for computing the requested URI */ - @ConfiguredOption + @Option.Configured RequestedUriDiscoveryContext requestedUriDiscovery(); /** diff --git a/webserver/http2/src/main/java/module-info.java b/webserver/http2/src/main/java/module-info.java index 3eae0d8f6e8..73913df2958 100644 --- a/webserver/http2/src/main/java/module-info.java +++ b/webserver/http2/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ requires io.helidon.builder.api; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive io.helidon.common.socket; requires transitive io.helidon.common.task; diff --git a/webserver/observe/config/pom.xml b/webserver/observe/config/pom.xml index c65e6695576..1a4d96b997c 100644 --- a/webserver/observe/config/pom.xml +++ b/webserver/observe/config/pom.xml @@ -85,14 +85,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -108,14 +113,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/health/pom.xml b/webserver/observe/health/pom.xml index 9d60f692678..97bba4619d7 100644 --- a/webserver/observe/health/pom.xml +++ b/webserver/observe/health/pom.xml @@ -96,14 +96,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -119,14 +124,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/info/pom.xml b/webserver/observe/info/pom.xml index 11499f07c64..016b9ba19ab 100644 --- a/webserver/observe/info/pom.xml +++ b/webserver/observe/info/pom.xml @@ -85,14 +85,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -108,14 +113,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/log/pom.xml b/webserver/observe/log/pom.xml index 068843606c3..e05c1a64434 100644 --- a/webserver/observe/log/pom.xml +++ b/webserver/observe/log/pom.xml @@ -90,14 +90,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -113,14 +118,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/metrics/pom.xml b/webserver/observe/metrics/pom.xml index dca3679ce61..6f8e9c31947 100644 --- a/webserver/observe/metrics/pom.xml +++ b/webserver/observe/metrics/pom.xml @@ -117,14 +117,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -140,14 +145,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/observe/pom.xml b/webserver/observe/observe/pom.xml index 1af0c22a494..373d44634ef 100644 --- a/webserver/observe/observe/pom.xml +++ b/webserver/observe/observe/pom.xml @@ -84,18 +84,23 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -107,18 +112,23 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/observe/tracing/pom.xml b/webserver/observe/tracing/pom.xml index 84bb37419b7..9fcdd26bc91 100644 --- a/webserver/observe/tracing/pom.xml +++ b/webserver/observe/tracing/pom.xml @@ -84,14 +84,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} @@ -107,14 +112,19 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/security/pom.xml b/webserver/security/pom.xml index e16c8362e9a..47591c12bff 100644 --- a/webserver/security/pom.xml +++ b/webserver/security/pom.xml @@ -139,36 +139,46 @@ - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/sse/pom.xml b/webserver/sse/pom.xml index e888736f9da..35b079f05c0 100644 --- a/webserver/sse/pom.xml +++ b/webserver/sse/pom.xml @@ -71,28 +71,28 @@ ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.config helidon-config-metadata-processor @@ -104,8 +104,18 @@ ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/RoutingTestBase.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/RoutingTestBase.java index 106bca990eb..ffe48e5297d 100644 --- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/RoutingTestBase.java +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/RoutingTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; // Use by both RoutingTest and RulesTest to share the same test methods -class RoutingTestBase { +abstract class RoutingTestBase { private static final Header MULTI_HANDLER = HeaderValues.createCached( HeaderNames.create("X-Multi-Handler"), "true"); static Http1Client client; diff --git a/webserver/webserver/pom.xml b/webserver/webserver/pom.xml index 338b5c481e1..2f10776a912 100644 --- a/webserver/webserver/pom.xml +++ b/webserver/webserver/pom.xml @@ -60,6 +60,10 @@ io.helidon.common helidon-common-tls + + io.helidon.logging + helidon-logging-common + io.helidon.http.media helidon-http-media @@ -72,11 +76,6 @@ io.helidon.common.features helidon-common-features - - io.helidon.config - helidon-config-metadata - true - io.helidon.config helidon-config-yaml @@ -148,32 +147,42 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - io.helidon.builder - helidon-builder-processor + io.helidon.config + helidon-config-metadata-processor ${helidon.version} - io.helidon.config - helidon-config-metadata-processor + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java index c040ddfb58d..93f38aa76bb 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.http.RequestedUriDiscoveryContext; import io.helidon.webserver.spi.ProtocolConfig; @@ -29,7 +27,8 @@ * HTTP/1.1 server configuration. */ @Prototype.Blueprint(decorator = Http1BuilderDecorator.class) -@Configured(provides = ProtocolConfig.class) +@Prototype.Configured +@Prototype.Provides(ProtocolConfig.class) interface Http1ConfigBlueprint extends ProtocolConfig { /** * Name of this configuration, in most cases the same as {@link #type()}. @@ -44,7 +43,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return maximal size in bytes */ - @ConfiguredOption("2048") + @Option.Configured + @Option.DefaultInt(2048) int maxPrologueLength(); /** @@ -52,7 +52,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return maximal header size */ - @ConfiguredOption("16384") + @Option.Configured + @Option.DefaultInt(16384) int maxHeadersSize(); /** @@ -67,7 +68,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return whether to validate headers */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean validateRequestHeaders(); /** @@ -82,7 +84,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return whether to validate headers */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean validateResponseHeaders(); /** @@ -90,7 +93,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return whether to validate path */ - @ConfiguredOption("true") + @Option.Configured + @Option.DefaultBoolean(true) boolean validatePath(); /** @@ -99,7 +103,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return {@code true} if logging should be enabled for received packets, {@code false} if no logging should be done */ - @ConfiguredOption(key = "recv-log", value = "true") + @Option.Configured("recv-log") + @Option.DefaultBoolean(true) boolean receiveLog(); /** @@ -108,7 +113,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return {@code true} if logging should be enabled for sent packets, {@code false} if no logging should be done */ - @ConfiguredOption(value = "true") + @Option.Configured + @Option.DefaultBoolean(true) boolean sendLog(); /** @@ -117,7 +123,8 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return if {@code true} answer with 100 continue immediately after expect continue */ - @ConfiguredOption("false") + @Option.Configured + @Option.DefaultBoolean(false) boolean continueImmediately(); /** @@ -125,7 +132,7 @@ interface Http1ConfigBlueprint extends ProtocolConfig { * * @return settings for computing the requested URI */ - @ConfiguredOption + @Option.Configured RequestedUriDiscoveryContext requestedUriDiscovery(); /** diff --git a/webserver/webserver/src/main/java/module-info.java b/webserver/webserver/src/main/java/module-info.java index be0d7aa2a79..d3c0042cbec 100644 --- a/webserver/webserver/src/main/java/module-info.java +++ b/webserver/webserver/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,6 @@ requires java.logging; // only used to keep logging active until shutdown hook finishes requires java.management; - requires static io.helidon.config.metadata; - requires transitive io.helidon.common.buffers; requires transitive io.helidon.common.context; requires transitive io.helidon.common.security; diff --git a/webserver/websocket/pom.xml b/webserver/websocket/pom.xml index 0ae70fb51ac..7003901f00a 100644 --- a/webserver/websocket/pom.xml +++ b/webserver/websocket/pom.xml @@ -45,11 +45,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - io.helidon.builder helidon-builder-api @@ -93,24 +88,24 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} - - io.helidon.builder - helidon-builder-processor - ${helidon.version} - io.helidon.config helidon-config-metadata-processor @@ -122,8 +117,18 @@ ${helidon.version} - io.helidon.common.processor - helidon-common-processor-helidon-copyright + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} diff --git a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java index 7ba935890a2..a354775fc37 100644 --- a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java +++ b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,22 +20,21 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.webserver.spi.ProtocolConfig; /** * WebSocket protocol configuration. */ @Prototype.Blueprint -@Configured(provides = ProtocolConfig.class) +@Prototype.Configured +@Prototype.Provides(ProtocolConfig.class) interface WsConfigBlueprint extends ProtocolConfig { /** * WebSocket origins. * * @return origins */ - @ConfiguredOption + @Option.Configured @Option.Singular Set origins(); @@ -53,7 +52,8 @@ default String type() { * * @return configuration name */ - @ConfiguredOption(WsUpgradeProvider.CONFIG_NAME) + @Option.Configured + @Option.Default(WsUpgradeProvider.CONFIG_NAME) @Override String name(); @@ -63,6 +63,7 @@ default String type() { * * @return max frame size to read */ - @ConfiguredOption(WsConnection.MAX_FRAME_LENGTH) + @Option.Configured + @Option.DefaultInt(WsConnection.MAX_FRAME_LENGTH) int maxFrameLength(); } diff --git a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConnection.java b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConnection.java index 29dd9c9a0a8..55faf658075 100644 --- a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConnection.java +++ b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public class WsConnection implements ServerConnection, WsSession { private static final System.Logger LOGGER = System.getLogger(WsConnection.class.getName()); - static final String MAX_FRAME_LENGTH = "1048576"; + static final int MAX_FRAME_LENGTH = 1048576; private final ConnectionContext ctx; private final HttpPrologue prologue; diff --git a/webserver/websocket/src/main/java/module-info.java b/webserver/websocket/src/main/java/module-info.java index c2504bab0f8..a2917e0bc21 100644 --- a/webserver/websocket/src/main/java/module-info.java +++ b/webserver/websocket/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ requires io.helidon.http; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive io.helidon.webserver; requires transitive io.helidon.websocket;