package liquibase.serializer.core.string;

import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.serializer.SnapshotSerializer;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.SnapshotControl;
import liquibase.structure.CatalogLevelObject;
import liquibase.structure.DatabaseLevelObject;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.*;
import liquibase.util.StringUtils;

import java.io.*;
import java.util.*;

public class StringSnapshotSerializerReadable implements SnapshotSerializer {

    private static final int INDENT_LENGTH = 4;

    @Override
    public String[] getValidFileExtensions() {
        return new String[]{"txt"};
    }

    @Override
    public String serialize(LiquibaseSerializable object, boolean pretty) {
        try {
            StringBuilder buffer = new StringBuilder();
            DatabaseSnapshot snapshot = ((DatabaseSnapshot) object);
            Database database = snapshot.getDatabase();

            buffer.append("Database snapshot for ").append(database.getConnection().getURL()).append("\n");
            addDivider(buffer);
            buffer.append("Database type: ").append(database.getDatabaseProductName()).append("\n");
            buffer.append("Database version: ").append(database.getDatabaseProductVersion()).append("\n");
            buffer.append("Database user: ").append(database.getConnection().getConnectionUserName()).append("\n");

            SnapshotControl snapshotControl = snapshot.getSnapshotControl();
            List<Class> includedTypes = sort(snapshotControl.getTypesToInclude());

            buffer.append("Included types:\n" ).append(StringUtils.indent(StringUtils.join(includedTypes, "\n", new StringUtils.StringUtilsFormatter<Class>() {
                @Override
                public String toString(Class obj) {
                    return obj.getName();
                }
            }))).append("\n");


            List<Schema> schemas = sort(snapshot.get(Schema.class), new Comparator<Schema>() {
                @Override
                public int compare(Schema o1, Schema o2) {
                    return o1.toString().compareTo(o2.toString());
                }
            });

            for (Schema schema : schemas) {
                if (database.supportsSchemas()) {
                    buffer.append("\nCatalog & Schema: ").append(schema.getCatalogName()).append(" / ").append(schema.getName()).append("\n");
                } else {
                    buffer.append("\nCatalog: ").append(schema.getCatalogName()).append("\n");
                }

                StringBuilder catalogBuffer = new StringBuilder();
                for (Class type : includedTypes) {
                    if (type.equals(Schema.class) || type.equals(Catalog.class) || type.equals(Column.class)) {
                        continue;
                    }
                    List<DatabaseObject> objects = new ArrayList<DatabaseObject>(snapshot.get(type));
                    ListIterator<DatabaseObject> iterator = objects.listIterator();
                    while (iterator.hasNext()) {
                        DatabaseObject next = iterator.next();
                        if (next instanceof DatabaseLevelObject) {
                            continue;
                        }

                        Schema objectSchema = next.getSchema();
                        if (objectSchema == null) {
                            if (!(next instanceof CatalogLevelObject) || !((CatalogLevelObject) next).getCatalog().equals(schema.getCatalog())) {
                                iterator.remove();
                            }
                        } else if (!objectSchema.equals(schema)) {
                            iterator.remove();
                        }
                    }
                    outputObjects(objects, type, catalogBuffer);
                }
                buffer.append(StringUtils.indent(catalogBuffer.toString(), INDENT_LENGTH));

            }

            return buffer.toString().replace("\r\n", "\n").replace("\r", "\n"); //standardize all newline chars

        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }

    }

    protected void outputObjects(List objects, Class type, StringBuilder catalogBuffer) {
        List<? extends DatabaseObject> databaseObjects = sort(objects);
        if (databaseObjects.size() > 0) {
            catalogBuffer.append(type.getName()).append(":\n");

            StringBuilder typeBuffer = new StringBuilder();
            for (DatabaseObject databaseObject : databaseObjects) {
                typeBuffer.append(databaseObject.getName()).append("\n");
                typeBuffer.append(StringUtils.indent(serialize(databaseObject, null), INDENT_LENGTH)).append("\n");
            }

            catalogBuffer.append(StringUtils.indent(typeBuffer.toString(), INDENT_LENGTH)).append("\n");
        }
    }

