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