package cn.org.rapid_framework.generator; 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.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; 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 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 List templateRootDirs = new ArrayList(); private String outRootDir; private boolean ignoreTemplateGenerateException = true; private String removeExtensions = System.getProperty("generator.removeExtensions",".ftl,.vm"); private boolean isCopyBinaryFile = true; private String includes = System.getProperty("generator.includes"); // 需要处理的模板,使用逗号分隔符,示例值: java_src/**,java_test/** private String excludes = System.getProperty("generator.excludes"); // 不需要处理的模板,使用逗号分隔符,示例值: java_src/**,java_test/** private String sourceEncoding = System.getProperty("generator.sourceEncoding","UTF-8"); private String outputEncoding = System.getProperty("generator.outputEncoding","UTF-8"); public Generator() { } public void setTemplateRootDir(File templateRootDir) { setTemplateRootDirs(new File[]{templateRootDir}); } public void setTemplateRootDirs(File[] templateRootDirs) { this.templateRootDirs = Arrays.asList(templateRootDirs); } public void addTemplateRootDir(File f) { templateRootDirs.add(f); } 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(sourceEncoding == null) throw new IllegalArgumentException("sourceEncoding must be not null"); this.sourceEncoding = sourceEncoding; } public String getOutputEncoding() { return outputEncoding; } public void setOutputEncoding(String outputEncoding) { if(outputEncoding == null) throw new IllegalArgumentException("outputEncoding must be not null"); 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 v) { if(v == null) throw new IllegalArgumentException("outRootDir must be not null"); this.outRootDir = v; } 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; } private void processTemplateRootDirs(Map templateModel,Map filePathModel,boolean isDelete) throws Exception { if(StringHelper.isBlank(getOutRootDir())) throw new IllegalStateException("'outRootDir' property must be not null."); if(templateRootDirs.size() == 0) throw new IllegalStateException("'templateRootDirs' cannot empty"); GeneratorException ge = new GeneratorException("generator occer error, Generator BeanInfo:"+BeanHelper.describe(this)); for(int i = 0; i < this.templateRootDirs.size(); i++) { File templateRootDir = (File)templateRootDirs.get(i); List<Exception> exceptions = scanTemplatesAndProcess(templateRootDir,templateModel,filePathModel,isDelete); ge.addAll(exceptions); } if(!ge.exceptions.isEmpty()) throw ge; } private List<Exception> scanTemplatesAndProcess(File templateRootDir, 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().executeDelete(templateRootDir, templateModel,filePathModel, srcFile); }else { new TemplateProcessor().executeGenerate(templateRootDir, templateModel,filePathModel, srcFile); } }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 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); GLogger.println("[copy binary file by extention] from:"+srcFile+" => "+new File(getOutRootDir(),outputFilepath)); IOHelper.copyAndClose(new FileInputStream(srcFile), new FileOutputStream(new File(getOutRootDir(),outputFilepath))); return; } String outputFilepath = null; try { outputFilepath = proceeForOutputFilepath(filePathModel,templateFile); initGeneratorControlProperties(srcFile); processTemplateForGeneratorControl(templateModel, templateFile); if(gg.isIgnoreOutput()) { GLogger.println("[not generate] by gg.isIgnoreOutput()=true on template:"+templateFile); return; } if(outputFilepath != null ) { generateNewFileOrInsertIntoFile(templateFile,outputFilepath, templateModel); } }catch(Exception e) { throw new RuntimeException("generate oucur error,templateFile is:" + templateFile+" => "+ outputFilepath+" 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; } initGeneratorControlProperties(srcFile); gg.deleteGeneratedFile = true; processTemplateForGeneratorControl(templateModel, templateFile); String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile); GLogger.println("[delete file] file:"+new File(gg.getOutRoot(),outputFilepath).getAbsolutePath()); new File(gg.getOutRoot(),outputFilepath).delete(); } private void initGeneratorControlProperties(File srcFile) 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); } 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/"); 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.mkdir(gg.getOutRoot(),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; } GLogger.println("[generate]\t template:"+templateFile+" ==> "+outputFilepath); FreemarkerHelper.processTemplate(template, templateModel, absoluteOutputFilePath,gg.getOutputEncoding()); } } static class GeneratorHelper { 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(includes == null) 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.debug("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; } } public static class GeneratorModel implements java.io.Serializable{ public Map filePathModel; public Map templateModel; public GeneratorModel(Map templateModel, Map filePathModel) { this.templateModel = templateModel; this.filePathModel = filePathModel; } } }