Skip to content

Commit

Permalink
Make javax-annotations optional for E4 Injector
Browse files Browse the repository at this point in the history
Rework the E4 InjectorImpl and introduce the AnnotationLookup class to
handle a potential absence of javax-annotations at runtime.

Print a warning if the 'old' javax-annotations are available at runtime
to encourage users to migrate.
The warning can be suppressed with the VM-property
-Declipse.e4.inject.javax.warning=false

Make the remaining imports of javax.inject/annotation packages optional.

Annotations who's classes cannot be loaded at runtime are silently
ignored by the JVM, as if they where not declared.

Part of eclipse-platform/eclipse.platform.releng.aggregator#1056
  • Loading branch information
HannesWell committed Nov 3, 2023
1 parent b434b24 commit 2cc7b9f
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Bundle-ActivationPolicy: lazy
Require-Bundle: org.eclipse.e4.core.di
Bundle-RequiredExecutionEnvironment: JavaSE-17
Import-Package: jakarta.inject;version="[2.0.0,3.0.0)",
javax.inject;version="[1.0.0,2.0.0)",
javax.inject;version="[1.0.0,2.0.0)";resolution:=optional,
org.osgi.framework;version="[1.5.0,2.0.0)",
org.osgi.service.event;version="[1.3.0,2.0.0)"
Export-Package: org.eclipse.e4.core.contexts;version="1.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@
* <p>
* If annotations are supported by the runtime, matching of methods and fields
* to be injected is also performed using the annotations defined in packages
* javax.inject and org.eclipse.e4.core.di.annotations.
* jakarta.inject and org.eclipse.e4.core.di.annotations.
* </p>
* <p>
* The injection of values is generally done as a number of calls. User objects
* that want to finalize the injected data (for instance, to perform
* calculations based on multiple injected values) can place such calculations
* in a method with the <code>javax.annotation.PostConstruct</code> annotation.
* in a method with the <code>jakarta.annotation.PostConstruct</code>
* annotation.
* </p>
* <p>
* When injecting values, all fields are injected prior to injection of methods.
Expand All @@ -55,7 +56,7 @@
* <p>
* When a context is disposed, the injection factory will attempt to notify all
* injected objects by calling methods with the
* <code>javax.annotation.PreDestroy</code> annotation.
* <code>jakarta.annotation.PreDestroy</code> annotation.
*
* This class is not intended to be extended by clients.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.eclipse.e4.core.di.suppliers.IObjectDescriptor;
import org.eclipse.e4.core.di.suppliers.IRequestor;
import org.eclipse.e4.core.di.suppliers.PrimaryObjectSupplier;
import org.eclipse.e4.core.internal.di.AnnotationLookup;
import org.eclipse.e4.core.internal.di.Requestor;

public class ContextObjectSupplier extends PrimaryObjectSupplier {
Expand Down Expand Up @@ -188,13 +189,9 @@ else if (targetContext.containsKey(keys[i]))
}

