Index: /torque-generator/src/java/org/apache/torque/task/TorqueDataModelTask.java =================================================================== --- /torque-generator/src/java/org/apache/torque/task/TorqueDataModelTask.java (revision 482607) +++ /torque-generator/src/java/org/apache/torque/task/TorqueDataModelTask.java (working copy) @@ -19,7 +19,16 @@ * under the License. */ +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.io.Writer; import java.util.ArrayList; import java.util.Date; import java.util.Hashtable; @@ -35,7 +44,14 @@ import org.apache.torque.engine.database.model.Database; import org.apache.torque.engine.database.transform.XmlToAppData; import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.context.Context; +import org.apache.velocity.exception.MethodInvocationException; +import org.apache.velocity.exception.ParseErrorException; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; +import org.apache.velocity.runtime.resource.loader.FileResourceLoader; +import org.apache.velocity.texen.Generator; import org.apache.velocity.texen.ant.TexenTask; /** @@ -48,328 +64,626 @@ * @author Jason van Zyl * @author Daniel Rall */ -public class TorqueDataModelTask extends TexenTask -{ - /** - * XML that describes the database model, this is transformed - * into the application model object. - */ - protected String xmlFile; +public class TorqueDataModelTask extends TexenTask { - /** Fileset of XML schemas which represent our data models. */ - protected List filesets = new ArrayList(); + /** + * XML that describes the database model, this is transformed + * into the application model object. + */ + protected String xmlFile; - /** Data models that we collect. One from each XML schema file. */ - protected List dataModels = new ArrayList(); + /** Fileset of XML schemas which represent our data models. */ + protected List filesets = new ArrayList(); - /** Velocity context which exposes our objects in the templates. */ - protected Context context; + /** Data models that we collect. One from each XML schema file. */ + protected List dataModels = new ArrayList(); - /** - * Map of data model name to database name. - * Should probably stick to the convention of them being the same but - * I know right now in a lot of cases they won't be. - */ - protected Hashtable dataModelDbMap; + /** Velocity context which exposes our objects in the templates. */ + protected Context context; - /** - * Hashtable containing the names of all the databases - * in our collection of schemas. - */ - protected Hashtable databaseNames; + /** + * Map of data model name to database name. + * Should probably stick to the convention of them being the same but + * I know right now in a lot of cases they won't be. + */ + protected Hashtable dataModelDbMap; - //!! This is probably a crappy idea having the sql file -> db map - // here. I can't remember why I put it here at the moment ... - // maybe I was going to map something else. It can probably - // move into the SQL task. + /** + * Hashtable containing the names of all the databases + * in our collection of schemas. + */ + protected Hashtable databaseNames; - /** - * Name of the properties file that maps an SQL file - * to a particular database. - */ - protected String sqldbmap; + //!! This is probably a crappy idea having the sql file -> db map + // here. I can't remember why I put it here at the moment ... + // maybe I was going to map something else. It can probably + // move into the SQL task. - /** The target database(s) we are generating SQL for. */ - private String targetDatabase; + /** + * Name of the properties file that maps an SQL file + * to a particular database. + */ + protected String sqldbmap; - /** Target Java package to place the generated files in. */ - private String targetPackage; + /** The target database(s) we are generating SQL for. */ + private String targetDatabase; + /** Target Java package to place the generated files in. */ + private String targetPackage; - /** - * Set the sqldbmap. - * - * @param sqldbmap th db map - */ - public void setSqlDbMap(String sqldbmap) - { - //!! Make all these references files not strings. - this.sqldbmap = getProject().resolveFile(sqldbmap).toString(); - } - /** - * Get the sqldbmap. - * - * @return String sqldbmap. - */ - public String getSqlDbMap() - { - return sqldbmap; - } + /** + * Set the sqldbmap. + * + * @param sqldbmap th db map + */ + public void setSqlDbMap(String sqldbmap) + { + //!! Make all these references files not strings. + this.sqldbmap = getProject().resolveFile(sqldbmap).toString(); + } - /** - * Return the data models that have been processed. - * - * @return List data models - */ - public List getDataModels() - { - return dataModels; - } + /** + * Get the sqldbmap. + * + * @return String sqldbmap. + */ + public String getSqlDbMap() + { + return sqldbmap; + } - /** - * Return the data model to database name map. - * - * @return Hashtable data model name to database name map. - */ - public Hashtable getDataModelDbMap() - { - return dataModelDbMap; - } + /** + * Return the data models that have been processed. + * + * @return List data models + */ + public List getDataModels() + { + return dataModels; + } - /** - * Get the xml schema describing the application model. - * - * @return String xml schema file. - */ - public String getXmlFile() - { - return xmlFile; - } + /** + * Return the data model to database name map. + * + * @return Hashtable data model name to database name map. + */ + public Hashtable getDataModelDbMap() + { + return dataModelDbMap; + } - /** - * Set the xml schema describing the application model. - * - * @param xmlFile The new XmlFile value - */ - public void setXmlFile(String xmlFile) - { - this.xmlFile = getProject().resolveFile(xmlFile).toString(); - } + /** + * Get the xml schema describing the application model. + * + * @return String xml schema file. + */ + public String getXmlFile() + { + return xmlFile; + } - /** - * Adds a set of xml schema files (nested fileset attribute). - * - * @param set a Set of xml schema files - */ - public void addFileset(FileSet set) - { - filesets.add(set); - } + /** + * Set the xml schema describing the application model. + * + * @param xmlFile The new XmlFile value + */ + public void setXmlFile(String xmlFile) + { + this.xmlFile = getProject().resolveFile(xmlFile).toString(); + } - /** - * Get the current target database. - * - * @return String target database(s) - */ - public String getTargetDatabase() - { - return targetDatabase; - } + /** + * Adds a set of xml schema files (nested fileset attribute). + * + * @param set a Set of xml schema files + */ + public void addFileset(FileSet set) + { + filesets.add(set); + } - /** - * Set the current target database. (e.g. mysql, oracle, ..) - * - * @param v target database(s) - */ - public void setTargetDatabase(String v) - { - targetDatabase = v; - } + /** + * Get the current target database. + * + * @return String target database(s) + */ + public String getTargetDatabase() + { + return targetDatabase; + } - /** - * Get the current target package. - * - * @return return target java package. - */ - public String getTargetPackage() + /** + * Set the current target database. (e.g. mysql, oracle, ..) + * + * @param v target database(s) + */ + public void setTargetDatabase(String v) + { + targetDatabase = v; + } + + /** + * Get the current target package. + * + * @return return target java package. + */ + public String getTargetPackage() + { + return targetPackage; + } + + /** + * Set the current target package. This is where generated java classes will + * live. + * + * @param v target java package. + */ + public void setTargetPackage(String v) + { + targetPackage = v; + } + + /** + * Set up the initial context for generating the SQL from the XML schema. + * + * @return the context + * @throws Exception + */ + public Context initControlContext() throws Exception + { + XmlToAppData xmlParser; + + if (xmlFile == null && filesets.isEmpty()) + { + throw new BuildException("You must specify an XML schema or " + + "fileset of XML schemas!"); + } + + try + { + if (xmlFile != null) + { + // Transform the XML database schema into + // data model object. + xmlParser = new XmlToAppData(getTargetDatabase(), + getTargetPackage()); + Database ad = xmlParser.parseFile(xmlFile); + ad.setFileName(grokName(xmlFile)); + dataModels.add(ad); + } + else + { + // Deal with the filesets. + for (int i = 0; i < filesets.size(); i++) + { + FileSet fs = (FileSet) filesets.get(i); + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File srcDir = fs.getDir(getProject()); + + String[] dataModelFiles = ds.getIncludedFiles(); + + // Make a transaction for each file + for (int j = 0; j < dataModelFiles.length; j++) + { + File f = new File(srcDir, dataModelFiles[j]); + xmlParser = new XmlToAppData(getTargetDatabase(), + getTargetPackage()); + Database ad = xmlParser.parseFile(f.toString()); + ad.setFileName(grokName(f.toString())); + dataModels.add(ad); + } + } + } + + Iterator i = dataModels.iterator(); + databaseNames = new Hashtable(); + dataModelDbMap = new Hashtable(); + + // Different datamodels may state the same database + // names, we just want the unique names of databases. + while (i.hasNext()) + { + Database database = (Database) i.next(); + databaseNames.put(database.getName(), database.getName()); + dataModelDbMap.put(database.getFileName(), database.getName()); + } + } + catch (EngineException ee) + { + throw new BuildException(ee); + } + + context = new VelocityContext(); + + // Place our set of data models into the context along + // with the names of the databases as a convenience for now. + context.put("dataModels", dataModels); + context.put("databaseNames", databaseNames); + context.put("targetDatabase", targetDatabase); + context.put("targetPackage", targetPackage); + + return context; + } + + /** + * Change type of "now" to java.util.Date + * + * @see org.apache.velocity.texen.ant.TexenTask#populateInitialContext(org.apache.velocity.context.Context) + */ + protected void populateInitialContext(Context context) throws Exception + { + super.populateInitialContext(context); + context.put("now", new Date()); + } + + /** + * Gets a name to use for the application's data model. + * + * @param xmlFile The path to the XML file housing the data model. + * @return The name to use for the AppData. + */ + private String grokName(String xmlFile) + { + // This can't be set from the file name as it is an unreliable + // method of naming the descriptor. Not everyone uses the same + // method as I do in the TDK. jvz. + + String name = "data-model"; + int i = xmlFile.lastIndexOf(System.getProperty("file.separator")); + if (i != -1) + { + // Creep forward to the start of the file name. + i++; + + int j = xmlFile.lastIndexOf('.'); + if (i < j) + { + name = xmlFile.substring(i, j); + } + else + { + // Weirdo + name = xmlFile.substring(i); + } + } + return name; + } + + /** + * Override Texen's context properties to map the + * torque.xxx properties (including defaults set by the + * org/apache/torque/defaults.properties) to just xxx. + * + *

+ * Also, move xxx.yyy properties to xxxYyy as Velocity + * doesn't like the xxx.yyy syntax. + *

+ * + * @param file the file to read the properties from + */ + public void setContextProperties(String file) + { + super.setContextProperties(file); + + // Map the torque.xxx elements from the env to the contextProperties + Hashtable env = super.getProject().getProperties(); + for (Iterator i = env.entrySet().iterator(); i.hasNext();) + { + Map.Entry entry = (Map.Entry) i.next(); + String key = (String) entry.getKey(); + if (key.startsWith("torque.")) + { + String newKey = key.substring("torque.".length()); + int j = newKey.indexOf("."); + while (j != -1) + { + newKey = + newKey.substring(0, j) + + StringUtils.capitalize(newKey.substring(j + 1)); + j = newKey.indexOf("."); + } + + contextProperties.setProperty(newKey, entry.getValue()); + } + } + } + + + /** + * This message fragment (telling users to consult the log or + * invoke ant with the -debug flag) is appended to rethrown + * exception messages. + */ + private final static String ERR_MSG_FRAGMENT = + ". For more information consult the velocity log, or invoke ant " + + "with the -debug flag."; + + /** + * This method creates an VelocityEngine instance, parses + * every template and creates the corresponding output. + * + * Unfortunately the TextenTask.execute() method makes + * everything for us but we just want to set our own + * VelocityTemplateLoader. Maybe it would be better + * to ask the Velocity team if they can change there + * API in this class. + * + * @see org.apache.velocity.texen.ant.TexenTask#execute() + */ + public void execute() throws BuildException + { + // Make sure the template path is set. + if (templatePath == null && useClasspath == false) { - return targetPackage; - } + throw new BuildException( + "The template path needs to be defined if you are not using " + + "the classpath for locating templates!"); + } - /** - * Set the current target package. This is where generated java classes will - * live. - * - * @param v target java package. - */ - public void setTargetPackage(String v) + // Make sure the control template is set. + if (controlTemplate == null) { - targetPackage = v; - } + throw new BuildException("The control template needs to be defined!"); + } - /** - * Set up the initial context for generating the SQL from the XML schema. - * - * @return the context - * @throws Exception - */ - public Context initControlContext() throws Exception + // Make sure the output directory is set. + if (outputDirectory == null) { - XmlToAppData xmlParser; + throw new BuildException("The output directory needs to be defined!"); + } + + // Make sure there is an output file. + if (outputFile == null) + { + throw new BuildException("The output file needs to be defined!"); + } + + VelocityEngine ve = new VelocityEngine(); + + try + { + // Setup the Velocity Runtime. + if (templatePath != null) + { + log("Using templatePath: " + templatePath, project.MSG_VERBOSE); + ve.setProperty( + VelocityEngine.FILE_RESOURCE_LOADER_PATH, templatePath); - if (xmlFile == null && filesets.isEmpty()) + // TR: We need our own FileResourceLoader + ve.addProperty( + VelocityEngine.RESOURCE_LOADER, "torquefile"); + ve.setProperty( + "torquefile." + VelocityEngine.RESOURCE_LOADER + ".class", + TorqueFileResourceLoader.class.getName()); + } + + if (useClasspath) { - throw new BuildException("You must specify an XML schema or " - + "fileset of XML schemas!"); + log("Using classpath"); + // TR: We need our own ClasspathResourceLoader + ve.addProperty( + VelocityEngine.RESOURCE_LOADER, "classpath"); + + ve.setProperty( + "classpath." + VelocityEngine.RESOURCE_LOADER + ".class", + TorqueClasspathResourceLoader.class.getName()); + + ve.setProperty( + "classpath." + VelocityEngine.RESOURCE_LOADER + + ".cache", "false"); + + ve.setProperty( + "classpath." + VelocityEngine.RESOURCE_LOADER + + ".modificationCheckInterval", "2"); } + + ve.init(); - try + // Create the text generator. + Generator generator = Generator.getInstance(); + generator.setVelocityEngine(ve); + generator.setOutputPath(outputDirectory); + generator.setInputEncoding(inputEncoding); + generator.setOutputEncoding(outputEncoding); + + if (templatePath != null) { - if (xmlFile != null) + generator.setTemplatePath(templatePath); + } + + // Make sure the output directory exists, if it doesn't + // then create it. + File file = new File(outputDirectory); + if (! file.exists()) + { + file.mkdirs(); + } + + String path = outputDirectory + File.separator + outputFile; + log("Generating to file " + path, project.MSG_INFO); + Writer writer = generator.getWriter(path, outputEncoding); + + // The generator and the output path should + // be placed in the init context here and + // not in the generator class itself. + Context c = initControlContext(); + + // Everything in the generator class should be + // pulled out and placed in here. What the generator + // class does can probably be added to the Velocity + // class and the generator class can probably + // be removed all together. + populateInitialContext(c); + + // Feed all the options into the initial + // control context so they are available + // in the control/worker templates. + if (contextProperties != null) + { + Iterator i = contextProperties.getKeys(); + + while (i.hasNext()) { - // Transform the XML database schema into - // data model object. - xmlParser = new XmlToAppData(getTargetDatabase(), - getTargetPackage()); - Database ad = xmlParser.parseFile(xmlFile); - ad.setFileName(grokName(xmlFile)); - dataModels.add(ad); - } - else - { - // Deal with the filesets. - for (int i = 0; i < filesets.size(); i++) + String property = (String) i.next(); + String value = contextProperties.getString(property); + + // Now lets quickly check to see if what + // we have is numeric and try to put it + // into the context as an Integer. + try { - FileSet fs = (FileSet) filesets.get(i); - DirectoryScanner ds = fs.getDirectoryScanner(getProject()); - File srcDir = fs.getDir(getProject()); - - String[] dataModelFiles = ds.getIncludedFiles(); - - // Make a transaction for each file - for (int j = 0; j < dataModelFiles.length; j++) + c.put(property, new Integer(value)); + } + catch (NumberFormatException nfe) + { + // Now we will try to place the value into + // the context as a boolean value if it + // maps to a valid boolean value. + String booleanString = + contextProperties.testBoolean(value); + + if (booleanString != null) + { + c.put(property, new Boolean(booleanString)); + } + else { - File f = new File(srcDir, dataModelFiles[j]); - xmlParser = new XmlToAppData(getTargetDatabase(), - getTargetPackage()); - Database ad = xmlParser.parseFile(f.toString()); - ad.setFileName(grokName(f.toString())); - dataModels.add(ad); + // We are going to do something special + // for properties that have a "file.contents" + // suffix: for these properties will pull + // in the contents of the file and make + // them available in the context. So for + // a line like the following in a properties file: + // + // license.file.contents = license.txt + // + // We will pull in the contents of license.txt + // and make it available in the context as + // $license. This should make texen a little + // more flexible. + if (property.endsWith("file.contents")) + { + // We need to turn the license file from relative to + // absolute, and let Ant help :) + value = org.apache.velocity.util.StringUtils.fileContentsToString( + project.resolveFile(value).getCanonicalPath()); + + property = property.substring( + 0, property.indexOf("file.contents") - 1); + } + + c.put(property, value); } } } - - Iterator i = dataModels.iterator(); - databaseNames = new Hashtable(); - dataModelDbMap = new Hashtable(); - - // Different datamodels may state the same database - // names, we just want the unique names of databases. - while (i.hasNext()) - { - Database database = (Database) i.next(); - databaseNames.put(database.getName(), database.getName()); - dataModelDbMap.put(database.getFileName(), database.getName()); - } } - catch (EngineException ee) - { - throw new BuildException(ee); - } + + writer.write(generator.parse(controlTemplate, c)); + writer.flush(); + writer.close(); + generator.shutdown(); + cleanup(); + } + catch( BuildException e) + { + throw e; + } + catch( MethodInvocationException e ) + { + throw new BuildException( + "Exception thrown by '" + e.getReferenceName() + "." + + e.getMethodName() +"'" + ERR_MSG_FRAGMENT, + e.getWrappedThrowable()); + } + catch( ParseErrorException e ) + { + throw new BuildException("Velocity syntax error" + ERR_MSG_FRAGMENT ,e); + } + catch( ResourceNotFoundException e ) + { + throw new BuildException("Resource not found" + ERR_MSG_FRAGMENT,e); + } + catch( Exception e ) + { + throw new BuildException("Generation failed" + ERR_MSG_FRAGMENT ,e); + } + } - context = new VelocityContext(); - // Place our set of data models into the context along - // with the names of the databases as a convenience for now. - context.put("dataModels", dataModels); - context.put("databaseNames", databaseNames); - context.put("targetDatabase", targetDatabase); - context.put("targetPackage", targetPackage); - - return context; + /** + * This method filters the template and replaces some + * unwanted characters. For example it removes leading + * spaces in front of velocity commands and replaces + * tabs with spaces to prevent bounces in different + * code editors with different tab-width-setting. + */ + protected InputStream filter(InputStream resource) throws IOException { + InputStreamReader streamReader = new InputStreamReader(resource); + LineNumberReader lineNumberReader = new LineNumberReader(streamReader); + String line = null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = null; + + // maybe we have to swap outputEncoding and streamReader.getEncoding() + try { + // try the wanted encoding + ps = new PrintStream(baos,true,outputEncoding); + } catch (UnsupportedEncodingException exception1) { + try { + // try the encoding from the stream + ps = new PrintStream(baos,true,streamReader.getEncoding()); + } catch (UnsupportedEncodingException exception2) { + // use system default + ps = new PrintStream(baos,true); + } } - /** - * Change type of "now" to java.util.Date - * - * @see org.apache.velocity.texen.ant.TexenTask#populateInitialContext(org.apache.velocity.context.Context) - */ - protected void populateInitialContext(Context context) throws Exception - { - super.populateInitialContext(context); - context.put("now", new Date()); + while ((line = lineNumberReader.readLine()) != null) { + // remove leading spaces in front of velocity commands and comments + line = line.replaceAll("^\\s*#", "#"); + // replace tabs with spaces to prevent bounces in editors + line = line.replaceAll("\t"," "); + ps.println(line); } + ps.flush(); + ps.close(); - /** - * Gets a name to use for the application's data model. - * - * @param xmlFile The path to the XML file housing the data model. - * @return The name to use for the AppData. - */ - private String grokName(String xmlFile) - { - // This can't be set from the file name as it is an unreliable - // method of naming the descriptor. Not everyone uses the same - // method as I do in the TDK. jvz. + return new ByteArrayInputStream(baos.toByteArray()); + } - String name = "data-model"; - int i = xmlFile.lastIndexOf(System.getProperty("file.separator")); - if (i != -1) - { - // Creep forward to the start of the file name. - i++; + - int j = xmlFile.lastIndexOf('.'); - if (i < j) - { - name = xmlFile.substring(i, j); - } - else - { - // Weirdo - name = xmlFile.substring(i); - } - } - return name; - } + protected class TorqueClasspathResourceLoader extends ClasspathResourceLoader { /** - * Override Texen's context properties to map the - * torque.xxx properties (including defaults set by the - * org/apache/torque/defaults.properties) to just xxx. - * - *

- * Also, move xxx.yyy properties to xxxYyy as Velocity - * doesn't like the xxx.yyy syntax. - *

- * - * @param file the file to read the properties from + * @see org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader#getResourceStream(java.lang.String) */ - public void setContextProperties(String file) - { - super.setContextProperties(file); + public synchronized InputStream getResourceStream(String name) throws ResourceNotFoundException { + try { + return filter(super.getResourceStream(name)); + } catch (IOException uee) { + log(uee.getMessage()); + throw new ResourceNotFoundException(uee.getMessage()); + } + } + + } - // Map the torque.xxx elements from the env to the contextProperties - Hashtable env = super.getProject().getProperties(); - for (Iterator i = env.entrySet().iterator(); i.hasNext();) - { - Map.Entry entry = (Map.Entry) i.next(); - String key = (String) entry.getKey(); - if (key.startsWith("torque.")) - { - String newKey = key.substring("torque.".length()); - int j = newKey.indexOf("."); - while (j != -1) - { - newKey = - newKey.substring(0, j) - + StringUtils.capitalize(newKey.substring(j + 1)); - j = newKey.indexOf("."); - } + protected class TorqueFileResourceLoader extends FileResourceLoader { - contextProperties.setProperty(newKey, entry.getValue()); - } - } + /** + * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader#getResourceStream(java.lang.String) + */ + public synchronized InputStream getResourceStream(String name) throws ResourceNotFoundException { + try { + return filter(super.getResourceStream(name)); + } catch (IOException uee) { + log(uee.getMessage()); + throw new ResourceNotFoundException(uee.getMessage()); + } } + + } } +