001package io.dinject;
002
003import io.dinject.core.BeanContextFactory;
004import io.dinject.core.Builder;
005import io.dinject.core.BuilderFactory;
006import io.dinject.core.EnrichBean;
007import io.dinject.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   * Deprecated migrate to build().
353   */
354  @Deprecated
355  public BeanContext load() {
356    return build();
357  }
358
359  /**
360   * Build and return the bean context.
361   *
362   * @return The BeanContext
363   */
364  public BeanContext build() {
365    // sort factories by dependsOn
366    FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies);
367    ServiceLoader.load(BeanContextFactory.class).forEach(factoryOrder::add);
368
369    Set<String> moduleNames = factoryOrder.orderFactories();
370    if (moduleNames.isEmpty()) {
371      throw new IllegalStateException("No modules found suggests using Gradle and IDEA but with a setup issue?" +
372        " Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. " +
373        " Refer to https://dinject.io/docs/gradle#idea");
374    }
375    log.debug("building context with modules {}", moduleNames);
376    Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans, enrichBeans);
377    for (BeanContextFactory factory : factoryOrder.factories()) {
378      rootBuilder.addChild(factory.createContext(rootBuilder));
379    }
380
381    BeanContext beanContext = rootBuilder.build();
382    // entire graph built, fire postConstruct
383    beanContext.start();
384    if (shutdownHook) {
385      return new ShutdownAwareBeanContext(beanContext);
386    }
387    return beanContext;
388  }
389
390  /**
391   * Return the type that we map the supplied bean to.
392   */
393  private Class<?> suppliedType(Class<?> suppliedClass) {
394    Class<?> suppliedSuper = suppliedClass.getSuperclass();
395    if (Object.class.equals(suppliedSuper)) {
396      return suppliedClass;
397    } else {
398      // prefer to use the super type of the supplied bean (test double)
399      return suppliedSuper;
400    }
401  }
402
403  /**
404   * Internal shutdown hook.
405   */
406  private static class Hook extends Thread {
407
408    private final ShutdownAwareBeanContext context;
409
410    Hook(ShutdownAwareBeanContext context) {
411      this.context = context;
412    }
413
414    @Override
415    public void run() {
416      context.shutdown();
417    }
418  }
419
420  /**
421   * Proxy that handles shutdown hook registration and de-registration.
422   */
423  private static class ShutdownAwareBeanContext implements BeanContext {
424
425    private final BeanContext context;
426    private final Hook hook;
427    private boolean shutdown;
428
429    ShutdownAwareBeanContext(BeanContext context) {
430      this.context = context;
431      this.hook = new Hook(this);
432      Runtime.getRuntime().addShutdownHook(hook);
433    }
434
435    @Override
436    public String getName() {
437      return context.getName();
438    }
439
440    @Override
441    public String[] getProvides() {
442      return context.getProvides();
443    }
444
445    @Override
446    public String[] getDependsOn() {
447      return context.getDependsOn();
448    }
449
450    @Override
451    public <T> T getBean(Class<T> beanClass) {
452      return context.getBean(beanClass);
453    }
454
455    @Override
456    public <T> T getBean(Class<T> beanClass, String name) {
457      return context.getBean(beanClass, name);
458    }
459
460    @Override
461    public <T> BeanEntry<T> candidate(Class<T> type, String name) {
462      return context.candidate(type, name);
463    }
464
465    @Override
466    public List<Object> getBeansWithAnnotation(Class<?> annotation) {
467      return context.getBeansWithAnnotation(annotation);
468    }
469
470    @Override
471    public <T> List<T> getBeans(Class<T> interfaceType) {
472      return context.getBeans(interfaceType);
473    }
474
475    @Override
476    public <T> List<T> getBeansByPriority(Class<T> interfaceType) {
477      return context.getBeansByPriority(interfaceType);
478    }
479
480    @Override
481    public <T> List<T> sortByPriority(List<T> list) {
482      return context.sortByPriority(list);
483    }
484
485    @Override
486    public void start() {
487      context.start();
488    }
489
490    @Override
491    public void close() {
492      synchronized (this) {
493        if (!shutdown) {
494          Runtime.getRuntime().removeShutdownHook(hook);
495        }
496        context.close();
497      }
498    }
499
500    /**
501     * Close via shutdown hook.
502     */
503    void shutdown() {
504      synchronized (this) {
505        shutdown = true;
506        close();
507      }
508    }
509  }
510
511  /**
512   * Helper to order the BeanContextFactory based on dependsOn.
513   */
514  static class FactoryOrder {
515
516    private final Set<String> includeModules;
517    private final boolean suppliedBeans;
518    private final boolean ignoreMissingModuleDependencies;
519
520    private final Set<String> moduleNames = new LinkedHashSet<>();
521    private final List<BeanContextFactory> factories = new ArrayList<>();
522    private final List<FactoryState> queue = new ArrayList<>();
523    private final List<FactoryState> queueNoDependencies = new ArrayList<>();
524
525    private final Map<String, FactoryList> providesMap = new HashMap<>();
526
527    FactoryOrder(Set<String> includeModules, boolean suppliedBeans, boolean ignoreMissingModuleDependencies) {
528      this.includeModules = includeModules;
529      this.suppliedBeans = suppliedBeans;
530      this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies;
531    }
532
533    void add(BeanContextFactory factory) {
534
535      if (includeModule(factory)) {
536        FactoryState wrappedFactory = new FactoryState(factory);
537        providesMap.computeIfAbsent(factory.getName(), s -> new FactoryList()).add(wrappedFactory);
538        if (!isEmpty(factory.getProvides())) {
539          for (String feature : factory.getProvides()) {
540            providesMap.computeIfAbsent(feature, s -> new FactoryList()).add(wrappedFactory);
541          }
542        }
543        if (isEmpty(factory.getDependsOn())) {
544          if (!isEmpty(factory.getProvides())) {
545            // only has 'provides' so we can push this
546            push(wrappedFactory);
547          } else {
548            // hold until after all the 'provides only' modules are added
549            queueNoDependencies.add(wrappedFactory);
550          }
551        } else {
552          // queue it to process by dependency ordering
553          queue.add(wrappedFactory);
554        }
555      }
556    }
557
558    private boolean isEmpty(String[] values) {
559      return values == null || values.length == 0;
560    }
561
562    /**
563     * Return true of the factory (for the module) should be included.
564     */
565    private boolean includeModule(BeanContextFactory factory) {
566      return includeModules.isEmpty() || includeModules.contains(factory.getName());
567    }
568
569    /**
570     * Push the factory onto the build order (the wiring order for modules).
571     */
572    private void push(FactoryState factory) {
573      factory.setPushed();
574      factories.add(factory.getFactory());
575      moduleNames.add(factory.getName());
576    }
577
578    /**
579     * Order the factories returning the ordered list of module names.
580     */
581    Set<String> orderFactories() {
582      // push the 'no dependency' modules after the 'provides only' ones
583      // as this is more intuitive for the simple (only provides modules case)
584      for (FactoryState factoryState : queueNoDependencies) {
585        push(factoryState);
586      }
587      processQueue();
588      return moduleNames;
589    }
590
591    /**
592     * Return the list of factories in the order they should be built.
593     */
594    List<BeanContextFactory> factories() {
595      return factories;
596    }
597
598    /**
599     * Process the queue pushing the factories in order to satisfy dependencies.
600     */
601    private void processQueue() {
602
603      int count;
604      do {
605        count = processQueuedFactories();
606      } while (count > 0);
607
608      if (suppliedBeans || ignoreMissingModuleDependencies) {
609        // just push everything left assuming supplied beans
610        // will satisfy the required dependencies
611        for (FactoryState factoryState : queue) {
612          push(factoryState);
613        }
614
615      } else if (!queue.isEmpty()) {
616        StringBuilder sb = new StringBuilder();
617        for (FactoryState factory : queue) {
618          sb.append("Module [").append(factory.getName()).append("] has unsatisfied dependencies on modules:");
619          for (String depModuleName : factory.getDependsOn()) {
620            if (!moduleNames.contains(depModuleName)) {
621              sb.append(String.format(" [%s]", depModuleName));
622            }
623          }
624        }
625
626        sb.append(". Modules that were loaded ok are:").append(moduleNames);
627        sb.append(". Consider using BeanContextBuilder.withIgnoreMissingModuleDependencies() or BeanContextBuilder.withSuppliedBeans(...)");
628        throw new IllegalStateException(sb.toString());
629      }
630    }
631
632    /**
633     * Process the queued factories pushing them when all their (module) dependencies
634     * are satisfied.
635     * <p>
636     * This returns the number of factories added so once this returns 0 it is done.
637     */
638    private int processQueuedFactories() {
639
640      int count = 0;
641      Iterator<FactoryState> it = queue.iterator();
642      while (it.hasNext()) {
643        FactoryState factory = it.next();
644        if (satisfiedDependencies(factory)) {
645          // push the factory onto the build order
646          it.remove();
647          push(factory);
648          count++;
649        }
650      }
651      return count;
652    }
653
654    /**
655     * Return true if the (module) dependencies are satisfied for this factory.
656     */
657    private boolean satisfiedDependencies(FactoryState factory) {
658      for (String moduleOrFeature : factory.getDependsOn()) {
659        FactoryList factories = providesMap.get(moduleOrFeature);
660        if (factories == null || !factories.allPushed()) {
661          return false;
662        }
663      }
664      return true;
665    }
666  }
667
668  /**
669   * Wrapper on Factory holding the pushed state.
670   */
671  private static class FactoryState {
672
673    private final BeanContextFactory factory;
674    private boolean pushed;
675
676    private FactoryState(BeanContextFactory factory) {
677      this.factory = factory;
678    }
679
680    /**
681     * Set when factory is pushed onto the build/wiring order.
682     */
683    void setPushed() {
684      this.pushed = true;
685    }
686
687    boolean isPushed() {
688      return pushed;
689    }
690
691    BeanContextFactory getFactory() {
692      return factory;
693    }
694
695    String getName() {
696      return factory.getName();
697    }
698
699    String[] getDependsOn() {
700      return factory.getDependsOn();
701    }
702  }
703
704  /**
705   * List of factories for a given name or feature.
706   */
707  private static class FactoryList {
708
709    private final List<FactoryState> factories = new ArrayList<>();
710
711    void add(FactoryState factory) {
712      factories.add(factory);
713    }
714
715    /**
716     * Return true if all factories here have been pushed onto the build order.
717     */
718    boolean allPushed() {
719      for (FactoryState factory : factories) {
720        if (!factory.isPushed()) {
721          return false;
722        }
723      }
724      return true;
725    }
726  }
727
728}