LQMG.java

/*
 * Copyright (C) 2011 Everit Kft. (http://www.everit.biz)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.everit.persistence.lqmg;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.everit.persistence.liquibase.ext.osgi.EOSGiResourceAccessor;
import org.everit.persistence.liquibase.ext.osgi.LiquibaseEOSGiConstants;
import org.everit.persistence.liquibase.ext.osgi.util.BundleResource;
import org.everit.persistence.liquibase.ext.osgi.util.LiquibaseOSGiUtil;
import org.everit.persistence.lqmg.internal.ConfigPath;
import org.everit.persistence.lqmg.internal.ConfigurationContainer;
import org.everit.persistence.lqmg.internal.EquinoxHackUtilImpl;
import org.everit.persistence.lqmg.internal.HackUtil;
import org.everit.persistence.lqmg.internal.LQMGNamingStrategy;
import org.everit.persistence.lqmg.internal.liquibase.LQMGChangeExecListener;
import org.h2.Driver;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.osgi.framework.wiring.FrameworkWiring;

import com.querydsl.sql.codegen.MetaDataExporter;
import com.querydsl.sql.codegen.NamingStrategy;

import liquibase.Liquibase;
import liquibase.changelog.visitor.ChangeExecListener;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.core.H2Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ResourceAccessor;

/**
 * This class responsible for generate QueryDSL JAVA classes.
 */
public final class LQMG {

  public static final String CAPABILITY_LQMG_CONFIG_RESOURCE = "lqmg.config.resource";

  private static HackUtil frameworkUtil = new EquinoxHackUtilImpl();

  /**
   * The {@link Logger} instance for logging.
   */
  private static final Logger LOGGER = Logger.getLogger(LQMG.class.getName());

  private static void checkMatchingBundleResourceSize(final GenerationProperties parameters,
      final List<BundleResource> bundleResources) {
    if (bundleResources.size() > 1) {
      LOGGER.log(Level.WARNING,
          "Found multiple bundles containing matching capabilities for schema"
              + " expression: '" + parameters.capability
              + "'. Using the first one from list: "
              + bundleResources.toString());
    }
  }

  private static String createDataBaseURL(final String defaultSchema) {
    StringBuilder sb = new StringBuilder("jdbc:h2:mem:");
    if (defaultSchema != null) {
      sb.append(";INIT=CREATE SCHEMA IF NOT EXISTS \"").append(defaultSchema)
          .append("\"\\;SET SCHEMA \"")
          .append(defaultSchema).append("\"");
    }
    return sb.toString();
  }

  private static File createTempDirectory() throws IOException {
    final File temp = File.createTempFile("lqmg-",
        Long.toString(System.nanoTime()));

    if (!(temp.delete())) {
      throw new IOException("Could not delete temp file: "
          + temp.getAbsolutePath());
    }

    if (!(temp.mkdir())) {
      throw new IOException("Could not create temp directory: "
          + temp.getAbsolutePath());
    }

    return temp;
  }

  private static void deleteFolder(final File folder) {

    if (folder == null) {
      return;
    }

    File[] files = folder.listFiles();
    if (files != null) { // some JVMs return null for empty dirs
      for (File f : files) {
        if (f.isDirectory()) {
          LQMG.deleteFolder(f);
        } else {
          if (!f.delete()) {
            LOGGER.warning("Failed to delete file [" + f.getAbsolutePath() + "]");
          }
        }
      }
    }
    if (!folder.delete()) {
      LOGGER.warning("Failed to delete folder [" + folder.getAbsolutePath() + "]");
    }
  }

  private static void exportMetaData(final GenerationProperties parameters,
      final Connection connection, final ConfigurationContainer configurationContainer)
      throws SQLException {

    LOGGER.log(Level.INFO, "Start meta data export.");

    MetaDataExporter metaDataExporter = new MetaDataExporter();

    NamingStrategy namingStrategy =
        new LQMGNamingStrategy(configurationContainer, parameters.packages);

    metaDataExporter.setNamePrefix("");
    metaDataExporter.setNameSuffix("");
    metaDataExporter.setNamingStrategy(namingStrategy);
    metaDataExporter.setSchemaToPackage(true);
    metaDataExporter.setTargetFolder(new File(parameters.targetFolder));
    metaDataExporter.setInnerClassesForKeys(parameters.innerClassesForKeys);
    metaDataExporter.export(connection.getMetaData());

    LOGGER.log(Level.INFO, "Finish meta data export.");
  }

