001/*
002 *  Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016
017package com.mybatisflex.processor;
018
019import com.mybatisflex.annotation.Column;
020import com.mybatisflex.annotation.ColumnAlias;
021import com.mybatisflex.annotation.Table;
022import com.mybatisflex.processor.builder.ContentBuilder;
023import com.mybatisflex.processor.config.ConfigurationKey;
024import com.mybatisflex.processor.config.MybatisFlexConfig;
025import com.mybatisflex.processor.entity.ColumnInfo;
026import com.mybatisflex.processor.entity.TableInfo;
027import com.mybatisflex.processor.util.FileUtil;
028import com.mybatisflex.processor.util.StrUtil;
029
030import javax.annotation.processing.AbstractProcessor;
031import javax.annotation.processing.Filer;
032import javax.annotation.processing.ProcessingEnvironment;
033import javax.annotation.processing.RoundEnvironment;
034import javax.lang.model.SourceVersion;
035import javax.lang.model.element.*;
036import javax.lang.model.type.DeclaredType;
037import javax.lang.model.type.TypeKind;
038import javax.lang.model.type.TypeMirror;
039import javax.lang.model.util.Elements;
040import javax.lang.model.util.Types;
041import javax.tools.JavaFileObject;
042import java.io.*;
043import java.math.BigDecimal;
044import java.math.BigInteger;
045import java.sql.Time;
046import java.sql.Timestamp;
047import java.time.*;
048import java.time.chrono.JapaneseDate;
049import java.util.*;
050
051/**
052 * MyBatis Flex Processor.
053 *
054 * @author 王帅, CloudPlayer
055 * @since 2023-06-22
056 */
057public class MybatisFlexProcessor extends AbstractProcessor {
058
059    private static final List<String> DEFAULT_SUPPORT_COLUMN_TYPES = Arrays.asList(
060        int.class.getName(), Integer.class.getName(),
061        short.class.getName(), Short.class.getName(),
062        long.class.getName(), Long.class.getName(),
063        float.class.getName(), Float.class.getName(),
064        double.class.getName(), Double.class.getName(),
065        boolean.class.getName(), Boolean.class.getName(),
066        Date.class.getName(), java.sql.Date.class.getName(), Time.class.getName(), Timestamp.class.getName(),
067        Instant.class.getName(), LocalDate.class.getName(), LocalDateTime.class.getName(), LocalTime.class.getName(),
068        OffsetDateTime.class.getName(), OffsetTime.class.getName(), ZonedDateTime.class.getName(),
069        Year.class.getName(), Month.class.getName(), YearMonth.class.getName(), JapaneseDate.class.getName(),
070        byte[].class.getName(), Byte[].class.getName(), Byte.class.getName(),
071        BigInteger.class.getName(), BigDecimal.class.getName(),
072        char.class.getName(), String.class.getName(), Character.class.getName()
073    );
074
075    private Filer filer;
076    private Types typeUtils;
077    private Elements elementUtils;
078    private MybatisFlexConfig configuration;
079
080    @Override
081    public synchronized void init(ProcessingEnvironment processingEnvironment) {
082        super.init(processingEnvironment);
083        this.filer = processingEnvironment.getFiler();
084        this.elementUtils = processingEnvironment.getElementUtils();
085        this.typeUtils = processingEnvironment.getTypeUtils();
086        this.configuration = new MybatisFlexConfig(filer);
087    }
088
089    @Override
090    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
091        if (!roundEnv.processingOver()) {
092
093            // 不启用 APT 功能
094            if ("false".equalsIgnoreCase(configuration.get(ConfigurationKey.ENABLE))) {
095                return true;
096            }
097
098            System.out.println("mybatis flex processor run start...");
099
100            // 是否所有的类常量都生成在 Tables 类里
101            boolean allInTablesEnable = "true".equalsIgnoreCase(configuration.get(ConfigurationKey.ALL_IN_TABLES_ENABLE));
102
103            StringBuilder importBuilder;
104            StringBuilder fieldBuilder;
105
106            if (allInTablesEnable) {
107                importBuilder = new StringBuilder();
108                fieldBuilder = new StringBuilder();
109            } else {
110                fieldBuilder = null;
111                importBuilder = null;
112            }
113
114            // 其他配置选项
115            String genPath = configuration.get(ConfigurationKey.GEN_PATH);
116
117            // all in Tables 配置
118            String allInTablesPackage = configuration.get(ConfigurationKey.ALL_IN_TABLES_PACKAGE);
119            String allInTablesClassName = configuration.get(ConfigurationKey.ALL_IN_TABLES_CLASS_NAME);
120
121            // mapper 配置
122            String mapperGenerateEnable = configuration.get(ConfigurationKey.MAPPER_GENERATE_ENABLE);
123            String mapperAnnotation = configuration.get(ConfigurationKey.MAPPER_ANNOTATION);
124            String mapperPackage = configuration.get(ConfigurationKey.MAPPER_PACKAGE);
125            String mapperBaseClass = configuration.get(ConfigurationKey.MAPPER_BASE_CLASS);
126
127            // tableDef 配置
128            String tableDefClassSuffix = configuration.get(ConfigurationKey.TABLE_DEF_CLASS_SUFFIX);
129            String tableDefInstanceSuffix = configuration.get(ConfigurationKey.TABLE_DEF_INSTANCE_SUFFIX);
130            String tableDefPropertiesNameStyle = configuration.get(ConfigurationKey.TABLE_DEF_PROPERTIES_NAME_STYLE);
131            String[] tableDefIgnoreEntitySuffixes = configuration.get(ConfigurationKey.TABLE_DEF_IGNORE_ENTITY_SUFFIXES).split(",");
132
133            // 如果不指定 Tables 生成包,那么 Tables 文件就会和最后一个 entity 文件在同一个包
134            String entityClassReference = null;
135
136            // 获取需要生成的类,开始构建文件
137            Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Table.class);
138
139            for (Element entityClassElement : elementsAnnotatedWith) {
140
141                // 获取 Table 注解
142                Table table = entityClassElement.getAnnotation(Table.class);
143
144                assert table != null;
145
146                // 类属性 fix: https://gitee.com/mybatis-flex/mybatis-flex/issues/I7I08X
147                Set<ColumnInfo> columnInfos = new TreeSet<>();
148                // 默认查询的属性,非 isLarge 字段
149                List<String> defaultColumns = new ArrayList<>();
150
151                TypeElement classElement = (TypeElement) entityClassElement;
152
153                do {
154                    // 获取类属性和默认查询字段
155                    fillColumnInfoList(columnInfos, defaultColumns, (TypeElement) entityClassElement, classElement, table.camelToUnderline());
156                    classElement = (TypeElement) typeUtils.asElement(classElement.getSuperclass());
157                } while (classElement != null);
158
159                // 获取 entity 类名
160                String entityClass = entityClassElement.toString();
161                String entityClassName = StrUtil.getClassName(entityClass);
162
163                // 处理 entity 后缀
164                for (String entityIgnoreSuffix : tableDefIgnoreEntitySuffixes) {
165                    if (entityClassName.endsWith(entityIgnoreSuffix.trim())) {
166                        entityClassName = entityClassName.substring(0, entityClassName.length() - entityIgnoreSuffix.length());
167                        break;
168                    }
169                }
170
171                TableInfo tableInfo = new TableInfo();
172                tableInfo.setEntityName(entityClass);
173                tableInfo.setEntitySimpleName(entityClassName);
174                tableInfo.setSchema(table.schema());
175                tableInfo.setTableName(table.value());
176                tableInfo.setEntityComment(elementUtils.getDocComment(entityClassElement));
177
178                // 生成 TableDef 文件
179                String tableDefPackage = StrUtil.buildTableDefPackage(entityClass);
180                String tableDefClassName = entityClassName.concat(tableDefClassSuffix);
181                String tableDefContent = ContentBuilder.buildTableDef(tableInfo, allInTablesEnable, tableDefPackage, tableDefClassName
182                    , tableDefPropertiesNameStyle, tableDefInstanceSuffix, columnInfos, defaultColumns);
183                // 将文件所依赖的 Element 传入 Filer 中,表示此 TableDef 依赖这个类,以保证增量编译时不丢失内容。
184                processGenClass(genPath, tableDefPackage, tableDefClassName, tableDefContent, entityClassElement);
185
186                if (allInTablesEnable) {
187                    // 标记 entity 类,如果没有配置 Tables 生成位置,以 entity 位置为准
188                    entityClassReference = entityClass;
189                    // 构建 Tables 常量属性及其导包
190                    ContentBuilder.buildTablesField(importBuilder, fieldBuilder, tableInfo, tableDefClassSuffix, tableDefPropertiesNameStyle, tableDefInstanceSuffix);
191                }
192
193                // 是否生成 Mapper 文件
194                if ("true".equalsIgnoreCase(mapperGenerateEnable) && table.mapperGenerateEnable()) {
195                    String realMapperPackage = StrUtil.isBlank(mapperPackage) ? StrUtil.buildMapperPackage(entityClass) : mapperPackage;
196                    String mapperClassName = entityClassName.concat("Mapper");
197                    boolean mapperAnnotationEnable = "true".equalsIgnoreCase(mapperAnnotation);
198                    String mapperClassContent = ContentBuilder.buildMapper(tableInfo, realMapperPackage, mapperClassName, mapperBaseClass, mapperAnnotationEnable);
199                    // 生成的 Mapper 依赖于此 Element。
200                    processGenClass(genPath, realMapperPackage, mapperClassName, mapperClassContent, entityClassElement);
201                }
202            }
203            // 确定了要生成 Tables 类,且拥有至少一个被 Table 注解的类时再生成 Tables 类。
204            if (allInTablesEnable && entityClassReference != null) {
205                // 生成 Tables 文件
206                String realTablesPackage = StrUtil.isBlank(allInTablesPackage) ? StrUtil.buildTableDefPackage(entityClassReference) : allInTablesPackage;
207                String realTablesClassName = StrUtil.isBlank(allInTablesClassName) ? "Tables" : allInTablesClassName;
208                String tablesContent = ContentBuilder.buildTables(importBuilder, fieldBuilder, realTablesPackage, allInTablesClassName);
209                processGenClass(genPath, realTablesPackage, realTablesClassName, tablesContent, elementsAnnotatedWith.toArray(new Element[0]));
210            }
211        }
212        return false;
213    }
214
215    @Override
216    public Set<String> getSupportedAnnotationTypes() {
217        Set<String> supportedAnnotationTypes = new HashSet<>();
218        supportedAnnotationTypes.add(Table.class.getCanonicalName());
219        return supportedAnnotationTypes;
220    }
221
222    @Override
223    public SourceVersion getSupportedSourceVersion() {
224        return SourceVersion.latestSupported();
225    }
226
227
228    /**
229     * 通过 classElement 操作起所有字段,生成 ColumnInfo 并填充 columnInfos 结合
230     */
231    private void fillColumnInfoList(Set<ColumnInfo> columnInfos, List<String> defaultColumns, TypeElement baseElement, TypeElement classElement, boolean camelToUnderline) {
232        for (Element fieldElement : classElement.getEnclosedElements()) {
233
234            // all fields
235            if (ElementKind.FIELD == fieldElement.getKind()) {
236
237                Set<Modifier> modifiers = fieldElement.getModifiers();
238                if (modifiers.contains(Modifier.STATIC)) {
239                    // ignore static fields
240                    continue;
241                }
242
243                Column column = fieldElement.getAnnotation(Column.class);
244                if (column != null && column.ignore()) {
245                    continue;
246                }
247
248
249                // 获取 typeHandlerClass 的名称,通过 column.typeHandler() 获取会抛出异常:MirroredTypeException:
250                // 参考 https://stackoverflow.com/questions/7687829/java-6-annotation-processing-getting-a-class-from-an-annotation
251                final String[] typeHandlerClass = {""};
252                List<? extends AnnotationMirror> annotationMirrors = fieldElement.getAnnotationMirrors();
253                for (AnnotationMirror annotationMirror : annotationMirrors) {
254                    annotationMirror.getElementValues().forEach((executableElement, annotationValue) -> {
255                        if ("typeHandler".contentEquals(executableElement.getSimpleName())) {
256                            typeHandlerClass[0] = annotationValue.toString();
257                        }
258                    });
259                }
260
261                TypeMirror typeMirror = fieldElement.asType();
262                Element element = typeUtils.asElement(typeMirror);
263                if (element != null) {
264                    typeMirror = element.asType();
265                }
266
267                String typeString = typeMirror.toString().trim();
268
269                TypeElement typeElement = null;
270                if (typeMirror.getKind() == TypeKind.DECLARED) {
271                    typeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
272                }
273
274                // 未配置 typeHandler 的情况下,只支持基本数据类型,不支持比如 list set 或者自定义的类等
275                if ((column == null || "org.apache.ibatis.type.UnknownTypeHandler".equals(typeHandlerClass[0]))
276                    && !DEFAULT_SUPPORT_COLUMN_TYPES.contains(typeString)
277                    && (typeElement != null && ElementKind.ENUM != typeElement.getKind())
278                ) {
279                    continue;
280                }
281
282                String property = fieldElement.toString();
283
284                String columnName;
285                if (column != null && !StrUtil.isBlank(column.value())) {
286                    columnName = column.value();
287                } else {
288                    if (camelToUnderline) {
289                        columnName = StrUtil.camelToUnderline(property);
290                    } else {
291                        columnName = property;
292                    }
293                }
294
295                String[] alias = getColumnAliasByGetterMethod(baseElement, property);
296                if (alias == null || alias.length == 0) {
297                    ColumnAlias columnAlias = fieldElement.getAnnotation(ColumnAlias.class);
298                    if (columnAlias != null) {
299                        alias = columnAlias.value();
300                    }
301                }
302
303                ColumnInfo columnInfo = new ColumnInfo();
304                columnInfo.setProperty(property);
305                columnInfo.setColumn(columnName);
306                columnInfo.setAlias(alias);
307                columnInfo.setComment(elementUtils.getDocComment(fieldElement));
308
309                columnInfos.add(columnInfo);
310
311                if (column == null || (!column.isLarge() && !column.isLogicDelete())) {
312                    defaultColumns.add(columnName);
313                }
314            }
315        }
316    }
317
318
319    private String[] getColumnAliasByGetterMethod(TypeElement baseElement, String property) {
320        if (baseElement == null) {
321            return null;
322        }
323        for (Element enclosedElement : baseElement.getEnclosedElements()) {
324            if (ElementKind.METHOD == enclosedElement.getKind()) {
325                String methodName = enclosedElement.toString();
326                if (StrUtil.isGetterMethod(methodName, property)) {
327                    ColumnAlias columnAlias = enclosedElement.getAnnotation(ColumnAlias.class);
328                    if (columnAlias != null) {
329                        return columnAlias.value();
330                    } else {
331                        // 重写方法,忽略别名
332                        return null;
333                    }
334                }
335            }
336        }
337        return getColumnAliasByGetterMethod((TypeElement) typeUtils.asElement(baseElement.getSuperclass()), property);
338    }
339
340
341    private void processGenClass(String genBasePath, String genPackageName, String className, String genContent, Element... elements) {
342        Writer writer = null;
343        try {
344            JavaFileObject sourceFile = filer.createSourceFile(genPackageName + "." + className, elements);
345            if (genBasePath == null || genBasePath.trim().length() == 0) {
346                writer = new OutputStreamWriter(sourceFile.openOutputStream(), configuration.get(ConfigurationKey.CHARSET));
347                writer.write(genContent);
348                writer.flush();
349                return;
350            }
351
352
353            String defaultGenPath = sourceFile.toUri().getPath();
354
355            // 真实的生成代码的目录
356            String realPath;
357
358            if (FileUtil.isAbsolutePath(genBasePath)) {
359                // 用户配置的路径为绝对路径
360                realPath = genBasePath;
361            } else {
362                // 配置的是相对路径,那么则以项目根目录为相对路径
363                String projectRootPath = FileUtil.getProjectRootPath(defaultGenPath);
364                realPath = new File(projectRootPath, genBasePath).getAbsolutePath();
365            }
366
367            // 通过在 test/java 目录下执行编译生成的
368            boolean fromTestSource = FileUtil.isFromTestSource(defaultGenPath);
369            if (fromTestSource) {
370                realPath = new File(realPath, "src/test/java").getAbsolutePath();
371            } else {
372                realPath = new File(realPath, "src/main/java").getAbsolutePath();
373            }
374
375            File genJavaFile = new File(realPath, (genPackageName + "." + className).replace(".", "/") + ".java");
376            if (!genJavaFile.getParentFile().exists() && !genJavaFile.getParentFile().mkdirs()) {
377                System.err.println(">>>>> ERROR: can not mkdirs by mybatis-flex processor for: " + genJavaFile.getParentFile());
378                return;
379            }
380
381            writer = new PrintWriter(genJavaFile, configuration.get(ConfigurationKey.CHARSET));
382            writer.write(genContent);
383            writer.flush();
384        } catch (IOException e) {
385            e.printStackTrace();
386        } finally {
387            if (writer != null) {
388                try {
389                    writer.close();
390                } catch (IOException ignored) {
391                    // do nothing here.
392                }
393            }
394        }
395    }
396
397}