source: trunk/fmgVen/src/com/fmguler/ven/support/LiquibaseConverter.java @ 36

Last change on this file since 36 was 36, checked in by fmguler, 12 years ago

Fixes #6 - Added support class LiquibaseConverter which generates liquibase changelog xml according to the given domain packages. It adds createTable tags with columns, and foreign key constraints. Note that generated xml might bee needed to changed, for the specific needs of the application.

Using this converter, developer can quickly create db schema from Java domain objects, and start working on business logic.

File size: 12.5 KB
Line 
1/*
2 *  fmgVen - A Convention over Configuration Java ORM Tool
3 *  Copyright 2011 Fatih Mehmet Güler
4 *
5 *  Licensed under the Apache License, Version 2.0 (the "License");
6 *  you may not use this file except in compliance with the License.
7 *  You may obtain a copy of the License at
8 *
9 *       http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 *  under the License.
17 */
18package com.fmguler.ven.support;
19
20import com.fmguler.ven.util.Convert;
21import java.beans.PropertyDescriptor;
22import java.io.File;
23import java.io.IOException;
24import java.net.URL;
25import java.util.ArrayList;
26import java.util.Date;
27import java.util.Enumeration;
28import java.util.HashMap;
29import java.util.HashSet;
30import java.util.Iterator;
31import java.util.LinkedList;
32import java.util.List;
33import java.util.Map;
34import java.util.Set;
35import org.springframework.beans.BeanWrapper;
36import org.springframework.beans.BeanWrapperImpl;
37
38/**
39 * Convert domain objects to Liquibase changeset xml
40 * @author Fatih Mehmet Güler
41 */
42public class LiquibaseConverter {
43    private Set domainPackages;
44    private Map dbClasses;
45    private List unhandledForeignKeyConstraints;
46    private Set processedTables;
47    private String author;
48    private String schema = "public";
49    private int changeSetId = 0;
50    private static final int COLUMN_TYPE_VARCHAR_1000 = 0;
51    private static final int COLUMN_TYPE_TEXT = 1;
52    private static final int COLUMN_TYPE_DATE = 2;
53    private static final int COLUMN_TYPE_BOOLEAN = 3;
54    private static final int COLUMN_TYPE_INT = 4;
55    private static final int COLUMN_TYPE_DOUBLE = 5;
56
57    public LiquibaseConverter() {
58        domainPackages = new HashSet();
59        //the predefined database classes;
60        dbClasses = new HashMap();
61        dbClasses.put(Integer.class, new Integer(COLUMN_TYPE_INT));
62        dbClasses.put(String.class, new Integer(COLUMN_TYPE_VARCHAR_1000));
63        dbClasses.put(Date.class, new Integer(COLUMN_TYPE_DATE));
64        dbClasses.put(Double.class, new Integer(COLUMN_TYPE_DOUBLE));
65        dbClasses.put(Boolean.class, new Integer(COLUMN_TYPE_BOOLEAN));
66        //implementation specific
67        unhandledForeignKeyConstraints = new LinkedList();
68        processedTables = new HashSet();
69    }
70
71    /**
72     * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
73     *
74     * @param packageName The base package
75     * @return The classes
76     * @throws ClassNotFoundException
77     * @throws IOException
78     */
79    private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException {
80        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
81        assert classLoader != null;
82        String path = packageName.replace('.', '/');
83        Enumeration resources = classLoader.getResources(path);
84        List dirs = new ArrayList();
85        while (resources.hasMoreElements()) {
86            URL resource = (URL)resources.nextElement();
87            dirs.add(new File(resource.getFile()));
88        }
89        ArrayList classes = new ArrayList();
90        Iterator it = dirs.iterator();
91        while (it.hasNext()) {
92            File directory = (File)it.next();
93            classes.addAll(findClasses(directory, packageName));
94        }
95
96        return (Class[])classes.toArray(new Class[classes.size()]);
97    }
98
99    /**
100     * Recursive method used to find all classes in a given directory and subdirs.
101     *
102     * @param directory   The base directory
103     * @param packageName The package name for classes found inside the base directory
104     * @return The classes
105     * @throws ClassNotFoundException
106     */
107    private static List findClasses(File directory, String packageName) throws ClassNotFoundException {
108        List classes = new ArrayList();
109        if (!directory.exists()) {
110            return classes;
111        }
112        File[] files = directory.listFiles();
113        for (int i = 0; i < files.length; i++) {
114            File file = files[i];
115            if (file.isDirectory()) {
116                assert file.getName().indexOf(".") != -1;
117                classes.addAll(findClasses(file, packageName + "." + file.getName()));
118            } else if (file.getName().endsWith(".class")) {
119                    classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
120                }
121        }
122        return classes;
123    }
124
125    /**
126     * Add the domain packages that will be considered persistent
127     * @param domainPackage the domain package
128     */
129    public void addDomainPackage(String domainPackage) {
130        domainPackages.add(domainPackage);
131    }
132
133    /**
134     * Set the author field in the changesets
135     */
136    public void setAuthor(String author) {
137        this.author = author;
138    }
139
140    /**
141     * Set the changeSetId start (default 0)
142     * @param changeSetId the id attribute in the changeset tag
143     */
144    public void setChangeSetIdStart(int changeSetId) {
145        this.changeSetId = changeSetId;
146    }
147
148    /**
149     * Set the schema to be added to liquibase tags
150     */
151    public void setSchema(String schema) {
152        this.schema = schema;
153    }
154
155    /**
156     * Convert the added domain packages to liquibase changeset xml
157     * @return changeset xml
158     */
159    public String convert() {
160        StringBuffer liquibaseXml = new StringBuffer();
161        Iterator it = domainPackages.iterator();
162        while (it.hasNext()) {
163            String domainPackage = (String)it.next();
164            try {
165                Class[] domainClasses = getClasses(domainPackage);
166                for (int i = 0; i < domainClasses.length; i++) {
167                    Class domainClass = domainClasses[i];
168                    convertClass(liquibaseXml, domainClass);
169                }
170            } catch (ClassNotFoundException ex) {
171                ex.printStackTrace();
172            } catch (IOException ex) {
173                ex.printStackTrace();
174            }
175        }
176
177        return liquibaseXml.toString();
178    }
179
180    //convert a single class
181    private void convertClass(StringBuffer liquibaseXml, Class domainClass) {
182        String objectName = Convert.toSimpleName(domainClass.getName());
183        String tableName = Convert.toDB(objectName);
184        startTagChangeSet(liquibaseXml, author, "" + (changeSetId++));
185        startTagCreateTable(liquibaseXml, schema, tableName);
186        addIdColumn(liquibaseXml, tableName);
187        List foreignKeyConstraints = new LinkedList();
188
189        BeanWrapper wr = new BeanWrapperImpl(domainClass);
190        PropertyDescriptor[] pdArr = wr.getPropertyDescriptors();
191
192        for (int i = 0; i < pdArr.length; i++) {
193            Class fieldClass = pdArr[i].getPropertyType(); //field class
194            String fieldName = pdArr[i].getName(); //field name
195            String columnName = Convert.toDB(pdArr[i].getName()); //column name
196
197            if (fieldName.equals("id")) {
198                continue;
199            }
200
201            //direct database class (Integer, String, Date, etc)
202            if (dbClasses.keySet().contains(fieldClass)) {
203                addPrimitiveColumn(liquibaseXml, columnName, ((Integer)dbClasses.get(fieldClass)).intValue());
204
205            }
206
207            //many to one association (object property)
208            if (fieldClass.getPackage() != null && domainPackages.contains(fieldClass.getPackage().getName())) {
209                addPrimitiveColumn(liquibaseXml, columnName + "_id", COLUMN_TYPE_INT);
210
211                //handle foreign key
212                String referencedTableName = Convert.toDB(Convert.toSimpleName(fieldClass.getName()));
213                Map fkey = new HashMap();
214                fkey.put("baseTableName", tableName);
215                fkey.put("baseColumnNames", columnName + "_id");
216                fkey.put("referencedTableName", referencedTableName);
217                if (processedTables.contains(referencedTableName)) foreignKeyConstraints.add(fkey);
218                else unhandledForeignKeyConstraints.add(fkey);
219            }
220
221        }
222        endTagCreateTable(liquibaseXml);
223        endTagChangeSet(liquibaseXml);
224
225        //mark table as processed
226        processedTables.add(tableName);
227        //add fkeys waiting for this table
228        notifyUnhandledForeignKeyConstraints(liquibaseXml, tableName);
229        //add fkeys not waiting for this table
230        Iterator it = foreignKeyConstraints.iterator();
231        while (it.hasNext()) {
232            Map fkey = (Map)it.next();
233            addForeignKeyConstraint(liquibaseXml, fkey);
234        }
235    }
236
237    //private methods
238    //--------------------------------------------------------------------------
239    //start changeset tag
240    private void startTagChangeSet(StringBuffer buffer, String author, String id) {
241        buffer.append("<changeSet author=\"").append(author).append("\" id=\"").append(id).append("\">\n");
242    }
243
244    //end changeset tag
245    private void endTagChangeSet(StringBuffer buffer) {
246        buffer.append("</changeSet>\n");
247    }
248
249    //start createtable tag
250    private void startTagCreateTable(StringBuffer buffer, String schema, String tableName) {
251        buffer.append("\t<createTable schemaName=\"").append(schema).append("\" tableName=\"").append(tableName).append("\">\n");
252    }
253
254    //end createtable tag
255    private void endTagCreateTable(StringBuffer buffer) {
256        buffer.append("\t</createTable>\n");
257    }
258
259    //id column tag
260    private void addIdColumn(StringBuffer buffer, String tableName) {
261        buffer.append("\t\t<column autoIncrement=\"true\" name=\"id\" type=\"serial\">\n");
262        buffer.append("\t\t\t<constraints nullable=\"false\" primaryKey=\"true\" primaryKeyName=\"").append(tableName).append("_pkey\"/>\n");
263        buffer.append("\t\t</column>\n");
264    }
265
266    //primitive column tag
267    private void addPrimitiveColumn(StringBuffer buffer, String columnName, int columnType) {
268        buffer.append("\t\t<column name=\"" + columnName + "\" type=\"" + getDataType(columnType) + "\"/>\n");
269    }
270
271    //foreign key constraint tag
272    private void addForeignKeyConstraint(StringBuffer buffer, Map fkey) {
273        startTagChangeSet(buffer, author, "" + (changeSetId++));
274        String baseTableName = (String)fkey.get("baseTableName");
275        String baseColumnNames = (String)fkey.get("baseColumnNames");
276        String referencedTableName = (String)fkey.get("referencedTableName");
277        String constraintName = baseTableName + "_" + baseColumnNames + "_fkey";
278        buffer.append("\t<addForeignKeyConstraint\n"
279                + "\t\tconstraintName=\"" + constraintName + "\"\n"
280                + "\t\tbaseTableSchemaName=\"" + schema + "\" baseTableName=\"" + baseTableName + "\" baseColumnNames=\"" + baseColumnNames + "\"\n"
281                + "\t\treferencedTableSchemaName=\"" + schema + "\" referencedTableName=\"" + referencedTableName + "\" referencedColumnNames=\"id\"\n"
282                + "\t\tdeferrable=\"false\" initiallyDeferred=\"false\" onDelete=\"CASCADE\" onUpdate=\"CASCADE\" />\n");
283        endTagChangeSet(buffer);
284    }
285
286    //handle fkeys waiting for a table
287    private void notifyUnhandledForeignKeyConstraints(StringBuffer buffer, String tableName) {
288        Iterator it = unhandledForeignKeyConstraints.iterator();
289        while (it.hasNext()) {
290            Map fkey = (Map)it.next();
291            if (fkey.get("referencedTableName").equals(tableName)) addForeignKeyConstraint(buffer, fkey);
292        }
293
294    }
295
296    private String getDataType(int columnType) {
297        //TODO: these are for postgresql, do it for all dbs, or make it generic.
298        switch (columnType) {
299            case COLUMN_TYPE_VARCHAR_1000:
300                return "VARCHAR(1000)";
301            case COLUMN_TYPE_TEXT:
302                return "TEXT(2147483647)";
303            case COLUMN_TYPE_DATE:
304                return "TIMESTAMP WITHOUT TIME ZONE"; //DATE? DATETIME? TIME?
305            case COLUMN_TYPE_BOOLEAN:
306                return "BOOLEAN";
307            case COLUMN_TYPE_INT:
308                return "int"; //BIGINT?
309            case COLUMN_TYPE_DOUBLE:
310                return "DOUBLE";
311        }
312        return "";
313    }
314}
Note: See TracBrowser for help on using the repository browser.