/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.empire.db.codegen; | |
import java.io.File; | |
import java.io.FileWriter; | |
import java.io.IOException; | |
import java.io.Writer; | |
import java.util.ArrayList; | |
import java.util.List; | |
import org.apache.empire.db.DBDatabase; | |
import org.apache.empire.db.DBTable; | |
import org.apache.empire.db.DBView; | |
import org.apache.empire.db.codegen.util.FileUtils; | |
import org.apache.velocity.Template; | |
import org.apache.velocity.VelocityContext; | |
import org.apache.velocity.app.VelocityEngine; | |
import org.apache.velocity.exception.ParseErrorException; | |
import org.apache.velocity.exception.ResourceNotFoundException; | |
import org.apache.velocity.runtime.RuntimeConstants; | |
import org.apache.velocity.runtime.log.CommonsLogLogChute; | |
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* This is the entry class for generating the java persistence model based on a | |
* database schema. It uses the Empire DB open-source framework to build a java | |
* persistence layer for an application. The Apache Velocity template engine is | |
* used to create the output interfaces and classes. | |
* <p> | |
* The Empire DB framework doesn't try to hide the underlying database and data | |
* model but instead embraces its power by modeling it within java. The result | |
* is a persistence layer that uses a more "object-oriented, type safe" SQL to | |
* access persistent data. | |
* <p> | |
* NOTE: THIS VERSION HAS SEVERE RESTRICTIONS: | |
* <ol> | |
* <li>Only tables are currently modeled (we'll add views to a later version).</li> | |
* <li>Table indexes are not yet modeled (exception is primary key). Again, this | |
* will be added to later editions.</li> | |
* <li>It is assumed that each table has a single INTEGER auto-generated primary | |
* key column that has the same name for all tables.</li> | |
* <li>It is assumed that each table has a single TIMESTAMP optimistic locking | |
* column that has the same name for all tables.</li> | |
* </ol> | |
*/ | |
public class CodeGenWriter { | |
private static final Logger log = LoggerFactory.getLogger(CodeGenWriter.class); | |
// Templates | |
public static final String DATABASE_TEMPLATE = "Database.vm"; | |
public static final String BASE_TABLE_TEMPLATE = "BaseTable.vm"; | |
public static final String TABLE_TEMPLATE = "Table.vm"; | |
public static final String BASE_VIEW_TEMPLATE = "BaseView.vm"; | |
public static final String VIEW_TEMPLATE = "View.vm"; | |
public static final String BASE_RECORD_TEMPLATE = "BaseRecord.vm"; | |
public static final String RECORD_TEMPLATE = "Record.vm"; | |
// Services | |
private final WriterService writerService; | |
private final VelocityEngine engine; | |
// Properties | |
private final CodeGenConfig config; | |
private File baseDir; | |
private File tableDir; | |
private File recordDir; | |
private File viewDir; | |
/** | |
* Constructor | |
*/ | |
public CodeGenWriter(CodeGenConfig config, WriterService writerService) { | |
this.writerService = writerService; | |
this.config = config; | |
this.engine = new VelocityEngine(); | |
// we have to keep this in sync with our logging system | |
// http://velocity.apache.org/engine/releases/velocity-1.5/developer-guide.html#simpleexampleofacustomlogger | |
engine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new CommonsLogLogChute()); | |
if(config.getTemplateFolder() == null){ | |
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); | |
engine.setProperty("classpath." + RuntimeConstants.RESOURCE_LOADER + ".class", ClasspathResourceLoader.class.getName()); | |
} else { | |
File templateFolder = new File(config.getTemplateFolder()); | |
if(!templateFolder.canRead()){ | |
throw new RuntimeException("Provided template folder missing or not readable: " + config.getTemplateFolder()); | |
} | |
} | |
// init engine | |
try { | |
engine.init(); | |
} catch (Exception e) { | |
log.error("A Exception occured on initializing the velocity-engine:", e); | |
throw new RuntimeException(e); | |
} | |
} | |
/** | |
* Overload using standard WriterService | |
* @param config | |
*/ | |
public CodeGenWriter(CodeGenConfig config) { | |
this(config, new WriterService(config)); | |
} | |
/** | |
* Generates the java code files for the database | |
* | |
* @param db | |
* the DBDatabase to generate files for | |
*/ | |
public List<File> generateCodeFiles(DBDatabase db) { | |
List<File> generatedFiles = new ArrayList<File>(); | |
// Prepare directories for generated source files | |
this.initDirectories(config); | |
// Create the DB class | |
generatedFiles.add(this.createDatabaseClass(db)); | |
// Create base table class | |
if (config.getTableBaseName().equals("DBTable")==false) { | |
generatedFiles.add(this.createBaseTableClass(db)); | |
} | |
// Create base record class | |
if (config.isGenerateRecords()) { | |
generatedFiles.add(this.createBaseRecordClass(db)); | |
} | |
// Create base view class | |
if (config.isGenerateViews() && config.getViewBaseName().equals("DBView")==false) { | |
generatedFiles.add(this.createBaseViewClass(db)); | |
} | |
// Create table classes, record interfaces and record classes | |
for (DBTable table : db.getTables()) { | |
if (!config.isNestTables()) { | |
// if table nesting is disabled, create separate table classes | |
generatedFiles.add(this.createTableClass(db, table)); | |
} | |
if (config.isGenerateRecords()) { | |
// generate record | |
generatedFiles.add(this.createRecordClass(db, table)); | |
} | |
} | |
// Create view classes | |
for (DBView view : db.getViews()) { | |
if (config.isGenerateViews() && !config.isNestViews()) { | |
// if table nesting is disabled, create separate table classes | |
generatedFiles.add(this.createViewClass(db, view)); | |
} | |
} | |
return generatedFiles; | |
} | |
private void initDirectories(CodeGenConfig config) { | |
// Create the directory structure for the generated source code. | |
File targetDir = new File(config.getTargetFolder()); | |
if (!targetDir.exists()) { | |
targetDir.mkdirs(); | |
} | |
// Create the base package directory | |
this.baseDir = FileUtils.getFileFromPackage(targetDir, config.getPackageName()); | |
// Clean out the directory so old code is wiped out. | |
FileUtils.cleanDirectory(this.baseDir); | |
boolean createTables = (!config.isNestTables() || !config.getTableBaseName().equals("DBTable")); | |
boolean createViews = config.isGenerateViews() && (!config.isNestViews() || !config.getViewBaseName().equals("DBView")); | |
boolean craeteRecords = config.isGenerateRecords(); | |
// createViews | |
if (!createViews) | |
config.setViewPackageName(config.getPackageName()); | |
// Create the table package directory | |
this.tableDir = (createTables ? FileUtils.getFileFromPackage(targetDir, config.getTablePackageName()) : null); | |
// Create the record package directory | |
this.recordDir = (craeteRecords ? FileUtils.getFileFromPackage(targetDir, config.getRecordPackageName()) : null); | |
// Create the record package directory | |
this.viewDir = (createViews ? FileUtils.getFileFromPackage(targetDir, config.getViewPackageName()) : null); | |
} | |
private File createDatabaseClass(DBDatabase db) { | |
File file = new File(baseDir, config.getDbClassName() + ".java"); | |
VelocityContext context = new VelocityContext(); | |
// TODO fall back to getPackageName() is the other names are not set | |
context.put("parser", writerService); | |
context.put("tableClassSuffix", config.getTableClassSuffix()); | |
context.put("basePackageName", config.getPackageName()); | |
context.put("dbClassName", config.getDbClassName()); | |
context.put("tablePackageName", config.getTablePackageName()); | |
context.put("viewPackageName", config.getViewPackageName()); | |
context.put("database", db); | |
context.put("nestTables", config.isNestTables()); | |
context.put("baseTableClassName", config.getTableBaseName()); | |
context.put("nestViews", config.isNestViews()); | |
context.put("templateFolder", config.getTemplateFolder()); | |
context.put("baseViewClassName", config.getViewBaseName()); | |
context.put("preserveRelationNames", config.isPreserveRelationNames()); | |
writeFile(file, DATABASE_TEMPLATE, context); | |
return file; | |
} | |
private File createBaseTableClass(DBDatabase db) { | |
File file = new File(tableDir, config.getTableBaseName() + ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("tablePackageName", config.getTablePackageName()); | |
context.put("baseTableClassName", config.getTableBaseName()); | |
writeFile(file, BASE_TABLE_TEMPLATE, context); | |
return file; | |
} | |
private File createTableClass(DBDatabase db, DBTable table) { | |
File file = new File(tableDir, writerService.getTableClassName(table.getName()) | |
+ ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("parser", writerService); | |
context.put("basePackageName", config.getPackageName()); | |
context.put("tablePackageName", config.getTablePackageName()); | |
context.put("baseTableClassName", config.getTableBaseName()); | |
context.put("dbClassName", config.getDbClassName()); | |
context.put("nestTables", config.isNestTables()); | |
context.put("table", table); | |
writeFile(file, TABLE_TEMPLATE, context); | |
return file; | |
} | |
private File createBaseViewClass(DBDatabase db) { | |
File file = new File(viewDir, config.getViewBaseName() + ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("viewPackageName", config.getViewPackageName()); | |
context.put("baseViewClassName", config.getViewBaseName()); | |
writeFile(file, BASE_VIEW_TEMPLATE, context); | |
return file; | |
} | |
private File createViewClass(DBDatabase db, DBView view) { | |
File file = new File(viewDir, writerService.getViewClassName(view.getName()) | |
+ ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("parser", writerService); | |
context.put("basePackageName", config.getPackageName()); | |
context.put("viewPackageName", config.getViewPackageName()); | |
context.put("baseViewClassName", config.getViewBaseName()); | |
context.put("dbClassName", config.getDbClassName()); | |
context.put("nestViews", config.isNestViews()); | |
context.put("view", view); | |
writeFile(file, VIEW_TEMPLATE, context); | |
return file; | |
} | |
private File createBaseRecordClass(DBDatabase db) { | |
File file = new File(recordDir, config.getRecordBaseName() + ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("baseRecordClassName", config.getRecordBaseName()); | |
context.put("basePackageName", config.getPackageName()); | |
context.put("tablePackageName", config.getTablePackageName()); | |
context.put("recordPackageName", config.getRecordPackageName()); | |
context.put("baseTableClassName", config.getTableBaseName()); | |
writeFile(file, BASE_RECORD_TEMPLATE, context); | |
return file; | |
} | |
private File createRecordClass(DBDatabase db, DBTable table) { | |
File file = new File(recordDir, writerService.getRecordClassName(table.getName()) + ".java"); | |
VelocityContext context = new VelocityContext(); | |
context.put("parser", writerService); | |
context.put("basePackageName", config.getPackageName()); | |
// If the tables shall be nested within the database classe, their include path for the records needs to be changed | |
if (config.isNestTables()) | |
context.put("tablePackageName", config.getPackageName() + "." + config.getDbClassName()); | |
else | |
context.put("tablePackageName", config.getTablePackageName()); | |
context.put("recordPackageName", config.getRecordPackageName()); | |
context.put("baseRecordClassName", config.getRecordBaseName()); | |
context.put("dbClassName", config.getDbClassName()); | |
context | |
.put("createRecordProperties", config | |
.isCreateRecordProperties()); | |
context.put("table", table); | |
writeFile(file, RECORD_TEMPLATE, context); | |
return file; | |
} | |
private void writeFile(File file, String template, | |
VelocityContext context) { | |
String templatePath; | |
if(config.getTemplateFolder() == null) { | |
templatePath = template; | |
} else { | |
templatePath = config.getTemplateFolder()+ System.getProperty("file.separator") + template; | |
} | |
Writer writer = null; | |
try { | |
log.info("Writing " + file); | |
Template velocityTemplate = engine.getTemplate(templatePath); | |
writer = new FileWriter(file); | |
velocityTemplate.merge(context, writer); | |
} catch (IOException e) { | |
log.error(e.getMessage(), e); | |
} catch (ResourceNotFoundException e) { | |
log.error(e.getMessage(), e); | |
} catch (ParseErrorException e) { | |
log.error(e.getMessage(), e); | |
} catch (Exception e) { | |
log.error(e.getMessage(), e); | |
} finally { | |
FileUtils.close(writer); | |
} | |
} | |
} |