View Javadoc

1   /* Version 1.0 based on Apache Software License 1.1
2    *
3    * Copyright (c) 2003 Piotr Maj and DBMonster developers. All rights
4    * reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions are
8    * met:
9    *
10   * 1. Redistributions of source code must retain the above copyright
11   *    notice, this list of conditions and the following disclaimer.
12   *
13   * 2. Redistributions in binary form must reproduce the above copyright
14   *    notice, this list of conditions and the following disclaimer in the
15   *    documentation and/or other materials provided with the distribution.
16   *
17   * 3. The end-user documentation included with the redistribution, if any,
18   *    must include the following acknowledgment:
19   *
20   *    "This product includes software developed by DBMonster developers
21   *    (http://dbmonster.kernelpanic.pl/)."
22   *
23   *  Alternately, this acknowledgment may appear in the software itself,
24   *  if and wherever such third-party acknowledgments normally appear.
25   *
26   * 4. The name "DBMonster" must not be used to endorse or promote products
27   *    derived from this software without prior written permission. For
28   *    written permission, please contact piotr.maj@kernelpanic.pl.
29   *
30   * 5. Products derived from this software may not be called "DBMonster",
31   *    nor may "DBMonster" appear in their name, without prior written
32   *    permission of Piotr Maj.
33   *
34   * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
35   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
36   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
37   * IN NO EVENT SHALL THE DBMONSTER DEVELOPERS BE LIABLE FOR ANY DIRECT,
38   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
39   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
40   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
41   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
43   * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
44   * POSSIBILITY OF SUCH DAMAGE.
45   */
46  
47  package pl.kernelpanic.dbmonster.schema;
48  
49  import java.io.File;
50  import java.io.FileInputStream;
51  import java.io.InputStream;
52  import java.io.Writer;
53  import java.lang.reflect.Method;
54  import java.lang.reflect.Modifier;
55  import java.net.URL;
56  import java.util.ArrayList;
57  import java.util.Collections;
58  import java.util.HashMap;
59  import java.util.Iterator;
60  import java.util.List;
61  import java.util.Map;
62  
63  import pl.kernelpanic.dbmonster.generator.DataGenerator;
64  import pl.kernelpanic.dbmonster.generator.KeyGenerator;
65  
66  import org.apache.commons.beanutils.BeanUtils;
67  import org.apache.commons.digester.AbstractObjectCreationFactory;
68  import org.apache.commons.digester.Digester;
69  import org.apache.commons.logging.Log;
70  import org.xml.sax.Attributes;
71  import org.xml.sax.ErrorHandler;
72  import org.xml.sax.SAXException;
73  import org.xml.sax.SAXParseException;
74  
75  /***
76   * Utility to manipulate schema.
77   *
78   * @author Piotr Maj <piotr.maj@kernelpanic.pl>
79   *
80   * @version $Id: SchemaUtil.html,v 1.1 2007/06/21 08:38:14 sbahloul Exp $
81   */
82  public final class SchemaUtil {
83  
84      /***
85       * DTD identifier.
86       */
87      public static final String DTD =
88          "-//kernelpanic.pl//DBMonster Database Schema DTD 1.1//EN";
89  
90      /***
91       * DTD url.
92       */
93      public static final String DTD_URL =
94          "http://dbmonster.kernelpanic.pl/dtd/dbmonster-schema-1.1.dtd";
95  
96      /***
97       * Project key.
98       */
99      public static final String PROJECT = "project";
100 
101     /***
102      * Schema key.
103      */
104     public static final String SCHEMA_WRAPPER = "schema";
105 
106     /***
107      * Table key.
108      */
109     public static final String TABLE = "table";
110 
111     /***
112      * Column key.
113      */
114     public static final String COLUMN = "column";
115 
116     /***
117      * Primary key key.
118      */
119     public static final String KEY = "key";
120 
121     /***
122      * Key generator key.
123      */
124     public static final String KEY_GENERATOR = "key_generator";
125 
126     /***
127      * Data generator key.
128      */
129     public static final String DATA_GENERATOR = "data_generator";
130 
131     /***
132      * Holds the excluded property names.
133      */
134     private static Map exclusions = new HashMap();
135 
136     /***
137      * System dependent line separator.
138      */
139     public static final String CRLF = System.getProperty("line.separator");
140 
141     static {
142         exclusions.put(SchemaUtil.PROJECT,
143             new String[] {"fileName", "properties", "jdbcViaProperties"});
144         exclusions.put(SchemaUtil.SCHEMA_WRAPPER,
145             new String[] {"fileName", "schema"});
146         exclusions.put(SchemaUtil.TABLE,
147             new String[] {"key", "schema"});
148         exclusions.put(SchemaUtil.KEY,
149             new String[] {"table", "generator"});
150         exclusions.put(SchemaUtil.COLUMN,
151             new String[] {"table", "generator", "value"});
152         exclusions.put(SchemaUtil.KEY_GENERATOR,
153             new String[] {"key"});
154         exclusions.put(SchemaUtil.DATA_GENERATOR,
155             new String[] {"column"});
156     }
157 
158     /***
159      * We do not need any public constructor.
160      */
161     private SchemaUtil() {
162 
163     }
164 
165     /***
166      * Loads a schema from a file.
167      *
168      * @param fileName the name of the file which contains schema definition.
169      * @param log logger
170      *
171      * @return the schema
172      *
173      * @throws Exception if schema cannot be loaded.
174      */
175     public static final Schema loadSchema(final String fileName,
176             final Log log) throws Exception {
177         return loadSchema(fileName, log, null);
178     }
179  
180     public static final Schema loadSchema(final String fileName,
181         final Log log, final ClassLoader classloader)
182         throws Exception {
183 
184         File f = new File(fileName);
185         FileInputStream fis = new FileInputStream(f);
186         Schema schema = loadSchema(fis, log, classloader);
187 
188         String homePath = f.getParent();
189         schema.setHome(homePath);
190         return schema;
191     }
192 
193     /***
194      * Loads a schema from an url.
195      *
196      * @param url url
197      * @param log logger
198      *
199      * @return the schema
200      *
201      * @throws Exception on errors
202      */
203     public static final Schema loadSchema(final URL url, final Log log)
204         throws Exception {
205 
206         return loadSchema(url.openStream(), log);
207     }
208 
209      /***
210      * Loads a schema from an input stream.
211      *
212      * @param is input stream
213      * @param log logger
214      *
215      * @return the schema
216      *
217      * @throws Exception if schema cannot be loaded.
218      */
219     public static final Schema loadSchema(final InputStream is, final Log log)
220     throws Exception {
221         return loadSchema(is, log, null);
222     }
223 
224     public static final Schema loadSchema(final InputStream is, final Log log, final ClassLoader classloader)
225         throws Exception {
226 
227         Schema schema = null;
228         ErrorHandler errorHandler = new ErrorHandler() {
229             public final void warning(final SAXParseException exception)
230                 throws SAXException {
231                 throw exception;
232             }
233 
234             public final void error(final SAXParseException exception)
235                 throws SAXException {
236                 throw exception;
237             }
238 
239             public final void fatalError(final SAXParseException exception)
240                 throws SAXException {
241                 throw exception;
242             }
243         };
244 
245         URL url = SchemaUtil.class.getResource(
246             "/pl/kernelpanic/dbmonster/resources/dbmonster-schema-1.1.dtd");
247         Digester digester = new Digester();
248         if (log != null) {
249             digester.setLogger(log);
250         }
251         digester.register(DTD, url.toString());
252         if (classloader != null) {
253             digester.setClassLoader(classloader);
254         } else {
255             digester.setUseContextClassLoader(true);
256         }
257 
258         digester.setErrorHandler(errorHandler);
259         digester.setValidating(true);
260 
261         digester.addObjectCreate("dbmonster-schema", Schema.class);
262         digester.addCallMethod("dbmonster-schema/name", "setName", 0);
263         digester.addObjectCreate("*/table", Table.class);
264         digester.addSetProperties("*/table");
265 
266         digester.addObjectCreate("*/table/key", Key.class);
267         digester.addSetProperties("*/table/key");
268         digester.addFactoryCreate("*/table/key/generator",
269             new KeyGeneratorFactory());
270         digester.addSetProperty("*/table/key/generator/property",
271             "name", "value");
272         digester.addSetNext("*/table/key/generator",
273             "setGenerator", KeyGenerator.class.getName());
274         digester.addSetNext("*/table/key", "setKey", Key.class.getName());
275         digester.addSetNext("*/table", "addTable", Table.class.getName());
276         digester.addObjectCreate("*/table/column", Column.class);
277         digester.addSetProperties("*/table/column");
278         digester.addSetNext("*/table/column", "addColumn",
279             Column.class.getName());
280         digester.addFactoryCreate("*/table/column/generator",
281             new GeneratorFactory());
282         digester.addSetProperties("*/table/column/generator");
283         digester.addSetNext("*/table/column/generator", "setGenerator",
284             DataGenerator.class.getName());
285         digester.addSetProperty("*/table/column/generator/property",
286             "name", "value");
287 
288         schema = (Schema) digester.parse(is);
289 
290         return schema;
291     }
292     /***
293      * Validate schema.
294      *
295      * @param schema schema to validate
296      *
297      * @return list of error messages or <code>null</code> if schema is ok
298      */
299     public static final List validateSchema(final Schema schema) {
300         List errors = new ArrayList();
301         String name = schema.getName();
302         if (name == null || "".equals(name)) {
303             errors.add("Schema has no name.");
304         }
305         List tables = schema.getTables();
306         if (tables.isEmpty()) {
307             errors.add("Schema " + name + " must have at least one table.");
308         }
309         for (int i = 0; i < tables.size(); i++) {
310             Table t = (Table) tables.get(i);
311             List tableErrors = validateTable(t);
312             if (tableErrors != null) {
313                 errors.addAll(tableErrors);
314             }
315         }
316         if (errors.isEmpty()) {
317             return null;
318         }
319         return errors;
320     }
321 
322     /***
323      * Validates table.
324      *
325      * @param table table to validate
326      *
327      * @return list of error messages or null if table is OK.
328      */
329     public static final List validateTable(final Table table) {
330         List errors = new ArrayList();
331         String name = table.getName();
332         if (name == null || "".equals(name)) {
333             errors.add("Table has no name!");
334         }
335         if (table.getKey() == null && table.getColumns().isEmpty()) {
336             errors.add(
337                 "Table " + name + " must have a key or at least one column.");
338         }
339         Key key = table.getKey();
340         if (key != null) {
341             List keyErrors = validateKey(key);
342             if (keyErrors != null) {
343                 errors.addAll(keyErrors);
344             }
345         }
346         List columns = table.getColumns();
347         for (int i = 0; i < columns.size(); i++) {
348             Column c = (Column) columns.get(i);
349             List columnErrors = validateColumn(c);
350             if (columnErrors != null) {
351                 errors.addAll(columnErrors);
352             }
353         }
354         if (errors.isEmpty()) {
355             return null;
356         }
357         return errors;
358     }
359     /***
360      * Validates key.
361      *
362      * @param key key to validate
363      *
364      * @return list of error messages or <code>null</code> is key is ok
365      */
366     public static final List validateKey(final Key key) {
367         List errors = new ArrayList();
368         if (key.getGenerator() == null) {
369             errors.add(
370                 "Primary key for table " + key.getTable().getName()
371                 + " has no generator");
372         }
373         if (errors.isEmpty()) {
374             return null;
375         }
376         return errors;
377     }
378 
379     /***
380      * Validates the column.
381      *
382      * @param column column to validate
383      *
384      * @return list of error messages or <code>null</code> if column is ok
385      */
386     public static final List validateColumn(final Column column) {
387         List errors = new ArrayList();
388         String name = column.getName();
389         if (name == null || "".equals(name)) {
390             errors.add("One column in table " + column.getTable().getName()
391                 + " has no name.");
392         }
393         if (column.getGenerator() == null) {
394             errors.add("Column " + name + " in table "
395              + column.getTable().getName() + " has no generator.");
396         }
397         if (errors.isEmpty()) {
398             return null;
399         }
400         return errors;
401     }
402 
403     /***
404      * Returns object's properties. A property is the one which has
405      * a public getter and setter and is not reserved DBMonster's property.
406      *
407      * @param object an schema element
408      * @return list of properties
409      */
410     public static final List getProperties(final Object object) {
411         List retList = new ArrayList();
412         try {
413             Class clazz = object.getClass();
414             Method[] methods = clazz.getMethods();
415             Map map = new HashMap();
416 
417             for (int i = 0; i < methods.length; i++) {
418                 Method m = methods[i];
419                 String mName = m.getName();
420                 if (mName.startsWith("get") || mName.startsWith("set")) {
421                     if (Modifier.isPublic(m.getModifiers())) {
422                         map.put(mName, mName);
423                     }
424                 }
425             }
426 
427             Iterator it = map.keySet().iterator();
428             while (it.hasNext()) {
429                 String name = (String) it.next();
430                 if (name.startsWith("get")) {
431                     String getter = name;
432                     String setter = "s" + getter.substring(1);
433                     String method = getter.substring(3);
434                     char ch = method.charAt(0);
435                     method = Character.toLowerCase(ch) + method.substring(1);
436                     if (map.containsKey(setter)) {
437                         if (!SchemaUtil.isHidden(object, method)) {
438                             retList.add(method);
439                         }
440                     }
441                 }
442             }
443         } catch (Exception e) {
444             System.err.println(e.getMessage());
445         }
446         Collections.sort(retList);
447         return retList;
448     }
449 
450     /***
451      * Checks if property is public and not excluded.
452      *
453      * @param object an object to check
454      * @param name property name
455      * @return <code>true</code> if property is hidden
456      */
457     public static final boolean isHidden(
458         final Object object, final String name) {
459 
460         String[] excl = null;
461         if (object instanceof Table) {
462             excl = (String[]) exclusions.get(SchemaUtil.TABLE);
463         } else if (object instanceof Key) {
464             excl = (String[]) exclusions.get(SchemaUtil.KEY);
465         } else if (object instanceof KeyGenerator) {
466             excl = (String[]) exclusions.get(SchemaUtil.KEY_GENERATOR);
467         } else if (object instanceof Column) {
468             excl = (String[]) exclusions.get(SchemaUtil.COLUMN);
469         } else {
470             excl = (String[]) exclusions.get(SchemaUtil.DATA_GENERATOR);
471         }
472 
473         for (int i = 0; i < excl.length; i++) {
474             if (name.equals(excl[i])) {
475                 return true;
476             }
477         }
478         return false;
479     }
480 
481     /***
482      * Dumps the schema to XML file.
483      *
484      * @param writer writer we are appending to
485      * @param schema schema to dump
486      *
487      * @throws Exception on errors
488      */
489     public static final void serializeSchema(
490         final Writer writer,
491         final Schema schema) throws Exception {
492         writer.write("<?xml version=\"1.0\"?>");
493         writer.write(CRLF);
494         writer.write("<!DOCTYPE dbmonster-schema PUBLIC \"");
495         writer.write(DTD);
496         writer.write("\" \"");
497         writer.write(DTD_URL);
498         writer.write("\">");
499         writer.write(CRLF);
500         writer.write("<dbmonster-schema>");
501         writer.write(CRLF);
502         writer.write("  <name>");
503         writer.write(schema.getName());
504         writer.write("</name>");
505         writer.write(CRLF);
506         Iterator it = schema.getTables().iterator();
507         while (it.hasNext()) {
508             Table table = (Table) it.next();
509             serializeTable(writer, table);
510         }
511         writer.write("</dbmonster-schema>");
512         writer.write(CRLF);
513         writer.flush();
514     }
515 
516     /***
517      * Dumps a table to XML representation.
518      *
519      * @param writer writer we are appengind to
520      * @param table table to serialize
521      *
522      * @throws Exception on errors
523      */
524     public static final void serializeTable(
525         final Writer writer,
526         final Table table) throws Exception {
527         writer.write("  <table name=\"");
528         writer.write(table.getName());
529         writer.write("\" rows=\"");
530         writer.write(String.valueOf(table.getRows()));
531         writer.write("\">");
532         writer.write(CRLF);
533         if (table.getKey() != null) {
534             serializeKey(writer, table.getKey());
535         }
536         Iterator it = table.getColumns().iterator();
537         while (it.hasNext()) {
538             Column column = (Column) it.next();
539             serializeColumn(writer, column);
540         }
541         writer.write("  </table>");
542         writer.write(CRLF);
543     }
544 
545     /***
546      * Dumps a key to XML representation.
547      *
548      * @param writer writer we are appending to
549      * @param key key to dump
550      *
551      * @throws Exception on errors
552      */
553     public static final void serializeKey(
554         final Writer writer,
555         final Key key) throws Exception {
556         writer.write("    <key databaseDefault=\"");
557         writer.write(String.valueOf(key.getDatabaseDefault()));
558         writer.write("\">");
559         writer.write(CRLF);
560         serializeGenerator(writer, key.getGenerator());
561         writer.write("    </key>");
562         writer.write(CRLF);
563     }
564 
565     /***
566      * Dumps a column to XML.
567      *
568      * @param writer writer we are appending to
569      * @param column column to dump
570      *
571      * @throws Exception on errors
572      */
573     public static final void serializeColumn(
574         final Writer writer,
575         final Column column) throws Exception {
576         writer.write("    <column name=\"");
577         writer.write(column.getName());
578         writer.write("\" databaseDefault=\"");
579         writer.write(String.valueOf(column.getDatabaseDefault()));
580         writer.write("\">");
581         writer.write(CRLF);
582         serializeGenerator(writer, column.getGenerator());
583         writer.write("    </column>");
584         writer.write(CRLF);
585     }
586 
587     /***
588      * Dumps a generator to XML.
589      *
590      * @param writer writter we are appending to
591      * @param generator generator
592      *
593      * @throws Exception on errors
594      */
595     public static final void serializeGenerator(
596         final Writer writer,
597         final Object generator) throws Exception {
598         writer.write("      <generator type=\"");
599         writer.write(generator.getClass().getName());
600         writer.write("\">");
601         writer.write(CRLF);
602         List properties = getProperties(generator);
603         Iterator it = properties.iterator();
604         while (it.hasNext()) {
605             String property = (String) it.next();
606             writer.write("        <property name=\"");
607             writer.write(property);
608             writer.write("\" value=\"");
609             String value = BeanUtils.getProperty(generator, property);
610             if (value != null) {
611                 writer.write(value);
612             }
613             writer.write("\"/>");
614             writer.write(CRLF);
615         }
616         writer.write("      </generator>");
617         writer.write(CRLF);
618     }
619 }
620 
621 /***
622  * Data generators factory.
623  */
624 final class GeneratorFactory extends AbstractObjectCreationFactory {
625     /***
626      * This method creates a data generator object.
627      *
628      * @param attributes SAX attributes
629      *
630      * @return created data generator
631      *
632      * @throws Exception if generator cannot be created
633      */
634     public final Object createObject(final Attributes attributes)
635         throws Exception {
636 
637         String className = attributes.getValue("type");
638         Class clazz = Class.forName(className);
639         Object o = clazz.newInstance();
640         return o;
641     }
642 }
643 
644 /***
645  * Key generators factory.
646  */
647 final class KeyGeneratorFactory extends AbstractObjectCreationFactory {
648     /***
649      * This method creates a key generator object.
650      *
651      * @param attributes SAX attributes
652      *
653      * @return created key generator
654      *
655      * @throws Exception if generator cannot be created
656      */
657     public final Object createObject(final Attributes attributes)
658         throws Exception {
659 
660         String className = attributes.getValue("type");
661         Class clazz = Class.forName(className);
662         Object o = clazz.newInstance();
663         return o;
664     }
665 }