View Javadoc
1   /*
2    *    Copyright 2015-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.freemarker.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.freemarker.FreeMarkerLanguageDriver;
27  import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig;
28  import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig.TemplateFileConfig.PathProviderConfig;
29  
30  /**
31   * The SQL provider class that return the SQL template file path.
32   * <p>
33   * <b>IMPORTANT: This class required to use with mybatis 3.5.1+</b> and need to use with SQL provider annotation (such
34   * as {@link org.apache.ibatis.annotations.SelectProvider} as follow:
35   * <p>
36   *
37   * <pre>
38   * package com.example.mapper;
39   *
40   * public interface BaseMapper&lt;T&gt; {
41   *
42   *   &#64;Options(useGeneratedKeys = true, keyProperty = "id")
43   *   &#64;InsertProvider(type = TemplateFilePathProvider.class)
44   *   void insert(T entity);
45   *
46   *   &#64;UpdateProvider(type = TemplateFilePathProvider.class)
47   *   void update(T entity);
48   *
49   *   &#64;DeleteProvider(type = TemplateFilePathProvider.class)
50   *   void delete(T entity);
51   *
52   *   &#64;SelectProvider(type = TemplateFilePathProvider.class)
53   *   T findById(Integer id);
54   *
55   * }
56   * </pre>
57   *
58   * <pre>
59   * package com.example.mapper;
60   *
61   * public interface NameMapper extends BaseMapper {
62   *
63   *   &#64;SelectProvider(type = TemplateFilePathProvider.class)
64   *   List&lt;Name&gt; findByConditions(NameConditions conditions);
65   *
66   * }
67   * </pre>
68   *
69   * @author Kazuki Shimizu
70   *
71   * @version 1.2.0
72   */
73  public class TemplateFilePathProvider {
74  
75    private static final PathGenerator DEFAULT_PATH_GENERATOR = TemplateFilePathProvider::generateTemplatePath;
76    private static final FreeMarkerLanguageDriverConfig DEFAULT_LANGUAGE_DRIVER_CONFIG = FreeMarkerLanguageDriverConfig
77        .newInstance();
78  
79    private static PathGenerator pathGenerator = DEFAULT_PATH_GENERATOR;
80    private static FreeMarkerLanguageDriverConfig languageDriverConfig = DEFAULT_LANGUAGE_DRIVER_CONFIG;
81  
82    private static ConcurrentMap<ProviderContext, String> cache = new ConcurrentHashMap<>();
83  
84    private TemplateFilePathProvider() {
85      // NOP
86    }
87  
88    /**
89     * Set custom implementation for {@link PathGenerator}.
90     *
91     * @param pathGenerator
92     *          a instance for generating a template file path
93     */
94    public static void setCustomTemplateFilePathGenerator(PathGenerator pathGenerator) {
95      TemplateFilePathProvider.pathGenerator = Optional.ofNullable(pathGenerator).orElse(DEFAULT_PATH_GENERATOR);
96    }
97  
98    /**
99     * Set a configuration instance for {@link FreeMarkerLanguageDriver}.
100    * <p>
101    * By default, {@link FreeMarkerLanguageDriverConfig#newInstance()} will used.
102    * <p>
103    * If you applied an user define {@link FreeMarkerLanguageDriverConfig} for {@link FreeMarkerLanguageDriver}, please
104    * same instance to the this class.
105    *
106    * @param languageDriverConfig
107    *          A user defined {@link FreeMarkerLanguageDriverConfig}
108    */
109   public static void setLanguageDriverConfig(FreeMarkerLanguageDriverConfig languageDriverConfig) {
110     TemplateFilePathProvider.languageDriverConfig = Optional.ofNullable(languageDriverConfig)
111         .orElse(DEFAULT_LANGUAGE_DRIVER_CONFIG);
112   }
113 
114   /**
115    * Provide an SQL scripting string(template file path).
116    * <p>
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}.ftl</li>
121    * <li>com/example/mapper/NameMapper/NameMapper-{methodName}.ftl (fallback using default database)</li>
122    * <li>com/example/mapper/BaseMapper/BaseMapper-{methodName}-{databaseId}.ftl (fallback using declaring class of
123    * method)</li>
124    * <li>com/example/mapper/BaseMapper/BaseMapper-{methodName}.ftl (fallback using declaring class of method and default
125    * database)</li>
126    * </ul>
127    *
128    * @param context
129    *          a context of SQL provider
130    *
131    * @return an SQL scripting string(template file path)
132    */
133   @SuppressWarnings("unused")
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(".ftl");
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 }