Skip to content

Commit

Permalink
Implement DI scopes
Browse files Browse the repository at this point in the history
... and reuse them for Sisu
  • Loading branch information
gnodet committed Sep 12, 2024
1 parent 2b13a43 commit d9ad7a4
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 284 deletions.
9 changes: 4 additions & 5 deletions maven-api-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ under the License.
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-settings</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-di</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-api</artifactId>
Expand Down Expand Up @@ -132,11 +136,6 @@ under the License.
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-di</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-named-locks</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.internal.impl.di;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.function.Supplier;

import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.di.Key;
import org.apache.maven.di.Scope;
import org.apache.maven.di.impl.DIException;

/**
* MojoExecutionScope
*/
public class MojoExecutionScope implements Scope {

protected static final class ScopeState {
private final Map<Key<?>, Supplier<?>> seeded = new HashMap<>();

private final Map<Key<?>, Object> provided = new HashMap<>();

public <T> void seed(Class<T> clazz, Supplier<T> value) {
seeded.put(Key.of(clazz), value);
}

public Collection<Object> provided() {
return provided.values();
}
}

private final ThreadLocal<LinkedList<ScopeState>> values = new ThreadLocal<>();

public MojoExecutionScope() {}

public static <T> Supplier<T> seededKeySupplier(Class<? extends T> clazz) {
return () -> {
throw new IllegalStateException(
"No instance of " + clazz.getName() + " is bound to the mojo execution scope.");
};
}

public void enter() {
LinkedList<ScopeState> stack = values.get();
if (stack == null) {
stack = new LinkedList<>();
values.set(stack);
}
stack.addFirst(new ScopeState());
}

protected ScopeState getScopeState() {
LinkedList<ScopeState> stack = values.get();
if (stack == null || stack.isEmpty()) {
throw new IllegalStateException();
}
return stack.getFirst();
}

public void exit() {
final LinkedList<ScopeState> stack = values.get();
if (stack == null || stack.isEmpty()) {
throw new IllegalStateException();
}
stack.removeFirst();
if (stack.isEmpty()) {
values.remove();
}
}

public <T> void seed(Class<T> clazz, Supplier<T> value) {
getScopeState().seed(clazz, value);
}

public <T> void seed(Class<T> clazz, final T value) {
seed(clazz, (Supplier<T>) () -> value);
}

@SuppressWarnings("unchecked")
@Nonnull
public <T> Supplier<T> scope(@Nonnull Key<T> key, @Nonnull Supplier<T> unscoped) {
return () -> {
LinkedList<ScopeState> stack = values.get();
if (stack == null || stack.isEmpty()) {
throw new DIException("Cannot access " + key + " outside of a scoping block");
}

ScopeState state = stack.getFirst();

Supplier<?> seeded = state.seeded.get(key);

if (seeded != null) {
return (T) seeded.get();
}

T provided = (T) state.provided.get(key);
if (provided == null && unscoped != null) {
provided = unscoped.get();
state.provided.put(key, provided);
}

return provided;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.internal.impl.di;

import org.apache.maven.di.impl.DIException;

public class OutOfScopeException extends DIException {
public OutOfScopeException(String message) {
super(message);
}

public OutOfScopeException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.internal.impl.di;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
import java.util.stream.Stream;

import org.apache.maven.di.Key;
import org.apache.maven.di.Scope;
import org.apache.maven.di.impl.Types;

public class SessionScope implements Scope {

/**
* ScopeState
*/
protected static final class ScopeState {
private final Map<Key<?>, CachingProvider<?>> provided = new ConcurrentHashMap<>();

public <T> void seed(Class<T> clazz, Supplier<T> value) {
provided.put(Key.of(clazz), new CachingProvider<>(value));
}

@SuppressWarnings("unchecked")
public <T> Supplier<T> scope(Key<T> key, Supplier<T> unscoped) {
Supplier<?> provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped));
return (Supplier<T>) provider;
}

public Collection<CachingProvider<?>> providers() {
return provided.values();
}
}

protected final List<ScopeState> values = new CopyOnWriteArrayList<>();

public void enter() {
values.add(0, new ScopeState());
}

protected ScopeState getScopeState() {
if (values.isEmpty()) {
throw new OutOfScopeException("Cannot access session scope outside of a scoping block");
}
return values.get(0);
}

public void exit() {
if (values.isEmpty()) {
throw new IllegalStateException();
}
values.remove(0);
}

public <T> void seed(Class<T> clazz, Supplier<T> value) {
getScopeState().seed(clazz, value);
}

public <T> void seed(Class<T> clazz, T value) {
seed(clazz, (Supplier<T>) () -> value);
}

@Override
public <T> Supplier<T> scope(Key<T> key, Supplier<T> unscoped) {
// Lazy evaluating provider
return () -> {
if (values.isEmpty()) {
return createProxy(key, unscoped);
} else {
return getScopeState().scope(key, unscoped).get();
}
};
}

@SuppressWarnings("unchecked")
protected <T> T createProxy(Key<T> key, Supplier<T> unscoped) {
InvocationHandler dispatcher = (proxy, method, args) -> {
method.setAccessible(true);
try {
return method.invoke(getScopeState().scope(key, unscoped).get(), args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
};
Class<T> superType = (Class<T>) Types.getRawType(key.getType());
Class<?>[] interfaces = getInterfaces(superType);
return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher);
}

protected Class<?>[] getInterfaces(Class<?> superType) {
if (superType.isInterface()) {
return new Class<?>[] {superType};
} else {
for (Annotation a : superType.getAnnotations()) {
Class<? extends Annotation> annotationType = a.annotationType();
if (isTypeAnnotation(annotationType)) {
try {
Class<?>[] value =
(Class<?>[]) annotationType.getMethod("value").invoke(a);
if (value.length == 0) {
value = superType.getInterfaces();
}
List<Class<?>> nonInterfaces =
Stream.of(value).filter(c -> !c.isInterface()).toList();
if (!nonInterfaces.isEmpty()) {
throw new IllegalArgumentException(
"The Typed annotation must contain only interfaces but the following types are not: "
+ nonInterfaces);
}
return value;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
}
throw new IllegalArgumentException("The use of session scoped proxies require "
+ "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation");
}
}

protected boolean isTypeAnnotation(Class<? extends Annotation> annotationType) {
return "org.apache.maven.api.di.Typed".equals(annotationType.getName());
}

/**
* A provider wrapping an existing provider with a cache
* @param <T> the provided type
*/
protected static class CachingProvider<T> implements Supplier<T> {
private final Supplier<T> provider;
private volatile T value;

CachingProvider(Supplier<T> provider) {
this.provider = provider;
}

public T value() {
return value;
}

@Override
public T get() {
if (value == null) {
synchronized (this) {
if (value == null) {
value = provider.get();
}
}
}
return value;
}
}

public static <T> Supplier<T> seededKeySupplier(Class<? extends T> clazz) {
return () -> {
throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope.");
};
}
}
Loading

0 comments on commit d9ad7a4

Please sign in to comment.