private String getKey(IObjectDescriptor descriptor) {
if (descriptor.hasQualifier(javax.inject.Named.class)) {
javax.inject.Named namedAnnotation = descriptor.getQualifier(javax.inject.Named.class);
return namedAnnotation.value();
}
if (descriptor.hasQualifier(jakarta.inject.Named.class)) {
jakarta.inject.Named namedAnnotation = descriptor.getQualifier(jakarta.inject.Named.class);
return namedAnnotation.value();
String value = AnnotationLookup.getQualifierValue(descriptor);
if (value != null) {
return value;
}
Type elementType = descriptor.getDesiredType();
return typeToString(elementType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ Bundle-Version: 1.8.200.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-17
Export-Package: org.eclipse.e4.core.di.annotations;version="1.6.0"
Import-Package: jakarta.inject;version="[2.0.0,3.0.0)",
javax.inject;version="[1.0.0,2.0.0)"
javax.inject;version="[1.0.0,2.0.0)";resolution:=optional
Bundle-Vendor: %Bundle-Vendor
Automatic-Module-Name: org.eclipse.e4.core.di.annotations
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Bundle-RequiredExecutionEnvironment: JavaSE-17
Export-Package: org.eclipse.e4.core.di.extensions;version="0.16.0"
Bundle-Localization: fragment
Import-Package: jakarta.inject;version="[2.0.0,3.0.0)",
javax.inject;version="[1.0.0,2.0.0)"
javax.inject;version="[1.0.0,2.0.0)";resolution:=optional
Automatic-Module-Name: org.eclipse.e4.core.di.extensions
6 changes: 3 additions & 3 deletions runtime/bundles/org.eclipse.e4.core.di/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Export-Package: org.eclipse.e4.core.di;version="1.7.0",
org.eclipse.e4.core.internal.di.osgi;x-internal:=true,
org.eclipse.e4.core.internal.di.shared;x-friends:="org.eclipse.e4.core.contexts,org.eclipse.e4.core.di.extensions.supplier"
Require-Bundle: org.eclipse.e4.core.di.annotations;bundle-version="[1.4.0,2.0.0)";visibility:=reexport
Import-Package: javax.annotation;version="[1.3.5,2.0.0)",
javax.inject;version="[1.0.0,2.0.0)",
Import-Package: jakarta.annotation;version="[2,3)",
jakarta.inject;version="[2,3)",
jakarta.annotation;version="[2,3)",
javax.annotation;version="[1.3.0,2.0.0)";resolution:=optional,
javax.inject;version="[1.0.0,2.0.0)";resolution:=optional,
org.eclipse.osgi.framework.log;version="1.1.0",
org.osgi.framework;version="[1.8.0,2.0.0)",
org.osgi.util.tracker;version="[1.5.1,2.0.0)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* set of optional qualifiers.
* </p>
*
* @see javax.inject.Qualifier
* @see jakarta.inject.Qualifier
* @noextend This interface is not intended to be extended by clients.
* @noimplement This interface is not intended to be implemented by clients.
* @since 1.7
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*******************************************************************************
* Copyright (c) 2023, 2023 Hannes Wellmann and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Hannes Wellmann - initial API and implementation
*******************************************************************************/

package org.eclipse.e4.core.internal.di;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;
import org.eclipse.e4.core.di.IInjector;
import org.eclipse.e4.core.di.suppliers.IObjectDescriptor;
import org.eclipse.e4.core.di.suppliers.PrimaryObjectSupplier;

/**
* A utility class to ease the look-up of jakarta/javax.inject and
* jakarta/javax.annotation annotations and types as mutual replacements, while
* being able to handle the absence of javax-classes in the runtime.
*
* If support for javax-annotations is removed, this class can be simplified to
* only handle jakarta-annotations, then all method can be inlined and this
* class eventually deleted, together with the entire test-project
* org.eclipse.e4.core.tests.
*/
public class AnnotationLookup {
private AnnotationLookup() {
}

public static record AnnotationProxy(List<Class<? extends Annotation>> classes) {
public AnnotationProxy {
classes = List.copyOf(classes);
}

public boolean isPresent(AnnotatedElement element) {
for (Class<? extends Annotation> annotationClass : classes) {
if (element.isAnnotationPresent(annotationClass)) {
return true;
}
}
return false;
}
}

static final AnnotationProxy INJECT = createProxyForClasses(jakarta.inject.Inject.class,
() -> javax.inject.Inject.class);
static final AnnotationProxy SINGLETON = createProxyForClasses(jakarta.inject.Singleton.class,
() -> javax.inject.Singleton.class);
static final AnnotationProxy QUALIFIER = createProxyForClasses(jakarta.inject.Qualifier.class,
() -> javax.inject.Qualifier.class);

static final AnnotationProxy PRE_DESTROY = createProxyForClasses(jakarta.annotation.PreDestroy.class,
() -> javax.annotation.PreDestroy.class);
public static final AnnotationProxy POST_CONSTRUCT = createProxyForClasses(jakarta.annotation.PostConstruct.class,
() -> javax.annotation.PostConstruct.class);

static final AnnotationProxy OPTIONAL = createProxyForClasses(org.eclipse.e4.core.di.annotations.Optional.class,
null);

private static AnnotationProxy createProxyForClasses(Class<? extends Annotation> jakartaAnnotationClass,
Supplier<Class<? extends Annotation>> javaxAnnotationClass) {
List<Class<?>> classes = getAvailableClasses(jakartaAnnotationClass, javaxAnnotationClass);
@SuppressWarnings({ "rawtypes", "unchecked" })
List<Class<? extends Annotation>> annotationClasses = (List) classes;
return new AnnotationProxy(annotationClasses);
}

private static final List<Class<?>> PROVIDER_TYPES = getAvailableClasses(jakarta.inject.Provider.class,
() -> javax.inject.Provider.class);

static boolean isProvider(Type type) {
for (Class<?> clazz : PROVIDER_TYPES) {
if (clazz.equals(type)) {
return true;
}
}
return false;
}

@FunctionalInterface
private interface ProviderFactory {
Object create(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider);
}

private static final ProviderFactory PROVIDER_FACTORY;
static {
ProviderFactory factory;
try {
/**
* This subclass solely exists for the purpose to not require the presence of
* the javax.inject.Provider interface in the runtime when the base-class is
* loaded. This can be deleted when support for javax is removed form the
* E4-injector.
*/
class JavaxCompatibilityProviderImpl<T> extends ProviderImpl<T> implements javax.inject.Provider<T> {
public JavaxCompatibilityProviderImpl(IObjectDescriptor descriptor, IInjector injector,
PrimaryObjectSupplier provider) {
super(descriptor, injector, provider);
}
}
factory = JavaxCompatibilityProviderImpl::new;
// Attempt to load the class early in order to enforce an early class-loading
// and to be able to handle the NoClassDefFoundError below in case
// javax-Provider is not available in the runtime:
factory.create(null, null, null);
} catch (NoClassDefFoundError e) {
factory = ProviderImpl::new;
}
PROVIDER_FACTORY = factory;
}

public static Object getProvider(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider) {
return PROVIDER_FACTORY.create(descriptor, injector, provider);
}

public static String getQualifierValue(IObjectDescriptor descriptor) {
var annotations = NAMED_ANNOTATION2VALUE_GETTER.entrySet();
for (Entry<Class<? extends Annotation>, Function<Annotation, String>> entry : annotations) {
Class<? extends Annotation> annotationClass = entry.getKey();
if (descriptor.hasQualifier(annotationClass)) {
Annotation namedAnnotation = descriptor.getQualifier(annotationClass);
return entry.getValue().apply(namedAnnotation);
}
}
return null;
}

private static final Map<Class<? extends Annotation>, Function<Annotation, String>> NAMED_ANNOTATION2VALUE_GETTER;

static {
Map<Class<? extends Annotation>, Function<Annotation, String>> annotation2valueGetter = new HashMap<>();
annotation2valueGetter.put(jakarta.inject.Named.class, a -> ((jakarta.inject.Named) a).value());
loadJavaxClass(
() -> annotation2valueGetter.put(javax.inject.Named.class, a -> ((javax.inject.Named) a).value()));
NAMED_ANNOTATION2VALUE_GETTER = Map.copyOf(annotation2valueGetter);
}

private static List<Class<?>> getAvailableClasses(Class<?> jakartaClass, Supplier<? extends Class<?>> javaxClass) {
List<Class<?>> classes = new ArrayList<>();
classes.add(jakartaClass);
if (javaxClass != null) {
loadJavaxClass(() -> classes.add(javaxClass.get()));
}
return classes;
}

private static boolean javaxWarningPrinted = false;

private static void loadJavaxClass(Runnable run) {
try {
run.run();
if (!javaxWarningPrinted) {
if (Boolean.parseBoolean(System.getProperty("eclipse.e4.inject.javax.warning", "true"))) { //$NON-NLS-1$//$NON-NLS-2$
@SuppressWarnings("nls")
String message = """
WARNING: Annotation classes from the 'javax.inject' or 'javax.annotation' package found.
It is recommended to migrate to the corresponding replacements in the jakarta namespace.
The Eclipse E4 Platform will remove support for those javax-annotations in a future release.
To suppress this warning set the VM property: -Declipse.e4.inject.javax.warning=false
""";
System.err.println(message);
}
javaxWarningPrinted = true;
}
} catch (NoClassDefFoundError e) {
// Ignore exception: javax-annotation seems to be unavailable in the runtime
}
}

}
Loading

0 comments on commit 2cc7b9f

Please sign in to comment.