From cfeafb4b3635b41a9628a244df3b2af60005ed8d Mon Sep 17 00:00:00 2001 From: asp_ly Date: Sat, 28 Dec 2024 19:47:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=88=90=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=9C=8D=E5=8A=A1=E4=BB=A3=E7=A0=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 6 + .../backend/deploy/entity/ProjectGroup.java | 12 +- .../framework/generator/CodeGenerator.java | 106 ++++++++ .../framework/utils/CodeGeneratorUtils.java | 257 ++++++++++++++++++ .../db/migration/V1.0.1__init_data.sql | 6 +- .../main/resources/templates/controller.ftl | 28 ++ .../main/resources/templates/converter.ftl | 13 + backend/src/main/resources/templates/dto.ftl | 24 ++ .../src/main/resources/templates/entity.ftl | 28 ++ .../src/main/resources/templates/query.ftl | 31 +++ .../main/resources/templates/repository.ftl | 12 + .../src/main/resources/templates/service.ftl | 12 + .../main/resources/templates/serviceImpl.ftl | 18 ++ 13 files changed, 541 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java create mode 100644 backend/src/main/resources/templates/controller.ftl create mode 100644 backend/src/main/resources/templates/converter.ftl create mode 100644 backend/src/main/resources/templates/dto.ftl create mode 100644 backend/src/main/resources/templates/entity.ftl create mode 100644 backend/src/main/resources/templates/query.ftl create mode 100644 backend/src/main/resources/templates/repository.ftl create mode 100644 backend/src/main/resources/templates/service.ftl create mode 100644 backend/src/main/resources/templates/serviceImpl.ftl diff --git a/backend/pom.xml b/backend/pom.xml index 16c963ab..e68b5775 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -235,6 +235,12 @@ json-schema-validator 1.0.86 + + + org.freemarker + freemarker + 2.3.32 + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java index f94e190c..af3e9c36 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java @@ -1,20 +1,11 @@ package com.qqchen.deploy.backend.deploy.entity; -import com.fasterxml.jackson.annotation.JsonManagedReference; import com.qqchen.deploy.backend.deploy.enums.ProjectGroupTypeEnum; +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; import com.qqchen.deploy.backend.framework.domain.Entity; -import com.qqchen.deploy.backend.system.entity.User; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.FetchMode; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * 项目组实体 @@ -25,6 +16,7 @@ import java.util.Set; @Table(name = "deploy_project_group") //@SQLDelete(sql = "UPDATE deploy_project_group SET deleted = TRUE WHERE id = ?; DELETE FROM deploy_project_group_environment WHERE project_group_id = ?") //@Where(clause = "deleted = false") +@LogicDelete public class ProjectGroup extends Entity { /** diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java new file mode 100644 index 00000000..9fc2430c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java @@ -0,0 +1,106 @@ +package com.qqchen.deploy.backend.framework.generator; + +import com.qqchen.deploy.backend.framework.utils.CodeGeneratorUtils; +import com.qqchen.deploy.backend.framework.utils.CodeGeneratorUtils.GeneratorConfig; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Scanner; + +/** + * 代码生成器主入口 + */ +@Slf4j +public class CodeGenerator { + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + try { + // 1. 输入模块信息 + System.out.println("请输入模块名称(中文,如:用户):"); + String moduleName = scanner.nextLine(); + + System.out.println("请输入类名(英文,如:User):"); + String className = scanner.nextLine(); + + System.out.println("请输入基础包路径(如:com.qqchen.deploy.backend.system):"); + String basePackage = scanner.nextLine(); + + System.out.println("请输入输出路径(如:src/main/java):"); + String outputPath = scanner.nextLine(); + + System.out.println("请输入表名(如:sys_user):"); + String tableName = scanner.nextLine(); + + System.out.println("请输入URL路径(如:user):"); + String urlPath = scanner.nextLine(); + + // 2. 输入建表SQL + System.out.println("请输入CREATE TABLE SQL语句(以分号结束):"); + StringBuilder sqlBuilder = new StringBuilder(); + String line; + while (!(line = scanner.nextLine()).contains(";")) { + sqlBuilder.append(line).append("\n"); + } + sqlBuilder.append(line); + String createTableSql = sqlBuilder.toString(); + + // 3. 验证输入 + if (StringUtils.isAnyBlank(moduleName, className, basePackage, outputPath, tableName, urlPath, createTableSql)) { + throw new IllegalArgumentException("所有输入项都不能为空"); + } + + // 4. 配置生成器 + GeneratorConfig config = new GeneratorConfig(); + config.setModuleName(moduleName); + config.setClassName(className); + config.setBasePackage(basePackage); + config.setOutputPath(outputPath); + config.setTableName(tableName); + config.setUrlPath(urlPath); + + // 5. 生成代码 + CodeGeneratorUtils.generateCode(createTableSql, config); + + System.out.println("代码生成成功!"); + System.out.println("生成的文件路径:" + outputPath); + + } catch (Exception e) { + log.error("代码生成失败", e); + System.err.println("代码生成失败:" + e.getMessage()); + } finally { + scanner.close(); + } + } + + /** + * 通过API方式生成代码 + */ + public static void generate(String moduleName, String className, String basePackage, + String outputPath, String tableName, String urlPath, String createTableSql) { + try { + // 1. 验证输入 + if (StringUtils.isAnyBlank(moduleName, className, basePackage, outputPath, tableName, urlPath, createTableSql)) { + throw new IllegalArgumentException("所有输入项都不能为空"); + } + + // 2. 配置生成器 + GeneratorConfig config = new GeneratorConfig(); + config.setModuleName(moduleName); + config.setClassName(className); + config.setBasePackage(basePackage); + config.setOutputPath(outputPath); + config.setTableName(tableName); + config.setUrlPath(urlPath); + + // 3. 生成代码 + CodeGeneratorUtils.generateCode(createTableSql, config); + + log.info("代码生成成功!输出路径:{}", outputPath); + } catch (Exception e) { + log.error("代码生成失败", e); + throw new RuntimeException("代码生成失败:" + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java new file mode 100644 index 00000000..48afe2c1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java @@ -0,0 +1,257 @@ +package com.qqchen.deploy.backend.framework.utils; + +import com.qqchen.deploy.backend.framework.generator.CodeGenerator; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 代码生成器工具类 + */ +@Slf4j +public class CodeGeneratorUtils { + + @Data + public static class FieldInfo { + private String fieldName; // 字段名 + + private String fieldType; // 字段类型 + + private String columnName; // 数据库列名 + + private String comment; // 注释 + + private boolean nullable; // 是否可空 + + private boolean isPrimaryKey; // 是否主键 + } + + @Data + public static class GeneratorConfig { + private String moduleName; // 模块名称(中文) + + private String className; // 类名 + + private String basePackage; // 基础包路径 + + private String outputPath; // 输出路径 + + private String tableName; // 表名 + + private String urlPath; // URL路径 + + private List fields; // 字段信息 + } + + private static final Configuration configuration; + + static { + configuration = new Configuration(Configuration.VERSION_2_3_32); + configuration.setClassLoaderForTemplateLoading(CodeGeneratorUtils.class.getClassLoader(), "templates"); + configuration.setDefaultEncoding("UTF-8"); + } + + /** + * 生成所有代码文件 + * + * @param createTableSql CREATE TABLE SQL语句 + * @param config 生成器配置 + */ + public static void generateCode(String createTableSql, GeneratorConfig config) { + try { + // 解析SQL获取字段信息 + config.setFields(parseSqlFields(createTableSql)); + + // 生成各种类型的文件 + generateFile("entity.ftl", config, "/entity/" + config.getClassName() + ".java"); + generateFile("dto.ftl", config, "/dto/" + config.getClassName() + "DTO.java"); + generateFile("query.ftl", config, "/query/" + config.getClassName() + "Query.java"); + generateFile("repository.ftl", config, "/repository/I" + config.getClassName() + "Repository.java"); + generateFile("service.ftl", config, "/service/I" + config.getClassName() + "Service.java"); + generateFile("serviceImpl.ftl", config, "/service/impl/" + config.getClassName() + "ServiceImpl.java"); + generateFile("controller.ftl", config, "/controller/" + config.getClassName() + "ApiController.java"); + generateFile("converter.ftl", config, "/converter/" + config.getClassName() + "Converter.java"); + + log.info("代码生成完成,输出路径: {}", config.getOutputPath()); + } catch (Exception e) { + log.error("代码生成失败", e); + throw new RuntimeException("代码生成失败", e); + } + } + + /** + * 解析CREATE TABLE SQL获取字段信息 + */ + private static List parseSqlFields(String createTableSql) { + List fields = new ArrayList<>(); + // 移除多余的空白字符和换行 + createTableSql = createTableSql.replaceAll("\\s+", " ").trim(); + + // 提取表定义部分 + int startIndex = createTableSql.indexOf("("); + int endIndex = createTableSql.lastIndexOf(")"); + if (startIndex == -1 || endIndex == -1) { + throw new IllegalArgumentException("无效的CREATE TABLE语句"); + } + + // 获取字段定义部分 + String fieldDefinitions = createTableSql.substring(startIndex + 1, endIndex); + + // 按逗号分割字段定义 + String[] fieldDefs = fieldDefinitions.split(","); + + for (String fieldDef : fieldDefs) { + fieldDef = fieldDef.trim(); + + // 跳过主键定义等非字段定义行 + if (fieldDef.toLowerCase().startsWith("primary key") || + fieldDef.toLowerCase().startsWith("key") || + fieldDef.toLowerCase().startsWith("constraint") || + fieldDef.toLowerCase().startsWith("index") || + fieldDef.isEmpty()) { + continue; + } + + // 解析字段定义 + String[] parts = fieldDef.split("\\s+", 3); + if (parts.length < 2) { + continue; + } + + FieldInfo field = new FieldInfo(); + + // 设置字段名(移除反引号) + field.setColumnName(parts[0].replace("`", "")); + + // 设置字段类型 + String typeStr = parts[1].toUpperCase(); + if (typeStr.contains("(")) { + typeStr = typeStr.substring(0, typeStr.indexOf("(")); + } + field.setFieldType(sqlTypeToJavaType(typeStr)); + + // 设置字段名(驼峰命名) + field.setFieldName(columnToField(field.getColumnName())); + + // 设置是否可空 + field.setNullable(!fieldDef.toLowerCase().contains("not null")); + + // 设置注释 + int commentIndex = fieldDef.toLowerCase().indexOf("comment"); + if (commentIndex != -1) { + String comment = fieldDef.substring(commentIndex + 7).trim(); + // 提取引号中的注释内容 + if (comment.startsWith("'") && comment.endsWith("'")) { + field.setComment(comment.substring(1, comment.length() - 1)); + } + } + + // 设置是否为主键 + field.setPrimaryKey(fieldDef.toLowerCase().contains("primary key")); + + // 基础字段不需要生成 + if (!isBaseField(field.getColumnName())) { + fields.add(field); + } + } + + return fields; + } + + /** + * 判断是否为基础字段(这些字段由父类Entity提供) + */ + private static boolean isBaseField(String columnName) { + return columnName.equals("id") || + columnName.equals("create_by") || + columnName.equals("create_time") || + columnName.equals("update_by") || + columnName.equals("update_time") || + columnName.equals("version") || + columnName.equals("deleted"); + } + + /** + * 生成单个文件 + */ + private static void generateFile(String templateName, GeneratorConfig config, String subPath) throws IOException, TemplateException { + Template template = configuration.getTemplate(templateName); + String outputPath = config.getOutputPath() + subPath; + File outputFile = new File(outputPath); + + // 确保目录存在 + outputFile.getParentFile().mkdirs(); + + // 准备数据模型 + Map dataModel = new HashMap<>(); + dataModel.put("basePackage", config.getBasePackage()); + dataModel.put("moduleName", config.getModuleName()); + dataModel.put("className", config.getClassName()); + dataModel.put("tableName", config.getTableName()); + dataModel.put("fields", config.getFields()); + dataModel.put("urlPath", config.getUrlPath()); + + // 生成文件 + try (Writer writer = new FileWriter(outputFile)) { + template.process(dataModel, writer); + } + + log.info("生成文件: {}", outputPath); + } + + /** + * 数据库列名转Java字段名 + */ + private static String columnToField(String columnName) { + String[] parts = columnName.toLowerCase().split("_"); + StringBuilder result = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) { + result.append(StringUtils.capitalize(parts[i])); + } + return result.toString(); + } + + /** + * SQL类型转Java类型 + */ + private static String sqlTypeToJavaType(String sqlType) { + sqlType = sqlType.toUpperCase(); + switch (sqlType) { + case "VARCHAR": + case "CHAR": + case "TEXT": + return "String"; + case "INT": + case "INTEGER": + return "Integer"; + case "BIGINT": + return "Long"; + case "DECIMAL": + return "BigDecimal"; + case "DATETIME": + case "TIMESTAMP": + return "LocalDateTime"; + case "DATE": + return "LocalDate"; + case "BIT": + case "TINYINT": + return "Boolean"; + default: + return "String"; + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql index 943da9d0..b95943ba 100644 --- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql +++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql @@ -79,9 +79,11 @@ VALUES (203, '环境管理', '/deploy/environments', '/src/pages/Deploy/Environment/List/index', 'CloudOutlined', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), -(204, '部署配置管理', '/deploy/deployment', '/src/pages/Deploy/Deployment/List/index', 'CloudOutlined', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), +(204, '部署配置管理', '/deploy/deployment', '/src/pages/Deploy/Deployment/List/index', 'CloudOutlined', 2, 200, 4, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), + +(205, 'Jenkins管理', '/deploy/jenkins', '/src/pages/Deploy/Jenkins/List/index', 'CloudOutlined', 2, 200, 5, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 三方系统 -(205, '三方系统管理', '/deploy/external', '/src/pages/Deploy/external/index', 'ApiOutlined', 2, 200, 70, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); +(206, '三方系统管理', '/deploy/external', '/src/pages/Deploy/external/index', 'ApiOutlined', 2, 200, 6, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); -- 初始化角色数据 INSERT INTO sys_role (id, create_time, code, name, type, description, sort) diff --git a/backend/src/main/resources/templates/controller.ftl b/backend/src/main/resources/templates/controller.ftl new file mode 100644 index 00000000..8145ad37 --- /dev/null +++ b/backend/src/main/resources/templates/controller.ftl @@ -0,0 +1,28 @@ +package ${basePackage}.controller; + +import com.qqchen.deploy.backend.framework.controller.BaseController; +import ${basePackage}.entity.${className}; +import ${basePackage}.dto.${className}DTO; +import ${basePackage}.query.${className}Query; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * ${moduleName} Controller + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/${urlPath}") +@Tag(name = "${moduleName}管理", description = "${moduleName}管理相关接口") +public class ${className}ApiController extends BaseController<${className}, ${className}DTO, Long, ${className}Query> { + + @Override + protected void exportData(HttpServletResponse response, List<${className}DTO> data) { + // TODO: 实现导出逻辑 + log.info("导出${moduleName}数据,数据量:{}", data.size()); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/converter.ftl b/backend/src/main/resources/templates/converter.ftl new file mode 100644 index 00000000..6714457b --- /dev/null +++ b/backend/src/main/resources/templates/converter.ftl @@ -0,0 +1,13 @@ +package ${basePackage}.converter; + +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import ${basePackage}.entity.${className}; +import ${basePackage}.dto.${className}DTO; +import org.mapstruct.Mapper; + +/** + * ${moduleName} Converter + */ +@Mapper(config = BaseConverter.class) +public interface ${className}Converter extends BaseConverter<${className}, ${className}DTO> { +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/dto.ftl b/backend/src/main/resources/templates/dto.ftl new file mode 100644 index 00000000..8de6489b --- /dev/null +++ b/backend/src/main/resources/templates/dto.ftl @@ -0,0 +1,24 @@ +package ${basePackage}.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; + +/** + * ${moduleName} DTO + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ${className}DTO extends BaseDTO { + +<#list fields as field> + <#if field.comment??> + /** + * ${field.comment} + */ + + private ${field.fieldType} ${field.fieldName}; + + +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/entity.ftl b/backend/src/main/resources/templates/entity.ftl new file mode 100644 index 00000000..6768128a --- /dev/null +++ b/backend/src/main/resources/templates/entity.ftl @@ -0,0 +1,28 @@ +package ${basePackage}.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; + +/** + * ${moduleName}实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "${tableName}") +public class ${className} extends Entity { + +<#list fields as field> + <#if field.comment??> + /** + * ${field.comment} + */ + + @Column(name = "${field.columnName}"<#if !field.nullable>, nullable = false) + private ${field.fieldType} ${field.fieldName}; + + +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/query.ftl b/backend/src/main/resources/templates/query.ftl new file mode 100644 index 00000000..81ba09dc --- /dev/null +++ b/backend/src/main/resources/templates/query.ftl @@ -0,0 +1,31 @@ +package ${basePackage}.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; + +/** + * ${moduleName}查询对象 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ${className}Query extends BaseQuery { + +<#list fields as field> + <#if field.comment??> + /** + * ${field.comment} + */ + + <#if field.fieldType == "String"> + @QueryField(field = "${field.columnName}", type = QueryType.LIKE) + <#else> + @QueryField(field = "${field.columnName}") + + private ${field.fieldType} ${field.fieldName}; + + +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/repository.ftl b/backend/src/main/resources/templates/repository.ftl new file mode 100644 index 00000000..468c5991 --- /dev/null +++ b/backend/src/main/resources/templates/repository.ftl @@ -0,0 +1,12 @@ +package ${basePackage}.repository; + +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import ${basePackage}.entity.${className}; +import org.springframework.stereotype.Repository; + +/** + * ${moduleName} Repository + */ +@Repository +public interface I${className}Repository extends IBaseRepository<${className}, Long> { +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/service.ftl b/backend/src/main/resources/templates/service.ftl new file mode 100644 index 00000000..96a7ae8a --- /dev/null +++ b/backend/src/main/resources/templates/service.ftl @@ -0,0 +1,12 @@ +package ${basePackage}.service; + +import com.qqchen.deploy.backend.framework.service.IBaseService; +import ${basePackage}.entity.${className}; +import ${basePackage}.dto.${className}DTO; +import ${basePackage}.query.${className}Query; + +/** + * ${moduleName} Service接口 + */ +public interface I${className}Service extends IBaseService<${className}, ${className}DTO, Long, ${className}Query> { +} \ No newline at end of file diff --git a/backend/src/main/resources/templates/serviceImpl.ftl b/backend/src/main/resources/templates/serviceImpl.ftl new file mode 100644 index 00000000..ccdc2ce7 --- /dev/null +++ b/backend/src/main/resources/templates/serviceImpl.ftl @@ -0,0 +1,18 @@ +package ${basePackage}.service.impl; + +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import ${basePackage}.entity.${className}; +import ${basePackage}.dto.${className}DTO; +import ${basePackage}.query.${className}Query; +import ${basePackage}.service.I${className}Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * ${moduleName} Service实现 + */ +@Slf4j +@Service +public class ${className}ServiceImpl extends BaseServiceImpl<${className}, ${className}DTO, Long, ${className}Query> + implements I${className}Service { +} \ No newline at end of file