| 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 |  */ | 
|---|
| 18 | package com.fmguler.ven.support; | 
|---|
| 19 |  | 
|---|
| 20 | import com.fmguler.ven.util.Convert; | 
|---|
| 21 | import java.beans.PropertyDescriptor; | 
|---|
| 22 | import java.io.File; | 
|---|
| 23 | import java.io.IOException; | 
|---|
| 24 | import java.net.URL; | 
|---|
| 25 | import java.util.ArrayList; | 
|---|
| 26 | import java.util.Date; | 
|---|
| 27 | import java.util.Enumeration; | 
|---|
| 28 | import java.util.HashMap; | 
|---|
| 29 | import java.util.HashSet; | 
|---|
| 30 | import java.util.Iterator; | 
|---|
| 31 | import java.util.LinkedList; | 
|---|
| 32 | import java.util.List; | 
|---|
| 33 | import java.util.Map; | 
|---|
| 34 | import java.util.Set; | 
|---|
| 35 | import org.springframework.beans.BeanWrapper; | 
|---|
| 36 | import org.springframework.beans.BeanWrapperImpl; | 
|---|
| 37 |  | 
|---|
| 38 | /** | 
|---|
| 39 |  * Convert domain objects to Liquibase changeset xml | 
|---|
| 40 |  * @author Fatih Mehmet Güler | 
|---|
| 41 |  */ | 
|---|
| 42 | public 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 | } | 
|---|