  /**
   * Generate the JAVA classes to QueryDSL from LiquiBase XML.
   *
   * @param parameters
   *          the parameters for the generation. See more {@link GenerationProperties}.
   */
  public static void generate(final GenerationProperties parameters) {
    Framework osgiContainer = null;
    File tempDirectory = null;
    try {
      tempDirectory = LQMG.createTempDirectory();
      osgiContainer =
          LQMG.startOSGiContainer(parameters.bundleLocations, tempDirectory.getAbsolutePath());
      List<BundleResource> bundleResources = LiquibaseOSGiUtil
          .findBundlesBySchemaExpression(parameters.capability,
              osgiContainer.getBundleContext(), Bundle.RESOLVED);

      if (bundleResources.size() == 0) {
        if (parameters.hackWires) {
          LOGGER.info(
              "No matching bundle found. Trying to find unresolved bundles and hack their wires.");
          frameworkUtil.hackBundles(osgiContainer, tempDirectory);
          FrameworkWiring frameworkWiring = osgiContainer.adapt(FrameworkWiring.class);
          frameworkWiring.resolveBundles(null);

          bundleResources = LiquibaseOSGiUtil
              .findBundlesBySchemaExpression(parameters.capability,
                  osgiContainer.getBundleContext(), Bundle.RESOLVED);
        } else {
          LOGGER.severe("No matching bundle found. Probably setting hackWires to true would help");
        }
        if (bundleResources.size() == 0) {
          LQMG.throwCapabilityNotFound(parameters, osgiContainer);
        }
      }

      LQMG.checkMatchingBundleResourceSize(parameters, bundleResources);

      BundleResource bundleResource = bundleResources.get(0);
      LQMG.tryCodeGeneration(parameters, bundleResource.bundle, bundleResource.attributes);

    } catch (IOException e) {

      LOGGER.log(Level.SEVERE, "Could not create temp directory", e);
      return;

    } catch (BundleException e) {

      LOGGER.log(Level.SEVERE, "Could not start embedded OSGi framework", e);

    } finally {

      LQMG.stopFramework(osgiContainer);
      LQMG.deleteFolder(tempDirectory);
    }

  }

  private static void logUnresolvedBundles(final Framework osgiContainer) {
    BundleContext systemBundleContext = osgiContainer.getBundleContext();
    Bundle[] bundles = systemBundleContext.getBundles();

    for (Bundle bundle : bundles) {
      if (bundle.getState() == Bundle.INSTALLED) {
        try {
          bundle.start();
        } catch (BundleException e) {
          LOGGER.log(Level.WARNING, "The bundle " + bundle.toString() + " could not be resolved",
              e);
        }
      }
    }
  }

