001: package liquibase.diff;
002:
003: import liquibase.change.*;
004: import liquibase.database.Database;
005: import liquibase.database.structure.*;
006: import liquibase.exception.JDBCException;
007: import liquibase.parser.LiquibaseSchemaResolver;
008: import liquibase.parser.xml.XMLChangeLogParser;
009: import liquibase.xml.DefaultXmlWriter;
010: import liquibase.xml.XmlWriter;
011: import org.w3c.dom.Document;
012: import org.w3c.dom.Element;
013:
014: import javax.xml.parsers.DocumentBuilder;
015: import javax.xml.parsers.DocumentBuilderFactory;
016: import javax.xml.parsers.ParserConfigurationException;
017: import java.io.IOException;
018: import java.io.PrintStream;
019: import java.util.*;
020:
021: public class DiffResult {
022:
023: private Long baseId = new Date().getTime();
024: private int changeNumber = 1;
025:
026: private Database baseDatabase;
027: private Database targetDatabase;
028:
029: private DatabaseSnapshot baseSnapshot;
030: private DatabaseSnapshot targetSnapshot;
031:
032: private DiffComparison productName;
033: private DiffComparison productVersion;
034:
035: private SortedSet<Table> missingTables = new TreeSet<Table>();
036: private SortedSet<Table> unexpectedTables = new TreeSet<Table>();
037:
038: private SortedSet<View> missingViews = new TreeSet<View>();
039: private SortedSet<View> unexpectedViews = new TreeSet<View>();
040:
041: private SortedSet<Column> missingColumns = new TreeSet<Column>();
042: private SortedSet<Column> unexpectedColumns = new TreeSet<Column>();
043: private SortedSet<Column> changedColumns = new TreeSet<Column>();
044:
045: private SortedSet<ForeignKey> missingForeignKeys = new TreeSet<ForeignKey>();
046: private SortedSet<ForeignKey> unexpectedForeignKeys = new TreeSet<ForeignKey>();
047:
048: private SortedSet<Index> missingIndexes = new TreeSet<Index>();
049: private SortedSet<Index> unexpectedIndexes = new TreeSet<Index>();
050:
051: private SortedSet<PrimaryKey> missingPrimaryKeys = new TreeSet<PrimaryKey>();
052: private SortedSet<PrimaryKey> unexpectedPrimaryKeys = new TreeSet<PrimaryKey>();
053:
054: private SortedSet<Sequence> missingSequences = new TreeSet<Sequence>();
055: private SortedSet<Sequence> unexpectedSequences = new TreeSet<Sequence>();
056:
057: public DiffResult(DatabaseSnapshot baseDatabase,
058: DatabaseSnapshot targetDatabase) {
059: this .baseDatabase = baseDatabase.getDatabase();
060: this .targetDatabase = targetDatabase.getDatabase();
061:
062: this .baseSnapshot = baseDatabase;
063: this .targetSnapshot = targetDatabase;
064: }
065:
066: public DiffComparison getProductName() {
067: return productName;
068: }
069:
070: public void setProductName(DiffComparison productName) {
071: this .productName = productName;
072: }
073:
074: public DiffComparison getProductVersion() {
075: return productVersion;
076: }
077:
078: public void setProductVersion(DiffComparison product) {
079: this .productVersion = product;
080: }
081:
082: public void addMissingTable(Table table) {
083: missingTables.add(table);
084: }
085:
086: public SortedSet<Table> getMissingTables() {
087: return missingTables;
088: }
089:
090: public void addUnexpectedTable(Table table) {
091: unexpectedTables.add(table);
092: }
093:
094: public SortedSet<Table> getUnexpectedTables() {
095: return unexpectedTables;
096: }
097:
098: public void addMissingView(View viewName) {
099: missingViews.add(viewName);
100: }
101:
102: public SortedSet<View> getMissingViews() {
103: return missingViews;
104: }
105:
106: public void addUnexpectedView(View viewName) {
107: unexpectedViews.add(viewName);
108: }
109:
110: public SortedSet<View> getUnexpectedViews() {
111: return unexpectedViews;
112: }
113:
114: public void addMissingColumn(Column columnName) {
115: missingColumns.add(columnName);
116: }
117:
118: public SortedSet<Column> getMissingColumns() {
119: return missingColumns;
120: }
121:
122: public void addUnexpectedColumn(Column columnName) {
123: unexpectedColumns.add(columnName);
124: }
125:
126: public SortedSet<Column> getUnexpectedColumns() {
127: return unexpectedColumns;
128: }
129:
130: public void addChangedColumn(Column columnName) {
131: changedColumns.add(columnName);
132: }
133:
134: public SortedSet<Column> getChangedColumns() {
135: return changedColumns;
136: }
137:
138: public void addMissingForeignKey(ForeignKey fkName) {
139: missingForeignKeys.add(fkName);
140: }
141:
142: public SortedSet<ForeignKey> getMissingForeignKeys() {
143: return missingForeignKeys;
144: }
145:
146: public void addUnexpectedForeignKey(ForeignKey fkName) {
147: unexpectedForeignKeys.add(fkName);
148: }
149:
150: public SortedSet<ForeignKey> getUnexpectedForeignKeys() {
151: return unexpectedForeignKeys;
152: }
153:
154: public void addMissingIndex(Index fkName) {
155: missingIndexes.add(fkName);
156: }
157:
158: public SortedSet<Index> getMissingIndexes() {
159: return missingIndexes;
160: }
161:
162: public void addUnexpectedIndex(Index fkName) {
163: unexpectedIndexes.add(fkName);
164: }
165:
166: public SortedSet<Index> getUnexpectedIndexes() {
167: return unexpectedIndexes;
168: }
169:
170: public void addMissingPrimaryKey(PrimaryKey primaryKey) {
171: missingPrimaryKeys.add(primaryKey);
172: }
173:
174: public SortedSet<PrimaryKey> getMissingPrimaryKeys() {
175: return missingPrimaryKeys;
176: }
177:
178: public void addUnexpectedPrimaryKey(PrimaryKey primaryKey) {
179: unexpectedPrimaryKeys.add(primaryKey);
180: }
181:
182: public SortedSet<PrimaryKey> getUnexpectedPrimaryKeys() {
183: return unexpectedPrimaryKeys;
184: }
185:
186: public void addMissingSequence(Sequence sequence) {
187: missingSequences.add(sequence);
188: }
189:
190: public SortedSet<Sequence> getMissingSequences() {
191: return missingSequences;
192: }
193:
194: public void addUnexpectedSequence(Sequence sequence) {
195: unexpectedSequences.add(sequence);
196: }
197:
198: public SortedSet<Sequence> getUnexpectedSequences() {
199: return unexpectedSequences;
200: }
201:
202: public void printResult(PrintStream out) throws JDBCException {
203: out.println("Base Database: "
204: + targetDatabase.getConnectionUsername() + " "
205: + targetDatabase.getConnectionURL());
206: out.println("Target Database: "
207: + baseDatabase.getConnectionUsername() + " "
208: + baseDatabase.getConnectionURL());
209:
210: printComparision("Product Name", productName, out);
211: printComparision("Product Version", productVersion, out);
212: printSetComparison("Missing Tables", getMissingTables(), out);
213: printSetComparison("Unexpected Tables", getUnexpectedTables(),
214: out);
215: printSetComparison("Missing Views", getMissingViews(), out);
216: printSetComparison("Unexpected Views", getUnexpectedViews(),
217: out);
218: printSetComparison("Missing Columns", getMissingColumns(), out);
219: printSetComparison("Unexpected Columns",
220: getUnexpectedColumns(), out);
221: printColumnComparison(getChangedColumns(), out);
222: printSetComparison("Missing Foreign Keys",
223: getMissingForeignKeys(), out);
224: printSetComparison("Unexpected Foreign Keys",
225: getUnexpectedForeignKeys(), out);
226: printSetComparison("Missing Primary Keys",
227: getMissingPrimaryKeys(), out);
228: printSetComparison("Unexpected Primary Keys",
229: getUnexpectedPrimaryKeys(), out);
230: printSetComparison("Missing Indexes", getMissingIndexes(), out);
231: printSetComparison("Unexpected Indexes",
232: getUnexpectedIndexes(), out);
233: printSetComparison("Missing Sequences", getMissingSequences(),
234: out);
235: printSetComparison("Unexpected Sequences",
236: getUnexpectedSequences(), out);
237: }
238:
239: private void printSetComparison(String title, SortedSet<?> objects,
240: PrintStream out) {
241: out.print(title + ": ");
242: if (objects.size() == 0) {
243: out.println("NONE");
244: } else {
245: out.println();
246: for (Object object : objects) {
247: out.println(" " + object);
248: }
249: }
250: }
251:
252: private void printColumnComparison(
253: SortedSet<Column> changedColumns, PrintStream out) {
254: out.print("Changed Columns: ");
255: if (changedColumns.size() == 0) {
256: out.println("NONE");
257: } else {
258: out.println();
259: for (Column column : changedColumns) {
260: out.println(" " + column);
261: Column baseColumn = baseSnapshot.getColumn(column);
262: if (baseColumn.isDataTypeDifferent(column)) {
263: out.println(" from "
264: + baseColumn
265: .getDataTypeString(baseDatabase)
266: + " to "
267: + targetSnapshot.getColumn(column)
268: .getDataTypeString(targetDatabase));
269: }
270: if (baseColumn.isNullabilityDifferent(column)) {
271: Boolean nowNullable = targetSnapshot.getColumn(
272: column).isNullable();
273: if (nowNullable == null) {
274: nowNullable = Boolean.TRUE;
275: }
276: if (nowNullable) {
277: out.println(" now nullable");
278: } else {
279: out.println(" now not null");
280: }
281: }
282: }
283: }
284: }
285:
286: private void printComparision(String title,
287: DiffComparison comparison, PrintStream out) {
288: out.print(title + ":");
289: if (comparison.areTheSame()) {
290: out.println(" EQUAL");
291: } else {
292: out.println();
293: out.println(" Base: '" + comparison.getBaseVersion()
294: + "'");
295: out.println(" Target: '"
296: + comparison.getTargetVersion() + "'");
297: }
298:
299: }
300:
301: public void printChangeLog(PrintStream out, Database targetDatabase)
302: throws ParserConfigurationException, IOException {
303: this
304: .printChangeLog(out, targetDatabase,
305: new DefaultXmlWriter());
306: }
307:
308: /**
309: * Prints changeLog that would bring the base database to be the same as the target database
310: */
311: public void printChangeLog(PrintStream out,
312: Database targetDatabase, XmlWriter xmlWriter)
313: throws ParserConfigurationException, IOException {
314: DocumentBuilderFactory factory = DocumentBuilderFactory
315: .newInstance();
316: DocumentBuilder documentBuilder = factory.newDocumentBuilder();
317: documentBuilder
318: .setEntityResolver(new LiquibaseSchemaResolver());
319:
320: Document doc = documentBuilder.newDocument();
321:
322: Element changeLogElement = doc
323: .createElement("databaseChangeLog");
324: changeLogElement.setAttribute("xmlns",
325: "http://www.liquibase.org/xml/ns/dbchangelog/"
326: + XMLChangeLogParser.getSchemaVersion());
327: changeLogElement.setAttribute("xmlns:xsi",
328: "http://www.w3.org/2001/XMLSchema-instance");
329: changeLogElement
330: .setAttribute(
331: "xsi:schemaLocation",
332: "http://www.liquibase.org/xml/ns/dbchangelog/"
333: + XMLChangeLogParser.getSchemaVersion()
334: + " http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-"
335: + XMLChangeLogParser.getSchemaVersion()
336: + ".xsd");
337:
338: doc.appendChild(changeLogElement);
339:
340: List<Change> changes = new ArrayList<Change>();
341: addMissingTableChanges(changes, targetDatabase);
342: addUnexpectedTableChanges(changes);
343: addMissingColumnChanges(changes, targetDatabase);
344: addUnexpectedColumnChanges(changes);
345: addChangedColumnChanges(changes);
346: addMissingPrimaryKeyChanges(changes);
347: addUnexpectedPrimaryKeyChanges(changes);
348: addMissingIndexChanges(changes);
349: addUnexpectedIndexChanges(changes);
350: addMissingForeignKeyChanges(changes);
351: addUnexpectedForeignKeyChanges(changes);
352: addMissingSequenceChanges(changes);
353: addUnexpectedSequenceChanges(changes);
354: addMissingViewChanges(changes);
355: addUnexpectedViewChanges(changes);
356:
357: for (Change change : changes) {
358: Element changeSet = doc.createElement("changeSet");
359: changeSet.setAttribute("author", "diff-generated");
360: changeSet.setAttribute("id", generateId());
361:
362: changeSet.appendChild(change.createNode(doc));
363: doc.getDocumentElement().appendChild(changeSet);
364: }
365:
366: xmlWriter.write(doc, out);
367:
368: out.flush();
369: }
370:
371: private String generateId() {
372: return baseId.toString() + "-" + changeNumber++;
373: }
374:
375: private void addUnexpectedIndexChanges(List<Change> changes) {
376: for (Index index : getUnexpectedIndexes()) {
377:
378: DropIndexChange change = new DropIndexChange();
379: change.setTableName(index.getTable().getName());
380: change.setIndexName(index.getName());
381:
382: changes.add(change);
383: }
384: }
385:
386: private void addMissingIndexChanges(List<Change> changes) {
387: for (Index index : getMissingIndexes()) {
388:
389: CreateIndexChange change = new CreateIndexChange();
390: change.setTableName(index.getTable().getName());
391: change.setIndexName(index.getName());
392:
393: for (String columnName : index.getColumns()) {
394: ColumnConfig column = new ColumnConfig();
395: column.setName(columnName);
396: change.addColumn(column);
397: }
398: changes.add(change);
399: }
400: }
401:
402: private void addUnexpectedPrimaryKeyChanges(List<Change> changes) {
403: for (PrimaryKey pk : getUnexpectedPrimaryKeys()) {
404:
405: DropPrimaryKeyChange change = new DropPrimaryKeyChange();
406: change.setTableName(pk.getTable().getName());
407: change.setConstraintName(pk.getName());
408:
409: changes.add(change);
410: }
411: }
412:
413: private void addMissingPrimaryKeyChanges(List<Change> changes) {
414: for (PrimaryKey pk : getMissingPrimaryKeys()) {
415:
416: AddPrimaryKeyChange change = new AddPrimaryKeyChange();
417: change.setTableName(pk.getTable().getName());
418: change.setConstraintName(pk.getName());
419: change.setColumnNames(pk.getColumnNames());
420:
421: changes.add(change);
422: }
423: }
424:
425: private void addUnexpectedForeignKeyChanges(List<Change> changes) {
426: for (ForeignKey fk : getUnexpectedForeignKeys()) {
427:
428: DropForeignKeyConstraintChange change = new DropForeignKeyConstraintChange();
429: change.setConstraintName(fk.getName());
430: change.setBaseTableName(fk.getPrimaryKeyTable().getName());
431:
432: changes.add(change);
433: }
434: }
435:
436: private void addMissingForeignKeyChanges(List<Change> changes) {
437: for (ForeignKey fk : getMissingForeignKeys()) {
438:
439: AddForeignKeyConstraintChange change = new AddForeignKeyConstraintChange();
440: change.setConstraintName(fk.getName());
441:
442: change.setReferencedTableName(fk.getPrimaryKeyTable()
443: .getName());
444: change.setReferencedColumnNames(fk.getPrimaryKeyColumns());
445:
446: change.setBaseTableName(fk.getForeignKeyTable().getName());
447: change.setBaseColumnNames(fk.getForeignKeyColumns());
448:
449: change.setDeferrable(fk.isDeferrable());
450: change.setInitiallyDeferred(fk.isInitiallyDeferred());
451:
452: changes.add(change);
453: }
454: }
455:
456: private void addUnexpectedSequenceChanges(List<Change> changes) {
457: for (Sequence sequence : getUnexpectedSequences()) {
458:
459: DropSequenceChange change = new DropSequenceChange();
460: change.setSequenceName(sequence.getName());
461:
462: changes.add(change);
463: }
464: }
465:
466: private void addMissingSequenceChanges(List<Change> changes) {
467: for (Sequence sequence : getMissingSequences()) {
468:
469: CreateSequenceChange change = new CreateSequenceChange();
470: change.setSequenceName(sequence.getName());
471:
472: changes.add(change);
473: }
474: }
475:
476: private void addUnexpectedColumnChanges(List<Change> changes) {
477: for (Column column : getUnexpectedColumns()) {
478: if (!shouldModifyColumn(column)) {
479: continue;
480: }
481:
482: DropColumnChange change = new DropColumnChange();
483: change.setTableName(column.getTable().getName());
484: change.setColumnName(column.getName());
485:
486: changes.add(change);
487: }
488: }
489:
490: private void addMissingViewChanges(List<Change> changes) {
491: for (View view : getMissingViews()) {
492:
493: CreateViewChange change = new CreateViewChange();
494: change.setViewName(view.getName());
495: String selectQuery = view.getDefinition();
496: if (selectQuery == null) {
497: selectQuery = "COULD NOT DETERMINE VIEW QUERY";
498: }
499: change.setSelectQuery(selectQuery);
500:
501: changes.add(change);
502: }
503: }
504:
505: private void addChangedColumnChanges(List<Change> changes) {
506: for (Column column : getChangedColumns()) {
507: if (!shouldModifyColumn(column)) {
508: continue;
509: }
510:
511: boolean foundDifference = false;
512: Column baseColumn = baseSnapshot.getColumn(column);
513: if (column.isDataTypeDifferent(baseColumn)) {
514: ColumnConfig columnConfig = new ColumnConfig();
515: columnConfig.setName(column.getName());
516: columnConfig.setType(baseColumn
517: .getDataTypeString(targetDatabase));
518:
519: ModifyColumnChange change = new ModifyColumnChange();
520: change.setTableName(column.getTable().getName());
521: change.addColumn(columnConfig);
522:
523: changes.add(change);
524: foundDifference = true;
525: }
526: if (column.isNullabilityDifferent(baseColumn)) {
527: if (baseColumn.isNullable() == null
528: || baseColumn.isNullable()) {
529: DropNotNullConstraintChange change = new DropNotNullConstraintChange();
530: change.setTableName(column.getTable().getName());
531: change.setColumnName(column.getName());
532: change.setColumnDataType(baseColumn
533: .getDataTypeString(targetDatabase));
534:
535: changes.add(change);
536: foundDifference = true;
537: } else {
538: AddNotNullConstraintChange change = new AddNotNullConstraintChange();
539: change.setTableName(column.getTable().getName());
540: change.setColumnName(column.getName());
541: change.setColumnDataType(baseColumn
542: .getDataTypeString(targetDatabase));
543:
544: changes.add(change);
545: foundDifference = true;
546: }
547:
548: }
549: if (!foundDifference) {
550: throw new RuntimeException("Unknown difference");
551: }
552: }
553: }
554:
555: private boolean shouldModifyColumn(Column column) {
556: return column.getView() == null
557: && !baseDatabase.isLiquibaseTable(column.getTable()
558: .getName());
559:
560: }
561:
562: private void addUnexpectedViewChanges(List<Change> changes) {
563: for (View view : getUnexpectedViews()) {
564:
565: DropViewChange change = new DropViewChange();
566: change.setViewName(view.getName());
567:
568: changes.add(change);
569: }
570: }
571:
572: private void addMissingColumnChanges(List<Change> changes,
573: Database database) {
574: for (Column column : getMissingColumns()) {
575: if (!shouldModifyColumn(column)) {
576: continue;
577: }
578:
579: AddColumnChange change = new AddColumnChange();
580: change.setTableName(column.getTable().getName());
581:
582: ColumnConfig columnConfig = new ColumnConfig();
583: columnConfig.setName(column.getName());
584:
585: String dataType = column.getDataTypeString(database);
586:
587: columnConfig.setType(dataType);
588:
589: columnConfig
590: .setDefaultValue(database
591: .convertJavaObjectToString(column
592: .getDefaultValue()));
593:
594: change.addColumn(columnConfig);
595:
596: changes.add(change);
597: }
598: }
599:
600: private void addMissingTableChanges(List<Change> changes,
601: Database database) {
602: for (Table missingTable : getMissingTables()) {
603: if (baseDatabase.isLiquibaseTable(missingTable.getName())) {
604: continue;
605: }
606:
607: CreateTableChange change = new CreateTableChange();
608: change.setTableName(missingTable.getName());
609: if (missingTable.getRemarks() != null) {
610: change.setRemarks(missingTable.getRemarks());
611: }
612:
613: for (Column column : missingTable.getColumns()) {
614: ColumnConfig columnConfig = new ColumnConfig();
615: columnConfig.setName(column.getName());
616: columnConfig
617: .setType(column.getDataTypeString(database));
618:
619: ConstraintsConfig constraintsConfig = null;
620: if (column.isPrimaryKey()) {
621: PrimaryKey primaryKey = null;
622: for (PrimaryKey pk : getMissingPrimaryKeys()) {
623: if (pk.getTable().getName().equalsIgnoreCase(
624: missingTable.getName())) {
625: primaryKey = pk;
626: }
627: }
628:
629: if (primaryKey == null
630: || primaryKey.getColumnNamesAsList().size() == 1) {
631: constraintsConfig = new ConstraintsConfig();
632: constraintsConfig.setPrimaryKey(true);
633:
634: if (primaryKey != null) {
635: getMissingPrimaryKeys().remove(primaryKey);
636: }
637: }
638: }
639:
640: if (column.isAutoIncrement()) {
641: columnConfig.setAutoIncrement(true);
642: }
643:
644: if (column.isNullable() != null && !column.isNullable()) {
645: if (constraintsConfig == null) {
646: constraintsConfig = new ConstraintsConfig();
647: }
648:
649: constraintsConfig.setNullable(false);
650: }
651: if (constraintsConfig != null) {
652: columnConfig.setConstraints(constraintsConfig);
653: }
654:
655: Object defaultValue = column.getDefaultValue();
656: if (defaultValue == null) {
657: //do nothing
658: } else if (column.isAutoIncrement()) {
659: //do nothing
660: } else if (defaultValue instanceof Date) {
661: columnConfig
662: .setDefaultValueDate((Date) defaultValue);
663: } else if (defaultValue instanceof Boolean) {
664: columnConfig
665: .setDefaultValueBoolean(((Boolean) defaultValue));
666: } else if (defaultValue instanceof Number) {
667: columnConfig
668: .setDefaultValueNumeric(((Number) defaultValue));
669: } else {
670: columnConfig.setDefaultValue(defaultValue
671: .toString());
672: }
673:
674: change.addColumn(columnConfig);
675: }
676:
677: changes.add(change);
678: }
679: }
680:
681: private void addUnexpectedTableChanges(List<Change> changes) {
682: for (Table unexpectedTable : getUnexpectedTables()) {
683: DropTableChange change = new DropTableChange();
684: change.setTableName(unexpectedTable.getName());
685:
686: changes.add(change);
687: }
688: }
689:
690: }
|