Skip to content

Commit a0fbb03

Browse files
authored
feat(dependency-injection): add toOverriding and asAllInterfaces binding API improvements (#31)
1 parent 76e5f08 commit a0fbb03

5 files changed

Lines changed: 427 additions & 11 deletions

File tree

dependency-injection/src/main/java/build/codemodel/injection/BindingBuilder.java

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,6 +25,7 @@
2525
import jakarta.inject.Qualifier;
2626

2727
import java.lang.annotation.Annotation;
28+
import java.util.function.Predicate;
2829
import java.util.function.Supplier;
2930

3031
/**
@@ -96,4 +97,69 @@ public interface BindingBuilder<T> {
9697
* @return this {@link BindingBuilder} to permit fluent-style method invocation
9798
*/
9899
BindingBuilder<T> with(AnnotationTypeUsage qualifier);
100+
101+
/**
102+
* Replaces an existing {@link Binding} for this type with the specified value, or registers a new
103+
* {@link Binding} if none exists. Unlike {@link #to(Object)}, this does not throw
104+
* {@link BindingAlreadyExistsException} when a binding is already registered.
105+
*
106+
* @param value the value
107+
* @return the newly registered {@link Binding}
108+
*/
109+
default Binding<T> toOverriding(final T value) {
110+
throw new UnsupportedOperationException("toOverriding is not supported by this BindingBuilder");
111+
}
112+
113+
/**
114+
* Replaces an existing {@link Binding} for this type with an instance of the specified concrete
115+
* {@link Class}, or registers a new {@link Binding} if none exists.
116+
*
117+
* @param implementationClass the concrete {@link Class} to bind
118+
* @return the newly registered {@link Binding}
119+
*/
120+
default Binding<T> toOverriding(final Class<? extends T> implementationClass) {
121+
throw new UnsupportedOperationException("toOverriding is not supported by this BindingBuilder");
122+
}
123+
124+
/**
125+
* Replaces an existing {@link Binding} for this type with the specified {@link Supplier}, or registers
126+
* a new {@link Binding} if none exists.
127+
*
128+
* @param supplier the {@link Supplier}
129+
* @return the newly registered {@link Binding}
130+
*/
131+
default Binding<T> toOverriding(final Supplier<T> supplier) {
132+
throw new UnsupportedOperationException("toOverriding is not supported by this BindingBuilder");
133+
}
134+
135+
/**
136+
* Registers a {@link Binding} of the bound value to each non-{@code java.*} interface in its type
137+
* hierarchy. Equivalent to calling {@link #asAllInterfaces(Predicate)} with a filter that excludes
138+
* interfaces whose package name starts with {@code "java."}.
139+
*
140+
* <p>Example:
141+
* <pre>{@code
142+
* context.bind(myService).asAllInterfaces();
143+
* // equivalent to registering bind(ServiceInterface.class).to(myService) for every user interface
144+
* }</pre>
145+
*
146+
* @throws UnsupportedOperationException if called on a builder that was not created via
147+
* {@link Context#bind(Object)}
148+
*/
149+
default void asAllInterfaces() {
150+
throw new UnsupportedOperationException("asAllInterfaces is not supported by this BindingBuilder");
151+
}
152+
153+
/**
154+
* Registers a {@link Binding} of the bound value to each interface in its type hierarchy that matches
155+
* the supplied {@link Predicate}.
156+
*
157+
* @param filter a {@link Predicate} applied to each interface {@link Class}; only matching interfaces
158+
* are bound
159+
* @throws UnsupportedOperationException if called on a builder that was not created via
160+
* {@link Context#bind(Object)}
161+
*/
162+
default void asAllInterfaces(final Predicate<Class<?>> filter) {
163+
throw new UnsupportedOperationException("asAllInterfaces is not supported by this BindingBuilder");
164+
}
99165
}

dependency-injection/src/main/java/build/codemodel/injection/Context.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,6 +25,7 @@
2525

2626
import java.util.function.BiFunction;
2727
import java.util.function.Function;
28+
import java.util.function.Predicate;
2829

