package cn.org.rapid_framework.generator;
import static cn.org.rapid_framework.generator.GeneratorConstants.GENERATOR_EXCLUDES;
import static cn.org.rapid_framework.generator.GeneratorConstants.GENERATOR_INCLUDES;
import static cn.org.rapid_framework.generator.GeneratorConstants.GENERATOR_OUTPUT_ENCODING;
import static cn.org.rapid_framework.generator.GeneratorConstants.GENERATOR_REMOVE_EXTENSIONS;
import static cn.org.rapid_framework.generator.GeneratorConstants.GENERATOR_SOURCE_ENCODING;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import cn.org.rapid_framework.generator.util.AntPathMatcher;
import cn.org.rapid_framework.generator.util.BeanHelper;
import cn.org.rapid_framework.generator.util.FileHelper;
import cn.org.rapid_framework.generator.util.FreemarkerHelper;
import cn.org.rapid_framework.generator.util.GLogger;
import cn.org.rapid_framework.generator.util.GeneratorException;
import cn.org.rapid_framework.generator.util.IOHelper;
import cn.org.rapid_framework.generator.util.StringHelper;
import cn.org.rapid_framework.generator.util.ZipUtils;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
/**
* 代码生成器核心引擎
*
* 主要提供以下两个方法供外部使用
* <pre>
* generateBy() 用于生成文件
* deleteBy() 用于删除生成的文件
* </pre>
*
* @author badqiu
* @email badqiu(a)gmail.com
*/
public class Generator {
private static final String GENERATOR_INSERT_LOCATION = "generator-insert-location";
private ArrayList<File> templateRootDirs = new ArrayList<File>();
private String outRootDir;
private boolean ignoreTemplateGenerateException = true;
private String removeExtensions = GeneratorProperties.getProperty(GENERATOR_REMOVE_EXTENSIONS);
private boolean isCopyBinaryFile = true;
private String includes = GeneratorProperties.getProperty(GENERATOR_INCLUDES); // 需要处理的模板,使用逗号分隔符,示例值: java_src/**,java_test/**
private String excludes = GeneratorProperties.getProperty(GENERATOR_EXCLUDES); // 不需要处理的模板,使用逗号分隔符,示例值: java_src/**,java_test/**
private String sourceEncoding = GeneratorProperties.getProperty(GENERATOR_SOURCE_ENCODING);
private String outputEncoding = GeneratorProperties.getProperty(GENERATOR_OUTPUT_ENCODING);
public Generator() {
}
public void setTemplateRootDir(File templateRootDir) {
setTemplateRootDirs(new File[]{templateRootDir});
}
/**
* 设置模板目录,支持用逗号分隔多个模板目录,如 template/rapid,template/company
* @param templateRootDir
*/
public void setTemplateRootDir(String templateRootDir) {
setTemplateRootDirs(StringHelper.tokenizeToStringArray(templateRootDir,","));
}
public void setTemplateRootDirs(File... templateRootDirs) {
this.templateRootDirs = new ArrayList<File>(Arrays.asList(templateRootDirs));
}
public void setTemplateRootDirs(String... templateRootDirs) {
ArrayList<File> tempDirs = new ArrayList<File>();
for(String dir : templateRootDirs) {
tempDirs.add(FileHelper.getFile(dir));
}
this.templateRootDirs = tempDirs;
}
public void addTemplateRootDir(File file) {
templateRootDirs.add(file);
}
public void addTemplateRootDir(String file) {
templateRootDirs.add(FileHelper.getFile(file));
}
public boolean isIgnoreTemplateGenerateException() {
return ignoreTemplateGenerateException;
}
public void setIgnoreTemplateGenerateException(boolean ignoreTemplateGenerateException) {
this.ignoreTemplateGenerateException = ignoreTemplateGenerateException;
}
public boolean isCopyBinaryFile() {
return isCopyBinaryFile;
}
public void setCopyBinaryFile(boolean isCopyBinaryFile) {
this.isCopyBinaryFile = isCopyBinaryFile;
}
public String getSourceEncoding() {
return sourceEncoding;
}
public void setSourceEncoding(String sourceEncoding) {
if(StringHelper.isBlank(sourceEncoding)) throw new IllegalArgumentException("sourceEncoding must be not empty");
this.sourceEncoding = sourceEncoding;
}
public String getOutputEncoding() {
return outputEncoding;
}
public void setOutputEncoding(String outputEncoding) {
if(StringHelper.isBlank(outputEncoding)) throw new IllegalArgumentException("outputEncoding must be not empty");
this.outputEncoding = outputEncoding;
}
public void setIncludes(String includes) {
this.includes = includes;
}
/** 设置不处理的模板路径,可以使用ant类似的值,使用逗号分隔,示例值: **\*.ignore */
public void setExcludes(String excludes) {
this.excludes = excludes;
}
public void setOutRootDir(String rootDir) {
if(rootDir == null) throw new IllegalArgumentException("outRootDir must be not null");
this.outRootDir = rootDir;
}
public String getOutRootDir() {
// if(outRootDir == null) throw new IllegalStateException("'outRootDir' property must be not null.");
return outRootDir;
}
public void setRemoveExtensions(String removeExtensions) {
this.removeExtensions = removeExtensions;
}
public void deleteOutRootDir() throws IOException {
if(StringHelper.isBlank(getOutRootDir())) throw new IllegalStateException("'outRootDir' property must be not null.");
GLogger.println("[delete dir] "+getOutRootDir());
FileHelper.deleteDirectory(new File(getOutRootDir()));
}
/**
* 生成文件
* @param templateModel 生成器模板可以引用的变量
* @param filePathModel 文件路径可以引用的变量
* @throws Exception
*/
public Generator generateBy(Map templateModel,Map filePathModel) throws Exception {
processTemplateRootDirs(templateModel, filePathModel,false);
return this;
}
/**
* 删除生成的文件
* @param templateModel 生成器模板可以引用的变量
* @param filePathModel 文件路径可以引用的变量
* @return
* @throws Exception
*/
public Generator deleteBy(Map templateModel,Map filePathModel) throws Exception {
processTemplateRootDirs(templateModel, filePathModel,true);
return this;
}
@SuppressWarnings("unchecked")
private void processTemplateRootDirs(Map templateModel,Map filePathModel,boolean isDelete) throws Exception {
if(StringHelper.isBlank(getOutRootDir())) throw new IllegalStateException("'outRootDir' property must be not empty.");
if(templateRootDirs == null || templateRootDirs.size() == 0) throw new IllegalStateException("'templateRootDirs' must be not empty");
GLogger.debug("******* Template reference variables *********",templateModel);
GLogger.debug("\n\n******* FilePath reference variables *********",filePathModel);
//生成 路径值,如 pkg=com.company.project 将生成 pkg_dir=com/company/project的值
templateModel.putAll(GeneratorHelper.getDirValuesMap(templateModel));
filePathModel.putAll(GeneratorHelper.getDirValuesMap(filePathModel));
GeneratorException ge = new GeneratorException("generator occer error, Generator BeanInfo:"+BeanHelper.describe(this));
List<File> processedTemplateRootDirs = processTemplateRootDirs();
for(int i = 0; i < processedTemplateRootDirs.size(); i++) {
File templateRootDir = (File)processedTemplateRootDirs.get(i);
List<Exception> exceptions = scanTemplatesAndProcess(templateRootDir,processedTemplateRootDirs,templateModel,filePathModel,isDelete);
ge.addAll(exceptions);
}
if(!ge.exceptions.isEmpty()) throw ge;
}
/**
* 用于子类覆盖,预处理模板目录,如执行文件解压动作
**/
protected List<File> processTemplateRootDirs() throws Exception {
return unzipIfTemplateRootDirIsZipFile();
}
/**
* 解压模板目录,如果模板目录是一个zip,jar文件 . 并且支持指定 zip文件的子目录作为模板目录,通过 !号分隔
* 指定zip文件: c:\\some.zip
* 指定zip文件子目录: c:\some.zip!/folder/
* @throws MalformedURLException
**/
private List<File> unzipIfTemplateRootDirIsZipFile() throws MalformedURLException {
List<File> unzipIfTemplateRootDirIsZipFile = new ArrayList<File>();
for(int i = 0; i < this.templateRootDirs.size(); i++) {
File file = templateRootDirs.get(i);
String templateRootDir = FileHelper.toFilePathIfIsURL(file);
String subFolder = "";
int zipFileSeperatorIndexOf = templateRootDir.indexOf("!");
if(zipFileSeperatorIndexOf >= 0) {
subFolder = templateRootDir.substring(zipFileSeperatorIndexOf+1);
templateRootDir = templateRootDir.substring(0,zipFileSeperatorIndexOf);
}
if(new File(templateRootDir).isFile()) {
File tempDir = ZipUtils.unzip2TempDir(new File(templateRootDir),"tmp_generator_template_folder_for_zipfile");
unzipIfTemplateRootDirIsZipFile.add(new File(tempDir,subFolder));
}else {
unzipIfTemplateRootDirIsZipFile.add(new File(templateRootDir,subFolder));
}
}
return unzipIfTemplateRootDirIsZipFile;
}
/**
* 搜索templateRootDir目录下的所有文件并生成东西
* @param templateRootDir 用于搜索的模板目录
* @param templateRootDirs freemarker用于装载模板的目录
*/
private List<Exception> scanTemplatesAndProcess(File templateRootDir,List<File> templateRootDirs,Map templateModel,Map filePathModel,boolean isDelete) throws Exception {
if(templateRootDir == null) throw new IllegalStateException("'templateRootDir' must be not null");
GLogger.println("-------------------load template from templateRootDir = '"+templateRootDir.getAbsolutePath()+"' outRootDir:"+new File(outRootDir).getAbsolutePath());
List srcFiles = FileHelper.searchAllNotIgnoreFile(templateRootDir);
List<Exception> exceptions = new ArrayList();
for(int i = 0; i < srcFiles.size(); i++) {
File srcFile = (File)srcFiles.get(i);
try {
if(isDelete){
new TemplateProcessor(templateRootDirs).executeDelete(templateRootDir, templateModel,filePathModel, srcFile);
}else {
long start = System.currentTimeMillis();
new TemplateProcessor(templateRootDirs).executeGenerate(templateRootDir, templateModel,filePathModel, srcFile);
GLogger.perf("genereate by tempate cost time:"+(System.currentTimeMillis() - start)+"ms");
}
}catch(Exception e) {
if (ignoreTemplateGenerateException) {
GLogger.warn("iggnore generate error,template is:" + srcFile+" cause:"+e);
exceptions.add(e);
} else {
throw e;
}
}
}
return exceptions;
}
/**
* 单个模板文件的处理器
**/
private class TemplateProcessor {
private GeneratorControl gg = new GeneratorControl();
private List<File> templateRootDirs = new ArrayList<File>();
public TemplateProcessor(List<File> templateRootDirs) {
super();
this.templateRootDirs = templateRootDirs;
}
private void executeGenerate(File templateRootDir,Map templateModel, Map filePathModel ,File srcFile) throws SQLException, IOException,TemplateException {
String templateFile = FileHelper.getRelativePath(templateRootDir, srcFile);
if(GeneratorHelper.isIgnoreTemplateProcess(srcFile, templateFile,includes,excludes)) {
return;
}
if(isCopyBinaryFile && FileHelper.isBinaryFile(srcFile)) {
String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile);
File outputFile = new File(getOutRootDir(),outputFilepath);
GLogger.println("[copy binary file by extention] from:"+srcFile+" => "+outputFile);
FileHelper.parentMkdir(outputFile);
IOHelper.copyAndClose(new FileInputStream(srcFile), new FileOutputStream(outputFile));
return;
}
try {
String outputFilepath = proceeForOutputFilepath(filePathModel,templateFile);
initGeneratorControlProperties(srcFile,outputFilepath);
processTemplateForGeneratorControl(templateModel, templateFile);
if(gg.isIgnoreOutput()) {
GLogger.println("[not generate] by gg.isIgnoreOutput()=true on template:"+templateFile);
return;
}
if(StringHelper.isNotBlank(gg.getOutputFile())) {
generateNewFileOrInsertIntoFile(templateFile,gg.getOutputFile(), templateModel);
}
}catch(Exception e) {
throw new RuntimeException("generate oucur error,templateFile is:" + templateFile+" => "+ gg.getOutputFile()+" cause:"+e, e);
}
}
private void executeDelete(File templateRootDir,Map templateModel, Map filePathModel ,File srcFile) throws SQLException, IOException,TemplateException {
String templateFile = FileHelper.getRelativePath(templateRootDir, srcFile);
if(GeneratorHelper.isIgnoreTemplateProcess(srcFile, templateFile,includes,excludes)) {
return;
}
String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile);
initGeneratorControlProperties(srcFile,outputFilepath);
gg.deleteGeneratedFile = true;
processTemplateForGeneratorControl(templateModel, templateFile);
GLogger.println("[delete file] file:"+new File(gg.getOutputFile()).getAbsolutePath());
new File(gg.getOutputFile()).delete();
}
private void initGeneratorControlProperties(File srcFile,String outputFile) throws SQLException {
gg.setSourceFile(srcFile.getAbsolutePath());
gg.setSourceFileName(srcFile.getName());
gg.setSourceDir(srcFile.getParent());
gg.setOutRoot(getOutRootDir());
gg.setOutputEncoding(outputEncoding);
gg.setSourceEncoding(sourceEncoding);
gg.setMergeLocation(GENERATOR_INSERT_LOCATION);
gg.setOutputFile(outputFile);
}
private void processTemplateForGeneratorControl(Map templateModel,String templateFile) throws IOException, TemplateException {
templateModel.put("gg", gg);
Template template = getFreeMarkerTemplate(templateFile);
template.process(templateModel, IOHelper.NULL_WRITER);
}
/** 处理文件路径的变量变成输出路径 */
private String proceeForOutputFilepath(Map filePathModel,String templateFile) throws IOException {
String outputFilePath = templateFile;
//TODO 删除兼容性的@testExpression
int testExpressionIndex = -1;
if((testExpressionIndex = templateFile.indexOf('@')) != -1) {
outputFilePath = templateFile.substring(0, testExpressionIndex);
String testExpressionKey = templateFile.substring(testExpressionIndex+1);
Object expressionValue = filePathModel.get(testExpressionKey);
if(expressionValue == null) {
System.err.println("[not-generate] WARN: test expression is null by key:["+testExpressionKey+"] on template:["+templateFile+"]");
return null;
}
if(!"true".equals(String.valueOf(expressionValue))) {
GLogger.println("[not-generate]\t test expression '@"+testExpressionKey+"' is false,template:"+templateFile);
return null;
}
}
for(String removeExtension : removeExtensions.split(",")) {
if(outputFilePath.endsWith(removeExtension)) {
outputFilePath = outputFilePath.substring(0,outputFilePath.length() - removeExtension.length());
break;
}
}
Configuration conf = GeneratorHelper.newFreeMarkerConfiguration(templateRootDirs, sourceEncoding,"/filepath/processor/");
//使freemarker支持过滤,如 ${className?lower_case} 现在为 ${className^lower_case}
outputFilePath = outputFilePath.replace('^', '?');
return FreemarkerHelper.processTemplateString(outputFilePath,filePathModel,conf);
}
private Template getFreeMarkerTemplate(String templateName) throws IOException {
return GeneratorHelper.newFreeMarkerConfiguration(templateRootDirs, sourceEncoding,templateName).getTemplate(templateName);
}
private void generateNewFileOrInsertIntoFile( String templateFile,String outputFilepath, Map templateModel) throws Exception {
Template template = getFreeMarkerTemplate(templateFile);
template.setOutputEncoding(gg.getOutputEncoding());
File absoluteOutputFilePath = FileHelper.parentMkdir(outputFilepath);
if(absoluteOutputFilePath.exists()) {
StringWriter newFileContentCollector = new StringWriter();
if(GeneratorHelper.isFoundInsertLocation(gg,template, templateModel, absoluteOutputFilePath, newFileContentCollector)) {
GLogger.println("[insert]\t generate content into:"+outputFilepath);
IOHelper.saveFile(absoluteOutputFilePath, newFileContentCollector.toString(),gg.getOutputEncoding());
return;
}
}
if(absoluteOutputFilePath.exists() && !gg.isOverride()) {
GLogger.println("[not generate]\t by gg.isOverride()=false and outputFile exist:"+outputFilepath);
return;
}
if(absoluteOutputFilePath.exists()) {
GLogger.println("[override]\t template:"+templateFile+" ==> "+outputFilepath);
}else {
GLogger.println("[generate]\t template:"+templateFile+" ==> "+outputFilepath);
}
FreemarkerHelper.processTemplate(template, templateModel, absoluteOutputFilePath,gg.getOutputEncoding());
}
}
static class GeneratorHelper {
/**
* 生成 路径值,如 pkg=com.company.project 将生成 pkg_dir=com/company/project的值
* @param map
* @return
*/
public static Map getDirValuesMap(Map map) {
Map dirValues = new HashMap();
Set<Object> keys = map.keySet();
for(Object key : keys) {
Object value = map.get(key);
if(key instanceof String && value instanceof String) {
String dirKey = key+"_dir";
String dirValue = value.toString().replace('.', '/');
dirValues.put(dirKey, dirValue);
}
}
return dirValues;
}
public static boolean isIgnoreTemplateProcess(File srcFile,String templateFile,String includes,String excludes) {
if(srcFile.isDirectory() || srcFile.isHidden())
return true;
if(templateFile.trim().equals(""))
return true;
if(srcFile.getName().toLowerCase().endsWith(".include")){
GLogger.println("[skip]\t\t endsWith '.include' template:"+templateFile);
return true;
}
templateFile = templateFile.replace('\\', '/');
for(String exclude : StringHelper.tokenizeToStringArray(excludes,",")) {
if(new AntPathMatcher().match(exclude.replace('\\', '/'), templateFile)) return true;
}
if(StringHelper.isBlank(includes)) {
return false;
}
for(String include : StringHelper.tokenizeToStringArray(includes,",")) {
if(new AntPathMatcher().match(include.replace('\\', '/'), templateFile)) return false;
}
return true;
}
private static boolean isFoundInsertLocation(GeneratorControl gg,Template template, Map model, File outputFile, StringWriter newFileContent) throws IOException, TemplateException {
LineNumberReader reader = new LineNumberReader(new FileReader(outputFile));
String line = null;
boolean isFoundInsertLocation = false;
//FIXME 持续性的重复生成会导致out of memory
PrintWriter writer = new PrintWriter(newFileContent);
while((line = reader.readLine()) != null) {
writer.println(line);
// only insert once
if(!isFoundInsertLocation && line.indexOf(gg.getMergeLocation()) >= 0) {
template.process(model,writer);
writer.println();
isFoundInsertLocation = true;
}
}
writer.close();
reader.close();
return isFoundInsertLocation;
}
public static Configuration newFreeMarkerConfiguration(List<File> templateRootDirs,String defaultEncoding,String templateName) throws IOException {
Configuration conf = new Configuration();
FileTemplateLoader[] templateLoaders = new FileTemplateLoader[templateRootDirs.size()];
for(int i = 0; i < templateRootDirs.size(); i++) {
templateLoaders[i] = new FileTemplateLoader((File)templateRootDirs.get(i));
}
MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(templateLoaders);
conf.setTemplateLoader(multiTemplateLoader);
conf.setNumberFormat("###############");
conf.setBooleanFormat("true,false");
conf.setDefaultEncoding(defaultEncoding);
// String autoIncludes = new File(new File(templateName).getParent(),"macro.include").getPath();
// List<String> availableAutoInclude = FreemarkerHelper.getAvailableAutoInclude(conf, Arrays.asList("macro.include",autoIncludes));
// conf.setAutoIncludes(availableAutoInclude);
// GLogger.info("[set Freemarker.autoIncludes]"+availableAutoInclude+" for templateName:"+templateName);
List<String> autoIncludes = getParentPaths(templateName,"macro.include");
List<String> availableAutoInclude = FreemarkerHelper.getAvailableAutoInclude(conf,autoIncludes);
conf.setAutoIncludes(availableAutoInclude);
GLogger.trace("set Freemarker.autoIncludes:"+availableAutoInclude+" for templateName:"+templateName+" autoIncludes:"+autoIncludes);
return conf;
}
public static List<String> getParentPaths(String templateName,String suffix) {
String array[] = StringHelper.tokenizeToStringArray(templateName, "\\/");
List<String> list = new ArrayList<String>();
list.add(suffix);
list.add(File.separator+suffix);
String path = "";
for(int i = 0; i < array.length; i++){
path = path + File.separator + array[i];
list.add(path + File.separator+suffix);
}
return list;
}
}
@SuppressWarnings("unchecked")
public static class GeneratorModel implements java.io.Serializable{
private static final long serialVersionUID = -6430787906037836995L;
/** 用于存放'模板'可以引用的变量 */
public Map templateModel = new HashMap();
/** 用于存放'文件路径'可以引用的变量 */
public Map filePathModel = new HashMap();
public GeneratorModel() {
}
public GeneratorModel(Map templateModel, Map filePathModel) {
this.templateModel = templateModel;
this.filePathModel = filePathModel;
}
}
}