  /**
   * HACK to make Equinox using the classloader of the system even if LQMG is called multiple times.
   */
  private static void resetFrameworkProperties() {
    // FIXME avoid having to do this hack!!! equinox internal classes should be available via the
    // jvm classloader
    Class<FrameworkProperties> clazz = FrameworkProperties.class;
    try {
      Field propertiesField = clazz.getDeclaredField("properties");
      propertiesField.setAccessible(true);
      propertiesField.set(null, null);
    } catch (NoSuchFieldException e) {
      throw new RuntimeException(e);
    } catch (SecurityException e) {
      throw new RuntimeException(e);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  private static Framework startOSGiContainer(final String[] bundleLocations,
      final String tempDirPath) throws BundleException {

    FrameworkFactory frameworkFactory = ServiceLoader
        .load(FrameworkFactory.class).iterator().next();

    Map<String, String> config = new HashMap<String, String>();
    config.put("osgi.configuration.area", tempDirPath);
    config.put("osgi.baseConfiguration.area", tempDirPath);
    config.put("osgi.sharedConfiguration.area", tempDirPath);
    config.put("osgi.instance.area", tempDirPath);
    config.put("osgi.user.area", tempDirPath);
    config.put("osgi.hook.configurators.exclude",
        "org.eclipse.core.runtime.internal.adaptor.EclipseLogHook");

    LQMG.resetFrameworkProperties();
    Framework framework = frameworkFactory.newFramework(config);
    framework.start();

    BundleContext systemBundleContext = framework.getBundleContext();
    for (String bundleLocation : bundleLocations) {
      try {
        systemBundleContext.installBundle(bundleLocation);
      } catch (BundleException e) {
        LOGGER.log(Level.WARNING, "Could not start bundle " + bundleLocation, e);
      }
    }
    FrameworkWiring frameworkWiring = framework
        .adapt(FrameworkWiring.class);
    frameworkWiring.resolveBundles(null);

    return framework;
  }

  private static void stopFramework(final Framework osgiContainer) {
    if (osgiContainer != null) {
      try {
        osgiContainer.stop();
        osgiContainer.waitForStop(0);
      } catch (BundleException e) {
        LOGGER.log(Level.SEVERE, "Could not stop embedded OSGi container during code generation",
            e);
      } catch (InterruptedException e) {
        LOGGER.log(Level.SEVERE, "Stopping of embedded OSGi container was interrupted", e);
        Thread.currentThread().interrupt();
      }
    }
  }

  private static void throwCapabilityNotFound(final GenerationProperties parameters,
      final Framework osgiContainer) {

    LQMG.logUnresolvedBundles(osgiContainer);

    throw new LQMGException(
        "Could not find matching capability in any of the bundles for schema expression: "
            + parameters.capability,
        null);
  }

  private static void tryCodeGeneration(
      final GenerationProperties parameters, final Bundle bundle,
      final Map<String, Object> bundleCapabilityAttributes) {

    LOGGER.log(Level.INFO, "Load driver.");
    Driver h2Driver = Driver.load();
    LOGGER.log(Level.INFO, "Loaded driver.");
    Connection connection = null;
    try {
      LOGGER.log(Level.INFO, "Creating connection.");
      String defaultSchema = parameters.defaultSchema;
      String jdbcURL = LQMG.createDataBaseURL(defaultSchema);
      connection = h2Driver.connect(jdbcURL, new Properties());
      LOGGER.log(Level.INFO, "Created connection.");

      LOGGER.log(Level.INFO, "Get database.");
      AbstractJdbcDatabase database = new H2Database();
      database.setCaseSensitive(true);
      database.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS);
      database.setLiquibaseSchemaName("PUBLIC");
      if (defaultSchema != null) {
        database.setDefaultSchemaName(defaultSchema);
      }
      database.setConnection(new JdbcConnection(connection));

      LOGGER.log(Level.INFO, "Start LiquiBase and update.");

      ResourceAccessor resourceAccessor =
          new EOSGiResourceAccessor(bundle, bundleCapabilityAttributes);

      String schemaResource =
          (String) bundleCapabilityAttributes
              .get(LiquibaseEOSGiConstants.CAPABILITY_ATTR_RESOURCE);
      Liquibase liquibase = new Liquibase(schemaResource, resourceAccessor, database);

      ConfigurationContainer configContainer = new ConfigurationContainer();

      if (parameters.configurationPath != null) {
        configContainer.addConfiguration(new ConfigPath(null, parameters.configurationPath));
      }

      ChangeExecListener lqmgChangeExecListener = new LQMGChangeExecListener(configContainer);
      liquibase.setChangeExecListener(lqmgChangeExecListener);

      liquibase.update((String) null);
      LOGGER.log(Level.INFO, "Finish LiquiBase and update.");

      LQMG.exportMetaData(parameters, connection, configContainer);
    } catch (SQLException e) {
      LOGGER.log(Level.SEVERE, e.getMessage(), e);
      // error to create connection.
      // error connection.getMetaData
      // error when export database.
      throw new LQMGException(
          "Error during try to connection the database.", e);
    } catch (LiquibaseException e) {
      // liquibase.update(null);
      LOGGER.log(Level.SEVERE, e.getMessage(), e);
      throw new LQMGException(
          "Error during processing XML file; "
              + parameters.capability,
          e);
    } finally {
      if (connection != null) {
        try {
          connection.close();
          LOGGER.log(Level.INFO, "Connection closed.");
        } catch (SQLException e) {
          LOGGER.log(Level.SEVERE, e.getMessage(), e);
          throw new LQMGException(
              "Closing the connection was unsuccessful.", e);
        }
      }
    }
  }

  /**
   * Simple constructor.
   */
  private LQMG() {
    // private constructor for utility class.
  }

}