View Javadoc
1   /*
2    *    Copyright 2013-2023 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.cdi;
17  
18  import jakarta.enterprise.event.Observes;
19  import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
20  import jakarta.enterprise.inject.spi.AnnotatedMember;
21  import jakarta.enterprise.inject.spi.AnnotatedType;
22  import jakarta.enterprise.inject.spi.Extension;
23  import jakarta.enterprise.inject.spi.InjectionPoint;
24  import jakarta.enterprise.inject.spi.InjectionTarget;
25  import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
26  import jakarta.enterprise.inject.spi.ProcessInjectionTarget;
27  import jakarta.enterprise.inject.spi.ProcessProducer;
28  import jakarta.enterprise.inject.spi.WithAnnotations;
29  import jakarta.inject.Named;
30  import jakarta.inject.Qualifier;
31  
32  import java.lang.annotation.Annotation;
33  import java.lang.reflect.Type;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.HashSet;
37  import java.util.List;
38  import java.util.Set;
39  
40  import org.apache.ibatis.session.SqlSession;
41  import org.apache.ibatis.session.SqlSessionFactory;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  /**
46   * MyBatis CDI extension.
47   *
48   * @author Frank D. Martinez [mnesarco]
49   */
50  public class MybatisExtension implements Extension {
51  
52    private static final Logger LOGGER = LoggerFactory.getLogger(MybatisExtension.class.getName());
53  
54    private final Set<BeanKey> sessionProducers = new HashSet<>();
55  
56    private final Set<Type> mapperTypes = new HashSet<>();
57  
58    private final Set<InjectionPoint> injectionPoints = new HashSet<>();
59  
60    /**
61     * Collect types of all mappers annotated with Mapper.
62     *
63     * @param <T>
64     *          the generic type
65     * @param pat
66     *          the pat
67     */
68    protected <T> void processAnnotatedType(
69        @Observes @WithAnnotations({ Mapper.class }) final ProcessAnnotatedType<T> pat) {
70      final AnnotatedType<T> at = pat.getAnnotatedType();
71      if (at.isAnnotationPresent(Mapper.class)) {
72        LOGGER.info("MyBatis CDI Module - Found class with @Mapper-annotation: {}", at.getJavaClass().getSimpleName());
73        this.mapperTypes.add(at.getBaseType());
74      }
75    }
76  
77    /**
78     * Collect all SqlSessionFactory producers annotated with SessionFactoryProvider.
79     *
80     * @param <T>
81     *          the generic type
82     * @param <X>
83     *          the generic type
84     * @param pp
85     *          the pp
86     */
87    @SuppressWarnings("unchecked")
88    protected <T, X> void processProducer(@Observes final ProcessProducer<T, X> pp) {
89      final AnnotatedMember<T> am = pp.getAnnotatedMember();
90      final boolean isAnnotated = am.isAnnotationPresent(SessionFactoryProvider.class);
91      final boolean isSqlSessionFactory = am.getBaseType().equals(SqlSessionFactory.class);
92      final String logData = String.join(".", am.getJavaMember().getDeclaringClass().getSimpleName(),
93          am.getJavaMember().getName());
94      if (isAnnotated) {
95        if (isSqlSessionFactory) {
96          LOGGER.info("MyBatis CDI Module - SqlSessionFactory producer {}", logData);
97          this.sessionProducers.add(new BeanKey((Class<Type>) (Type) SqlSession.class, am.getAnnotations()));
98        } else {
99          LOGGER.error("MyBatis CDI Module - Invalid return type (Must be SqlSessionFactory): {}", logData);
100         pp.addDefinitionError(new MybatisCdiConfigurationException(
101             String.format("SessionFactoryProvider producers must return SqlSessionFactory (%s)", logData)));
102       }
103     } else if (isSqlSessionFactory) {
104       LOGGER.warn(
105           "MyBatis CDI Module - Ignored SqlSessionFactory producer because it is not annotated with @SessionFactoryProvider: {}",
106           logData);
107     }
108   }
109 
110   /**
111    * Collect all targets to match Mappers and Session providers dependency.
112    *
113    * @param <X>
114    *          the generic type
115    * @param event
116    *          the event
117    */
118   protected <X> void processInjectionTarget(@Observes ProcessInjectionTarget<X> event) {
119     final InjectionTarget<X> it = event.getInjectionTarget();
120     this.injectionPoints.addAll(it.getInjectionPoints());
121   }
122 
123   /**
124    * Register all mybatis injectable beans.
125    *
126    * @param abd
127    *          the abd
128    */
129   @SuppressWarnings("unchecked")
130   protected void afterBeanDiscovery(@Observes final AfterBeanDiscovery abd) {
131     LOGGER.info("MyBatis CDI Module - Activated");
132 
133     Set<BeanKey> mappers = new HashSet<>();
134     Set<BeanKey> sessionTargets = new HashSet<>();
135 
136     for (InjectionPoint ip : injectionPoints) {
137       if (this.mapperTypes.contains(ip.getAnnotated().getBaseType())) {
138         LOGGER.info("MyBatis CDI Module - Found a bean, which needs a Mapper {}", ip.getAnnotated().getBaseType());
139         mappers.add(new BeanKey((Class<Type>) ip.getAnnotated().getBaseType(), ip.getAnnotated().getAnnotations()));
140       } else if (SqlSession.class.equals(ip.getAnnotated().getBaseType())) {
141         sessionTargets
142             .add(new BeanKey((Class<Type>) ip.getAnnotated().getBaseType(), ip.getAnnotated().getAnnotations()));
143       }
144     }
145     this.injectionPoints.clear();
146 
147     // Mappers -----------------------------------------------------------------
148     for (BeanKey key : mappers) {
149       LOGGER.info("MyBatis CDI Module - Managed Mapper dependency: {}, {}", key.getKey(), key.type.getName());
150       abd.addBean(key.createBean());
151     }
152     this.mapperTypes.clear();
153 
154     // SqlSessionFactories -----------------------------------------------------
155     for (BeanKey key : this.sessionProducers) {
156       LOGGER.info("MyBatis CDI Module - Managed SqlSession: {}, {}", key.getKey(), key.type.getName());
157       abd.addBean(key.createBean());
158       sessionTargets.remove(key);
159     }
160     this.sessionProducers.clear();
161 
162     // Unmanaged SqlSession targets --------------------------------------------
163     for (BeanKey key : sessionTargets) {
164       LOGGER.warn("MyBatis CDI Module - Unmanaged SqlSession: {}, {}", key.getKey(), key.type.getName());
165     }
166 
167   }
168 
169   /**
170    * Unique key for fully qualified Mappers and Sessions.
171    */
172   private static final class BeanKey implements Comparable<BeanKey> {
173 
174     private final String key;
175 
176     private final List<Annotation> qualifiers;
177 
178     private final Class<Type> type;
179 
180     private final String sqlSessionManagerName;
181 
182     public BeanKey(Class<Type> type, Set<Annotation> annotations) {
183       this.type = type;
184       this.qualifiers = sort(filterQualifiers(annotations));
185 
186       // Create key = type(.qualifier)*(.name)?
187       final StringBuilder sb = new StringBuilder();
188       String name = null;
189       sb.append(type.getName());
190       for (Annotation q : this.qualifiers) {
191         if (q instanceof Named) {
192           name = ((Named) q).value();
193         } else {
194           sb.append(".").append(q.annotationType().getSimpleName());
195         }
196       }
197       if (name != null) {
198         sb.append("_").append(name);
199       }
200       this.key = sb.toString();
201       this.sqlSessionManagerName = name;
202     }
203 
204     private Set<Annotation> filterQualifiers(Set<Annotation> annotations) {
205       final Set<Annotation> set = new HashSet<>();
206       for (Annotation a : annotations) {
207         if (a.annotationType().isAnnotationPresent(Qualifier.class)) {
208           set.add(a);
209         }
210       }
211       return set;
212     }
213 
214     private List<Annotation> sort(Set<Annotation> annotations) {
215       final List<Annotation> list = new ArrayList<>(annotations);
216       Collections.sort(list, (a, b) -> a.getClass().getName().compareTo(b.getClass().getName()));
217       return list;
218     }
219 
220     @Override
221     public int compareTo(BeanKey o) {
222       return this.key.compareTo(o.key);
223     }
224 
225     @Override
226     public int hashCode() {
227       int hash = 3;
228       return 43 * hash + (this.key != null ? this.key.hashCode() : 0);
229     }
230 
231     @Override
232     public boolean equals(Object obj) {
233       if (obj == null || this.getClass() != obj.getClass()) {
234         return false;
235       }
236       final BeanKey other = (BeanKey) obj;
237       return !(this.key == null ? other.key != null : !this.key.equals(other.key));
238     }
239 
240     public MyBatisBean createBean() {
241       return new MyBatisBean(this.key, this.type, new HashSet<>(this.qualifiers), this.sqlSessionManagerName);
242     }
243 
244     public String getKey() {
245       return this.key;
246     }
247 
248     @Override
249     public String toString() {
250       return this.key;
251     }
252 
253   }
254 
255 }