2930
/**
3031
* An {@link Injector} that manages zero or more {@link Binding}s to be used for injection into {@link InjectionPoint}s.
@@ -51,6 +52,22 @@
5152
public interface Context
5253
extends Injector, Binder {
5354

55+
/**
56+
* Creates a {@link BindingBuilder} pre-loaded with the specified instance value, enabling
57+
* {@link BindingBuilder#asAllInterfaces()} and {@link BindingBuilder#asAllInterfaces(Predicate)} to
58+
* register the value against every interface in its type hierarchy in a single call.
59+
*
60+
* <p>Example:
61+
* <pre>{@code
62+
* context.bind(myService).asAllInterfaces();
63+
* }</pre>
64+
*
65+
* @param <T> the type of the instance
66+
* @param value the instance to bind
67+
* @return a {@link BindingBuilder} loaded with the value
68+
*/
69+
<T> BindingBuilder<T> bind(T value);
70+
5471
/**
5572
* Creates an instance of the specified class by locating and injecting a constructor annotated
5673
* with {@link Inject} with values resolved from the {@link Context}. Should an injectable

dependency-injection/src/main/java/build/codemodel/injection/InjectionContext.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
import java.util.ArrayList;
3333
import java.util.Collection;
3434
import java.util.IdentityHashMap;
35+
import java.util.LinkedHashSet;
3536
import java.util.List;
3637
import java.util.Objects;
3738
import java.util.Optional;
3839
import java.util.Set;
3940
import java.util.concurrent.ConcurrentHashMap;
4041
import java.util.concurrent.CopyOnWriteArrayList;
4142
import java.util.function.BiFunction;
43+
import java.util.function.Predicate;
4244
import java.util.function.Supplier;
4345
import java.util.stream.Collectors;
4446
import java.util.stream.Stream;
@@ -154,6 +156,36 @@ public Binding<T> to(final Supplier<T> supplier) {
154156

155157
return addBinding(dependency, new SupplierBinding<>(dependency, supplier));
156158
}
159+
160+
@Override
161+
public Binding<T> toOverriding(final T value) {
162+
final var dependency = IndependentDependency.of(
163+
typeUsage,
164+
this.injectionFramework::getQualifierAnnotationTypes);
165+
return replaceBinding(dependency, SingletonValueBinding.of(dependency, value));
166+
}
167+
168+
@Override
169+
public Binding<T> toOverriding(final Class<? extends T> concreteClass) {
170+
Objects.requireNonNull(concreteClass, "The Binding Value Class must not be null");
171+
final var dependency = IndependentDependency.of(
172+
typeUsage,
173+
this.injectionFramework::getQualifierAnnotationTypes);
174+
final var typeDescriptor = codeModel.getJDKTypeDescriptor(concreteClass)
175+
.orElseThrow(() -> new IllegalArgumentException(
176+
"Could not resolve a TypeDescriptor for " + concreteClass));
177+
return replaceBinding(dependency, this.injectionFramework.isSingleton(typeDescriptor)
178+
? new LazySingletonClassBinding<>(dependency, concreteClass)
179+
: new NonSingletonClassBinding<T>(dependency, concreteClass));
180+
}
181+
182+
@Override
183+
public Binding<T> toOverriding(final Supplier<T> supplier) {
184+
final var dependency = IndependentDependency.of(
185+
typeUsage,
186+
this.injectionFramework::getQualifierAnnotationTypes);
187+
return replaceBinding(dependency, new SupplierBinding<>(dependency, supplier));
188+
}
157189
};
158190
}
159191

@@ -175,6 +207,96 @@ private <T> Binding<T> addBinding(final Dependency dependency, final Binding<T>
175207
return binding;
176208
}
177209

210+
/**
211+
* Replaces or adds the specified {@link Dependency} {@link Binding} without throwing when a binding
212+
* already exists.
213+
*
214+
* @param dependency the {@link Dependency}
215+
* @param binding the {@link Binding}
216+
* @return the {@link Binding} that was registered
217+
*/
218+
private <T> Binding<T> replaceBinding(final Dependency dependency, final Binding<T> binding) {
219+
this.bindingsByDependency.put(dependency, binding);
220+
return binding;
221+
}
222+
223+
@Override
224+
@SuppressWarnings("unchecked")
225+
public <T> BindingBuilder<T> bind(final T value) {
226+
Objects.requireNonNull(value, "The value must not be null");
227+
228+
final var codeModel = this.injectionFramework.codeModel();
229+
final var type = (Class<T>) value.getClass();
230+
final var typeUsage = codeModel.getTypeUsage(type);
231+
232+
return new AbstractBindingBuilder<>(this.injectionFramework, typeUsage) {
233+
@Override
234+
public Binding<T> to(final T v) {
235+
final var dependency = IndependentDependency.of(
236+
typeUsage,
237+
this.injectionFramework::getQualifierAnnotationTypes);
238+
return addBinding(dependency, SingletonValueBinding.of(dependency, v));
239+
}
240+
241+
@Override
242+
public Binding<T> to(final Class<? extends T> concreteClass) {
243+
Objects.requireNonNull(concreteClass, "The Binding Value Class must not be null");
244+
final var dependency = IndependentDependency.of(
245+
typeUsage,
246+
this.injectionFramework::getQualifierAnnotationTypes);
247+
final var typeDescriptor = codeModel.getJDKTypeDescriptor(concreteClass)
248+
.orElseThrow(() -> new IllegalArgumentException(
249+
"Could not resolve a TypeDescriptor for " + concreteClass));
250+
return addBinding(dependency, this.injectionFramework.isSingleton(typeDescriptor)
251+
? new LazySingletonClassBinding<>(dependency, concreteClass)
252+
: new NonSingletonClassBinding<>(dependency, concreteClass));
253+
}
254+
255+
@Override
256+
public Binding<T> to(final Supplier<T> supplier) {
257+
final var dependency = IndependentDependency.of(
258+
typeUsage,
259+
this.injectionFramework::getQualifierAnnotationTypes);
260+
return addBinding(dependency, new SupplierBinding<>(dependency, supplier));
261+
}
262+
263+
@Override
264+
public void asAllInterfaces() {
265+
asAllInterfaces(iface -> !iface.getPackageName().startsWith("java."));
266+
}
267+
268+
@Override
269+
@SuppressWarnings({"unchecked", "rawtypes"})
270+
public void asAllInterfaces(final Predicate<Class<?>> filter) {
271+
Objects.requireNonNull(filter, "The filter must not be null");
272+
collectInterfaces(type, new LinkedHashSet<>())
273+
.stream()
274+
.filter(filter)
275+
.forEach(iface -> InjectionContext.this.bind((Class) iface).to(value));
276+
}
277+
};
278+
}
279+
280+
/**
281+
* Collects all interfaces from the type hierarchy of the given class, including inherited ones,
282+
* into the provided accumulator set.
283+
*
284+
* @param type the class to walk
285+
* @param result the set to accumulate interfaces into
286+
* @return the same set, populated
287+
*/
288+
private static Set<Class<?>> collectInterfaces(final Class<?> type, final Set<Class<?>> result) {
289+
if (type == null || type == Object.class) {
290+
return result;
291+
}
292+
for (final Class<?> iface : type.getInterfaces()) {
293+
result.add(iface);
294+
collectInterfaces(iface, result);
295+
}
296+
collectInterfaces(type.getSuperclass(), result);
297+
return result;
298+
}
299+
178300
@Override
179301
@SuppressWarnings("unchecked")
180302
public <T> MultiBinder<T> bindSet(final Class<T> type) {

dependency-injection/src/main/java/build/codemodel/injection/Modules.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,9 @@ public <T> MultiBinder<T> bindSet(final Class<T> type) {
8989
*
9090
* @param <T> the type being bound
9191
*/
92-
private static class SuppressingBindingBuilder<T>
92+
private record SuppressingBindingBuilder<T>(BindingBuilder<T> delegate)
9393
implements BindingBuilder<T> {
9494

95-
private final BindingBuilder<T> delegate;
96-
97-
SuppressingBindingBuilder(final BindingBuilder<T> delegate) {
98-
this.delegate = delegate;
99-
}
100-
10195
@Override
10296
public Binding<T> to(final T value) {
10397
try {
@@ -125,6 +119,21 @@ public Binding<T> to(final Supplier<T> supplier) {
125119
}
126120
}
127121

122+
@Override
123+
public Binding<T> toOverriding(final T value) {
124+
return this.delegate.toOverriding(value);
125+
}
126+
127+
@Override
128+
public Binding<T> toOverriding(final Class<? extends T> implementationClass) {
129+
return this.delegate.toOverriding(implementationClass);
130+
}
131+
132+
@Override
133+
public Binding<T> toOverriding(final Supplier<T> supplier) {
134+
return this.delegate.toOverriding(supplier);
135+
}
136+
128137
@Override
129138
public BindingBuilder<T> as(final String name) {
130139
this.delegate.as(name);

0 commit comments

Comments
 (0)