    private String serialize(final DatabaseObject databaseObject, final DatabaseObject parentObject) {

        StringBuilder buffer = new StringBuilder();

        final List<String> attributes = sort(databaseObject.getAttributes());
        for (String attribute : attributes) {
            if (attribute.equals("name")) {
                continue;
            }
            if (attribute.equals("schema")) {
                continue;
            }
            if (attribute.equals("catalog")) {
                continue;
            }
            Object value = databaseObject.getAttribute(attribute, Object.class);

            if (value instanceof Schema) {
                continue;
            }

            if (value instanceof DatabaseObject) {
                if (parentObject != null && ((DatabaseObject) value).getSnapshotId().equals(parentObject.getSnapshotId())) {
                    continue;
                }

                boolean expandContainedObjects = shouldExpandNestedObject(value, databaseObject);

                if (expandContainedObjects) {
                    value = ((DatabaseObject) value).getName()+"\n"+StringUtils.indent(serialize((DatabaseObject) value, databaseObject), INDENT_LENGTH);
                } else {
                    value = databaseObject.getSerializableFieldValue(attribute);
                }
            } else if (value instanceof Collection) {
                if (((Collection) value).size() == 0) {
                    value = null;
                } else {
                    if (((Collection) value).iterator().next() instanceof DatabaseObject) {
                        value = StringUtils.join(new TreeSet<DatabaseObject>((Collection<DatabaseObject>) value), "\n", new StringUtils.StringUtilsFormatter() {
                            @Override
                            public String toString(Object obj) {
                                if (obj instanceof DatabaseObject) {
                                    if (shouldExpandNestedObject(obj, databaseObject)) {
                                        return ((DatabaseObject) obj).getName()+"\n"+StringUtils.indent(serialize(((DatabaseObject) obj), databaseObject), INDENT_LENGTH);
                                    } else {
                                        return ((DatabaseObject) obj).getName();
                                    }
                                } else {
                                    return obj.toString();
                                }
                            }
                        });
                        value = "\n"+StringUtils.indent((String) value, INDENT_LENGTH);
                    } else {
                        value = databaseObject.getSerializableFieldValue(attribute);
                    }
                }
            } else {
                value = databaseObject.getSerializableFieldValue(attribute);
            }
            if (value != null) {
                buffer.append(attribute).append(": ").append(value).append("\n");
            }
        }

        return buffer.toString().replaceFirst("\n$", "");

    }

    protected boolean shouldExpandNestedObject(Object nestedValue, DatabaseObject container) {
        return container instanceof Table || container instanceof View;
    }

    protected void addDivider(StringBuilder buffer) {
        buffer.append("-----------------------------------------------------------------\n");
    }

    private List sort(Collection objects) {
        return sort(objects, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Comparable) {
                    return ((Comparable) o1).compareTo(o2);
                } else if (o1 instanceof Class) {
                    return ((Class) o1).getName().compareTo(((Class) o2).getName());
                } else {
                    throw new ClassCastException(o1.getClass().getName()+" cannot be cast to java.lang.Comparable or java.lang.Class");
                }
            }
        });
    }

    private <T> List<T> sort(Collection objects, Comparator<T> comparator) {
        List returnList = new ArrayList(objects);
        Collections.sort(returnList, comparator);

        return returnList;
    }

    @Override
    public void write(DatabaseSnapshot snapshot, OutputStream out) throws IOException {
        out.write(serialize(snapshot, true).getBytes(LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()));
    }

    @Override
    public int getPriority() {
        return PRIORITY_DEFAULT;
    }
}

@@ -5965,32 +5965,84 @@
 (DatabaseObject)
+ value).getSnapshotId() != null && ((DatabaseObject)
  value).getSnaps

test that Snapshot has an Id before test equality