View Javadoc
1   /*
2    *    Copyright 2018-2022 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.scripting.thymeleaf.support;
17  
18  import java.io.IOException;
19  import java.lang.reflect.Method;
20  import java.util.Optional;
21  import java.util.concurrent.ConcurrentHashMap;
22  import java.util.concurrent.ConcurrentMap;
23  
24  import org.apache.ibatis.builder.annotation.ProviderContext;
25  import org.apache.ibatis.io.Resources;
26  import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriver;
27  import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriverConfig;
28  import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriverConfig.TemplateFileConfig.PathProviderConfig;
29  
30  /**
31   * The SQL provider class that return the SQL template file path. <br>
32   * <b>IMPORTANT: This class required to use with mybatis 3.5.1+</b> and need to use with SQL provider annotation (such
33   * as {@link org.apache.ibatis.annotations.SelectProvider} as follow: <br>
34   * <br>
35   *
36   * <pre>
37   * package com.example.mapper;
38   *
39   * public interface BaseMapper&lt;T&gt; {
40   *
41   *   &#64;Options(useGeneratedKeys = true, keyProperty = "id")
42   *   &#64;InsertProvider(type = TemplateFilePathProvider.class)
43   *   void insert(T entity);
44   *
45   *   &#64;UpdateProvider(type = TemplateFilePathProvider.class)
46   *   void update(T entity);
47   *
48   *   &#64;DeleteProvider(type = TemplateFilePathProvider.class)
49   *   void delete(T entity);
50   *
51   *   &#64;SelectProvider(type = TemplateFilePathProvider.class)
52   *   T findById(Integer id);
53   *
54   * }
55   * </pre>
56   *
57   * <pre>
58   * package com.example.mapper;
59   *
60   * public interface NameMapper extends BaseMapper {
61   *
62   *   &#64;SelectProvider(type = TemplateFilePathProvider.class)
63   *   List&lt;Name&gt; findByConditions(NameConditions conditions);
64   *
65   * }
66   * </pre>
67   *
68   * @author Kazuki Shimizu
69   *
70   * @version 1.0.1
71   */
72  public class TemplateFilePathProvider {
73  
74    private static final PathGenerator DEFAULT_PATH_GENERATOR = TemplateFilePathProvider::generateTemplatePath;
75    private static final ThymeleafLanguageDriverConfig DEFAULT_LANGUAGE_DRIVER_CONFIG = ThymeleafLanguageDriverConfig
76        .newInstance();
77  
78    private static PathGenerator pathGenerator = DEFAULT_PATH_GENERATOR;
79    private static ThymeleafLanguageDriverConfig languageDriverConfig = DEFAULT_LANGUAGE_DRIVER_CONFIG;
80  
81    private static ConcurrentMap<ProviderContext, String> cache = new ConcurrentHashMap<>();
82  
83    private TemplateFilePathProvider() {
84      // NOP
85    }
86  
87    /**
88     * Set custom implementation for {@link PathGenerator}.
89     *
90     * @param pathGenerator
91     *          a instance for generating a template file path
92     */
93    public static void setCustomTemplateFilePathGenerator(PathGenerator pathGenerator) {
94      TemplateFilePathProvider.pathGenerator = Optional.ofNullable(pathGenerator).orElse(DEFAULT_PATH_GENERATOR);
95    }
96  
97    /**
98     * Set a configuration instance for {@link ThymeleafLanguageDriver}.
99     * <p>
100    * By default, {@link ThymeleafLanguageDriverConfig#newInstance()} will used.
101    * </p>
102    * <p>
103    * If you applied an user define {@link ThymeleafLanguageDriverConfig} for {@link ThymeleafLanguageDriver}, please
104    * same instance to the this class.
105    * </p>
106    *
107    * @param languageDriverConfig
108    *          A user defined {@link ThymeleafLanguageDriverConfig}
109    */
110   public static void setLanguageDriverConfig(ThymeleafLanguageDriverConfig languageDriverConfig) {
111     TemplateFilePathProvider.languageDriverConfig = Optional.ofNullable(languageDriverConfig)
112         .orElse(DEFAULT_LANGUAGE_DRIVER_CONFIG);
113   }
114 
115   /**
116    * Provide an SQL scripting string(template file path). <br>
117    * By default implementation, a template file path resolve following format and priority order. If does not match all,
118    * it throw an exception that indicate not found a template file.
119    * <ul>
120    * <li>com/example/mapper/NameMapper/NameMapper-{methodName}-{databaseId}.sql</li>
121    * <li>com/example/mapper/NameMapper/NameMapper-{methodName}.sql (fallback using default database)</li>
122    * <li>com/example/mapper/BaseMapper/BaseMapper-{methodName}-{databaseId}.sql (fallback using declaring class of
123    * method)</li>
124    * <li>com/example/mapper/BaseMapper/BaseMapper-{methodName}.sql (fallback using declaring class of method and default
125    * database)</li>
126    * </ul>
127    * <br>
128    *
129    * @param context
130    *          a context of SQL provider
131    *
132    * @return an SQL scripting string(template file path)
133    */
134   public static String provideSql(ProviderContext context) {
135     return languageDriverConfig.getTemplateFile().getPathProvider().isCacheEnabled()
136         ? cache.computeIfAbsent(context, c -> providePath(c.getMapperType(), c.getMapperMethod(), c.getDatabaseId()))
137         : providePath(context.getMapperType(), context.getMapperMethod(), context.getDatabaseId());
138   }
139 
140   /**
141    * Clear cache.
142    */
143   public static void clearCache() {
144     cache.clear();
145   }
146 
147   static String providePath(Class<?> mapperType, Method mapperMethod, String databaseId) {
148     boolean fallbackDeclaringClass = mapperType != mapperMethod.getDeclaringClass();
149     boolean fallbackDatabase = databaseId != null;
150     String path = pathGenerator.generatePath(mapperType, mapperMethod, databaseId);
151     if (exists(path)) {
152       return path;
153     }
154     if (fallbackDatabase) {
155       path = pathGenerator.generatePath(mapperType, mapperMethod, null);
156       if (exists(path)) {
157         return path;
158       }
159     }
160     if (fallbackDeclaringClass) {
161       path = pathGenerator.generatePath(mapperMethod.getDeclaringClass(), mapperMethod, databaseId);
162       if (exists(path)) {
163         return path;
164       }
165       if (fallbackDatabase) {
166         path = pathGenerator.generatePath(mapperMethod.getDeclaringClass(), mapperMethod, null);
167         if (exists(path)) {
168           return path;
169         }
170       }
171     }
172     throw new IllegalStateException("The SQL template file not found. mapperType:[" + mapperType + "] mapperMethod:["
173         + mapperMethod + "] databaseId:[" + databaseId + "]");
174   }
175 
176   private static String generateTemplatePath(Class<?> type, Method method, String databaseId) {
177     Package pkg = type.getPackage();
178     String packageName = pkg == null ? "" : pkg.getName();
179     String className = type.getName().substring(packageName.length() + (packageName.isEmpty() ? 0 : 1));
180 
181     PathProviderConfig pathProviderConfig = languageDriverConfig.getTemplateFile().getPathProvider();
182     StringBuilder path = new StringBuilder();
183     if (!pathProviderConfig.getPrefix().isEmpty()) {
184       path.append(pathProviderConfig.getPrefix());
185     }
186     if (pathProviderConfig.isIncludesPackagePath() && !packageName.isEmpty()) {
187       path.append(packageName.replace('.', '/')).append('/');
188     }
189     path.append(className);
190     if (pathProviderConfig.isSeparateDirectoryPerMapper()) {
191       path.append('/');
192       if (pathProviderConfig.isIncludesMapperNameWhenSeparateDirectory()) {
193         path.append(className).append('-');
194       }
195     } else {
196       path.append('-');
197     }
198     path.append(method.getName());
199     if (databaseId != null) {
200       path.append('-').append(databaseId);
201     }
202     path.append(".sql");
203     return path.toString();
204   }
205 
206   private static boolean exists(String path) {
207     String basePath = languageDriverConfig.getTemplateFile().getBaseDir();
208     String actualPath = basePath.isEmpty() ? path : basePath + (basePath.endsWith("/") ? "" : "/") + path;
209     try {
210       Resources.getResourceURL(actualPath);
211       return true;
212     } catch (IOException e) {
213       return false;
214     }
215   }
216 
217   /**
218    * The interface that implements a function for generating template file path.
219    */
220   @FunctionalInterface
221   public interface PathGenerator {
222 
223     /**
224      * Generate a template file path.
225      *
226      * @param type
227      *          mapper interface type that specified provider (or declaring interface type of mapper method)
228      * @param method
229      *          a mapper method that specified provider
230      * @param databaseId
231      *          a database id that provided from {@link org.apache.ibatis.mapping.DatabaseIdProvider}
232      *
233      * @return a template file path
234      */
235     String generatePath(Class<?> type, Method method, String databaseId);
236 
237   }
238 
239 }