diff --git a/bom/pom.xml b/bom/pom.xml index 2be055d04e5..dedb883fd78 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1402,6 +1402,11 @@ helidon-pico-types ${helidon.version} + + io.helidon.pico + helidon-pico-spi + ${helidon.version} + diff --git a/pico/pom.xml b/pico/pom.xml index d6288a5990e..83d3301b3b7 100644 --- a/pico/pom.xml +++ b/pico/pom.xml @@ -46,7 +46,7 @@ api - + spi types builder diff --git a/pico/spi/pom.xml b/pico/spi/pom.xml new file mode 100644 index 00000000000..087137b270d --- /dev/null +++ b/pico/spi/pom.xml @@ -0,0 +1,110 @@ + + + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-spi + Helidon Pico SPI + + + + true + + + + + io.helidon.pico + helidon-pico-types + + + io.helidon.common + helidon-common + + + jakarta.inject + jakarta.inject-api + compile + + + io.helidon.pico + helidon-pico-api + provided + + + io.helidon.pico.builder + helidon-pico-builder-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + javax.inject + javax.inject + ${javax.injection.version} + provided + true + + + javax.annotation + javax.annotation-api + provided + true + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.pico.builder + helidon-pico-builder-processor + ${helidon.version} + + + + + + + + diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLog.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLog.java new file mode 100644 index 00000000000..b3905268210 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLog.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; + +/** + * Tracks the transformations of {@link ServiceProvider}'s {@link ActivationStatus} in lifecycle activity (i.e., activation startup + * and deactivation shutdown). + * + * @see Activator + * @see DeActivator + */ +public interface ActivationLog { + + /** + * Expected to be called during service creation and activation to capture the activation log transcripts. + * + * @param entry the log entry to record + * @return the activation log entry + */ + ActivationLogEntry recordActivationEvent(ActivationLogEntry entry); + + /** + * Optionally provide a means to query the activation log. + * + * @return the optional query API of log activation records + */ + default Optional toQuery() { + return Optional.empty(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogEntry.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogEntry.java new file mode 100644 index 00000000000..719d30f7aad --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogEntry.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import io.helidon.pico.builder.api.Builder; + +/** + * Log entry for lifecycle related events (i.e., activation startup and deactivation shutdown). + * + * @see io.helidon.pico.spi.ActivationLog + * @see io.helidon.pico.spi.Activator + * @see io.helidon.pico.spi.DeActivator + * @param the service type + */ +@Builder +interface ActivationLogEntry { + + /** + * The activation event. + */ + enum Event { + + /** + * Starting. + */ + STARTING, + + /** + * Finished. + */ + FINISHED + } + + /** + * The managing service provider. + * + * @return the managing service provider + */ + ServiceProvider serviceProvider(); + + /** + * The event. + * + * @return the event + */ + Event event(); + + /** + * The starting activation phase. + * + * @return the starting activation phase + */ + ActivationPhase startingActivationPhase(); + + /** + * The eventual/desired/target activation phase. + * + * @return the eventual/desired/target activation phase + */ + ActivationPhase targetActivationPhase(); + + /** + * The finishing phase at the time of this event / log entry. + * + * @return the actual finishing phase + */ + ActivationPhase finishingActivationPhase(); + + /** + * The finishing activation status at the time of this event / log entry. + * + * @return the activation status + */ + ActivationStatus finishingStatus(); + + /** + * The time this event was generated. + * + * @return the time of the event + */ + OffsetDateTime time(); + + /** + * Any observed error during activation. + * + * @return any observed error + */ + Optional error(); + + /** + * The thread id that the event occurred on. + * + * @return the thread id + */ + Long getThreadId(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogQuery.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogQuery.java new file mode 100644 index 00000000000..fe9ef4f233c --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationLogQuery.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.List; + +/** + * Provide a means to query the activation log. + * + * @see ActivationLog + */ +public interface ActivationLogQuery { + + /** + * Clears the activation log. + */ + void clear(); + + /** + * The full transcript of all services phase transitions being managed. + * + * @return the activation log if log capture is enabled + */ + List> fullActivationLog(); + + /** + * A filtered list only including service providers. + * + * @param serviceProviders the filter + * @return the filtered activation log if log capture is enabled + */ + List> serviceProviderActivationLog(ServiceProvider... serviceProviders); + + /** + * A filtered list only including service providers. + * + * @param serviceTypeNames the filter + * @return the filtered activation log if log capture is enabled + */ + List> serviceProviderActivationLog(String... serviceTypeNames); + + /** + * A filtered list only including service providers. + * + * @param instances the filter + * @return the filtered activation log if log capture is enabled + */ + List> managedServiceActivationLog(Object... instances); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationPhase.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationPhase.java new file mode 100644 index 00000000000..115affaad8d --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationPhase.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * Forms a progression of full activation and deactivation. + */ +public enum ActivationPhase { + + /** + * Starting state before anything happens activation-wise. + */ + INIT(0, false), + + /** + * Planned to be activated. + */ + PENDING(1, true), + + /** + * Starting to be activated. + */ + ACTIVATION_STARTING(2, true), + + /** + * Gathering dependencies. + */ + GATHERING_DEPENDENCIES(3, true), + + /** + * Constructing. + */ + CONSTRUCTING(4, true), + + /** + * Injecting (fields then methods). + */ + INJECTING(5, true), + + /** + * Calling any post construct method. + */ + POST_CONSTRUCTING(6, true), + + /** + * Finishing post construct method. + */ + ACTIVATION_FINISHING(7, true), + + /** + * Service is active. + */ + ACTIVE(8, true), + + /** + * About to call pre-destroy. + */ + PRE_DESTROYING(9, false), + + /** + * Destroyed (after calling any pre-destroy). + */ + DESTROYED(10, false); + + /** + * The sequence of activation or deactivation. + */ + private final int sequence; + + /** + * True if this phase is eligible for deactivation/shutdown. + */ + private final boolean eligibleForDeactivation; + + /** + * The sequence of activation or deactivation. + * + * @return the sequence + */ + public int sequence() { + return sequence; + } + + /** + * Determines whether this phase passes the gate for whether deactivation (PreDestroy) can be called. + * + * @return true if this phase is eligible to be included in shutdown processing. + * + * @see PicoServices#shutdown() + */ + public boolean eligibleForDeactivation() { + return eligibleForDeactivation; + } + + ActivationPhase(int value, boolean eligibleForDeactivation) { + this.sequence = value; + this.eligibleForDeactivation = eligibleForDeactivation; + } +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationResult.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationResult.java new file mode 100644 index 00000000000..2bb8098d8cc --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationResult.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Future; + +import io.helidon.pico.builder.api.Builder; + +/** + * Represents the result of a service activation or deactivation. + * + * @see Activator + * @see DeActivator + * + * @param The type of the associated activator + */ +@Builder +public interface ActivationResult { + + /** + * The service provider undergoing activation or deactivation. + * + * @return the service provider generating the result + */ + ServiceProvider serviceProvider(); + + /** + * Optionally, given by the implementation provider to indicate the future completion when the provider's + * {@link ActivationStatus} is {@link ActivationStatus#WARNING_SUCCESS_BUT_NOT_READY}. + * + * @return the future result, assuming how activation can be async in nature + */ + Optional>> finishedActivationResult(); + + /** + * The activation phase that was found at onset of the phase transition. + * + * @return the starting phase + */ + ActivationPhase startingActivationPhase(); + + /** + * The activation phase that was requested at the onset of the phase transition. + * + * @return the target, desired, ultimate phase requested + */ + ActivationPhase ultimateTargetActivationPhase(); + + /** + * The activation phase we finished successfully on. + * + * @return the actual finishing phase + */ + ActivationPhase finishingActivationPhase(); + + /** + * How did the activation finish. + * + * @return the finishing status + */ + ActivationStatus finishingStatus(); + + /** + * The containing activation log that tracked this result. + * + * @return the activation log + */ + Optional activationLog(); + + /** + * The services registry that was used. + * + * @return the services registry + */ + Optional services(); + + /** + * Any vendor/provider implementation specific codes. + * + * @return the status code, 0 being the normal/default value + */ + int statusCode(); + + /** + * Any vendor/provider implementation specific description. + * + * @return a developer friendly description (useful if an error occurs) + */ + Optional statusDescription(); + + /** + * Any throwable/exceptions that were observed during activation. + * + * @return the captured error + */ + Optional error(); + + /** + * Returns true if this result is finished. + * + * @return true if finished + */ + default boolean finished() { + Future> f = finishedActivationResult().orElse(null); + return (Objects.isNull(f) || f.isDone()); + } + + /** + * Returns true if this result is successful. + * + * @return true if successful + */ + default boolean success() { + return success(finishingStatus()); + } + + /** + * Returns true if the provided result status was successful (i.e., not {@link io.helidon.pico.spi.ActivationStatus#FAILURE}). + * + * @param status the activation result status + * @return true if successful + */ + static boolean success(ActivationStatus status) { + return (ActivationStatus.FAILURE != status); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ActivationStatus.java b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationStatus.java new file mode 100644 index 00000000000..f50fb3b4796 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ActivationStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * The activation status. This status applies to the {@link io.helidon.pico.spi.ActivationLogEntry} record. + * + * @see Activator + */ +public enum ActivationStatus { + + /** + * The service has been activated and is fully ready to receive requests. + */ + SUCCESS, + + /** + * The service has been activated but is still being started asynchronously, and is not fully ready yet to receive requests. + * Important note: This is NOT health related. Health is orthogonal to service bindings/activation and readiness. + */ + WARNING_SUCCESS_BUT_NOT_READY, + + /** + * A general warning during lifecycle. + */ + WARNING_GENERAL, + + /** + * Failed to activate to the given phase. + */ + FAILURE + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Activator.java b/pico/spi/src/main/java/io/helidon/pico/spi/Activator.java new file mode 100644 index 00000000000..ab5b7d8037a --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Activator.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.api.Contract; + +/** + * Activators are responsible for lifecycle creation and lazy activation of service providers. They are responsible for taking the + * {@link ServiceProvider}'s manage service instance from {@link ActivationPhase#PENDING} + * through {@link ActivationPhase#POST_CONSTRUCTING} (i.e., including any + * {@link PostConstructMethod} invocations, etc.), and finally into + * {@link ActivationPhase#ACTIVE} status. In Helidon's Pico reference implementation that additionally means this occurs avoiding + * the use of reflection at runtime. + *

+ * Assumption: + *

    + *
  1. Each {@link ServiceProvider} managing its backing service will have an activator strategy conforming to the DI + * specification.
  2. + *
  3. Each services activation is expected to be non-blocking, but may in fact require deferred blocking activities to become + * fully ready for runtime operation.
  4. + *
+ * Activation includes: + *
    + *
  1. Management of the service's {@link ActivationPhase}.
  2. + *
  3. Control over creation (i.e., invoke the ctor non-reflectively).
  4. + *
  5. Control over gathering the service requisite dependencies (ctor, field, setters) and optional activation of those.
  6. + *
  7. Invocation of any {@link PostConstructMethod}.
  8. + *
  9. Responsible to logging to the {@link ActivationLog} - see {@link PicoServices#activationLog()}.
  10. + *
+ * + * @param the managed service type being activated + * @see DeActivator + */ +@Contract +public interface Activator { + + /** + * Activate a managed service/provider. + * + * @param targetServiceProvider the target service provider + * @param ipInfoCtx the optional injection point context + * @param ultimateTargetPhase the desired target phase for activation + * @param throwOnFailure should the provider throw if an error is observed, alternatively will return a result with an + * error inside + * @return the result of the activation + */ + ActivationResult activate(ServiceProvider targetServiceProvider, + InjectionPointInfo ipInfoCtx, + ActivationPhase ultimateTargetPhase, + boolean throwOnFailure); + + /** + * Activate a managed service/provider. + * + * @param targetServiceProvider the target service provider + * @param ipInfoCtx the optional injection point context + * @param ultimateTargetPhase the desired target phase for activation + * @return the result of the activation + */ + default ActivationResult activate(ServiceProvider targetServiceProvider, + InjectionPointInfo ipInfoCtx, + ActivationPhase ultimateTargetPhase) { + return activate(targetServiceProvider, ipInfoCtx, ultimateTargetPhase, true); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Application.java b/pico/spi/src/main/java/io/helidon/pico/spi/Application.java new file mode 100644 index 00000000000..40e10989739 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Application.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.api.Contract; + +/** + * An Application instance, if available at runtime, will be expected to provide a blueprint for all service provider's injection + * points. + *

+ * Note: instances of this type are not eligible for injection. + * + * @see io.helidon.pico.spi.Module + */ +@Contract +public interface Application extends Named { + + /** + * Called by the provider implementation at bootstrapping time to bind all injection plans to each and every service provider. + * + * @param binder the binder used to register the service provider injection plans + */ + void configure(ServiceInjectionPlanBinder binder); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/DeActivator.java b/pico/spi/src/main/java/io/helidon/pico/spi/DeActivator.java new file mode 100644 index 00000000000..609597da8f0 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/DeActivator.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.api.Contract; + +/** + * DeActivators are responsible for lifecycle, transitioning a {@link ServiceProvider}'s from its current phase through any + * {@link jakarta.annotation.PreDestroy} method invocations, and into the + * {@link ActivationPhase#DESTROYED} phase. These are inverse agents of {@link Activator}. + * + * @param the type to deactivate + * @see io.helidon.pico.spi.Activator + */ +@Contract +public interface DeActivator { + + /** + * Deactivate a managed service. This will trigger any {@link jakarta.annotation.PreDestroy} method on the + * underlying service type instance. + * + * @param targetServiceProvider the service provider responsible for calling deactivate + * @param throwOnFailure indicates whether the provider should throw if an error is observed + * @return the result + */ + ActivationResult deactivate(ServiceProvider targetServiceProvider, + boolean throwOnFailure); + + /** + * Deactivate a managed service. This will trigger any {@link jakarta.annotation.PreDestroy} method on the + * underlying service type instance. + * + * @param targetServiceProvider the service provider responsible for calling deactivate + * @return the result + */ + default ActivationResult deactivate(ServiceProvider targetServiceProvider) { + return deactivate(targetServiceProvider, true); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/DefaultQualifierAndValue.java b/pico/spi/src/main/java/io/helidon/pico/spi/DefaultQualifierAndValue.java new file mode 100644 index 00000000000..9d1d42dc5ab --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/DefaultQualifierAndValue.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Objects; + +import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.pico.types.DefaultAnnotationAndValue; +import io.helidon.pico.types.DefaultTypeName; +import io.helidon.pico.types.TypeName; + +import jakarta.inject.Named; + +/** + * Describes a {@link jakarta.inject.Qualifier} type annotation associated with a service being provided or dependant upon. + * In the Helidon Pico framework these are generally determined at compile time to avoid any use of reflection in runtime. + */ +public class DefaultQualifierAndValue extends DefaultAnnotationAndValue + implements QualifierAndValue, Comparable { + + /** + * Represents a {@link jakarta.inject.Named} type name with no value. + */ + public static final TypeName NAMED = DefaultTypeName.create(Named.class); + + /** + * Represents a wildcard {@link #NAMED} qualifier. + */ + public static final QualifierAndValue WILDCARD_NAMED = DefaultQualifierAndValue.createNamed("*"); + + /** + * Ctor. + * + * @param b the builder + * @see #builder() + */ + protected DefaultQualifierAndValue(Builder b) { + super(b); + } + + /** + * Creates a {@link jakarta.inject.Named} qualifier. + * + * @param name the name + * @return named qualifier + */ + public static DefaultQualifierAndValue createNamed(String name) { + return (DefaultQualifierAndValue) builder().typeName(NAMED).value(name).build(); + } + + /** + * Creates a qualifier from an annotation. + * + * @param qualifierType the qualifier type + * @return qualifier + */ + public static DefaultQualifierAndValue create(Class qualifierType) { + return (DefaultQualifierAndValue) builder().typeName(DefaultTypeName.create(qualifierType)).build(); + } + + /** + * Creates a qualifier. + * + * @param qualifierType the qualifier type + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(Class qualifierType, String val) { + return (DefaultQualifierAndValue) builder().typeName(DefaultTypeName.create(qualifierType)).value(val).build(); + } + + /** + * Creates a qualifier. + * + * @param qualifierTypeName the qualifier + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(String qualifierTypeName, String val) { + return create(DefaultTypeName.createFromTypeName(qualifierTypeName), val); + } + + /** + * Creates a qualifier. + * + * @param qualifierType the qualifier + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(TypeName qualifierType, String val) { + return (DefaultQualifierAndValue) builder() + .typeName(qualifierType) + .value(val) + .build(); + } + + /** + * Converts from a named contextual qualifier (e.g., {@link jakarta.inject.Named}). + * + * @param qualifierTypeName the qualifier + * @param values the values + * @return qualifier + */ + public static DefaultQualifierAndValue create(TypeName qualifierTypeName, Map values) { + return (DefaultQualifierAndValue) builder() + .typeName(qualifierTypeName) + .values(values) + .build(); + } + + /** + * Converts from an {@link io.helidon.pico.types.AnnotationAndValue} to a {@link io.helidon.pico.spi.QualifierAndValue}. + * + * @param annotationAndValue the annotation and value + * @return the qualifier and value equivalent + */ + public static QualifierAndValue convert(AnnotationAndValue annotationAndValue) { + if (annotationAndValue instanceof QualifierAndValue) { + return (QualifierAndValue) annotationAndValue; + } + + DefaultAnnotationAndValue from = (DefaultAnnotationAndValue) annotationAndValue; + if (Objects.nonNull(annotationAndValue.values()) && !annotationAndValue.values().isEmpty()) { + return create(annotationAndValue.typeName(), from.values()); + } + + return create(annotationAndValue.typeName(), annotationAndValue.value().orElse(null)); + } + + @Override + public int compareTo(AnnotationAndValue other) { + return typeName().compareTo(other.typeName()); + } + + + /** + * Creates a builder for {@link io.helidon.pico.spi.QualifierAndValue}. + * + * @return a fluent builder + */ + public static Builder builder() { + return new Builder(); + } + + + /** + * The fluent builder. + */ + public static class Builder extends DefaultAnnotationAndValue.Builder { + /** + * Ctor. + */ + protected Builder() { + } + + /** + * Build the instance. + * + * @return the built instance + */ + @Override + public DefaultQualifierAndValue build() { + return new DefaultQualifierAndValue(this); + } + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/DefaultServiceInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/DefaultServiceInfo.java new file mode 100644 index 00000000000..a30d453deb3 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/DefaultServiceInfo.java @@ -0,0 +1,694 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import io.helidon.pico.types.AnnotationAndValue; + +/** + * The default/reference implementation for {@link ServiceInfo}. + */ +public class DefaultServiceInfo implements ServiceInfo { + private final String serviceTypeName; + private final Set contractsImplemented; + private final Set externalContractsImplemented; + private final Set scopeTypeNames; + private final Set qualifiers; + private final String activatorTypeName; + private final Integer runLevel; + private final Double weight; + private String moduleName; + + /** + * Copy ctor. + * + * @param src the source to copy + */ + protected DefaultServiceInfo(ServiceInfo src) { + this.serviceTypeName = src.serviceTypeName(); + this.contractsImplemented = new TreeSet<>(src.contractsImplemented()); + this.externalContractsImplemented = new LinkedHashSet<>(src.externalContractsImplemented()); + this.scopeTypeNames = new LinkedHashSet<>(src.scopeTypeNames()); + this.qualifiers = new LinkedHashSet<>(src.qualifiers()); + this.activatorTypeName = src.activatorTypeName(); + this.runLevel = src.runLevel(); + this.moduleName = src.moduleName().orElse(null); + this.weight = src.declaredWeight().orElse(null); + } + + /** + * Builder ctor. + * + * @param b the builder + * @see #builder() + */ + @SuppressWarnings("unchecked") + protected DefaultServiceInfo(Builder b) { + this.serviceTypeName = b.serviceTypeName; + this.contractsImplemented = Objects.isNull(b.contractsImplemented) + ? Collections.emptySet() : Collections.unmodifiableSet(new TreeSet(b.contractsImplemented)); + this.externalContractsImplemented = Objects.isNull(b.externalContractsImplemented) + ? Collections.emptySet() : Collections.unmodifiableSet(b.externalContractsImplemented); + this.scopeTypeNames = Objects.isNull(b.scopeTypeNames) + ? Collections.emptySet() : Collections.unmodifiableSet(b.scopeTypeNames); + this.qualifiers = Objects.isNull(b.qualifiers) + ? Collections.emptySet() : Collections.unmodifiableSet(b.qualifiers); + this.activatorTypeName = b.activatorTypeName; + this.runLevel = b.runLevel; + this.moduleName = b.moduleName; + this.weight = b.weight; + } + + @Override + public Set externalContractsImplemented() { + return externalContractsImplemented; + } + + @Override + public String activatorTypeName() { + return activatorTypeName; + } + + @Override + public Optional moduleName() { + return Optional.ofNullable(moduleName); + } + + @Override + public String serviceTypeName() { + return serviceTypeName; + } + + @Override + public Set scopeTypeNames() { + return scopeTypeNames; + } + + @Override + public Set qualifiers() { + return qualifiers; + } + + @Override + public Set contractsImplemented() { + return contractsImplemented; + } + + @Override + public Integer runLevel() { + return runLevel; + } + + @Override + public Optional declaredWeight() { + return Optional.ofNullable(weight); + } + + + @Override + public int hashCode() { + if (Objects.isNull(serviceTypeName)) { + return Objects.hashCode(contractsImplemented()); + } + + return Objects.hashCode(serviceTypeName()) ^ Objects.hashCode(contractsImplemented()); + } + + @Override + public boolean equals(Object another) { + if (Objects.isNull(another) || !(another instanceof ServiceInfo)) { + return false; + } + + return equals(serviceTypeName(), ((ServiceInfo) another).serviceTypeName()) + && equals(contractsImplemented(), ((ServiceInfo) another).contractsImplemented()) + && equals(qualifiers(), ((ServiceInfo) another).qualifiers()) + && equals(activatorTypeName(), ((ServiceInfo) another).activatorTypeName()) + && equals(runLevel(), ((ServiceInfo) another).runLevel()) + && equals(realizedWeight(), ((ServiceInfo) another).realizedWeight()) + && equals(moduleName(), ((ServiceInfo) another).moduleName()); + } + + /** + * Provides a facade over {@link java.util.Objects#equals(Object, Object)}. + * + * @param o1 an object + * @param o2 an object to compare with a1 for equality + * @return true if a1 equals a2 + */ + public static boolean equals(Object o1, Object o2) { + return Objects.equals(o1, o2); + } + + /** + * Set/override the module name for this service info. Generally this is only called internally by the framework. + * + * @param moduleName the module name + */ + public void moduleName(String moduleName) { + this.moduleName = moduleName; + } + + /** + * Matches is a looser form of equality check than {@link #equals(Object, Object)}. If a service matches criteria + * it is generally assumed to be viable for assignability. + * + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + * @see io.helidon.pico.spi.Services#lookup(ServiceInfo) + */ + @Override + public boolean matches(ServiceInfo criteria) { + return matches(this, criteria); + } + + /** + * Matches is a looser form of equality check than {@link #equals(Object, Object)}. If a service matches criteria + * it is generally assumed to be viable for assignability. + * + * @param src the target service info to evaluate + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + * @see io.helidon.pico.spi.Services#lookup(ServiceInfo) + */ + protected static boolean matches(ServiceInfo src, ServiceInfo criteria) { + if (Objects.isNull(criteria)) { + return true; + } + + boolean matches = matches(src.serviceTypeName(), criteria.serviceTypeName()); + if (matches && Objects.isNull(criteria.serviceTypeName())) { + matches = matches(src.contractsImplemented(), criteria.contractsImplemented()) + || matches(src.serviceTypeName(), criteria.contractsImplemented()); + } + return matches + && matches(src.scopeTypeNames(), criteria.scopeTypeNames()) + && matchesQualifiers(src.qualifiers(), criteria.qualifiers()) + && matches(src.activatorTypeName(), criteria.activatorTypeName()) + && matches(src.runLevel(), criteria.runLevel()) + && matchesWeight(src, criteria.declaredWeight().orElse(null)) + && matches(src.moduleName(), criteria.moduleName()); + } + + /** + * Matches qualifier collections. + * + * @param src the target service info to evaluate + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + */ + public static boolean matchesQualifiers(Set src, Set criteria) { + if (criteria.isEmpty()) { + return true; + } + + if (src.isEmpty()) { + return false; + } + + if (src.contains(DefaultQualifierAndValue.WILDCARD_NAMED)) { + return true; + } + + for (QualifierAndValue criteriaQualifier : criteria) { + if (src.contains(criteriaQualifier)) { + // NOP; + continue; + } else if (criteriaQualifier.typeName().equals(DefaultQualifierAndValue.NAMED)) { + if (criteriaQualifier.equals(DefaultQualifierAndValue.WILDCARD_NAMED) + || criteriaQualifier.value().isEmpty()) { + // any Named qualifier will match ... + boolean hasSameTypeAsCriteria = src.stream() + .anyMatch(q -> q.typeName().equals(criteriaQualifier.typeName())); + if (hasSameTypeAsCriteria) { + continue; + } + } else if (src.contains(DefaultQualifierAndValue.WILDCARD_NAMED)) { + continue; + } + return false; + } else if (criteriaQualifier.value().isEmpty()) { + Set sameTypeAsCriteriaSet = src.stream() + .filter(q -> q.typeName().equals(criteriaQualifier.typeName())) + .collect(Collectors.toSet()); + if (sameTypeAsCriteriaSet.isEmpty()) { + return false; + } + } else { + return false; + } + } + + return true; + } + + /** + * Weight matching is always less than any criteria specified. + * + * @param src the item being considered + * @param criteria the criteria + * + * @return true if there is a match + */ + protected static boolean matchesWeight(ServiceInfoBasics src, Double criteria) { + if (Objects.isNull(criteria)) { + return matches(src.declaredWeight().orElse(null), criteria); + } + Double srcWeight = weightOf(src); + return (srcWeight.compareTo(criteria) < 0); + } + + /** + * Resolves the literal weight of a {@link io.helidon.pico.spi.ServiceInfoBasics} that is passed. + * + * @param src the weight to calculate for + * @return the weight of the service info argument + */ + public static double weightOf(ServiceInfoBasics src) { + return Objects.isNull(src) ? DEFAULT_WEIGHT : src.realizedWeight(); + } + + private static boolean matches(Set src, Set criteria) { + if (Objects.isNull(criteria) || criteria.isEmpty()) { + return true; + } + + if (Objects.isNull(src)) { + return false; + } + + return src.containsAll(criteria); + } + + private static boolean matches(String src, Set criteria) { + if (Objects.isNull(criteria)) { + return true; + } + + if (Objects.isNull(src)) { + return false; + } + + return criteria.contains(src); + } + + private static boolean matches(Object src, Object criteria) { + if (Objects.isNull(criteria)) { + return true; + } + + return equals(src, criteria); + } + + /** + * Clone a service info and wrap it using {@link DefaultServiceInfo}. + * + * @param src the target to clone + * @return the cloned copy of the provided service info + */ + public static DefaultServiceInfo cloneCopy(ServiceInfo src) { + if (Objects.isNull(src)) { + return null; + } + + return new DefaultServiceInfo(src); + } + + /** + * Constructs an instance of {@link DefaultServiceInfo} with the provided serviceInfo that + * describes the instance provided. + * + * @param instance the instance provided + * @param serviceInfo the service info that describes the instance provided + * @return the {@link DefaultServiceInfo} instance that identifies the service instance and info + */ + public static DefaultServiceInfo toServiceInfo(Object instance, ServiceInfo serviceInfo) { + if (Objects.nonNull(serviceInfo)) { + return cloneCopy(serviceInfo); + } + return DefaultServiceInfo.builder() + .serviceTypeName(instance.getClass().getName()) + .build(); + } + + /** + * Constructs an instance of {@link DefaultServiceInfo} given a service type class and some + * basic information that describes the service type. + * + * @param serviceType the service type + * @param siBasics the basic information that describes the service type + * @return an instance of {@link DefaultServiceInfo} + */ + public static DefaultServiceInfo toServiceInfoFromClass(Class serviceType, ServiceInfoBasics siBasics) { + if (siBasics instanceof DefaultServiceInfo) { + assert (serviceType.getName().equals(siBasics.serviceTypeName())); + return (DefaultServiceInfo) siBasics; + } + + if (Objects.isNull(siBasics)) { + return DefaultServiceInfo.builder() + .serviceTypeName(serviceType.getName()) + .build(); + } + + return DefaultServiceInfo.builder() + .serviceTypeName(serviceType.getName()) + .scopeTypeNames(siBasics.scopeTypeNames()) + .contractsImplemented(siBasics.contractsImplemented()) + .qualifiers(siBasics.qualifiers()) + .runLevel(siBasics.runLevel()) + .weight(siBasics.declaredWeight().orElse(null)) + .build(); + } + + + /** + * Creates a fluent builder for this type. + * + * @return A builder for {@link io.helidon.pico.spi.DefaultServiceInfo}. + */ + @SuppressWarnings("unchecked") + public static Builder> builder() { + return new Builder(); + } + + /** + * Creates a fluent builder initialized with the current values of this instance. + * + * @return A builder initialized with the current attributes. + */ + @SuppressWarnings("unchecked") + public Builder> toBuilder() { + return new Builder(this); + } + + + /** + * The fluent builder for {@link ServiceInfo}. + * + * @param the builder type + * @param the concrete type being build + */ + public static class Builder> { + private String serviceTypeName; + private Set contractsImplemented; + private Set externalContractsImplemented; + private Set scopeTypeNames; + private Set qualifiers; + private String activatorTypeName; + private Integer runLevel; + private String moduleName; + private Double weight; + + /** + * Ctor. + * + * @see #builder() + */ + protected Builder() { + } + + /** + * Ctor. + * + * @param c the existing value object + * @see #toBuilder() + */ + protected Builder(C c) { + this.serviceTypeName = c.serviceTypeName(); + this.contractsImplemented = new LinkedHashSet<>(c.contractsImplemented()); + this.externalContractsImplemented = new LinkedHashSet<>(c.externalContractsImplemented()); + this.scopeTypeNames = new LinkedHashSet<>(c.scopeTypeNames()); + this.qualifiers = new LinkedHashSet<>(c.qualifiers()); + this.activatorTypeName = c.activatorTypeName(); + this.runLevel = c.runLevel(); + this.moduleName = c.moduleName().orElse(null); + this.weight = c.declaredWeight().orElse(null); + } + + /** + * Builds the {@link io.helidon.pico.spi.DefaultServiceInfo}. + * + * @return the fluent builder instance + */ + @SuppressWarnings("unchecked") + public C build() { + return (C) new DefaultServiceInfo(this); + } + + @SuppressWarnings("unchecked") + private B identity() { + return (B) this; + } + + /** + * Sets the mandatory serviceTypeName for this {@link ServiceInfo}. + * + * @param serviceTypeName the service type name + * @return this fluent builder + */ + public B serviceTypeName(String serviceTypeName) { + this.serviceTypeName = serviceTypeName; + return identity(); + } + + /** + * Sets the mandatory serviceTypeName for this {@link ServiceInfo}. + * + * @param serviceType the service type + * @return this fluent builder + */ + public B serviceType(Class serviceType) { + return serviceTypeName(serviceType.getName()); + } + + /** + * Sets the optional name for this {@link ServiceInfo}. + * + * @param name the name + * @return this fluent builder + */ + public B named(String name) { + return Objects.isNull(name) ? identity() : qualifier(DefaultQualifierAndValue.createNamed(name)); + } + + /** + * Adds a singular qualifier for this {@link ServiceInfo}. + * + * @param qualifier the qualifier + * @return this fluent builder + */ + public B qualifier(QualifierAndValue qualifier) { + if (Objects.nonNull(qualifier)) { + if (Objects.isNull(qualifiers)) { + qualifiers = new LinkedHashSet<>(); + } + qualifiers.add(qualifier); + } + return identity(); + } + + /** + * Sets the collection of qualifiers for this {@link ServiceInfo}. + * + * @param qualifiers the qualifiers + * @return this fluent builder + */ + public B qualifiers(Collection qualifiers) { + this.qualifiers = Objects.isNull(qualifiers) + ? null : new LinkedHashSet<>(qualifiers); + return identity(); + } + + /** + * Adds a singular contract implemented for this {@link ServiceInfo}. + * + * @param contractImplemented the contract implemented + * @return this fluent builder + */ + public B contractImplemented(String contractImplemented) { + if (Objects.nonNull(contractImplemented)) { + if (Objects.isNull(contractsImplemented)) { + contractsImplemented = new LinkedHashSet<>(); + } + contractsImplemented.add(contractImplemented); + } + return identity(); + } + + /** + * Adds a contract implemented. + * + * @param contract the contract type + * @return this fluent builder + */ + public B contractTypeImplemented(Class contract) { + return contractImplemented(contract.getName()); + } + + /** + * Sets the collection of contracts implemented for this {@link ServiceInfo}. + * + * @param contractsImplemented the contract names implemented + * @return this fluent builder + */ + public B contractsImplemented(Collection contractsImplemented) { + this.contractsImplemented = Objects.isNull(contractsImplemented) + ? null : new LinkedHashSet<>(contractsImplemented); + return identity(); + } + + /** + * Adds a singular external contract implemented for this {@link ServiceInfo}. + * + * @param contractImplemented the type name of the external contract implemented + * @return this fluent builder + */ + public B externalContractImplemented(String contractImplemented) { + if (Objects.nonNull(contractImplemented)) { + if (Objects.isNull(this.externalContractsImplemented)) { + this.externalContractsImplemented = new LinkedHashSet<>(); + } + this.externalContractsImplemented.add(contractImplemented); + } + return contractImplemented(contractImplemented); + } + + /** + * Adds an external contract implemented. + * + * @param contract the external contract type + * @return this fluent builder + */ + public B externalContractTypeImplemented(Class contract) { + return externalContractImplemented(contract.getName()); + } + + /** + * Sets the collection of contracts implemented for this {@link ServiceInfo}. + * + * @param contractsImplemented the external contract names implemented + * @return this fluent builder + */ + public B externalContractsImplemented(Collection contractsImplemented) { + this.externalContractsImplemented = Objects.isNull(contractsImplemented) + ? null : new LinkedHashSet<>(contractsImplemented); + return identity(); + } + + /** + * Adds a singular scope type name for this {@link ServiceInfo}. + * + * @param scopeTypeName the scope type name + * @return this fluent builder + */ + public B scopeTypeName(String scopeTypeName) { + if (Objects.nonNull(scopeTypeName)) { + if (Objects.isNull(this.scopeTypeNames)) { + this.scopeTypeNames = new LinkedHashSet<>(); + } + this.scopeTypeNames.add(scopeTypeName); + } + return identity(); + } + + /** + * Sets the scope type. + * + * @param scopeType the scope type + * @return this fluent builder + */ + public B scopeType(Class scopeType) { + return scopeTypeName(scopeType.getName()); + } + + /** + * sets the collection of scope type names declared for this {@link ServiceInfo}. + * + * @param scopeTypeNames the contract names implemented + * @return this fluent builder + */ + public B scopeTypeNames(Collection scopeTypeNames) { + this.scopeTypeNames = Objects.isNull(scopeTypeNames) + ? null : new LinkedHashSet<>(scopeTypeNames); + return identity(); + } + + /** + * Sets the activator type name. + * + * @param activatorTypeName the activator type name + * @return this fluent builder + */ + public B activatorTypeName(String activatorTypeName) { + this.activatorTypeName = activatorTypeName; + return identity(); + } + + /** + * Sets the activator type. + * + * @param activatorType the activator type + * @return this fluent builder + */ + public B activatorType(Class activatorType) { + return activatorTypeName(activatorType.getName()); + } + + /** + * Sets the run level value. + * + * @param runLevel the run level + * @return this fluent builder + */ + public B runLevel(Integer runLevel) { + this.runLevel = runLevel; + return identity(); + } + + /** + * Sets the module name value. + * + * @param moduleName the module name + * @return this fluent builder + */ + public B moduleName(String moduleName) { + this.moduleName = moduleName; + return identity(); + } + + /** + * Sets the weight value. + * + * @param weight the weight (aka priority) + * @return this fluent builder + */ + public B weight(Double weight) { + this.weight = weight; + return identity(); + } + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/DependenciesInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/DependenciesInfo.java new file mode 100644 index 00000000000..994ed22f69a --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/DependenciesInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents a per {@link ServiceInfo} mapping of {@link DependencyInfo}'s. + */ +public interface DependenciesInfo { + + /** + * Represents the set of dependencies for each {@link ServiceInfo}. + * + * @return map from the service info to its dependencies + */ + Map> serviceInfoDependencies(); + + /** + * Represents a flattened list of all dependencies. + * + * @return the flattened list of all dependencies + */ + List allDependencies(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/DependencyInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/DependencyInfo.java new file mode 100644 index 00000000000..3f4229c0347 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/DependencyInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Set; + +import io.helidon.pico.builder.api.Builder; + +/** + * Aggregates the set of {@link io.helidon.pico.spi.InjectionPointInfo}'s that are dependent upon a specific + * {@link io.helidon.pico.spi.ServiceInfo}. + */ +@Builder +public interface DependencyInfo { + + /** + * The service info describing what the injection point dependencies are dependent upon. + * + * @return the service info dependency + */ + ServiceInfo dependencyTo(); + + /** + * The set of injection points that depends upon {@link #dependencyTo()}. + * + * @return the set of dependencies + */ + Set injectionPointDependencies(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ElementInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/ElementInfo.java new file mode 100644 index 00000000000..e72b8b6fcc7 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ElementInfo.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.builder.api.Builder; +import io.helidon.pico.types.AnnotationAndValue; + +/** + * Abstractly describes method or field elements of a managed service type (i.e., fields, constructors, injectable methods, etc.). + */ +@Builder +public interface ElementInfo { + + /** + * The name assigned to constructors. + */ + String CTOR = ""; + + /** + * The kind of injection target. + */ + enum ElementKind { + /** + * The injectable constructor. Note that there can be at most 1 injectable constructor. + */ + CTOR, + + /** + * A field. + */ + FIELD, + + /** + * A method. + */ + METHOD + } + + /** + * The access describing the target injection point. + */ + enum Access { + /** + * public. + */ + PUBLIC, + + /** + * protected. + */ + PROTECTED, + + /** + * package private. + */ + PACKAGE_PRIVATE, + + /** + * private. + */ + PRIVATE + } + + /** + * The injection point/receiver kind. + * + * @return the kind + */ + ElementKind elementKind(); + + /** + * The access modifier on the injection point/receiver. + * + * @return the access + */ + Access access(); + + /** + * The element type name (e.g., method type or field type). + * + * @return the target receiver type name + */ + String elementTypeName(); + + /** + * The element name (e.g., method name or field name). + * + * @return the target receiver name + */ + String elementName(); + + /** + * If the element is a method or constructor then this is the ordinal argument position of that argument. + * + * @return the offset argument, 0 based, or empty if field type + */ + Optional elementOffset(); + + /** + * True if the injection point is static. + * + * @return true if static receiver + */ + boolean staticDecl(); + + /** + * The enclosing class name for the element. + * + * @return service type name + */ + String serviceTypeName(); + + /** + * The annotations on this element. + * + * @return the annotations on this element + */ + Set annotations(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/EventReceiver.java b/pico/spi/src/main/java/io/helidon/pico/spi/EventReceiver.java new file mode 100644 index 00000000000..e641abbe7d5 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/EventReceiver.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * A receiver of events from the {@link Services} registry. Only {@link ServiceProvider}'s + * that are bound to the global service registry are capable of receiving events. + * + * @see io.helidon.pico.spi.ServiceProviderBindable + */ +public interface EventReceiver { + + /** + * Events issued from the framework. + */ + enum Event { + + /** + * Called after all modules and services from those modules are initially loaded into the service registry. + */ + POST_BIND_ALL_MODULES, + + /** + * Called after {@link #POST_BIND_ALL_MODULES} to resolve any latent bindings, prior to {@link #SERVICES_READY}. + */ + FINAL_RESOLVE, + + /** + * The service registry is fully populated and ready. + */ + SERVICES_READY + + } + + /** + * Called at the end of module and service bindings, when all the services in the service registry have been populated. + * + * @param event the event + */ + void onEvent(Event event); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InjectionException.java b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionException.java new file mode 100644 index 00000000000..df930b9861f --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionException.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; + +/** + * Represents an injection exception. These might be thrown either at compile time or at runtime depending upon how the + * application is built. + */ +public class InjectionException extends PicoServiceProviderException { + + private final Optional activationLog; + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + */ + public InjectionException(String msg) { + this(msg, null, null); + } + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public InjectionException(String msg, Throwable cause, ServiceProvider serviceProvider) { + this(msg, cause, serviceProvider, Optional.empty()); + } + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + * @param log the activity log + */ + public InjectionException(String msg, Throwable cause, ServiceProvider serviceProvider, Optional log) { + super(msg, cause, serviceProvider); + this.activationLog = log; + } + + /** + * Returns the activation log if available. + * + * @return the optional activation log + */ + public Optional activationLog() { + return activationLog; + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointInfo.java new file mode 100644 index 00000000000..51c440e145d --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Set; + +import io.helidon.pico.builder.api.Builder; + +/** + * Describes a receiver for injection - identifies who/what is requesting an injection that needs to be satisfied. + */ +@Builder +public interface InjectionPointInfo extends ElementInfo { + + /** + * The identifying name for this injection point. The identity should be unique for the service type it is contained within. + *

+ * This method will return the {@link #baseIdentity()} when {@link #elementOffset()} is null. If not null + * then the elemOffset is part of the returned identity. + * + * @return the unique identity + */ + String identity(); + + /** + * The base identifying name for this injection point. If the element represents a function, then the function arguments + * are encoded in its base identity. + * + * @return the base identity of the element + */ + String baseIdentity(); + + /** + * The qualifiers on this element. + * + * @return The qualifiers on this element. + */ + Set qualifiers(); + + /** + * True if the injection point is of type {@link java.util.List}. + * + * @return true if list based receiver + */ + boolean listWrapped(); + + /** + * True if the injection point is of type {@link java.util.Optional}. + * + * @return true if optional based receiver + */ + boolean optionalWrapped(); + + /** + * True if the injection point is of type Provider (or Supplier). + * + * @return true if provider based receiver + */ + boolean providerWrapped(); + + /** + * The dependency this is dependent upon. + * + * @return The service info we are dependent upon. + */ + ServiceInfo dependencyToServiceInfo(); + + /** + * Creates a pre-configured instance with an identity and a service type dependency. + * + * @param identity the identity + * @param dependencyToServiceInfo the service type dependency + * @return the created instance + */ + static DefaultInjectionPointInfo create(String identity, + ServiceInfo dependencyToServiceInfo) { + return DefaultInjectionPointInfo.builder() + .identity(identity) + .dependencyToServiceInfo(dependencyToServiceInfo) + .build(); + } +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointProvider.java b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointProvider.java new file mode 100644 index 00000000000..6481e6da910 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InjectionPointProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import jakarta.inject.Provider; + +/** + * Provides ability to contextualize the injected service by the target receiver of the injection point dynamically + * at runtime. + * + * @param the type that the provider produces + */ +public interface InjectionPointProvider extends Provider { + + /** + * Get (or create) an instance of this service type. + */ + @Override + default T get() { + return get(null, null, true); + } + + /** + * Get (or create) an instance of this service type - tailored upon its scope and the target (but optional) injection point / context. + * + * @param ipInfoCtx optionally, the injection point context if known + * @param criteria optionally, the service info required by the injection point context if known + * @param expected the flag indicating whether the injection point is required to be furnished + * @return the resolved service best matching the criteria for the injection point in terms of weight, or null if the context is not supported + */ + T get(InjectionPointInfo ipInfoCtx, ServiceInfo criteria, boolean expected); + + /** + * Get (or create) a list of instance of this service type - tailored upon its scope and the target (but optional) injection point / context. + * + * @param ipInfoCtx optionally, the injection point context if known - + * @param criteria optionally, the service info required by the injection point context if known + * @param expected the flag indicating whether the injection point is required to be furnished + * @return the resolved services matching criteria for the injection point in order of weight, or null if the context is not supported + */ + default List getList(InjectionPointInfo ipInfoCtx, ServiceInfo criteria, boolean expected) { + T instance = get(ipInfoCtx, criteria, expected); + return (Objects.isNull(instance)) ? null : Collections.singletonList(instance); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Injector.java b/pico/spi/src/main/java/io/helidon/pico/spi/Injector.java new file mode 100644 index 00000000000..3075499804e --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Injector.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Used to perform programmatic activation and injection. + *

+ * Note that the reference implementation of Pico only performs non-reflective, compile-time generation of service activators + * for services that it manages. This Injector contract is mainly provided in order to allow other library extensions / + * implementations to extend the model to perform other types of injection point resolution. + */ +public interface Injector { + + /** + * The strategy the injector should attempt to apply. The reference implementation for Pico provider only handles + * {@link io.helidon.pico.spi.Injector.Strategy#ACTIVATOR} type. + */ + enum Strategy { + /** + * Activator based implies compile-time injection strategy. This is the preferred / default strategy. + */ + ACTIVATOR, + + /** + * Reflection based implies runtime injection strategy. Note: This is available for other 3rd parties of Pico that choose + * to use reflection as a strategy. + */ + REFLECTION, + + /** + * Any. Defers the strategy to the provider implementation's capabilities and configuration. + */ + ANY + } + + /** + * Called to activate and inject a manage service instance or service provider, putting it into + * {@link ActivationPhase#ACTIVE}. + *

+ * Note that if a {@link ServiceProvider} is passed in then the {@link Activator} + * will be used instead. In this case, then any {@link InjectorOptions#startAtPhase()} and + * {@link InjectorOptions#finishAtPhase()} arguments will be ignored. + * + * @param serviceOrServiceProvider the target instance or service provider being activated and injected + * @param opts the optional injector options + * @param resultHolder the optional holder to receive the activation results, or null to have the provider throw + * {@link InjectionException} + * @param the managed service instance type + * @return the same instance passed in, activated, or else null if an error occurred + * @throws InjectionException if an injection problem occurs + * @see Activator + */ + T activateInject(T serviceOrServiceProvider, + Optional opts, + Optional>> resultHolder) throws InjectionException; + + /** + * Called to deactivate a managed service or service provider, putting it into {@link ActivationPhase#DESTROYED}. + * If a managed service has a {@link jakarta.annotation.PreDestroy} annotated method then it will be called during + * this lifecycle event. + *

+ * Note that if a {@link ServiceProvider} is passed in then the {@link DeActivator} + * will be used instead. In this case, then any {@link InjectorOptions#startAtPhase()} and + * {@link InjectorOptions#finishAtPhase()} arguments will be ignored. + * + * @param serviceOrServiceProvider the service provider or instance registered and being managed + * @param opts the optional injector options + * @param the managed service instance type + * @return the result of the deactivation + * @see DeActivator + */ + ActivationResult deactivate(T serviceOrServiceProvider, + Optional opts) throws InjectionException; + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InjectorOptions.java b/pico/spi/src/main/java/io/helidon/pico/spi/InjectorOptions.java new file mode 100644 index 00000000000..c00fad716ad --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InjectorOptions.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; + +import io.helidon.pico.builder.api.Builder; + +/** + * Provides optional, contextual tunings to the {@link Injector}. + * + * @see Injector + */ +@Builder +public interface InjectorOptions { + + /** + * The optional starting phase for the {@link Activator} behind the {@link Injector}. + * The default is the current phase that the managed {@link io.helidon.pico.spi.ServiceProvider} is currently in. + * + * @return the optional target finish phase + */ + Optional startAtPhase(); + + /** + * The optional target finishing phase for the {@link Activator} behind the {@link Injector}. + * The default is {@link ActivationPhase#ACTIVE}. + * + * @return the optional target finish phase + */ + Optional finishAtPhase(); + + /** + * The optional recipient target, describing who and what is being injected. + * + * @return the optional target injection point info + */ + Optional ipInfo(); + + /** + * The optional services registry to use, defaulting to {@link PicoServices#services()}. + * + * @return the optional services registry to use + */ + Optional services(); + + /** + * The optional activation log that the injection should record its activity on. + * + * @return the optional activation log to use + */ + Optional log(); + + /** + * The optional injection strategy the injector should apply. The default is {@link Injector.Strategy#ANY}. + * + * @return the optional injector strategy to use + */ + Optional strategy(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Intercepted.java b/pico/spi/src/main/java/io/helidon/pico/spi/Intercepted.java new file mode 100644 index 00000000000..3e1df788575 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Intercepted.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +/** + * Indicates that type identified by {@link #value()} is being intercepted. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Qualifier +@Target(java.lang.annotation.ElementType.TYPE) +public @interface Intercepted { + + /** + * The target being intercepted. + * + * @return the target class being intercepted + */ + Class value(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InterceptedTrigger.java b/pico/spi/src/main/java/io/helidon/pico/spi/InterceptedTrigger.java new file mode 100644 index 00000000000..3e6c7048146 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InterceptedTrigger.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation that when used will trigger the enclosing service type marked with this annotation to become intercepted. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface InterceptedTrigger { + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Interceptor.java b/pico/spi/src/main/java/io/helidon/pico/spi/Interceptor.java new file mode 100644 index 00000000000..927b8738838 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Interceptor.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.api.Contract; + +/** + * Implementors of this contract must be {@link jakarta.inject.Named} according to the {@link Intercepted} + * annotation they support. + */ +@Contract +public interface Interceptor { + + /** + * Called during interception of the target V. The implementation typically should finish with the call to + * {@link Interceptor.Chain#proceed()}. + * + * @param ctx the invocation context + * @param chain the chain to call proceed on + * @param the return value type (or {@link java.lang.Void} for void method elements) + * @return the return value to the caller + */ + V proceed(InvocationContext ctx, Chain chain); + + + /** + * Represents the next in line for interception, terminating with a call to the wrapped service provider. + * + * @param the return value + */ + interface Chain { + /** + * Call the next interceptor in line, or finishing with the call to the service provider. + * + * @return the result of the call. + */ + V proceed(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InvocationContext.java b/pico/spi/src/main/java/io/helidon/pico/spi/InvocationContext.java new file mode 100644 index 00000000000..87273e586c2 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InvocationContext.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.List; +import java.util.Map; + +import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.pico.types.TypeName; +import io.helidon.pico.types.TypedElementName; + +/** + * Used by {@link io.helidon.pico.spi.Interceptor}. + */ +public interface InvocationContext { + + /** + * The root service provider being intercepted. + * + * @return the root service provider being intercepted + */ + ServiceProvider rootServiceProvider(); + + /** + * The service type name for the root service provider. + * + * @return the service type name for the root service provider + */ + TypeName serviceTypeName(); + + /** + * The annotations on the enclosing type. + * + * @return the annotations on the enclosing type + */ + List classAnnotations(); + + /** + * The element info represents the method (or the constructor) being invoked. + * + * @return the element info represents the method (or the constructor) being invoked + */ + TypedElementName elementInfo(); + + /** + * The method/element argument info. + * + * @return the method/element argument info. + */ + TypedElementName[] elementArgInfo(); + + /** + * The arguments to the method. + * + * @return the read/write method/element arguments + */ + Object[] elementArgs(); + + /** + * The contextual info that can be shared between interceptors. + * + * @return the read/write contextual data that is passed between each chained interceptor + */ + Map contextData(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/InvocationException.java b/pico/spi/src/main/java/io/helidon/pico/spi/InvocationException.java new file mode 100644 index 00000000000..87fc2ddafd4 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/InvocationException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * Wraps any checked exceptions that are thrown during the {@link Interceptor} invocations. + */ +public class InvocationException extends PicoServiceProviderException { + + /** + * Ctor. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public InvocationException(String msg, Throwable cause, ServiceProvider serviceProvider) { + super(msg, cause, serviceProvider); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Module.java b/pico/spi/src/main/java/io/helidon/pico/spi/Module.java new file mode 100644 index 00000000000..5983fad31f9 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Module.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.api.Contract; + +/** + * Provides aggregation of services to the "containing" (jar) module. + * Modules can be provided explicitly by the developer, or automatically if using a pico apt processor during compile time. + *

+ * Note: instances of this type are not eligible for injection. + * + * @see Application + */ +@Contract +public interface Module extends Named { + + /** + * Called by the provider implementation at bootstrapping time to bind all services / service providers to the + * service registry. + * + * @param binder the binder used to register the services to the registry + */ + void configure(ServiceBinder binder); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Named.java b/pico/spi/src/main/java/io/helidon/pico/spi/Named.java new file mode 100644 index 00000000000..163d69f4070 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Named.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; + +/** + * Provides a means to identify if the instance is named. + * + * @see jakarta.inject.Named + */ +public interface Named { + + /** + * The optional name for this instance. + * + * @return the name associated with this instance or empty if not available or known. + */ + default Optional name() { + return Optional.empty(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PicoException.java b/pico/spi/src/main/java/io/helidon/pico/spi/PicoException.java new file mode 100644 index 00000000000..2be4df116e1 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PicoException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * A general purpose exception. + * + * @see PicoServiceProviderException + * @see InjectionException + * @see InvocationException + */ +public class PicoException extends RuntimeException { + + /** + * A general purpose exception from Pico. + * + * @param msg the message + */ + public PicoException(String msg) { + super(msg); + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param cause the root cause + */ + public PicoException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PicoServiceProviderException.java b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServiceProviderException.java new file mode 100644 index 00000000000..64745d6f037 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServiceProviderException.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Objects; +import java.util.Optional; + +/** + * A general purpose exception from Pico. + */ +public class PicoServiceProviderException extends PicoException { + + private final ServiceProvider serviceProvider; + + /** + * A general purpose exception from Pico. + * + * @param msg the message + */ + public PicoServiceProviderException(String msg) { + super(msg); + this.serviceProvider = null; + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public PicoServiceProviderException(String msg, Throwable cause, ServiceProvider serviceProvider) { + super(msg, cause); + if (Objects.isNull(serviceProvider)) { + if (cause instanceof PicoServiceProviderException) { + serviceProvider = ((PicoServiceProviderException) cause).getServiceProvider().orElse(null); + } + } + this.serviceProvider = serviceProvider; + } + + /** + * The service provider that this exception pertains to, or empty if not related to any particular provider. + * + * @return the optional / contextual service provider + */ + public Optional> getServiceProvider() { + return Optional.ofNullable(serviceProvider); + } + + @Override + public String getMessage() { + return super.getMessage() + + (Objects.isNull(serviceProvider) + ? "" : (": service provider: " + ServiceProvider.toDescription(serviceProvider))); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PicoServices.java b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServices.java new file mode 100644 index 00000000000..c029259fc75 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServices.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Future; + +import io.helidon.common.Weighted; + +/** + * Abstract factory for all services provided by a single Helidon Pico provider implementation. + * An implementation of this interface must minimally supply a "services registry" - see {@link #services()}. + */ +public interface PicoServices extends Weighted { + + /** + * Get {@link PicoServices} instance if available. The highest {@link io.helidon.common.Weighted} service will be loaded + * and returned. + * + * @return Pico services instance + */ + static Optional picoServices() { + return PicoServicesHolder.picoServices(); + } + + /** + * The service registry. + * + * @return the services registry + */ + Services services(); + + /** + * Creates a service binder instance for a specified module. + * + * @param module the module to offer binding to dynamically, and typically only at early startup initialization + * + * @return the service binder capable of binding, or empty if not permitted/available + */ + default Optional createServiceBinder(Module module) { + return Optional.empty(); + } + + /** + * Optionally, the injector. + * + * @return the injector, or empty if not available + */ + default Optional injector() { + return Optional.empty(); + } + + /** + * Optionally, the service providers' configuration. + * + * @return the config, or empty if not available + */ + default Optional config() { + return Optional.empty(); + } + + /** + * Attempts to perform a graceful {@link io.helidon.pico.spi.Injector#deactivate(Object, java.util.Optional)} on all managed + * service instances in the {@link io.helidon.pico.spi.Services} registry. Since deactivation can take some time to + * complete, a future is returned that can be used for tracking purposes. A dedicated thread is started to manage the + * deactivation/shutdown procedure for all active services in the registry. + *

+ * If the service provider does not support shutdown an empty is returned. + *

+ * The default reference implementation for Pico will return a map of all service types that were deactivated to any + * throwable that was observed during that services shutdown sequence. + *

+ * The order in which services are deactivated is dependent upon whether the {@link #activationLog()} is available. + * If the activation log is available, then services will be shutdown in reverse chronological order as how they + * were started. If the activation log is not enabled or found to be empty then the deactivation will be in reverse + * order of {@link io.helidon.pico.api.RunLevel} from the highest value down to the lowest value. If two services share + * the same {@link io.helidon.pico.api.RunLevel} value then the ordering will be based upon the implementation's comparator. + *

+ * Note that the service registry is NOT prevented from usage during or after shutdown. This means that it is possible + * for services to still be in an active state in the service registry even after shutdown is completed. If this is + * of concern then the recommendation is for the caller to repeatedly call shutdown() until the map contents are empty. + * + * @return a future of a map of all managed service types deactivated to any throwable observed during deactivation + */ + default Optional>>> shutdown() { + return Optional.empty(); + } + + /** + * Optionally, the service provider activation log. + * + * @return the injector, or empty if not available + */ + default Optional activationLog() { + return Optional.empty(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesConfig.java b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesConfig.java new file mode 100644 index 00000000000..2a17affac73 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesConfig.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.pico.api.Contract; + +/** + * Provides optional config by the provider implementation. + */ +@Contract +public interface PicoServicesConfig { + /** + * The short name for pico. + */ + String NAME = "pico"; + + /** + * The fully qualified name for pico (used for system properties, etc). + */ + String FQN = "io.helidon." + NAME; + + /** + * The key association with the name of the provider implementation. + */ + String KEY_PROVIDER = FQN + ".provider"; + /** + * The key association with the version of the provider implementation. + */ + String KEY_VERSION = FQN + ".version"; + + /** + * Applicable during activation, this is the key that controls the timeout before deadlock detection errors being thrown. + */ + String KEY_DEADLOCK_TIMEOUT_IN_MILLIS = FQN + ".deadlock.timeout.millis"; + /** + * The default deadlock detection timeout in millis. + */ + Long DEFAULT_DEADLOCK_TIMEOUT_IN_MILLIS = 10000L; + + /** + * Applicable for capturing activation logs. + */ + String KEY_ACTIVATION_LOGS_ENABLED = FQN + ".activation.logs.enabled"; + /** + * The default value for this is false, meaning that the activation logs will not be recorded or logged. + */ + Boolean DEFAULT_ACTIVATION_LOGS_ENABLED = false; + + /** + * The key that models the services registry, and whether the registry can expand dynamically after program startup. + */ + String KEY_SUPPORTS_DYNAMIC = FQN + ".supports.dynamic"; + /** + * The default value for this is false, meaning that the services registry cannot be changed during runtime. + */ + Boolean DEFAULT_SUPPORTS_DYNAMIC = false; + + /** + * The key that represents whether the provider support reflection, and reflection based activation/injection. + */ + String KEY_SUPPORTS_REFLECTION = FQN + ".supports.reflection"; + /** + * The default value for this is false, meaning no reflection is available or provided in the implementation. + */ + Boolean DEFAULT_SUPPORTS_REFLECTION = false; + + /** + * Can the provider support compile-time activation/injection (i.e., {@link Activator}'s)? + */ + String KEY_SUPPORTS_COMPILE_TIME = FQN + ".supports.compiletime"; + /** + * The default value is true, meaning injection points are evaluated at compile-time. + */ + Boolean DEFAULT_SUPPORTS_COMPILE_TIME = true; + + /** + * Can the services registry activate services in a thread-safe manner? + */ + String KEY_SUPPORTS_THREAD_SAFE_ACTIVATION = FQN + ".supports.threadsafe.activation"; + /** + * The default is true, meaning the implementation is (or should be) thread safe. + */ + Boolean DEFAULT_SUPPORTS_THREAD_SAFE_ACTIVATION = true; + + /** + * The key to represent whether the provider support and is compliant w/ Jsr-330. + */ + String KEY_SUPPORTS_JSR330 = FQN + ".supports.jsr330"; + /** + * The default value is true. + */ + Boolean DEFAULT_SUPPORTS_JSR330 = true; + + /** + * Can the injector / activator support static injection? Note: this is optional in Jsr-330 + */ + String KEY_SUPPORTS_JSR330_STATIC = FQN + ".supports.jsr330.static"; + /** + * The default value is false. + */ + Boolean DEFAULT_SUPPORTS_STATIC = false; + /** + * Can the injector / activator support private injection? Note: this is optional in Jsr-330 + */ + String KEY_SUPPORTS_JSR330_PRIVATE = FQN + ".supports.jsr330.private"; + /** + * The default value is false. + */ + Boolean DEFAULT_SUPPORTS_PRIVATE = false; + + /** + * Indicates whether the {@link io.helidon.pico.spi.Module}(s) should be read at startup. The default value is true. + */ + String KEY_BIND_MODULES = FQN + ".bind.modules"; + /** + * The default value is true. + */ + Boolean DEFAULT_BIND_MODULES = true; + + /** + * Indicates whether the {@link io.helidon.pico.spi.Application}(s) should be used as an optimization at startup to + * avoid lookups. The default value is true. + */ + String KEY_BIND_APPLICATION = FQN + ".bind.application"; + /** + * The default value is true. + */ + Boolean DEFAULT_BIND_APPLICATION = true; + + /** + * Supports a query mechanism to determine if properties are available/set. + * + * @param the type for the config value + * @param key the key to query for config + * @param defaultValueSupplier the default value + * @return the configuration value for the associated key, or else the default value supplied + */ + T value(String key, Supplier defaultValueSupplier); + + /** + * Return a string-type key value. + * + * @param key the key + * @return the value, or null if no key is found + */ + default String stringValue(String key) { + Object val = value(key, null); + return Objects.nonNull(val) ? val.toString() : null; + } + + /** + * Supports a properties-based mechanism to determine configuration. + * + * @return all properties in this configuration + */ + default Optional> properties() { + return Optional.empty(); + } + + /** + * Return a boolean-type key value. + * + * @param val the default value + * @return the value supplier + */ + static Supplier defaultValue(Boolean val) { + return () -> val; + } + + /** + * Return a string-type key value. + * + * @param val the default value + * @return the value supplier + */ + static Supplier defaultValue(String val) { + return () -> val; + } + + /** + * Return a long-type key value. + * + * @param val the default value + * @return the value supplier + */ + static Supplier defaultValue(Long val) { + return () -> val; + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesHolder.java b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesHolder.java new file mode 100644 index 00000000000..700ab862215 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PicoServicesHolder.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; + +/** + * The holder for the active {@link io.helidon.pico.spi.PicoServices} instance. + */ +class PicoServicesHolder { + private static final LazyValue> INSTANCE = LazyValue.create(PicoServicesHolder::load); + + private PicoServicesHolder() { + } + + static Optional picoServices() { + return INSTANCE.get(); + } + + private static Optional load() { + return HelidonServiceLoader.create(ServiceLoader.load(PicoServices.class)) + .asList() + .stream() + .findFirst(); + } +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PostConstructMethod.java b/pico/spi/src/main/java/io/helidon/pico/spi/PostConstructMethod.java new file mode 100644 index 00000000000..573b79a9584 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PostConstructMethod.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * Represents the {@link jakarta.annotation.PostConstruct} method. + * + * @see Activator + */ +@FunctionalInterface +public interface PostConstructMethod { + + /** + * Represents the post-construct method. + */ + void postConstruct(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/PreDestroyMethod.java b/pico/spi/src/main/java/io/helidon/pico/spi/PreDestroyMethod.java new file mode 100644 index 00000000000..47923bee904 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/PreDestroyMethod.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * Represents the {@link jakarta.annotation.PreDestroy} method. + * + * @see DeActivator + */ +@FunctionalInterface +public interface PreDestroyMethod { + + /** + * Represents the pre destroy method. + */ + void preDestroy(); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/QualifierAndValue.java b/pico/spi/src/main/java/io/helidon/pico/spi/QualifierAndValue.java new file mode 100644 index 00000000000..b9601a082e9 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/QualifierAndValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import io.helidon.pico.types.AnnotationAndValue; + +/** + * Represents a tuple of the Qualifier and optionally any value. + * + * @see jakarta.inject.Qualifier + */ +public interface QualifierAndValue extends AnnotationAndValue { + + /** + * The qualifier/annotation type name. + * + * @return the qualifier/annotation type name + */ + default String qualifierTypeName() { + return typeName().name(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceBinder.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceBinder.java new file mode 100644 index 00000000000..00ccf93f68f --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceBinder.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Responsible for binding service providers to the service registry. + */ +public interface ServiceBinder { + + /** + * Bind a service provider instance into the backing {@link Services} service registry. + * + * @param serviceProvider the service provider to bind into the service registry + */ + void bind(ServiceProvider serviceProvider); + + /** + * Converts the array of contract types to their respective contract names. + * + * @param contractTypes the class types to convert + * + * @return the set of contract names + */ + default Set toContractNames(Class... contractTypes) { + Set result = new LinkedHashSet<>(); + for (Class clazz : contractTypes) { + result.add(clazz.getName()); + } + return result; + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfo.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfo.java new file mode 100644 index 00000000000..98cf0e5643f --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Optional; +import java.util.Set; + +/** + * Describes a managed service or injection point. + */ +public interface ServiceInfo extends ServiceInfoBasics { + + /** + * The managed services external contracts / interfaces. These should also be contained within + * {@link #contractsImplemented()}. External contracts are from other modules other than the module containing + * the implementation typically. + * + * @return the service external contracts implemented + */ + Set externalContractsImplemented(); + + /** + * The management agent (i.e., the activator) that is responsible for creating and activating - typically build-time created. + * + * @return the activator type name + */ + String activatorTypeName(); + + /** + * The name of the ascribed module, if known. + * + * @return the module name + */ + Optional moduleName(); + + /** + * Determines whether this service info matches the criteria for injection. + * + * @param criteria the criteria to compare against + * @return true if matches + */ + boolean matches(ServiceInfo criteria); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfoBasics.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfoBasics.java new file mode 100644 index 00000000000..d72c2ff4c17 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInfoBasics.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.Weighted; +import io.helidon.pico.api.RunLevel; + +import jakarta.inject.Singleton; + +/** + * Basic service info that describes a service provider type. + */ +public interface ServiceInfoBasics extends Weighted { + + /** + * The managed service implementation {@link Class}. + * + * @return the service type name + */ + String serviceTypeName(); + + /** + * The managed service assigned Scope's. + * + * @return the service scope type name + */ + default Set scopeTypeNames() { + return Collections.singleton(Singleton.class.getName()); + } + + /** + * The managed service assigned Qualifier's. + * + * @return the service qualifiers + */ + default Set qualifiers() { + return Collections.emptySet(); + } + + /** + * The managed services "announced" interfaces - aka {@link io.helidon.pico.api.Contract}'s. + * + * @return the service contracts implemented + */ + default Set contractsImplemented() { + return Collections.emptySet(); + } + + /** + * The optional {@link io.helidon.pico.api.RunLevel} ascribed to the service. + * + * @return the service's run level + */ + default Integer runLevel() { + return RunLevel.NORMAL; + } + + /** + * Weight that was declared on the type itself. + * + * @return the declared weight + * @see #realizedWeight() + */ + default Optional declaredWeight() { + return Optional.of(weight()); + } + + /** + * The realized weight will use the default weight if no weight was specified directly. + * + * @return the realized weight + * @see #weight() + */ + default double realizedWeight() { + return declaredWeight().orElse(weight()); + } + + /** + * Determines whether this matches the given contract. + * + * @param contract the contract + * @return true if the service type name or the set of contracts implemented equals the provided contract + */ + default boolean matchesContract(String contract) { + return (Objects.equals(serviceTypeName(), contract) || contractsImplemented().contains(contract)); + } + + /** + * Determines whether this matches the given contract. + * + * @param contract the contract + * @return true if the service type name or the set of contracts implemented equals the provided contract + */ + default boolean matchesContract(Class contract) { + if (matchesContract(contract.getName())) { + return true; + } + + Class[] supers = contract.getInterfaces(); + for (Class iface : supers) { + if (matchesContract(iface)) { + return true; + } + } + + return false; + } + + /** + * Returns true if the criteria provided is non-null or there are referenced service types or contracts. + * + * @param serviceInfo the service info to inspect + * @return true if contracts specified + */ + static boolean hasContracts(ServiceInfoBasics serviceInfo) { + if (Objects.isNull(serviceInfo)) { + return false; + } + + return (!serviceInfo.contractsImplemented().isEmpty() || Objects.nonNull(serviceInfo.serviceTypeName())); + } + + /** + * Returns true if the criteria provided is blank/empty. + * + * @param serviceInfo the service info to inspect + * @return true if no contracts specified + */ + static boolean isBlank(ServiceInfoBasics serviceInfo) { + if (Objects.isNull(serviceInfo)) { + return true; + } + + return (!hasContracts(serviceInfo) && serviceInfo.qualifiers().isEmpty()); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInjectionPlanBinder.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInjectionPlanBinder.java new file mode 100644 index 00000000000..3755486f252 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceInjectionPlanBinder.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +/** + * Responsible for registering the injection plan to the services in the service registry. + */ +public interface ServiceInjectionPlanBinder { + + /** + * Bind an injection plan to a service provider instance. + * @param serviceProvider the service provider to receive the injection plan. + * @return the binder to use for binding the injection plan to the service provider + */ + Binder bindTo(ServiceProvider serviceProvider); + + /** + * The binder builder for the service plan. + * @see io.helidon.pico.spi.InjectionPointInfo + */ + interface Binder { + /** + * Binds a single service provider to the injection point identified by {@link InjectionPointInfo#identity()}. + * It is assumed that the caller of this is aware of the proper cardinality for each injection point. + * + * @param ipIdentity the injection point identity + * @param serviceProvider the service provider to bind to this identity. + * @param the service type + * @return the binder builder + */ + Binder bind(String ipIdentity, ServiceProvider serviceProvider); + + /** + * Binds a list of service providers to the injection point identified by {@link InjectionPointInfo#identity()}. + * It is assumed that the caller of this is aware of the proper cardinality for each injection point. + * + * @param ipIdentity the injection point identity + * @param serviceProviders the list of service providers to bind to this identity. + * @return the binder builder + */ + Binder bindMany(String ipIdentity, ServiceProvider... serviceProviders); + + /** + * Represents a void / null bind, only applicable for an Optional injection point. + * + * @param ipIdentity the injection point identity + * @return the binder builder + */ + Binder bindVoid(String ipIdentity); + + /** + * Represents injection points that cannot be bound at startup, and instead must rely on a + * deferred resolver based binding. Typically, this represents some form of dynamic or configurable instance. + * + * @param ipIdentity the injection point identity + * @param serviceType the service type needing to be resolved + * @return the binder builder + */ + Binder resolvedBind(String ipIdentity, Class serviceType); + + /** + * Commits the bindings for this service provider. + */ + void commit(); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProvider.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProvider.java new file mode 100644 index 00000000000..2dfc8510fda --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProvider.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.helidon.common.Weighted; + +import jakarta.inject.Singleton; + +/** + * Provides management lifecycle around services. + * + * @param the type that this service provider manages + */ +public interface ServiceProvider extends InjectionPointProvider, Weighted { + + /** + * Describe the service provider physically and (globally) uniquely. + * + * @return the unique identity description + */ + String identity(); + + /** + * Describe the service provider conceptually. + * + * @return the logical description + */ + String description(); + + /** + * Is the service annotated by @Singleton. + * This is a Helper only, one can alternatively check {@link ServiceInfo#scopeTypeNames()}. + * + * @return true if the service is a singleton + */ + default boolean isSingletonScope() { + return serviceInfo().scopeTypeNames().contains(Singleton.class.getName()); + } + + /** + * Does the service provide singletons, does it always produce the same result for every call to {@link #get()}. + * I.e., if the managed service implements Provider or + * {@link InjectionPointProvider} then this typically is considered not a singleton provider. + * I.e., If the managed services is NOT {@link Singleton}, then it will be treated as per request / dependent + * scope. + * Note that this is similar in nature to RequestScope, except the "official" request scope is bound to the + * web request. Here, we are speaking about contextually any caller asking for a new instance of the service in + * question. The requester in question will ideally be able to identify itself to this provider via + * {@link InjectionPointProvider#get(InjectionPointInfo, ServiceInfo, boolean)} so that this provider can properly + * service the "provide" request. + * + * @return true if the service provider provides per-request instances for each caller. + */ + boolean isProvider(); + + /** + * The meta information that describes the service. Must remain immutable for the lifetime of the JVM post + * binding - ie., after {@link ServiceBinder#bind(ServiceProvider)} is called. + * + * @return the meta information describing the service + */ + ServiceInfo serviceInfo(); + + /** + * Provides the dependencies for this service provider if known, or null if not known or not available. + * + * @return the dependencies this service provider has or null if unknown or unavailable + */ + DependenciesInfo dependencies(); + + /** + * The current activation phase for this service provider. + * + * @return the activation phase + */ + ActivationPhase currentActivationPhase(); + + /** + * The agent responsible for activation - this will be non-null for build-time activators. If not present then + * an {@link Injector} must be used to reflectively activate. + * + * @return the activator + */ + Activator activator(); + + /** + * The agent responsible for deactivation - this will be non-null for build-time activators. If not present then + * an {@link Injector} must be used to reflectively deactivate. + * + * @return the deactivator to use or null if the service is not interested in deactivation + */ + DeActivator deActivator(); + + /** + * The optional method handling PreDestroy. + * + * @return the post-construct method or empty if there is none + */ + Optional postConstructMethod(); + + /** + * The optional method handling PostConstruct. + * + * @return the pre-destroy method or empty if there is none + */ + Optional preDestroyMethod(); + + /** + * The agent/instance to be used for binding this service provider to the pico application that is class code generated. + * + * @return the service provider that should be used for binding + * @see Module + * @see ServiceBinder + * @see ServiceProviderBindable + */ + ServiceProvider serviceProviderBindable(); + + /** + * Attempts to convert the provider argument to a {@link io.helidon.pico.spi.ServiceProvider} for proper + * description, falling back to {@link String#valueOf(Object)}. + * + * @param provider the provider / instance + * @return the description for the provider + */ + static String toDescription(Object provider) { + if (provider instanceof Optional) { + provider = ((Optional) provider).orElse(null); + } + + if (provider instanceof ServiceProvider) { + return ((ServiceProvider) provider).description(); + } + return String.valueOf(provider); + } + + /** + * Streams and converts a list of providers to a list of {@link #toDescription(Object)} for each. + * + * @param coll the collection of service provider / instances + * @return the logical string representations for the collection provided + */ + static List toDescriptions(Collection coll) { + if (Objects.isNull(coll)) { + return null; + } + + return coll.stream().map(ServiceProvider::toDescription).collect(Collectors.toList()); + } + + /** + * Converts a provider to a physical identity. + * + * @param provider the provider + * @return the description for the provider + */ + static String toIdentity(Object provider) { + if (provider instanceof Optional) { + provider = ((Optional) provider).orElse(null); + } + + if (provider instanceof ServiceProvider) { + return ((ServiceProvider) provider).identity(); + } + + if (Objects.isNull(provider)) { + return null; + } + + return provider.getClass().getSimpleName(); + } + + /** + * Stream / converts a list of providers to their identities via {@link #identity()}. + * + * @param coll the collection of service providers + * @return the identity string representation for the collection provided + */ + static List toIdentities(Collection coll) { + if (Objects.isNull(coll)) { + return null; + } + + return coll.stream().map(ServiceProvider::toIdentity).collect(Collectors.toList()); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderBindable.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderBindable.java new file mode 100644 index 00000000000..df40eecbfd0 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderBindable.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.Objects; +import java.util.Optional; + +/** + * An extension to {@link io.helidon.pico.spi.ServiceProvider} that allows for startup binding from a picoApplication, + * and thereby works in conjunction with the {@link io.helidon.pico.spi.ServiceBinder} during pico service registry + * initialization. + *

+ * The only guarantee the provider implementation has is ensuring that {@link io.helidon.pico.spi.Module} instances + * are bound to the pico services instances, as well as informed on the module name. + *

+ * Generally this class should be called internally by the framework, and typically occurs only during initialization sequences. + * + * @param the type that this service provider manages + * @see io.helidon.pico.spi.Application + * @see ServiceProvider#serviceProviderBindable() + */ +public interface ServiceProviderBindable extends ServiceProvider { + + /** + * Used to indicate that there is no ability to bind this service provider. + * @see ServiceProvider#serviceProviderBindable() + */ + ServiceProviderBindable NOT_BINDABLE = null; + + /** + * Called to inform a service provider the module name it is bound to. Will only be called when there is a non-null + * module name associated for the given {@link io.helidon.pico.spi.Module}. A service provider can be associated with + * 0..1 modules. + * + * @param moduleName the non-null module name + */ + void moduleName(String moduleName); + + /** + * Returns {@code true} if this service provider is intercepted. + * + * @return flag indicating whether this service provider is intercepted + */ + default boolean isIntercepted() { + return Objects.nonNull(interceptor()); + } + + /** + * Returns the service provider that intercepts this provider. + * + * @return the service provider that intercepts this provider + */ + Optional> interceptor(); + + /** + * Sets the interceptor for this service provider. + * + * @param interceptor the interceptor for this provider + */ + default void interceptor(ServiceProvider interceptor) { + // NOP; intended to be overridden if applicable + throw new UnsupportedOperationException(); + } + + /** + * Returns the bindable contract of the service provider passed. + * + * @param serviceProvider the service provider + * @return the bindable provider of the service provider, or empty if the service provider does not support bind + */ + @SuppressWarnings("unchecked") + static Optional> toBindableProvider(ServiceProvider serviceProvider) { + if (serviceProvider instanceof ServiceProviderBindable) { + return Optional.of((ServiceProviderBindable) serviceProvider); + } + + return Objects.isNull(serviceProvider) + ? Optional.empty() : Optional.ofNullable((ServiceProviderBindable) serviceProvider.serviceProviderBindable()); + } + + /** + * Returns the root provider of the service provider passed. + * + * @param serviceProvider the service provider + * @return the root provider of the service provider, falling back to the service provider passed + * @see #rootProvider() + */ + @SuppressWarnings("unchecked") + static ServiceProvider toRootProvider(ServiceProvider serviceProvider) { + Optional> bindable = toBindableProvider(serviceProvider); + serviceProvider = bindable.orElse(null); + if (bindable.isPresent()) { + serviceProvider = bindable.get(); + } + + Optional> rootProvider = ((ServiceProviderBindable) serviceProvider).rootProvider(); + return rootProvider.orElse(serviceProvider); + } + + /** + * Gets the root/parent provider for this service. A root/parent provider is intended to manage it's underlying + * providers. Note that "root" and "parent" are interchangeable here since there is at most one level of depth that occurs + * when {@link io.helidon.pico.spi.ServiceProvider}'s are wrapped by other providers. + * + * @return the root/parent provider or empty if this instance is the root provider + */ + default Optional> rootProvider() { + return Optional.empty(); + } + + /** + * Returns true if this provider is the root provider. + * + * @return indicates whether this provider is a root provider - the default is true. + */ + default boolean isRootProvider() { + return rootProvider().isEmpty(); + } + + /** + * Sets the root/parent provider for this instance. + * + * @param rootProvider sets the root provider + */ + default void rootProvider(ServiceProvider rootProvider) { + // NOP; intended to be overridden if applicable + throw new UnsupportedOperationException(); + } + + /** + * The instance of services this provider is bound to. A service provider can be associated with 0..1 services instance. + * If not set, the service provider should use {@link PicoServices#picoServices()} to ascertain the instance. + * + * @param picoServices the pico services instance + */ + default void picoServices(PicoServices picoServices) { + // NOP; intended to be overridden if applicable + } + + /** + * The binder can be provided by the service provider to deterministically set the injection plan at compile-time, and + * subsequently loaded at early startup initialization. + * + * @return binder used for this service provider, or empty if not capable or ineligible of being bound + */ + default Optional injectionPlanBinder() { + return Optional.empty(); + } + + /** + * Returns true if the given service provider is intercepted. + * + * @param serviceProvider the service provider to check + * @return true if service provider is intercepted + */ + static boolean isIntercepted(ServiceProvider serviceProvider) { + return ((serviceProvider instanceof ServiceProviderBindable) + && ((ServiceProviderBindable) serviceProvider).isIntercepted()); + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderProvider.java b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderProvider.java new file mode 100644 index 00000000000..94dff2ad291 --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/ServiceProviderProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.List; +import java.util.Map; + +/** + * Instances of these provide lists and maps of {@link io.helidon.pico.spi.ServiceProvider}s. + */ +public interface ServiceProviderProvider { + + /** + * Returns a list of all matching service providers, potentially including itself in the result. + * + * @param criteria the injection point criteria that must match + * @param wantThis if this instance matches criteria, do we want to return this instance as part of the result + * @param thisAlreadyMatches an optimization that signals to the implementation that this instance has already + * matched using the standard service info matching checks + * @return the list of service providers matching + */ + List> serviceProviders(ServiceInfo criteria, boolean wantThis, boolean thisAlreadyMatches); + + /** + * This method will only apply to the managed/slave instances being provided, not to itself as in the case for + * {@link #serviceProviders(io.helidon.pico.spi.ServiceInfo, boolean, boolean)}. + * + * @param criteria the injection point criteria that must match + * @return the map of managed service providers matching the criteria, identified by its key/context + */ + Map> managedServiceProviders(ServiceInfo criteria); + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/Services.java b/pico/spi/src/main/java/io/helidon/pico/spi/Services.java new file mode 100644 index 00000000000..db54a5c68bb --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/Services.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2022 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.pico.spi; + +import java.util.List; + +/** + * The service registry. The service registry generally has knowledge about all the services that are available within your + * application, along with the contracts (i.e., interfaces) they advertise, the qualifiers that optionally describe them, and oll + * of each services' dependencies on other service contracts, etc. + * + * Collectively these service instances are considered "the managed service instances" under Pico. A {@link ServiceProvider} wrapper + * provides lifecycle management on the underlying service instances that each provider "manages" in terms of activation, scoping, + * etc. The service providers are typically created during compile-time processing when the Pico APT processor is applied to your + * module (i.e., any service annotated using {@link jakarta.inject.Singleton}, + * {@link io.helidon.pico.api.Contract}, {@link jakarta.inject.Inject}, etc.) during compile time. Additionally, they can be built + * using the Pico maven-plugin. Note also that the maven-plugin can be used to "compute" your applications entire DI model + * at compile time, generating an {@link io.helidon.pico.spi.Application} class that will be used at startup when found by the + * Pico framework. + *

+ * This Services interface exposes a read-only set of methods providing access to these "managed service" providers, and available + * via one of the lookup methods provided. Once you resolve the service provider(s), the service provider can be activated by + * calling one of its get() methods. This is equivalent to the declarative form just using {@link jakarta.inject.Inject} instead. + * Note that activation of a service might result in activation chaining. For example, service A injects service B, etc. When + * service A is activated then service A's dependencies (i.e., injection points) need to be activated as well. To avoid long + * activation chaining, it is recommended to that users strive to use {@link jakarta.inject.Provider} injection whenever possible. + * Provider injection (a) breaks long activation chains from occurring by deferring activation until when those services are really + * needed, and (b) breaks circular references that lead to {@link io.helidon.pico.spi.InjectionException} during activation (i.e., + * service A injects B, and service B injects A). + */ +public interface Services { + + /** + * Retrieve the "first" (i.e., highest {@link io.helidon.common.Weighted} or {@link jakarta.annotation.Priority} service + * that implements a given contract type with the expectation that there is an implementation available. + * + * @param type the criteria to find + * @param the type of the service + * @return the best service provider + */ + default ServiceProvider lookupFirst(Class type) { + return lookupFirst(type, true); + } + + /** + * Retrieve the "first" service that implements a given contract type, throwing + * an PlatformRuntimeException if the service was not found and it is/was expected. + *

+ * Note that this will retrieve only unnamed service contracts. When looking up + * singleton service implementations (not contracts) this method will still work without + * needing to pass the name. + * + * @param type the criteria to find + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider + */ + default ServiceProvider lookupFirst(Class type, boolean expected) { + return lookupFirst(type, null, expected); + } + + /** + * Retrieves the named services. + * + * @param type the criteria to find + * @param name the name for the service + * @param the type of the service + * @return the best service provider + */ + default ServiceProvider lookupFirst(Class type, String name) { + return lookupFirst(type, name, true); + } + + /** + * Retrieves the first based upon criteria. + * + * @param type the criteria to find + * @param name the name for the service + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider + */ + ServiceProvider lookupFirst(Class type, String name, boolean expected); + + /** + * Retrieves the first based upon criteria. + * + * @param serviceInfo the criteria to find + * @param the type of the service + * @return the best service provider + */ + default ServiceProvider lookupFirst(ServiceInfo serviceInfo) { + return lookupFirst(serviceInfo, true); + } + + /** + * Retrieves the first based upon criteria. + * + * @param criteria the criteria to find + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider + */ + ServiceProvider lookupFirst(ServiceInfo criteria, boolean expected); + + /** + * Retrieve all services that implement a given contract type. + *

+ * Note that this will retrieve named and unnamed services in {@link jakarta.annotation.Priority} order, or empty list. + * + * @param type the criteria to find + * @param the type of the service + * @return the list of service providers matching criteria + */ + List> lookup(Class type); + + /** + * Retrieve all services that match the criteria. + *

+ * The default implementation simply invokes {@link #lookup(ServiceInfo, boolean)}. + * + * @param criteria the criteria to find + * @param the type of the service + * @return the list of service providers matching criteria + */ + List> lookup(ServiceInfo criteria); + + /** + * Retrieve all services that match the criteria. + *

+ * Note 1: that this will retrieve named and unnamed services in {@link jakarta.annotation.Priority} order. + * Note 2: if {@link ServiceInfo#weight()} is provided as non-null to the criteria, then only services with a lesser + * weight value will be returned. + * + * @param criteria the criteria to find + * @param the type of the service + * @param expected indicates whether the provider should throw if a match is not found + * + * @return the list of service providers matching criteria + */ + List> lookup(ServiceInfo criteria, boolean expected); + + /** + * Implementors can provide a means to use a "special" services registry that better applies to the target injection + * point context. + *

+ * The default reference implementation does not implement anything special here. + * + * @param ignoredCtx the injection point context to use to filter the services to what qualifies for this injection point + * + * @return the qualifying services relative to the given context + */ + default Services contextualServices(InjectionPointInfo ignoredCtx) { + return this; + } + +} diff --git a/pico/spi/src/main/java/io/helidon/pico/spi/package-info.java b/pico/spi/src/main/java/io/helidon/pico/spi/package-info.java new file mode 100644 index 00000000000..60930e7261b --- /dev/null +++ b/pico/spi/src/main/java/io/helidon/pico/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 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. + */ + +/** + * Types that are Pico SPI/provider facing. + */ +package io.helidon.pico.spi; diff --git a/pico/spi/src/main/java/module-info.java b/pico/spi/src/main/java/module-info.java new file mode 100644 index 00000000000..d7d38cb7a00 --- /dev/null +++ b/pico/spi/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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. + */ + +/** + * Library for Helidon Pico's SPI. + */ +module io.helidon.pico.spi { + requires static io.helidon.pico.api; + requires static io.helidon.pico.builder.api; + requires static jakarta.annotation; + requires jakarta.inject; + requires io.helidon.common; + requires io.helidon.pico.types; + + exports io.helidon.pico.spi; +} diff --git a/pico/spi/src/test/java/io/helidon/pico/spi/test/PriorityAndServiceTypeComparatorTest.java b/pico/spi/src/test/java/io/helidon/pico/spi/test/PriorityAndServiceTypeComparatorTest.java new file mode 100644 index 00000000000..8326ea2b526 --- /dev/null +++ b/pico/spi/src/test/java/io/helidon/pico/spi/test/PriorityAndServiceTypeComparatorTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022 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.pico.spi.test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + +import io.helidon.common.Weighted; +import io.helidon.common.Weights; +import io.helidon.pico.spi.test.testsubjects.PicoServices1; + +import io.helidon.pico.spi.test.testsubjects.PicoServices2; +import io.helidon.pico.spi.test.testsubjects.PicoServices3; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; + +/** + * Ensure the weights comparator from common works the way we expect it to. Sanity test for Pico... + */ +class PriorityAndServiceTypeComparatorTest { + + static final Comparator comparator = Weights.weightComparator(); + + @Test + void ordering() { + assertThat(comparator.compare(null, null), is(0)); + assertThat(comparator.compare(null, null), is(0)); + assertThat(comparator.compare("a", "a"), is(0)); + assertThat(comparator.compare("a", "b"), is(lessThan(0))); + assertThat(comparator.compare("b", "a"), is(greaterThan(0))); + assertThat(comparator.compare(1, 2), is(lessThan(0))); + + assertThat(comparator.compare(new HighWeight(), new HighWeight()), is(0)); + assertThat(comparator.compare(new LowWeight(), new HighWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new LowWeight(), new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new DefaultWeight(), new LowWeight()), is(lessThan(0))); + assertThat(comparator.compare(new DefaultWeight(), null), is(lessThan(0))); + assertThat(comparator.compare(null, new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new NoWeight(), null), is(lessThan(0))); + assertThat(comparator.compare(null, new NoWeight()), is(greaterThan(0))); + + assertThat(comparator.compare(new DefaultWeight(), new DefaultWeight()), is(0)); + assertThat(comparator.compare(new DefaultWeight(), new NoWeight()), lessThan(0)); + assertThat(comparator.compare(new NoWeight(), new NoWeight()), is(0)); + assertThat(comparator.compare(new NoWeight(), new DefaultWeight()), is(greaterThan(0))); + + assertThat(comparator.compare(new JustAClass(), new JustBClass()), is(lessThan(0))); + assertThat(comparator.compare(new JustBClass(), new JustAClass()), is(greaterThan(0))); + assertThat(comparator.compare(new JustBClass(), new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new DefaultWeight(), new JustAClass()), is(lessThan(0))); + assertThat(comparator.compare(null, new JustAClass()), is(greaterThan(0))); + assertThat(comparator.compare(new JustAClass(), null), is(lessThan(0))); + + ArrayList list = new ArrayList<>(Arrays.asList(new PicoServices1(), new PicoServices2(), new PicoServices3())); + list.sort(comparator); + assertThat(list.get(0), instanceOf(PicoServices2.class)); + assertThat(list.get(1), instanceOf(PicoServices3.class)); + assertThat(list.get(2), instanceOf(PicoServices1.class)); + } + + static class DefaultWeight implements Weighted { + } + + static class NoWeight implements Weighted { + public Integer getPriority() { + return null; + } + } + + static class LowWeight implements Weighted { + public Integer getPriority() { + return 1; + } + } + + static class HighWeight implements Weighted { + @Override + public double weight() { + return DEFAULT_WEIGHT + 10; + } + } + + static class JustAClass { + } + + static class JustBClass { + } + +} diff --git a/pico/spi/src/test/java/io/helidon/pico/spi/test/ServiceLoaderTest.java b/pico/spi/src/test/java/io/helidon/pico/spi/test/ServiceLoaderTest.java new file mode 100644 index 00000000000..6d748156cc0 --- /dev/null +++ b/pico/spi/src/test/java/io/helidon/pico/spi/test/ServiceLoaderTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.pico.spi.test; + +import io.helidon.pico.spi.PicoServices; +import io.helidon.pico.spi.test.testsubjects.PicoServices2; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Service loader test for Pico. + */ +class ServiceLoaderTest { + + /** + * Test basic loader. + */ + @Test + void testGetPicoServices() { + PicoServices picoServices = PicoServices.picoServices().get(); + assertThat(picoServices, notNullValue()); + assertThat(PicoServices2.class, sameInstance(picoServices.getClass())); + assertThat(picoServices, sameInstance(PicoServices.picoServices().get())); + } + +} diff --git a/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices1.java b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices1.java new file mode 100644 index 00000000000..b1caca3a3d6 --- /dev/null +++ b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices1.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 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.pico.spi.test.testsubjects; + +import io.helidon.common.Weight; + +import io.helidon.pico.spi.PicoServices; +import io.helidon.pico.spi.Services; + +import jakarta.inject.Singleton; + +@Singleton +@Weight(PicoServices1.WEIGHT) +public class PicoServices1 implements PicoServices { + static final double WEIGHT = 1; + + /** + * ctor + */ + public PicoServices1() { + int dummy = 1; + } + + @Override + public double weight() { + return WEIGHT; + } + + @Override + public Services services() { + return null; + } + +} diff --git a/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices2.java b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices2.java new file mode 100644 index 00000000000..824c65ee211 --- /dev/null +++ b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices2.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 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.pico.spi.test.testsubjects; + +import io.helidon.common.Weight; + +import io.helidon.pico.spi.PicoServices; +import io.helidon.pico.spi.Services; + +import jakarta.inject.Singleton; + +@Singleton +@Weight(PicoServices2.WEIGHT) +public class PicoServices2 implements PicoServices { + static final double WEIGHT = 20; + + @Override + public double weight() { + return WEIGHT; + } + + @Override + public Services services() { + return null; + } +} diff --git a/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices3.java b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices3.java new file mode 100644 index 00000000000..2454f9258ad --- /dev/null +++ b/pico/spi/src/test/java/io/helidon/pico/spi/test/testsubjects/PicoServices3.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 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.pico.spi.test.testsubjects; + +import io.helidon.common.Weight; + +import io.helidon.pico.spi.PicoServices; +import io.helidon.pico.spi.Services; + +import jakarta.inject.Singleton; + +@Singleton +@Weight(PicoServices3.WEIGHT) +public class PicoServices3 implements PicoServices { + static final double WEIGHT = 3; + + @Override + public double weight() { + return WEIGHT; + } + + @Override + public Services services() { + return null; + } +} diff --git a/pico/spi/src/test/java/module-info.java b/pico/spi/src/test/java/module-info.java new file mode 100644 index 00000000000..8b97756053d --- /dev/null +++ b/pico/spi/src/test/java/module-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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. + */ + +/** + * Pico SPI test module. + */ +module io.helidon.pico.spi.test { + requires org.junit.jupiter.api; + requires hamcrest.all; + requires jakarta.inject; + requires io.helidon.common; + requires transitive io.helidon.pico.spi; + + uses io.helidon.pico.spi.PicoServices; + + exports io.helidon.pico.spi.test.testsubjects; + exports io.helidon.pico.spi.test; + + provides io.helidon.pico.spi.PicoServices with + io.helidon.pico.spi.test.testsubjects.PicoServices1, + io.helidon.pico.spi.test.testsubjects.PicoServices2, + io.helidon.pico.spi.test.testsubjects.PicoServices3; +} diff --git a/pico/spi/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServices b/pico/spi/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServices new file mode 100644 index 00000000000..f1bae2be4e2 --- /dev/null +++ b/pico/spi/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServices @@ -0,0 +1,3 @@ +io.helidon.pico.spi.test.testsubjects.PicoServices1 +io.helidon.pico.spi.test.testsubjects.PicoServices2 +io.helidon.pico.spi.test.testsubjects.PicoServices3