001package io.avaje.inject;
002
003import io.avaje.inject.spi.Module;
004import io.avaje.inject.spi.PropertyRequiresPlugin;
005import io.avaje.lang.NonNullApi;
006import io.avaje.lang.Nullable;
007
008import java.lang.reflect.Type;
009import java.util.function.Consumer;
010import java.util.function.Supplier;
011
012/**
013 * Build a bean scope with options for shutdown hook and supplying external dependencies.
014 * <p>
015 * We can provide external dependencies that are then used in wiring the components.
016 * </p>
017 *
018 * <pre>{@code
019 *
020 *   // external dependencies
021 *   Pump pump = ...
022 *
023 *   BeanScope scope = BeanScope.builder()
024 *     .bean(pump)
025 *     .build();
026 *
027 *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
028 *   coffeeMaker.makeIt();
029 *
030 * }</pre>
031 */
032@NonNullApi
033public interface BeanScopeBuilder {
034
035  /**
036   * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook).
037   * <p>
038   * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime
039   * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy}
040   * lifecycle methods.
041   * </p>
042   * <pre>{@code
043   *
044   *   // automatically closed via try with resources
045   *
046   *   BeanScope scope = BeanScope.builder()
047   *     .shutdownHook(true)
048   *     .build());
049   *
050   *   // on JVM shutdown the preDestroy lifecycle methods are executed
051   *
052   * }</pre>
053   *
054   * @return This BeanScopeBuilder
055   */
056  BeanScopeBuilder shutdownHook(boolean shutdownHook);
057
058  /**
059   * Specify the modules to include in dependency injection.
060   * <p>
061   * Only beans related to the module are included in the BeanScope that is built.
062   * <p>
063   * When we do not explicitly specify modules then all modules that are not "custom scoped"
064   * are found and used via service loading.
065   *
066   * <pre>{@code
067   *
068   *   BeanScope scope = BeanScope.builder()
069   *     .modules(new CustomModule())
070   *     .build());
071   *
072   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
073   *   coffeeMaker.makeIt();
074   *
075   * }</pre>
076   *
077   * @param modules The modules that we want to include in dependency injection.
078   * @return This BeanScopeBuilder
079   */
080  BeanScopeBuilder modules(Module... modules);
081
082  /**
083   * Set the PropertyPlugin used for this scope. This is serviceloaded automatically of not set
084   *
085   * @param propertyRequiresPlugin The plugin for conditions based on properties
086   */
087  void propertyPlugin(PropertyRequiresPlugin propertyRequiresPlugin);
088
089  /**
090   * Return the PropertyPlugin used for this scope. This is useful for plugins that want to use
091   * the scopes wiring properties.
092   */
093  PropertyRequiresPlugin propertyPlugin();
094
095  /**
096   * Supply a bean to the scope that will be used instead of any similar bean in the scope.
097   *
098   * <p>This is typically expected to be used in tests and the bean supplied is typically a test
099   * double or mock.
100   *
101   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
102   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
103   * {@code @Nullable} annotation where the bean is wired.
104   *
105   * <pre>{@code
106   * // external dependencies
107   * Pump pump = ...
108   * Grinder grinder = ...
109   *
110   * BeanScope scope = BeanScope.builder()
111   *   .beans(pump, grinder)
112   *   .build();
113   *
114   * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
115   * coffeeMaker.makeIt();
116   *
117   * }</pre>
118   *
119   * @param beans Externally provided beans used when injecting a dependency for the bean or the
120   *              interface(s) it implements
121   * @return This BeanScopeBuilder
122   */
123  BeanScopeBuilder beans(Object... beans);
124
125  /**
126   * Add a supplied bean instance with the given injection type (typically an interface type).
127   *
128   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
129   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
130   * {@code @Nullable} annotation where the bean is wired.
131   *
132   * <pre>{@code
133   * Pump externalDependency = ...
134   *
135   * try (BeanScope scope = BeanScope.builder()
136   *   .bean(Pump.class, externalDependency)
137   *   .build()) {
138   *
139   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
140   *   coffeeMaker.makeIt();
141   *
142   *   Pump pump = scope.get(Pump.class);
143   *   assertThat(pump).isSameAs(externalDependency);
144   * }
145   *
146   * }</pre>
147   *
148   * @param type The dependency injection type this bean is target for
149   * @param bean The supplied bean instance to use for injection
150   */
151  <D> BeanScopeBuilder bean(Class<D> type, D bean);
152
153  /**
154   * Add a supplied bean instance with the given name and injection type.
155   *
156   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
157   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
158   * {@code @Nullable} annotation where the bean is wired.
159   *
160   * @param name The name qualifier
161   * @param type The dependency injection type this bean is target for
162   * @param bean The supplied bean instance to use for injection
163   */
164  <D> BeanScopeBuilder bean(String name, Class<D> type, D bean);
165
166  /**
167   * Add a supplied bean instance with the given name and generic type.
168   *
169   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
170   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
171   * {@code @Nullable} annotation where the bean is wired.
172   *
173   * @param name The name qualifier
174   * @param type The dependency injection type this bean is target for
175   * @param bean The supplied bean instance to use for injection
176   */
177  <D> BeanScopeBuilder bean(String name, Type type, D bean);
178
179  /**
180   * Add a supplied bean instance with a generic type.
181   *
182   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
183   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
184   * {@code @Nullable} annotation where the bean is wired.
185   *
186   * @param type The dependency injection type this bean is target for
187   * @param bean The supplied bean instance to use for injection
188   */
189  <D> BeanScopeBuilder bean(Type type, D bean);
190
191  /**
192   * Set the explicit profiles to use when building the scope.
193   *
194   * <p>If profiles are not set explicitly here they are read from the properties plugin.
195   */
196  BeanScopeBuilder profiles(String... profiles);
197
198  /**
199   * Add a supplied bean provider that acts as a default fallback for a dependency.
200   * <p>
201   * This provider is only called if nothing else provides the dependency. It effectively
202   * uses `@Secondary` priority.
203   *
204   * @param type     The type of the dependency
205   * @param provider The provider of the dependency.
206   */
207  default <D> BeanScopeBuilder provideDefault(Type type, Supplier<D> provider) {
208    return provideDefault(null, type, provider);
209  }
210
211  /**
212   * Add a supplied bean provider that acts as a default fallback for a dependency.
213   * <p>
214   * This provider is only called if nothing else provides the dependency. It effectively
215   * uses `@Secondary` priority.
216   *
217   * @param name     The name qualifier
218   * @param type     The type of the dependency
219   * @param provider The provider of the dependency.
220   */
221  <D> BeanScopeBuilder provideDefault(@Nullable String name, Type type, Supplier<D> provider);
222
223  /**
224   * Adds hooks that will execute after this scope is built.
225   */
226  BeanScopeBuilder addPostConstruct(Runnable postConstructHook);
227
228  /**
229   * Adds hook that will execute after this scope is built.
230   */
231  BeanScopeBuilder addPostConstruct(Consumer<BeanScope> postConstructHook);
232
233  /**
234   * Add hook that will execute before this scope is destroyed.
235   */
236  BeanScopeBuilder addPreDestroy(AutoCloseable preDestroyHook);
237
238  /**
239   * Add hook with a priority that will execute before this scope is destroyed.
240   * <p>
241   * Specify the priority of the destroy method to control its execution
242   * order relative to other destroy methods.
243   * <p>
244   * Low values for priority execute earlier than high values. All destroy methods
245   * without any explicit priority are given a value of 1000.
246   */
247  BeanScopeBuilder addPreDestroy(AutoCloseable preDestroyHook, int priority);
248
249  /**
250   * Set the ClassLoader to use when loading modules.
251   *
252   * @param classLoader The ClassLoader to use
253   */
254  BeanScopeBuilder classLoader(ClassLoader classLoader);
255
256  /**
257   * Use the given BeanScope as the parent. This becomes an additional
258   * source of beans that can be wired and accessed in this scope.
259   *
260   * @param parent The BeanScope that acts as the parent
261   */
262  BeanScopeBuilder parent(BeanScope parent);
263
264  /**
265   * Use the given BeanScope as the parent additionally specifying if beans
266   * added will effectively override beans that exist in the parent scope.
267   * <p>
268   * By default, child scopes will override a bean that exists in a parent scope.
269   * For testing purposes, parentOverride=false is used such that bean provided
270   * in parent test scopes are used (unless we mock() or spy() them).
271   * <p>
272   * See TestBeanScope in avaje-inject-test which has helper methods to build
273   * BeanScopes for testing with the "Global test scope" as a parent scope.
274   *
275   * @param parent         The BeanScope that acts as the parent
276   * @param parentOverride When false do not add beans that already exist on the parent.
277   *                       When true add beans regardless of whether they exist in the parent scope.
278   */
279  BeanScopeBuilder parent(BeanScope parent, boolean parentOverride);
280
281  /**
282   * Extend the builder to support testing using mockito with
283   * <code>withMock()</code> and <code>withSpy()</code> methods.
284   *
285   * @return The builder with extra testing support for mockito mocks and spies
286   */
287  BeanScopeBuilder.ForTesting forTesting();
288
289  /**
290   * Build and return the bean scope.
291   * <p>
292   * The BeanScope is effectively immutable in that all components are created
293   * and all PostConstruct lifecycle methods have been invoked.
294   * <p>
295   * The beanScope effectively contains eager singletons.
296   *
297   * @return The BeanScope
298   */
299  BeanScope build();
300
301  /**
302   * Extends the building with testing specific support for mocks and spies.
303   */
304  interface ForTesting extends BeanScopeBuilder {
305
306    /**
307     * Use a mockito mock when injecting this bean type.
308     *
309     * <pre>{@code
310     *
311     *   try (BeanScope scope = BeanScope.builder()
312     *     .forTesting()
313     *     .mock(Pump.class)
314     *     .mock(Grinder.class)
315     *     .build()) {
316     *
317     *
318     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
319     *     coffeeMaker.makeIt();
320     *
321     *     // this is a mockito mock
322     *     Grinder grinder = scope.get(Grinder.class);
323     *     verify(grinder).grindBeans();
324     *   }
325     *
326     * }</pre>
327     */
328    BeanScopeBuilder.ForTesting mock(Class<?> type);
329
330    /**
331     * Register as a Mockito mock with a qualifier name.
332     *
333     * <pre>{@code
334     *
335     *   try (BeanScope scope = BeanScope.builder()
336     *     .forTesting()
337     *     .mock(Store.class, "red")
338     *     .mock(Store.class, "blue")
339     *     .build()) {
340     *
341     *     ...
342     *   }
343     *
344     * }</pre>
345     */
346    BeanScopeBuilder.ForTesting mock(Class<?> type, String name);
347
348    /**
349     * Use a mockito mock when injecting this bean type additionally
350     * running setup on the mock instance.
351     *
352     * <pre>{@code
353     *
354     *   try (BeanScope scope = BeanScope.builder()
355     *     .forTesting()
356     *     .mock(Pump.class)
357     *     .mock(Grinder.class, grinder -> {
358     *
359     *       // setup the mock
360     *       when(grinder.grindBeans()).thenReturn("stub response");
361     *     })
362     *     .build()) {
363     *
364     *
365     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
366     *     coffeeMaker.makeIt();
367     *
368     *     // this is a mockito mock
369     *     Grinder grinder = scope.get(Grinder.class);
370     *     verify(grinder).grindBeans();
371     *   }
372     *
373     * }</pre>
374     */
375    <D> BeanScopeBuilder.ForTesting mock(Class<D> type, Consumer<D> consumer);
376
377    /**
378     * Use a mockito spy when injecting this bean type.
379     *
380     * <pre>{@code
381     *
382     *   try (BeanScope scope = BeanScope.builder()
383     *     .forTesting()
384     *     .spy(Pump.class)
385     *     .build()) {
386     *
387     *     // setup spy here ...
388     *     Pump pump = scope.get(Pump.class);
389     *     doNothing().when(pump).pumpSteam();
390     *
391     *     // act
392     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
393     *     coffeeMaker.makeIt();
394     *
395     *     verify(pump).pumpWater();
396     *     verify(pump).pumpSteam();
397     *   }
398     *
399     * }</pre>
400     */
401    BeanScopeBuilder.ForTesting spy(Class<?> type);
402
403    /**
404     * Register a Mockito spy with a qualifier name.
405     *
406     * <pre>{@code
407     *
408     *   try (BeanScope scope = BeanScope.builder()
409     *     .forTesting()
410     *     .spy(Store.class, "red")
411     *     .spy(Store.class, "blue")
412     *     .build()) {
413     *
414     *     ...
415     *   }
416     *
417     * }</pre>
418     */
419    BeanScopeBuilder.ForTesting spy(Class<?> type, String name);
420
421    /**
422     * Use a mockito spy when injecting this bean type additionally
423     * running setup on the spy instance.
424     *
425     * <pre>{@code
426     *
427     *   try (BeanScope scope = BeanScope.builder()
428     *     .forTesting()
429     *     .spy(Pump.class, pump -> {
430     *       // setup the spy
431     *       doNothing().when(pump).pumpWater();
432     *     })
433     *     .build()) {
434     *
435     *     // or setup here ...
436     *     Pump pump = scope.get(Pump.class);
437     *     doNothing().when(pump).pumpSteam();
438     *
439     *     // act
440     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
441     *     coffeeMaker.makeIt();
442     *
443     *     verify(pump).pumpWater();
444     *     verify(pump).pumpSteam();
445     *   }
446     *
447     * }</pre>
448     */
449    <D> BeanScopeBuilder.ForTesting spy(Class<D> type, Consumer<D> consumer);
450  }
451}