| 1 | /** |
| 2 | * Copyright 2009 ATG DUST Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * |
| 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | * |
| 9 | * Unless required by applicable law or agreed to in writing, software |
| 10 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | * See the License for the specific language governing permissions and limitations under the License. |
| 13 | */ |
| 14 | package atg.adapter.gsa; |
| 15 | |
| 16 | import java.sql.Connection; |
| 17 | import java.sql.SQLException; |
| 18 | import java.util.ArrayList; |
| 19 | import java.util.Iterator; |
| 20 | import java.util.List; |
| 21 | |
| 22 | |
| 23 | import org.apache.ddlutils.DatabaseOperationException; |
| 24 | import org.apache.ddlutils.Platform; |
| 25 | import org.apache.ddlutils.PlatformFactory; |
| 26 | import org.apache.ddlutils.model.Column; |
| 27 | import org.apache.ddlutils.model.Database; |
| 28 | import org.apache.ddlutils.model.ForeignKey; |
| 29 | import org.apache.ddlutils.model.IndexColumn; |
| 30 | import org.apache.ddlutils.model.Reference; |
| 31 | import org.apache.ddlutils.model.UniqueIndex; |
| 32 | |
| 33 | import atg.repository.RepositoryException; |
| 34 | |
| 35 | /** |
| 36 | * This class is used to generate drop and alter a database schema required for |
| 37 | * a given repository. It uses the Apache DDLUtils tools for the actual schema |
| 38 | * manipulation. To use the class first initialize it's "model" by passing a |
| 39 | * GSARepository to the constructor. Afterwards you may invoke the action |
| 40 | * methods such as: |
| 41 | * <ul> |
| 42 | * <li>createSchema - Creates schema including constraints. |
| 43 | * <li>dropSchema - Drops the schema including constraints. |
| 44 | * <li>alterSchema - Attempts to alter an existing schema into the one currently |
| 45 | * required for the given repository. |
| 46 | * </ul> |
| 47 | * These methods affect the DataSource used by the given GSARepository. If that |
| 48 | * DataSource is not accessible then these methods will fail. Schema |
| 49 | * modification may continue or fail on error. Set the <code>strict</code> |
| 50 | * property to true to always fail on error. The default is to continue on |
| 51 | * error. |
| 52 | * |
| 53 | * @version $Id: //test/UnitTests/base/main/src/Java/atg/adapter/gsa/ |
| 54 | * GSARepositorySchemaGenerator.java#1 $ |
| 55 | * @author adamb |
| 56 | */ |
| 57 | public class GSARepositorySchemaGenerator { |
| 58 | |
| 59 | // The repository upon which we are working. |
| 60 | public GSARepository mRepository = null; |
| 61 | // The DDLUtils Platform object |
| 62 | public Platform mPlatform = null; |
| 63 | // The DDLUtils Database Model |
| 64 | public Database mDatabase = null; |
| 65 | // Tool for mapping database types |
| 66 | public DatabaseTypeNameToJDBC mDatabaseTypeNameToJDBC = null; |
| 67 | |
| 68 | // ----------------------------- |
| 69 | /** |
| 70 | * Creates a new GSARepositorySchemaGenerator and initializes it with a model |
| 71 | * based upon the given repository. |
| 72 | * |
| 73 | * @param pRepository |
| 74 | * @throws RepositoryException |
| 75 | */ |
| 76 | public GSARepositorySchemaGenerator(GSARepository pRepository) |
| 77 | throws RepositoryException { |
| 78 | buildModel(pRepository); |
| 79 | } |
| 80 | |
| 81 | // ----------------------------- |
| 82 | /** |
| 83 | * Initialize this class with a model for the given repository. Any previous |
| 84 | * model will be discarded. |
| 85 | * |
| 86 | * @param pRepository |
| 87 | * @throws RepositoryException |
| 88 | */ |
| 89 | public void buildModel(GSARepository pRepository) throws RepositoryException { |
| 90 | mDatabaseTypeNameToJDBC = new DatabaseTypeNameToJDBC(pRepository |
| 91 | .getDatabaseTableInfo()); |
| 92 | mRepository = pRepository; |
| 93 | mPlatform = PlatformFactory.createNewPlatformInstance(pRepository |
| 94 | .getDataSource()); |
| 95 | mDatabase = new Database(); |
| 96 | mDatabase.setName(pRepository.getAbsoluteName()); |
| 97 | mDatabase.setVersion("1.0"); |
| 98 | String[] names = pRepository.getItemDescriptorNames(); |
| 99 | for (String name : names) { |
| 100 | GSAItemDescriptor desc = (GSAItemDescriptor) pRepository |
| 101 | .getItemDescriptor(name); |
| 102 | Table[] tables = desc.getTables(); |
| 103 | // first do primary tables |
| 104 | processTables(pRepository, tables, true); |
| 105 | } |
| 106 | for (String name : names) { |
| 107 | GSAItemDescriptor desc = (GSAItemDescriptor) pRepository |
| 108 | .getItemDescriptor(name); |
| 109 | Table[] tables = desc.getTables(); |
| 110 | // then do the rest |
| 111 | desc.getPrimaryTable(); |
| 112 | processTables(pRepository, tables, false); |
| 113 | } |
| 114 | |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Walks the tables of this repository building up a DDLUtils model. |
| 119 | * |
| 120 | * @param pRepository |
| 121 | * @param tables |
| 122 | * @param pPrimary |
| 123 | * - if True only processes primary tables |
| 124 | * @throws RepositoryException |
| 125 | */ |
| 126 | private void processTables(GSARepository pRepository, Table[] tables, |
| 127 | boolean pPrimary) throws RepositoryException { |
| 128 | for (Table table : tables) { |
| 129 | if (!table.isInherited() && (table.isPrimaryTable() == pPrimary)) { |
| 130 | // track tables here. if we have multiple repositories |
| 131 | // using the same table we don't want to double create. |
| 132 | // actually the problem is more a single |
| 133 | // repository that is reusing a table for multiple |
| 134 | // purposes |
| 135 | |
| 136 | List<GSARepository> repositoriesUsingTable = SchemaTracker |
| 137 | .getSchemaTracker().getTableToRepository().get(table.getName()); |
| 138 | if (repositoriesUsingTable != null |
| 139 | && repositoriesUsingTable.contains(pRepository)) { |
| 140 | pRepository.logWarning("Table " + table.getName() |
| 141 | + " already defined by repository " |
| 142 | + repositoriesUsingTable.toString() |
| 143 | + " skipping schema creation for this table. multi=" |
| 144 | + table.isMultiTable() + " auxiliary=" + table.isAuxiliaryTable() |
| 145 | + " primary=" + table.isPrimaryTable()); |
| 146 | } else { |
| 147 | // Only add the model if we have never seen this table created |
| 148 | buildSingleTableModel(mDatabase, table, pRepository); |
| 149 | repositoriesUsingTable = new ArrayList<GSARepository>(); |
| 150 | repositoriesUsingTable.add(pRepository); |
| 151 | } |
| 152 | SchemaTracker.getSchemaTracker().getTableToRepository().put( |
| 153 | table.getName(), repositoriesUsingTable); |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | // ----------------------------- |
| 159 | /** |
| 160 | * Adds the definition of the given table to the current DDLUtils database |
| 161 | * model. |
| 162 | * |
| 163 | * @param pDb |
| 164 | * @param pTable |
| 165 | * @param pRepository |
| 166 | * @throws RepositoryException |
| 167 | */ |
| 168 | void buildSingleTableModel(Database pDb, Table pTable, |
| 169 | GSARepository pRepository) throws RepositoryException { |
| 170 | TableColumns columns = new TableColumns(pTable, pRepository |
| 171 | .getDatabaseTableInfo()); |
| 172 | pTable.collectColumnsForName(columns); |
| 173 | AccessibleTableColumns atable = new AccessibleTableColumns(columns); |
| 174 | |
| 175 | // -------------------------- |
| 176 | // Table Definition |
| 177 | // -------------------------- |
| 178 | org.apache.ddlutils.model.Table t = new org.apache.ddlutils.model.Table(); |
| 179 | t.setName(pTable.getName()); |
| 180 | pDb.addTable(t); |
| 181 | |
| 182 | // -------------------------- |
| 183 | // Add Columns |
| 184 | // -------------------------- |
| 185 | |
| 186 | ColumnDefinitionNode columnDefinition = null; |
| 187 | boolean proceed = false; |
| 188 | |
| 189 | for (columnDefinition = atable.getHead(), proceed = true; columnDefinition != null |
| 190 | && proceed; columnDefinition = columnDefinition.mNext) { |
| 191 | // No need to iterate the next time if there is just one element in the |
| 192 | // linked list |
| 193 | if (atable.getHead() == atable.getTail()) |
| 194 | proceed = false; |
| 195 | |
| 196 | Column c = new Column(); |
| 197 | |
| 198 | // -------------------------- |
| 199 | // Column Name |
| 200 | // -------------------------- |
| 201 | |
| 202 | c.setName(columnDefinition.mColumnName); |
| 203 | t.addColumn(c); |
| 204 | |
| 205 | // -------------------------- |
| 206 | // Column Type |
| 207 | // -------------------------- |
| 208 | |
| 209 | setupColumnType(pRepository, columnDefinition, c); |
| 210 | |
| 211 | // -------------------------- |
| 212 | // Primary Key |
| 213 | // -------------------------- |
| 214 | |
| 215 | if (atable.getPrimaryKeys().contains(c.getName()) |
| 216 | || c.getName().equals(atable.getMultiColumnName())) { |
| 217 | c.setPrimaryKey(true); |
| 218 | } |
| 219 | |
| 220 | // -------------------------- |
| 221 | // Null/NotNull |
| 222 | // -------------------------- |
| 223 | |
| 224 | if (columnDefinition.mIsRequired |
| 225 | || atable.getPrimaryKeys().contains(columnDefinition.mColumnName)) { |
| 226 | c.setRequired(true); |
| 227 | } else { |
| 228 | c.setRequired(false); |
| 229 | } |
| 230 | |
| 231 | // -------------------------- |
| 232 | // Unique Index |
| 233 | // DDLUtils doesn't yet to UNIQUE constraints.. Hmph |
| 234 | // -------------------------- |
| 235 | |
| 236 | if (columnDefinition.mIsUnique) { |
| 237 | UniqueIndex uniqueIndex = new UniqueIndex(); |
| 238 | uniqueIndex.setName("uidx_" + t.getName() + "_" + c.getName()); |
| 239 | uniqueIndex.addColumn(new IndexColumn(c)); |
| 240 | t.addIndex(uniqueIndex); |
| 241 | } |
| 242 | |
| 243 | // -------------------------- |
| 244 | // References Constraint |
| 245 | // -------------------------- |
| 246 | |
| 247 | if (columnDefinition.mReferenced != null && !columns.mVersioned) { |
| 248 | |
| 249 | ForeignKey foreignKey = new ForeignKey(); |
| 250 | Reference reference = new Reference(); |
| 251 | String referencedTableName = columnDefinition.mReferenced.substring(0, |
| 252 | columnDefinition.mReferenced.indexOf("(")); |
| 253 | String referencedColumnName = columnDefinition.mReferenced.substring( |
| 254 | columnDefinition.mReferenced.indexOf("(") + 1, |
| 255 | columnDefinition.mReferenced.indexOf(")")); |
| 256 | org.apache.ddlutils.model.Table referencedTable = pDb |
| 257 | .findTable(referencedTableName); |
| 258 | foreignKey.setName(t.getName() + c.getName() + "FK" |
| 259 | + referencedTableName + referencedColumnName); |
| 260 | if (referencedTable != null) { |
| 261 | Column referencedColumn = referencedTable |
| 262 | .findColumn(referencedColumnName); |
| 263 | if (referencedTable.getName().equals(t.getName()) |
| 264 | && referencedColumn.getName().equals(c.getName())) { |
| 265 | pRepository |
| 266 | .logWarning("Skipping foreign key constraint, table and column are the same. Table.Column=" |
| 267 | + referencedTableName + "." + referencedColumnName); |
| 268 | } else { |
| 269 | reference.setForeignColumn(referencedColumn); |
| 270 | reference.setLocalColumn(c); |
| 271 | foreignKey.addReference(reference); |
| 272 | foreignKey.setForeignTable(referencedTable); |
| 273 | t.addForeignKey(foreignKey); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | // -------------------------- |
| 278 | // Foreign Keys |
| 279 | // -------------------------- |
| 280 | |
| 281 | if (atable.getForeignKeys() != null && !columns.mVersioned) { |
| 282 | // TODO: Add ForeignKeys |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | } |
| 288 | |
| 289 | /** |
| 290 | * Determines the appropriate jdbc type for the given ColumnDefinitionNode and |
| 291 | * sets that in Column "c". |
| 292 | * |
| 293 | * @param pRepository |
| 294 | * @param columnDefinition |
| 295 | * @param c |
| 296 | */ |
| 297 | void setupColumnType(GSARepository pRepository, |
| 298 | ColumnDefinitionNode columnDefinition, Column c) { |
| 299 | c.setDescription(columnDefinition.mDataTypeString); |
| 300 | String typeName = null; |
| 301 | String size = null; |
| 302 | if (columnDefinition.mDataTypeString.contains("(")) { |
| 303 | typeName = columnDefinition.mDataTypeString.substring(0, |
| 304 | columnDefinition.mDataTypeString.indexOf("(")); |
| 305 | size = columnDefinition.mDataTypeString.substring( |
| 306 | columnDefinition.mDataTypeString.indexOf("(") + 1, |
| 307 | columnDefinition.mDataTypeString.indexOf(")")); |
| 308 | } else { |
| 309 | typeName = columnDefinition.mDataTypeString; |
| 310 | } |
| 311 | |
| 312 | String precision = null; |
| 313 | String scale = null; |
| 314 | if (size != null) { |
| 315 | if (size.contains(",")) { |
| 316 | precision = size.substring(0, size.indexOf(",")); |
| 317 | scale = size.substring(size.indexOf(",") + 1, size.length()); |
| 318 | c.setPrecisionRadix(Integer.parseInt(precision.trim())); |
| 319 | c.setScale(Integer.parseInt(scale.trim())); |
| 320 | } else { |
| 321 | c.setSize(size); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | c.setTypeCode(mDatabaseTypeNameToJDBC.databaseTypeNametoJDBCType(typeName)); |
| 326 | } |
| 327 | |
| 328 | // ----------------------------- |
| 329 | /** |
| 330 | * Creates the schema based on the current model. If no model has been |
| 331 | * created, this method throws a NoModelException. |
| 332 | * |
| 333 | * @param pContinueOnError |
| 334 | * - If true, continue on error, else fail. |
| 335 | * @param pDrop |
| 336 | * - If true, drops schema first before attempting to create it. |
| 337 | * @throws SQLException |
| 338 | * @throws DatabaseOperationException |
| 339 | */ |
| 340 | public void createSchema(final boolean pContinueOnError, final boolean pDrop) |
| 341 | throws DatabaseOperationException, SQLException { |
| 342 | boolean success = new DoInAutoCommit(this, mRepository) |
| 343 | .doInAutoCommit(new AutoCommitable() { |
| 344 | @Override |
| 345 | public void doInAutoCommit(Connection pConnection) { |
| 346 | mPlatform.createTables(pConnection, mDatabase, pDrop, |
| 347 | pContinueOnError); |
| 348 | } |
| 349 | }); |
| 350 | if (!success) |
| 351 | throw new DatabaseOperationException("Failed to create tables."); |
| 352 | } |
| 353 | |
| 354 | // ----------------------------- |
| 355 | /** |
| 356 | * Drops the schema based on the current model. If no model has been created, |
| 357 | * this method throws a NoModelException. |
| 358 | * |
| 359 | * @param pContinueOnError |
| 360 | * - If true, continue on error, else fail. |
| 361 | * @throws SQLException |
| 362 | * @throws DatabaseOperationException |
| 363 | */ |
| 364 | public void dropSchema(final boolean pContinueOnError) |
| 365 | throws DatabaseOperationException, SQLException { |
| 366 | boolean success = new DoInAutoCommit(this, mRepository) |
| 367 | .doInAutoCommit(new AutoCommitable() { |
| 368 | @Override |
| 369 | public void doInAutoCommit(Connection pConnection) { |
| 370 | mPlatform.dropTables(pConnection, mDatabase, pContinueOnError); |
| 371 | } |
| 372 | }); |
| 373 | if (!success) |
| 374 | throw new DatabaseOperationException("Failed to drop tables."); |
| 375 | } |
| 376 | |
| 377 | // ----------------------------- |
| 378 | /** |
| 379 | * Alters the schema based on the current model. If no model has been created, |
| 380 | * this method throws a NoModelException. This method attempts to preserve the |
| 381 | * data in the target database. |
| 382 | * |
| 383 | * @param pContinueOnError |
| 384 | * - If true, fail on error, else continue on error. |
| 385 | * @throws SQLException |
| 386 | * @throws DatabaseOperationException |
| 387 | */ |
| 388 | public void alterSchema(final boolean pContinueOnError) |
| 389 | throws DatabaseOperationException, SQLException { |
| 390 | mPlatform.alterTables(mRepository.getConnection(), mDatabase, |
| 391 | pContinueOnError); |
| 392 | boolean success = new DoInAutoCommit(this, mRepository) |
| 393 | .doInAutoCommit(new AutoCommitable() { |
| 394 | @Override |
| 395 | public void doInAutoCommit(Connection pConnection) { |
| 396 | mPlatform.alterTables(pConnection, mDatabase, pContinueOnError); |
| 397 | } |
| 398 | }); |
| 399 | if (!success) |
| 400 | throw new DatabaseOperationException("Failed to alter tables."); |
| 401 | } |
| 402 | } |