[36] | 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 | } |
---|