net.ageto.gyrex.persistence.liquibase.LiquibaseSchemaSupport.java Source code

Java tutorial

Introduction

Here is the source code for net.ageto.gyrex.persistence.liquibase.LiquibaseSchemaSupport.java

Source

/**
 * Copyright (c) 2010 AGETO Service GmbH and others.
 * All rights reserved.
 *
 * This program and the accompanying materials are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Gunnar Wagenknecht - initial API and implementation
 */
package net.ageto.gyrex.persistence.liquibase;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import liquibase.Liquibase;
import liquibase.change.Change;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.lockservice.LockService;
import liquibase.resource.ResourceAccessor;
import liquibase.sql.Sql;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;

import org.eclipse.gyrex.persistence.storage.content.RepositoryContentType;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;

import org.osgi.framework.Bundle;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.ageto.gyrex.persistence.carbonado.storage.CarbonadoRepository;
import net.ageto.gyrex.persistence.carbonado.storage.spi.jdbc.DatabaseSchemaSupport;
import net.ageto.gyrex.persistence.liquibase.internal.LiquibaseActivator;

/**
 * Base class for Carbonado schema support using Liqubase
 */
public class LiquibaseSchemaSupport extends DatabaseSchemaSupport {

    private static final Logger LOG = LoggerFactory.getLogger(LiquibaseSchemaSupport.class);

    final Bundle bundle;
    final IPath basePath;

    final ClassLoader bundleClassLoader = new ClassLoader() {
        @Override
        protected java.lang.Class<?> findClass(final String name) throws ClassNotFoundException {
            return bundle.loadClass(name);
        };

        @Override
        protected URL findResource(final String name) {
            return bundle.getResource(name);
        };

        @Override
        protected java.util.Enumeration<URL> findResources(final String name) throws IOException {
            return bundle.getResources(name);
        };
    };

    final ResourceAccessor bundleResourceAccessor = new ResourceAccessor() {
        @Override
        public InputStream getResourceAsStream(final String file) throws IOException {
            final URL entry = bundle.getEntry(basePath.append(file).toString());
            if (null != entry)
                return entry.openStream();
            return null;
        }

        @Override
        public Enumeration<URL> getResources(final String packageName) throws IOException {
            return bundle.getResources(basePath.append(packageName).toString());
        }

        @Override
        public ClassLoader toClassLoader() {
            return bundleClassLoader;
        }
    };

    private final ConcurrentMap<RepositoryContentType, String> changeLogsByContentTypeMap = new ConcurrentHashMap<RepositoryContentType, String>(
            1);

    /**
     * Creates a new instance.
     * 
     * @param bundle
     *            the bundle providing the change logs
     * @param baseName
     *            the base name for resolving change log files
     * @param supportedContentTypes
     *            the supported content types
     */
    public LiquibaseSchemaSupport(final Bundle bundle, final String baseName) {
        this.bundle = bundle;
        basePath = new Path(baseName).makeAbsolute().addTrailingSeparator();
    }

    private String getChangeLogFile(final RepositoryContentType contentType) {
        final String changeLogFile = changeLogsByContentTypeMap.get(contentType);
        if (null == changeLogFile)
            throw new IllegalArgumentException(
                    String.format("no change log file for content type: %s", contentType.toString()));
        return changeLogFile;
    }

    @Override
    public RepositoryContentType[] getSupportedContentTypes() {
        final Set<RepositoryContentType> set = changeLogsByContentTypeMap.keySet();
        return set.toArray(new RepositoryContentType[set.size()]);
    }

    @Override
    public IStatus isProvisioned(final CarbonadoRepository repository, final RepositoryContentType contentType,
            final Connection connection) throws IllegalArgumentException {
        LOG.debug("Verify content type '{}' for repository '{}'", contentType.getMediaType(),
                repository.getRepositoryId());

        final LiquibaseCallable<IStatus> callable = new LiquibaseCallable<IStatus>() {
            @Override
            public IStatus runWithLiquibase(final Liquibase liquibase) throws LiquibaseException {
                LOG.debug("Collecting pending change sets.");
                final List<ChangeSet> changeSets = liquibase.listUnrunChangeSets(repository.getRepositoryId());

                // check if fine
                if (changeSets.isEmpty()) {
                    LOG.debug("No pending changes found. Database schema of repository {} is good to go.",
                            repository.getRepositoryId());
                    return new Status(IStatus.OK, LiquibaseActivator.SYMBOLIC_NAME,
                            String.format("No pending changes for content '%s' version %s.",
                                    contentType.getMediaTypeSubType(), contentType.getVersion()));
                }

                // build list of changes
                final MultiStatus result = new MultiStatus(LiquibaseActivator.SYMBOLIC_NAME, 0,
                        String.format("Pending changes for content '%s' version %s.",
                                contentType.getMediaTypeSubType(), contentType.getVersion()),
                        null);

                // log warning
                LOG.debug(
                        "Found pending change set. Database schema of repository {} needs to be migrated in order to support content of type {} (version {}).",
                        new Object[] { repository.getDescription(), contentType.getMediaType(),
                                contentType.getVersion() });

                // log pending change sets
                for (final ChangeSet changeSet : changeSets) {
                    LOG.debug("Found pending change set: {}", changeSet.toString(true));

                    final String id = changeSet.getId();
                    final String description = StringUtils.isBlank(changeSet.getDescription())
                            ? changeSet.getFilePath()
                            : changeSet.getDescription();
                    final String author = StringUtils.isBlank(changeSet.getAuthor()) ? "Mr. UNKNOWN"
                            : changeSet.getAuthor();

                    final MultiStatus changeStatus = new MultiStatus(LiquibaseActivator.SYMBOLIC_NAME, 0,
                            String.format("%s (by %s): %s", id, author, description), null);
                    final List<Change> changes = changeSet.getChanges();
                    for (final Change change : changes) {
                        final SqlStatement[] statements = change.generateStatements(liquibase.getDatabase());
                        for (final SqlStatement sqlStatement : statements) {
                            final Sql[] sqls = SqlGeneratorFactory.getInstance().generateSql(sqlStatement,
                                    liquibase.getDatabase());
                            for (final Sql sql : sqls) {
                                changeStatus.add(
                                        new Status(IStatus.ERROR, LiquibaseActivator.SYMBOLIC_NAME, sql.toSql()));
                            }
                        }
                    }
                    result.add(changeStatus);
                }

                // give up
                return result;
            }
        };

        return runLiquibaseOperation(repository, contentType, connection, callable);
    }

