001: package org.apache.torque.task;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.io.ByteArrayInputStream;
023: import java.io.ByteArrayOutputStream;
024: import java.io.File;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.InputStreamReader;
028: import java.io.LineNumberReader;
029: import java.io.PrintStream;
030: import java.io.UnsupportedEncodingException;
031: import java.io.Writer;
032: import java.util.ArrayList;
033: import java.util.Date;
034: import java.util.Hashtable;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.Map;
038:
039: import org.apache.commons.lang.StringUtils;
040: import org.apache.texen.Generator;
041: import org.apache.texen.ant.TexenTask;
042: import org.apache.tools.ant.BuildException;
043: import org.apache.tools.ant.DirectoryScanner;
044: import org.apache.tools.ant.types.FileSet;
045: import org.apache.torque.engine.EngineException;
046: import org.apache.torque.engine.database.model.Database;
047: import org.apache.torque.engine.database.transform.XmlToAppData;
048: import org.apache.velocity.VelocityContext;
049: import org.apache.velocity.app.VelocityEngine;
050: import org.apache.velocity.context.Context;
051: import org.apache.velocity.exception.MethodInvocationException;
052: import org.apache.velocity.exception.ParseErrorException;
053: import org.apache.velocity.exception.ResourceNotFoundException;
054: import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
055: import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
056:
057: /**
058: * A base torque task that uses either a single XML schema
059: * representing a data model, or a <fileset> of XML schemas.
060: * We are making the assumption that an XML schema representing
061: * a data model contains tables for a <strong>single</strong>
062: * database.
063: *
064: * @author <a href="mailto:jvanzyl@zenplex.com">Jason van Zyl</a>
065: * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
066: */
067: public class TorqueDataModelTask extends TexenTask {
068: /**
069: * XML that describes the database model, this is transformed
070: * into the application model object.
071: */
072: protected String xmlFile;
073:
074: /** Fileset of XML schemas which represent our data models. */
075: protected List filesets = new ArrayList();
076:
077: /** Data models that we collect. One from each XML schema file. */
078: protected List dataModels = new ArrayList();
079:
080: /** Velocity context which exposes our objects in the templates. */
081: protected Context context;
082:
083: /**
084: * Map of data model name to database name.
085: * Should probably stick to the convention of them being the same but
086: * I know right now in a lot of cases they won't be.
087: */
088: protected Hashtable dataModelDbMap;
089:
090: /**
091: * Hashtable containing the names of all the databases
092: * in our collection of schemas.
093: */
094: protected Hashtable databaseNames;
095:
096: //!! This is probably a crappy idea having the sql file -> db map
097: // here. I can't remember why I put it here at the moment ...
098: // maybe I was going to map something else. It can probably
099: // move into the SQL task.
100:
101: /**
102: * Name of the properties file that maps an SQL file
103: * to a particular database.
104: */
105: protected String sqldbmap;
106:
107: /** The target database(s) we are generating SQL for. */
108: private String targetDatabase;
109:
110: /** Target Java package to place the generated files in. */
111: private String targetPackage;
112:
113: /**
114: * Set the sqldbmap.
115: *
116: * @param sqldbmap th db map
117: */
118: public void setSqlDbMap(String sqldbmap) {
119: //!! Make all these references files not strings.
120: this .sqldbmap = getProject().resolveFile(sqldbmap).toString();
121: }
122:
123: /**
124: * Get the sqldbmap.
125: *
126: * @return String sqldbmap.
127: */
128: public String getSqlDbMap() {
129: return sqldbmap;
130: }
131:
132: /**
133: * Return the data models that have been processed.
134: *
135: * @return List data models
136: */
137: public List getDataModels() {
138: return dataModels;
139: }
140:
141: /**
142: * Return the data model to database name map.
143: *
144: * @return Hashtable data model name to database name map.
145: */
146: public Hashtable getDataModelDbMap() {
147: return dataModelDbMap;
148: }
149:
150: /**
151: * Get the xml schema describing the application model.
152: *
153: * @return String xml schema file.
154: */
155: public String getXmlFile() {
156: return xmlFile;
157: }
158:
159: /**
160: * Set the xml schema describing the application model.
161: *
162: * @param xmlFile The new XmlFile value
163: */
164: public void setXmlFile(String xmlFile) {
165: this .xmlFile = getProject().resolveFile(xmlFile).toString();
166: }
167:
168: /**
169: * Adds a set of xml schema files (nested fileset attribute).
170: *
171: * @param set a Set of xml schema files
172: */
173: public void addFileset(FileSet set) {
174: filesets.add(set);
175: }
176:
177: /**
178: * Get the current target database.
179: *
180: * @return String target database(s)
181: */
182: public String getTargetDatabase() {
183: return targetDatabase;
184: }
185:
186: /**
187: * Set the current target database. (e.g. mysql, oracle, ..)
188: *
189: * @param v target database(s)
190: */
191: public void setTargetDatabase(String v) {
192: targetDatabase = v;
193: }
194:
195: /**
196: * Get the current target package.
197: *
198: * @return return target java package.
199: */
200: public String getTargetPackage() {
201: return targetPackage;
202: }
203:
204: /**
205: * Set the current target package. This is where generated java classes will
206: * live.
207: *
208: * @param v target java package.
209: */
210: public void setTargetPackage(String v) {
211: targetPackage = v;
212: }
213:
214: /**
215: * Set up the initial context for generating the SQL from the XML schema.
216: *
217: * @return the context
218: * @throws Exception
219: */
220: public Context initControlContext() throws Exception {
221: XmlToAppData xmlParser;
222:
223: if (xmlFile == null && filesets.isEmpty()) {
224: throw new BuildException(
225: "You must specify an XML schema or "
226: + "fileset of XML schemas!");
227: }
228:
229: try {
230: if (xmlFile != null) {
231: // Transform the XML database schema into
232: // data model object.
233: xmlParser = new XmlToAppData(getTargetDatabase(),
234: getTargetPackage());
235: Database ad = xmlParser.parseFile(xmlFile);
236: ad.setFileName(grokName(xmlFile));
237: dataModels.add(ad);
238: } else {
239: // Deal with the filesets.
240: for (int i = 0; i < filesets.size(); i++) {
241: FileSet fs = (FileSet) filesets.get(i);
242: DirectoryScanner ds = fs
243: .getDirectoryScanner(getProject());
244: File srcDir = fs.getDir(getProject());
245:
246: String[] dataModelFiles = ds.getIncludedFiles();
247:
248: // Make a transaction for each file
249: for (int j = 0; j < dataModelFiles.length; j++) {
250: File f = new File(srcDir, dataModelFiles[j]);
251: xmlParser = new XmlToAppData(
252: getTargetDatabase(), getTargetPackage());
253: Database ad = xmlParser.parseFile(f.toString());
254: ad.setFileName(grokName(f.toString()));
255: dataModels.add(ad);
256: }
257: }
258: }
259:
260: Iterator i = dataModels.iterator();
261: databaseNames = new Hashtable();
262: dataModelDbMap = new Hashtable();
263:
264: // Different datamodels may state the same database
265: // names, we just want the unique names of databases.
266: while (i.hasNext()) {
267: Database database = (Database) i.next();
268: databaseNames.put(database.getName(), database
269: .getName());
270: dataModelDbMap.put(database.getFileName(), database
271: .getName());
272: }
273: } catch (EngineException ee) {
274: throw new BuildException(ee);
275: }
276:
277: context = new VelocityContext();
278:
279: // Place our set of data models into the context along
280: // with the names of the databases as a convenience for now.
281: context.put("dataModels", dataModels);
282: context.put("databaseNames", databaseNames);
283: context.put("targetDatabase", targetDatabase);
284: context.put("targetPackage", targetPackage);
285:
286: return context;
287: }
288:
289: /**
290: * Change type of "now" to java.util.Date
291: *
292: * @see org.apache.texen.ant.TexenTask#populateInitialContext(org.apache.velocity.context.Context)
293: */
294: protected void populateInitialContext(Context context)
295: throws Exception {
296: super .populateInitialContext(context);
297: context.put("now", new Date());
298: }
299:
300: /**
301: * Gets a name to use for the application's data model.
302: *
303: * @param xmlFile The path to the XML file housing the data model.
304: * @return The name to use for the <code>AppData</code>.
305: */
306: private String grokName(String xmlFile) {
307: // This can't be set from the file name as it is an unreliable
308: // method of naming the descriptor. Not everyone uses the same
309: // method as I do in the TDK. jvz.
310:
311: String name = "data-model";
312: int i = xmlFile.lastIndexOf(System
313: .getProperty("file.separator"));
314: if (i != -1) {
315: // Creep forward to the start of the file name.
316: i++;
317:
318: int j = xmlFile.lastIndexOf('.');
319: if (i < j) {
320: name = xmlFile.substring(i, j);
321: } else {
322: // Weirdo
323: name = xmlFile.substring(i);
324: }
325: }
326: return name;
327: }
328:
329: /**
330: * Override Texen's context properties to map the
331: * torque.xxx properties (including defaults set by the
332: * org/apache/torque/defaults.properties) to just xxx.
333: *
334: * <p>
335: * Also, move xxx.yyy properties to xxxYyy as Velocity
336: * doesn't like the xxx.yyy syntax.
337: * </p>
338: *
339: * @param file the file to read the properties from
340: */
341: public void setContextProperties(String file) {
342: super .setContextProperties(file);
343:
344: // Map the torque.xxx elements from the env to the contextProperties
345: Hashtable env = super .getProject().getProperties();
346: for (Iterator i = env.entrySet().iterator(); i.hasNext();) {
347: Map.Entry entry = (Map.Entry) i.next();
348: String key = (String) entry.getKey();
349: if (key.startsWith("torque.")) {
350: String newKey = key.substring("torque.".length());
351: int j = newKey.indexOf(".");
352: while (j != -1) {
353: newKey = newKey.substring(0, j)
354: + StringUtils.capitalize(newKey
355: .substring(j + 1));
356: j = newKey.indexOf(".");
357: }
358:
359: contextProperties.setProperty(newKey, entry.getValue());
360: }
361: }
362: }
363:
364: /**
365: * This message fragment (telling users to consult the log or
366: * invoke ant with the -debug flag) is appended to rethrown
367: * exception messages.
368: */
369: private final static String ERR_MSG_FRAGMENT = ". For more information consult the velocity log, or invoke ant "
370: + "with the -debug flag.";
371:
372: /**
373: * This method creates an VelocityEngine instance, parses
374: * every template and creates the corresponding output.
375: *
376: * Unfortunately the TextenTask.execute() method makes
377: * everything for us but we just want to set our own
378: * VelocityTemplateLoader.
379: * TODO: change once TEXEN-14 is resolved and out.
380: *
381: * @see org.apache.texen.ant.TexenTask#execute()
382: */
383: public void execute() throws BuildException {
384: // Make sure the template path is set.
385: if (templatePath == null && useClasspath == false) {
386: throw new BuildException(
387: "The template path needs to be defined if you are not using "
388: + "the classpath for locating templates!");
389: }
390:
391: // Make sure the control template is set.
392: if (controlTemplate == null) {
393: throw new BuildException(
394: "The control template needs to be defined!");
395: }
396:
397: // Make sure the output directory is set.
398: if (outputDirectory == null) {
399: throw new BuildException(
400: "The output directory needs to be defined!");
401: }
402:
403: // Make sure there is an output file.
404: if (outputFile == null) {
405: throw new BuildException(
406: "The output file needs to be defined!");
407: }
408:
409: VelocityEngine ve = new VelocityEngine();
410:
411: try {
412: // Setup the Velocity Runtime.
413: if (templatePath != null) {
414: log("Using templatePath: " + templatePath,
415: project.MSG_VERBOSE);
416: ve.setProperty("torque"
417: + VelocityEngine.FILE_RESOURCE_LOADER_PATH,
418: templatePath);
419:
420: // TR: We need our own FileResourceLoader
421: ve.addProperty(VelocityEngine.RESOURCE_LOADER,
422: "torquefile");
423: ve.setProperty("torquefile."
424: + VelocityEngine.RESOURCE_LOADER + ".instance",
425: new TorqueFileResourceLoader(this ));
426: }
427:
428: if (useClasspath) {
429: log("Using classpath");
430: // TR: We need our own ClasspathResourceLoader
431: ve.addProperty(VelocityEngine.RESOURCE_LOADER,
432: "classpath");
433:
434: ve.setProperty("classpath."
435: + VelocityEngine.RESOURCE_LOADER + ".instance",
436: new TorqueClasspathResourceLoader(this ));
437:
438: ve.setProperty("classpath."
439: + VelocityEngine.RESOURCE_LOADER + ".cache",
440: "false");
441:
442: ve.setProperty("classpath."
443: + VelocityEngine.RESOURCE_LOADER
444: + ".modificationCheckInterval", "2");
445: }
446:
447: ve.init();
448:
449: // Create the text generator.
450: Generator generator = Generator.getInstance();
451: generator.setVelocityEngine(ve);
452: generator.setOutputPath(outputDirectory);
453: generator.setInputEncoding(inputEncoding);
454: generator.setOutputEncoding(outputEncoding);
455:
456: if (templatePath != null) {
457: generator.setTemplatePath(templatePath);
458: }
459:
460: // Make sure the output directory exists, if it doesn't
461: // then create it.
462: File file = new File(outputDirectory);
463: if (!file.exists()) {
464: file.mkdirs();
465: }
466:
467: String path = outputDirectory + File.separator + outputFile;
468: log("Generating to file " + path, project.MSG_INFO);
469: Writer writer = generator.getWriter(path, outputEncoding);
470:
471: // The generator and the output path should
472: // be placed in the init context here and
473: // not in the generator class itself.
474: Context c = initControlContext();
475:
476: // Everything in the generator class should be
477: // pulled out and placed in here. What the generator
478: // class does can probably be added to the Velocity
479: // class and the generator class can probably
480: // be removed all together.
481: populateInitialContext(c);
482:
483: // Feed all the options into the initial
484: // control context so they are available
485: // in the control/worker templates.
486: if (contextProperties != null) {
487: Iterator i = contextProperties.getKeys();
488:
489: while (i.hasNext()) {
490: String property = (String) i.next();
491: String value = contextProperties
492: .getString(property);
493:
494: // Now lets quickly check to see if what
495: // we have is numeric and try to put it
496: // into the context as an Integer.
497: try {
498: c.put(property, new Integer(value));
499: } catch (NumberFormatException nfe) {
500: // Now we will try to place the value into
501: // the context as a boolean value if it
502: // maps to a valid boolean value.
503: String booleanString = contextProperties
504: .testBoolean(value);
505:
506: if (booleanString != null) {
507: c.put(property, new Boolean(booleanString));
508: } else {
509: // We are going to do something special
510: // for properties that have a "file.contents"
511: // suffix: for these properties will pull
512: // in the contents of the file and make
513: // them available in the context. So for
514: // a line like the following in a properties file:
515: //
516: // license.file.contents = license.txt
517: //
518: // We will pull in the contents of license.txt
519: // and make it available in the context as
520: // $license. This should make texen a little
521: // more flexible.
522: if (property.endsWith("file.contents")) {
523: // We need to turn the license file from
524: // relative to
525: // absolute, and let Ant help :)
526: value = org.apache.velocity.util.StringUtils
527: .fileContentsToString(project
528: .resolveFile(value)
529: .getCanonicalPath());
530:
531: property = property
532: .substring(
533: 0,
534: property
535: .indexOf("file.contents") - 1);
536: }
537:
538: c.put(property, value);
539: }
540: }
541: }
542: }
543:
544: writer.write(generator.parse(controlTemplate, c));
545: writer.flush();
546: writer.close();
547: generator.shutdown();
548: cleanup();
549: } catch (BuildException e) {
550: throw e;
551: } catch (MethodInvocationException e) {
552: throw new BuildException("Exception thrown by '"
553: + e.getReferenceName() + "." + e.getMethodName()
554: + "'" + ERR_MSG_FRAGMENT, e.getWrappedThrowable());
555: } catch (ParseErrorException e) {
556: throw new BuildException("Velocity syntax error"
557: + ERR_MSG_FRAGMENT, e);
558: } catch (ResourceNotFoundException e) {
559: throw new BuildException("Resource not found"
560: + ERR_MSG_FRAGMENT, e);
561: } catch (Exception e) {
562: throw new BuildException("Generation failed"
563: + ERR_MSG_FRAGMENT, e);
564: }
565: }
566:
567: /**
568: * This method filters the template and replaces some
569: * unwanted characters. For example it removes leading
570: * spaces in front of velocity commands and replaces
571: * tabs with spaces to prevent bounces in different
572: * code editors with different tab-width-setting.
573: *
574: * @param resource the input stream to filter
575: *
576: * @return the filtered input stream.
577: *
578: * @throws IOException if creating, reading or writing to a stream fails.
579: */
580: protected InputStream filter(InputStream resource)
581: throws IOException {
582: InputStreamReader streamReader;
583: if (inputEncoding != null) {
584: streamReader = new InputStreamReader(resource,
585: inputEncoding);
586: } else {
587: streamReader = new InputStreamReader(resource);
588: }
589: LineNumberReader lineNumberReader = new LineNumberReader(
590: streamReader);
591: String line = null;
592: ByteArrayOutputStream baos = new ByteArrayOutputStream();
593: PrintStream ps = null;
594: if (inputEncoding != null) {
595: ps = new PrintStream(baos, true, inputEncoding);
596: } else {
597: ps = new PrintStream(baos, true);
598: }
599:
600: while ((line = lineNumberReader.readLine()) != null) {
601: // remove leading spaces in front of velocity commands and comments
602: line = line.replaceAll("^\\s*#", "#");
603: // replace tabs with spaces to prevent bounces in editors
604: line = line.replaceAll("\t", " ");
605: ps.println(line);
606: }
607: ps.flush();
608: ps.close();
609:
610: return new ByteArrayInputStream(baos.toByteArray());
611: }
612:
613: /**
614: * A custom classpath resource loader which filters tabs and removes spaces
615: * from lines with velocity commands.
616: */
617: public static class TorqueClasspathResourceLoader extends
618: ClasspathResourceLoader {
619: /**
620: * The task in which this resource loader is used.
621: */
622: private TorqueDataModelTask task;
623:
624: /**
625: * Constructor.
626: *
627: * @param task the task in which this resource loader is used.
628: */
629: public TorqueClasspathResourceLoader(TorqueDataModelTask task) {
630: this .task = task;
631: }
632:
633: /**
634: * @see org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader#getResourceStream(java.lang.String)
635: */
636: public synchronized InputStream getResourceStream(String name)
637: throws ResourceNotFoundException {
638: InputStream source = null;
639: try {
640: source = super .getResourceStream(name);
641: return task.filter(source);
642: } catch (IOException uee) {
643: task.log(uee.getMessage());
644: throw new ResourceNotFoundException(uee.getMessage());
645: } finally {
646: if (source != null) {
647: try {
648: source.close();
649: } catch (IOException e) {
650: task.log(e.getMessage());
651: }
652: }
653: }
654: }
655: }
656:
657: /**
658: * A custom file resource loader which filters tabs and removes spaces
659: * from lines with velocity commands.
660: */
661: public static class TorqueFileResourceLoader extends
662: FileResourceLoader {
663: /**
664: * The task in which this resource loader is used.
665: */
666: private TorqueDataModelTask task;
667:
668: /**
669: * Constructor.
670: *
671: * @param task the task in which this resource loader is used.
672: */
673: public TorqueFileResourceLoader(TorqueDataModelTask task) {
674: this .task = task;
675: }
676:
677: /**
678: * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader#getResourceStream(java.lang.String)
679: */
680: public synchronized InputStream getResourceStream(String name)
681: throws ResourceNotFoundException {
682: InputStream source = null;
683: try {
684: source = super .getResourceStream(name);
685: return task.filter(source);
686: } catch (IOException uee) {
687: task.log(uee.getMessage());
688: throw new ResourceNotFoundException(uee.getMessage());
689: } finally {
690: if (source != null) {
691: try {
692: source.close();
693: } catch (IOException e) {
694: task.log(e.getMessage());
695: }
696: }
697: }
698: }
699: }
700: }
|