001package io.avaje.inject;
002
003import io.avaje.inject.core.BeanContextFactory;
004import io.avaje.inject.core.Builder;
005import io.avaje.inject.core.BuilderFactory;
006import io.avaje.inject.core.EnrichBean;
007import io.avaje.inject.core.SuppliedBean;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.HashMap;
014import java.util.Iterator;
015import java.util.LinkedHashSet;
016import java.util.List;
017import java.util.Map;
018import java.util.ServiceLoader;
019import java.util.Set;
020import java.util.function.Consumer;
021
022/**
023 * Build a bean context with options for shutdown hook and supplying test doubles.
024 * <p>
025 * We would choose to use BeanContextBuilder in test code (for component testing) as it gives us
026 * the ability to inject test doubles, mocks, spy's etc.
027 * </p>
028 *
029 * <pre>{@code
030 *
031 *   @Test
032 *   public void someComponentTest() {
033 *
034 *     MyRedisApi mockRedis = mock(MyRedisApi.class);
035 *     MyDbApi mockDatabase = mock(MyDbApi.class);
036 *
037 *     try (BeanContext context = new BeanContextBuilder()
038 *       .withBeans(mockRedis, mockDatabase)
039 *       .build()) {
040 *
041 *       // built with test doubles injected ...
042 *       CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
043 *       coffeeMaker.makeIt();
044 *
045 *       assertThat(...
046 *     }
047 *   }
048 *
049 * }</pre>
050 */
051public class BeanContextBuilder {
052
053  private static final Logger log = LoggerFactory.getLogger(BeanContextBuilder.class);
054
055  private boolean shutdownHook = true;
056
057  @SuppressWarnings("rawtypes")
058  private final List<SuppliedBean> suppliedBeans = new ArrayList<>();
059
060  @SuppressWarnings("rawtypes")
061  private final List<EnrichBean> enrichBeans = new ArrayList<>();
062
063  private final Set<String> includeModules = new LinkedHashSet<>();
064
065  private boolean ignoreMissingModuleDependencies;
066
067  /**
068   * Create a BeanContextBuilder to ultimately load and return a new BeanContext.
069   *
070   * <pre>{@code
071   *
072   *   try (BeanContext context = new BeanContextBuilder()
073   *     .build()) {
074   *
075   *     String makeIt = context.getBean(CoffeeMaker.class).makeIt();
076   *   }
077   * }</pre>
078   */
079  public BeanContextBuilder() {
080  }
081
082  /**
083   * Boot the bean context without registering a shutdown hook.
084   * <p>
085   * The expectation is that the BeanContextBuilder is closed via code or via using
086   * try with resources.
087   * </p>
088   * <pre>{@code
089   *
090   *   // automatically closed via try with resources
091   *
092   *   try (BeanContext context = new BeanContextBuilder()
093   *     .withNoShutdownHook()
094   *     .build()) {
095   *
096   *     String makeIt = context.getBean(CoffeeMaker.class).makeIt();
097   *   }
098   *
099   * }</pre>
100   *
101   * @return This BeanContextBuilder
102   */
103  public BeanContextBuilder withNoShutdownHook() {
104    this.shutdownHook = false;
105    return this;
106  }
107
108  /**
109   * Specify the modules to include in dependency injection.
110   * <p/>
111   * This is effectively a "whitelist" of modules names to include in the injection excluding
112   * any other modules that might otherwise exist in the classpath.
113   * <p/>
114   * We typically want to use this in component testing where we wish to exclude any other
115   * modules that exist on the classpath.
116   *
117   * <pre>{@code
118   *
119   *   @Test
120   *   public void someComponentTest() {
121   *
122   *     EmailServiceApi mockEmailService = mock(EmailServiceApi.class);
123   *
124   *     try (BeanContext context = new BeanContextBuilder()
125   *       .withBeans(mockEmailService)
126   *       .withModules("coffee")
127   *       .withIgnoreMissingModuleDependencies()
128   *       .build()) {
129   *
130   *       // built with test doubles injected ...
131   *       CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
132   *       coffeeMaker.makeIt();
133   *
134   *       assertThat(...
135   *     }
136   *   }
137   *
138   * }</pre>
139   *
140   * @param modules The names of modules that we want to include in dependency injection.
141   * @return This BeanContextBuilder
142   */
143  public BeanContextBuilder withModules(String... modules) {
144    this.includeModules.addAll(Arrays.asList(modules));
145    return this;
146  }
147
148  /**
149   * Set this when building a BeanContext (typically for testing) and supplied beans replace module dependencies.
150   * This means we don't need the usual module dependencies as supplied beans are used instead.
151   */
152  public BeanContextBuilder withIgnoreMissingModuleDependencies() {
153    this.ignoreMissingModuleDependencies = true;
154    return this;
155  }
156
157  /**
158   * Supply a bean to the context that will be used instead of any
159   * similar bean in the context.
160   * <p>
161   * This is typically expected to be used in tests and the bean
162   * supplied is typically a test double or mock.
163   * </p>
164   *
165   * <pre>{@code
166   *
167   *   Pump pump = mock(Pump.class);
168   *   Grinder grinder = mock(Grinder.class);
169   *
170   *   try (BeanContext context = new BeanContextBuilder()
171   *     .withBeans(pump, grinder)
172   *     .build()) {
173   *
174   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
175   *     coffeeMaker.makeIt();
176   *
177   *     Pump pump1 = context.getBean(Pump.class);
178   *     Grinder grinder1 = context.getBean(Grinder.class);
179   *
180   *     assertThat(pump1).isSameAs(pump);
181   *     assertThat(grinder1).isSameAs(grinder);
182   *
183   *     verify(pump).pumpWater();
184   *     verify(grinder).grindBeans();
185   *   }
186   *
187   * }</pre>
188   *
189   * @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements
190   * @return This BeanContextBuilder
191   */
192  @SuppressWarnings({"unchecked", "rawtypes"})
193  public BeanContextBuilder withBeans(Object... beans) {
194    for (Object bean : beans) {
195      suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
196    }
197    return this;
198  }
199
200  /**
201   * Add a supplied bean instance with the given injection type.
202   * <p>
203   * This is typically a test double often created by Mockito or similar.
204   * </p>
205   *
206   * <pre>{@code
207   *
208   *   try (BeanContext context = new BeanContextBuilder()
209   *     .withBean(Pump.class, mock)
210   *     .build()) {
211   *
212   *     Pump pump = context.getBean(Pump.class);
213   *     assertThat(pump).isSameAs(mock);
214   *
215   *     // act
216   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
217   *     coffeeMaker.makeIt();
218   *
219   *     verify(pump).pumpSteam();
220   *     verify(pump).pumpWater();
221   *   }
222   *
223   * }</pre>
224   *
225   * @param type The dependency injection type this bean is target for
226   * @param bean The supplied bean instance to use (typically a test mock)
227   */
228  public <D> BeanContextBuilder withBean(Class<D> type, D bean) {
229    suppliedBeans.add(new SuppliedBean<>(type, bean));
230    return this;
231  }
232
233  /**
234   * Use a mockito mock when injecting this bean type.
235   *
236   * <pre>{@code
237   *
238   *   try (BeanContext context = new BeanContextBuilder()
239   *     .withMock(Pump.class)
240   *     .withMock(Grinder.class, grinder -> {
241   *       // setup the mock
242   *       when(grinder.grindBeans()).thenReturn("stub response");
243   *     })
244   *     .build()) {
245   *
246   *
247   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
248   *     coffeeMaker.makeIt();
249   *
250   *     // this is a mockito mock
251   *     Grinder grinder = context.getBean(Grinder.class);
252   *     verify(grinder).grindBeans();
253   *   }
254   *
255   * }</pre>
256   */
257  public BeanContextBuilder withMock(Class<?> type) {
258    return withMock(type, null);
259  }
260
261  /**
262   * Use a mockito mock when injecting this bean type additionally
263   * running setup on the mock instance.
264   *
265   * <pre>{@code
266   *
267   *   try (BeanContext context = new BeanContextBuilder()
268   *     .withMock(Pump.class)
269   *     .withMock(Grinder.class, grinder -> {
270   *
271   *       // setup the mock
272   *       when(grinder.grindBeans()).thenReturn("stub response");
273   *     })
274   *     .build()) {
275   *
276   *
277   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
278   *     coffeeMaker.makeIt();
279   *
280   *     // this is a mockito mock
281   *     Grinder grinder = context.getBean(Grinder.class);
282   *     verify(grinder).grindBeans();
283   *   }
284   *
285   * }</pre>
286   */
287  public <D> BeanContextBuilder withMock(Class<D> type, Consumer<D> consumer) {
288    suppliedBeans.add(new SuppliedBean<>(type, null, consumer));
289    return this;
290  }
291
292  /**
293   * Use a mockito spy when injecting this bean type.
294   *
295   * <pre>{@code
296   *
297   *   try (BeanContext context = new BeanContextBuilder()
298   *     .withSpy(Pump.class)
299   *     .build()) {
300   *
301   *     // setup spy here ...
302   *     Pump pump = context.getBean(Pump.class);
303   *     doNothing().when(pump).pumpSteam();
304   *
305   *     // act
306   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
307   *     coffeeMaker.makeIt();
308   *
309   *     verify(pump).pumpWater();
310   *     verify(pump).pumpSteam();
311   *   }
312   *
313   * }</pre>
314   */
315  public BeanContextBuilder withSpy(Class<?> type) {
316    return withSpy(type, null);
317  }
318
319  /**
320   * Use a mockito spy when injecting this bean type additionally
321   * running setup on the spy instance.
322   *
323   * <pre>{@code
324   *
325   *   try (BeanContext context = new BeanContextBuilder()
326   *     .withSpy(Pump.class, pump -> {
327   *       // setup the spy
328   *       doNothing().when(pump).pumpWater();
329   *     })
330   *     .build()) {
331   *
332   *     // or setup here ...
333   *     Pump pump = context.getBean(Pump.class);
334   *     doNothing().when(pump).pumpSteam();
335   *
336   *     // act
337   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
338   *     coffeeMaker.makeIt();
339   *
340   *     verify(pump).pumpWater();
341   *     verify(pump).pumpSteam();
342   *   }
343   *
344   * }</pre>
345   */
346  public <D> BeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer) {
347    enrichBeans.add(new EnrichBean<>(type, consumer));
348    return this;
349  }
350
351  /**
352   * Build and return the bean context.
353   *
354   * @return The BeanContext
355   */
356  public BeanContext build() {
357    // sort factories by dependsOn
358    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies);
359    ServiceLoader.load(BeanContextFactory.class).forEach(factoryOrder::add);
360
361    Set<String> moduleNames = factoryOrder.orderFactories();
362    if (moduleNames.isEmpty()) {
363      throw new IllegalStateException("No modules found suggests using Gradle and IDEA but with a setup issue?" +
364        " Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. " +
365        " Refer to https://dinject.io/docs/gradle#idea");
366    }
367    log.debug("building context with modules {}", moduleNames);
368    Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans, enrichBeans);
369    for (BeanContextFactory factory : factoryOrder.factories()) {
370      rootBuilder.addChild(factory.createContext(rootBuilder));
371    }
372
373    BeanContext beanContext = rootBuilder.build();
374    // entire graph built, fire postConstruct
375    beanContext.start();
376    if (shutdownHook) {
377      return new ShutdownAwareBeanContext(beanContext);
378    }
379    return beanContext;
380  }
381
382  /**
383   * Return the type that we map the supplied bean to.
384   */
385  private Class<?> suppliedType(Class<?> suppliedClass) {
386    Class<?> suppliedSuper = suppliedClass.getSuperclass();
387    if (Object.class.equals(suppliedSuper)) {
388      return suppliedClass;
389    } else {
390      // prefer to use the super type of the supplied bean (test double)
391      return suppliedSuper;
392    }
393  }
394
395  /**
396   * Internal shutdown hook.
397   */
398  private static class Hook extends Thread {
399
400    private final ShutdownAwareBeanContext context;
401
402    Hook(ShutdownAwareBeanContext context) {
403      this.context = context;
404    }
405
406    @Override
407    public void run() {
408      context.shutdown();
409    }
410  }
411
412  /**
413   * Proxy that handles shutdown hook registration and de-registration.
414   */
415  private static class ShutdownAwareBeanContext implements BeanContext {
416
417    private final BeanContext context;
418    private final Hook hook;
419    private boolean shutdown;
420
421    ShutdownAwareBeanContext(BeanContext context) {
422      this.context = context;
423      this.hook = new Hook(this);
424      Runtime.getRuntime().addShutdownHook(hook);
425    }
426
427    @Override
428    public String getName() {
429      return context.getName();
430    }
431
432    @Override
433    public String[] getProvides() {
434      return context.getProvides();
435    }
436
437    @Override
438    public String[] getDependsOn() {
439      return context.getDependsOn();
440    }
441
442    @Override
443    public <T> T getBean(Class<T> beanClass) {
444      return context.getBean(beanClass);
445    }
446
447    @Override
448    public <T> T getBean(Class<T> beanClass, String name) {
449      return context.getBean(beanClass, name);
450    }
451
452    @Override
453    public <T> BeanEntry<T> candidate(Class<T> type, String name) {
454      return context.candidate(type, name);
455    }
456
457    @Override
458    public List<Object> getBeansWithAnnotation(Class<?> annotation) {
459      return context.getBeansWithAnnotation(annotation);
460    }
461
462    @Override
463    public <T> List<T> getBeans(Class<T> interfaceType) {
464      return context.getBeans(interfaceType);
465    }
466
467    @Override
468    public <T> List<T> getBeansByPriority(Class<T> interfaceType) {
469      return context.getBeansByPriority(interfaceType);
470    }
471
472    @Override
473    public <T> List<T> sortByPriority(List<T> list) {
474      return context.sortByPriority(list);
475    }
476
477    @Override
478    public void start() {
479      context.start();
480    }
481
482    @Override
483    public void close() {
484      synchronized (this) {
485        if (!shutdown) {
486          Runtime.getRuntime().removeShutdownHook(hook);
487        }
488        context.close();
489      }
490    }
491
492    /**
493     * Close via shutdown hook.
494     */
495    void shutdown() {
496      synchronized (this) {
497        shutdown = true;
498        close();
499      }
500    }
501  }
502
503  /**
504   * Helper to order the BeanContextFactory based on dependsOn.
505   */
506  static class FactoryOrder {
507
508    private final Set<String> includeModules;
509    private final boolean suppliedBeans;
510    private final boolean ignoreMissingModuleDependencies;
511
512    private final Set<String> moduleNames = new LinkedHashSet<>();
513    private final List<BeanContextFactory> factories = new ArrayList<>();
514    private final List<FactoryState> queue = new ArrayList<>();
515    private final List<FactoryState> queueNoDependencies = new ArrayList<>();
516
517    private final Map<String, FactoryList> providesMap = new HashMap<>();
518
519    FactoryOrder(Set<String> includeModules, boolean suppliedBeans, boolean ignoreMissingModuleDependencies) {
520      this.includeModules = includeModules;
521      this.suppliedBeans = suppliedBeans;
522      this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies;
523    }
524
525    void add(BeanContextFactory factory) {
526
527      if (includeModule(factory)) {
528        FactoryState wrappedFactory = new FactoryState(factory);
529        providesMap.computeIfAbsent(factory.getName(), s -> new FactoryList()).add(wrappedFactory);
530        if (!isEmpty(factory.getProvides())) {
531          for (String feature : factory.getProvides()) {
532            providesMap.computeIfAbsent(feature, s -> new FactoryList()).add(wrappedFactory);
533          }
534        }
535        if (isEmpty(factory.getDependsOn())) {
536          if (!isEmpty(factory.getProvides())) {
537            // only has 'provides' so we can push this
538            push(wrappedFactory);
539          } else {
540            // hold until after all the 'provides only' modules are added
541            queueNoDependencies.add(wrappedFactory);
542          }
543        } else {
544          // queue it to process by dependency ordering
545          queue.add(wrappedFactory);
546        }
547      }
548    }
549
550    private boolean isEmpty(String[] values) {
551      return values == null || values.length == 0;
552    }
553
554    /**
555     * Return true of the factory (for the module) should be included.
556     */
557    private boolean includeModule(BeanContextFactory factory) {
558      return includeModules.isEmpty() || includeModules.contains(factory.getName());
559    }
560
561    /**
562     * Push the factory onto the build order (the wiring order for modules).
563     */
564    private void push(FactoryState factory) {
565      factory.setPushed();
566      factories.add(factory.getFactory());
567      moduleNames.add(factory.getName());
568    }
569
570    /**
571     * Order the factories returning the ordered list of module names.
572     */
573    Set<String> orderFactories() {
574      // push the 'no dependency' modules after the 'provides only' ones
575      // as this is more intuitive for the simple (only provides modules case)
576      for (FactoryState factoryState : queueNoDependencies) {
577        push(factoryState);
578      }
579      processQueue();
580      return moduleNames;
581    }
582
583    /**
584     * Return the list of factories in the order they should be built.
585     */
586    List<BeanContextFactory> factories() {
587      return factories;
588    }
589
590    /**
591     * Process the queue pushing the factories in order to satisfy dependencies.
592     */
593    private void processQueue() {
594
595      int count;
596      do {
597        count = processQueuedFactories();
598      } while (count > 0);
599
600      if (suppliedBeans || ignoreMissingModuleDependencies) {
601        // just push everything left assuming supplied beans
602        // will satisfy the required dependencies
603        for (FactoryState factoryState : queue) {
604          push(factoryState);
605        }
606
607      } else if (!queue.isEmpty()) {
608        StringBuilder sb = new StringBuilder();
609        for (FactoryState factory : queue) {
610          sb.append("Module [").append(factory.getName()).append("] has unsatisfied dependencies on modules:");
611          for (String depModuleName : factory.getDependsOn()) {
612            if (!moduleNames.contains(depModuleName)) {
613              sb.append(String.format(" [%s]", depModuleName));
614            }
615          }
616        }
617
618        sb.append(". Modules that were loaded ok are:").append(moduleNames);
619        sb.append(". Consider using BeanContextBuilder.withIgnoreMissingModuleDependencies() or BeanContextBuilder.withSuppliedBeans(...)");
620        throw new IllegalStateException(sb.toString());
621      }
622    }
623
624    /**
625     * Process the queued factories pushing them when all their (module) dependencies
626     * are satisfied.
627     * <p>
628     * This returns the number of factories added so once this returns 0 it is done.
629     */
630    private int processQueuedFactories() {
631
632      int count = 0;
633      Iterator<FactoryState> it = queue.iterator();
634      while (it.hasNext()) {
635        FactoryState factory = it.next();
636        if (satisfiedDependencies(factory)) {
637          // push the factory onto the build order
638          it.remove();
639          push(factory);
640          count++;
641        }
642      }
643      return count;
644    }
645
646    /**
647     * Return true if the (module) dependencies are satisfied for this factory.
648     */
649    private boolean satisfiedDependencies(FactoryState factory) {
650      for (String moduleOrFeature : factory.getDependsOn()) {
651        FactoryList factories = providesMap.get(moduleOrFeature);
652        if (factories == null || !factories.allPushed()) {
653          return false;
654        }
655      }
656      return true;
657    }
658  }
659
660  /**
661   * Wrapper on Factory holding the pushed state.
662   */
663  private static class FactoryState {
664
665    private final BeanContextFactory factory;
666    private boolean pushed;
667
668    private FactoryState(BeanContextFactory factory) {
669      this.factory = factory;
670    }
671
672    /**
673     * Set when factory is pushed onto the build/wiring order.
674     */
675    void setPushed() {
676      this.pushed = true;
677    }
678
679    boolean isPushed() {
680      return pushed;
681    }
682
683    BeanContextFactory getFactory() {
684      return factory;
685    }
686
687    String getName() {
688      return factory.getName();
689    }
690
691    String[] getDependsOn() {
692      return factory.getDependsOn();
693    }
694  }
695
696  /**
697   * List of factories for a given name or feature.
698   */
699  private static class FactoryList {
700
701    private final List<FactoryState> factories = new ArrayList<>();
702
703    void add(FactoryState factory) {
704      factories.add(factory);
705    }
706
707    /**
708     * Return true if all factories here have been pushed onto the build order.
709     */
710    boolean allPushed() {
711      for (FactoryState factory : factories) {
712        if (!factory.isPushed()) {
713          return false;
714        }
715      }
716      return true;
717    }
718  }
719
720}