    @Override
    public IStatus provision(final CarbonadoRepository repository, final RepositoryContentType contentType,
            final Connection connection, final IProgressMonitor monitor) throws IllegalArgumentException {
        LOG.debug("Verify content type '{}' for repository '{}'", contentType.getMediaType(),
                repository.getRepositoryId());

        final LiquibaseCallable<IStatus> callable = new LiquibaseCallable<IStatus>() {
            @Override
            public IStatus runWithLiquibase(final Liquibase liquibase) throws LiquibaseException {
                LOG.debug("Collecting pending change sets...");
                final List<ChangeSet> changeSets = liquibase.listUnrunChangeSets(repository.getRepositoryId());

                // check if fine
                if (changeSets.isEmpty()) {
                    LOG.debug("No pending change sets found.");
                    return Status.OK_STATUS;
                }

                // log pending change sets
                for (final ChangeSet changeSet : changeSets) {
                    LOG.debug("Found pending change set: {}", changeSet.toString(true));
                }

                LOG.info("Migrating database schema for repository {}.", repository.getDescription());

                // perform migration
                try {
                    liquibase.update(repository.getRepositoryId());
                } catch (final LiquibaseException e) {
                    LOG.error("Error while updating database schema for repository {}. {}",
                            repository.getRepositoryId(), ExceptionUtils.getRootCauseMessage(e));
                    throw new IllegalStateException("Failed updating database for repository '"
                            + repository.getRepositoryId() + "'. " + e.getMessage(), e);
                }

                // sanity check
                final List<ChangeSet> unrunChangeSets = liquibase.listUnrunChangeSets(repository.getRepositoryId());
                if (!unrunChangeSets.isEmpty()) {
                    for (final ChangeSet changeSet : unrunChangeSets) {
                        LOG.debug("Found unrun change set AFTER migration: {}", changeSet.toString(true));
                    }
                    throw new IllegalStateException(
                            "Found pending change sets AFTER database migration for repository '"
                                    + repository.getRepositoryId() + "'. Please check database manually.");
                }

                LOG.info("Successfully migrated database schema for repository {}.", repository.getDescription());

                // fine
                return Status.OK_STATUS;
            }
        };

        return runLiquibaseOperation(repository, contentType, connection, callable);
    }

    public <T> T runLiquibaseOperation(final CarbonadoRepository repository,
            final RepositoryContentType contentType, final Connection connection,
            final LiquibaseCallable<T> callable) {
        final String changeLogFile = getChangeLogFile(contentType);

        Database database = null;
        try {
            // get database
            database = DatabaseFactory.getInstance()
                    .findCorrectDatabaseImplementation(new JdbcConnection(connection));

            // configure lock timeout
            final LockService lockService = LockService.getInstance(database);
            lockService.setChangeLogLockRecheckTime(500); // recheck every 500ms
            lockService.setChangeLogLockWaitTime(2000); // wait not more than 2s

            // create Liquibase object
            final Liquibase liquibase = new Liquibase(changeLogFile, bundleResourceAccessor, database);

            return callable.runWithLiquibase(liquibase);
        } catch (final LiquibaseException e) {
            throw new IllegalStateException("Failed to collect pending change sets for repository '"
                    + repository.getRepositoryId() + "'. " + e.getMessage(), e);
        } finally {
            // cleanup
            if (null != database) {
                LockService.removeInstance(database);
            }
        }
    }

    /**
     * Sets a change log file for the specified content type.
     * 
     * @param contentType
     *            the content type
     * @param changeLogFile
     *            the change log file
     */
    public void setChangeLog(final RepositoryContentType contentType, final String changeLogFile) {
        changeLogsByContentTypeMap.put(contentType, changeLogFile);
    }
}