All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jknack.mwa.jpa.JpaFixtures Maven / Gradle / Ivy

package com.github.jknack.mwa.jpa;

import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.Validate.notEmpty;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hibernate.ejb.HibernateEntityManager;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.metadata.ClassMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ResourceUtils;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.extensions.compactnotation.CompactConstructor;
import org.yaml.snakeyaml.extensions.compactnotation.CompactData;
import org.yaml.snakeyaml.nodes.ScalarNode;

/**
 * Load a set of test files in JSON format.
 *
 * @author edgar.espina
 *
 */
public final class JpaFixtures {

  /**
   * The logging system.
   */
  private static final Logger logger = LoggerFactory.getLogger(JpaFixtures.class);

  /**
   * Not allowed.
   */
  private JpaFixtures() {
  }

  /**
   * Persist any entity found under the base directory.
   *
   * @param applicationContext The application's context. Required.
   * @param emf The entity manager factory. Required.
   * @param baseDir The base directory. Required.
   * @param metadata The map with entities names and classes.
   */
  public static void load(final ApplicationContext applicationContext,
      final EntityManagerFactory emf, final String baseDir,
      final Map metadata) {
    notNull(applicationContext, "The application's context is required.");
    notNull(emf, "The entity manager factory is required.");
    notEmpty(baseDir, "The baseDir is required.");
    notNull(metadata, "The classes are required.");

    try {
      // load models...
      Iterable entities = load(applicationContext, metadata, baseDir);
      for (Object entity : entities) {
        EntityManager em = emf.createEntityManager();
        SessionImplementor session = (SessionImplementor) ((HibernateEntityManager) em)
            .getSession();

        EntityTransaction trx = em.getTransaction();
        boolean rollback = true;
        try {
          ClassMetadata cmetadata = metadata.get(entity.getClass().getName());
          trx.begin();
          Serializable id = cmetadata.getIdentifier(entity, session);
          if (id == null) {
            // Persist works if id generation is generated by database.
            em.persist(entity);
          } else {
            // Find the entity under the given id.
            Object existing = em.find(entity.getClass(), id);
            if (existing == null) {
              // doesn't exist, use merge and avoid detached instances error generated by persist.
              em.merge(entity);
            } else {
              // It exists! force a failure and report the problem in the next catch statement.
              em.persist(entity);
            }
          }
          trx.commit();
          rollback = false;
        } catch (EntityExistsException ex) {
          logger.warn("Entity exists: "
              + ToStringBuilder.reflectionToString(entity, ToStringStyle.MULTI_LINE_STYLE));
          logger.debug("Entity exists: ", ex);
        } catch (PersistenceException ex) {
          // See https://hibernate.onjira.com/browse/HHH-4131
          Throwable cause = ex.getCause();
          if (cause != null && cause.getMessage().startsWith("detached")) {
            logger.warn("Entity exists: "
                + ToStringBuilder.reflectionToString(entity, ToStringStyle.MULTI_LINE_STYLE));
            logger.debug("Entity exists: ", ex);
          } else {
            throw ex;
          }
        } finally {
          if (rollback && trx.isActive()) {
            trx.rollback();
          }
          em.close();
        }
      }
    } catch (Exception ex) {
      throw new IllegalStateException("Unable to load fixtures", ex);
    }
  }

  /**
   * Load YAML files and parse them.
   *
   * @param applicationContext The application's context. Required.
   * @param metadata Keys are entity's name, values are Java classes.
   * @param baseDir It will be recursively scanned for .yml files.
   * @param  Entity types.
   * @return A set of objects
   * @throws IOException If resources fail to read.
   */
  @SuppressWarnings({"unchecked", "rawtypes" })
  public static  Iterable load(final ApplicationContext applicationContext,
      final Map metadata, final String baseDir) throws IOException {
    String pattern = new File(baseDir, "**/*.yml").toString().replace('\\', '/');

    logger.debug("Searching for: {}", pattern);

    Resource[] resources = getResources(applicationContext, pattern);
    Yaml yaml = newYaml(applicationContext, metadata);
    Set result = new LinkedHashSet();
    for (Resource resource : resources) {
      logger.debug("  yaml file found: {}", resource);
      String yml = toString(resource);
      Object root = yaml.load(yml);
      if (root instanceof List) {
        List objects = (List) root;
        for (Object object : objects) {
          result.add((T) object);
        }
      } else {
        result.add((T) root);
      }
    }
    return result;
  }

  /**
   * Read resources list and fallback to classpath search if resources are not found.
   *
   * @param resolver A resource resolver.
   * @param pattern The pattern to look for.
   * @return A list of resources.
   * @throws IOException If file location doesn't exist.
   */
  private static Resource[] getResources(final ResourcePatternResolver resolver,
      final String pattern) throws IOException {
    Resource[] resources;
    try {
      resources = resolver.getResources(pattern);
    } catch (FileNotFoundException ex) {
      // Fallback to classpath
      String fallbackPattern = ResourceUtils.CLASSPATH_URL_PREFIX + pattern;
      logger.debug("Resources not found: {}, looking at: {}", pattern, fallbackPattern);
      try {
        resources = resolver.getResources(fallbackPattern);
      } catch (FileNotFoundException iex) {
        resources = new Resource[0];
      }
    }
    Arrays.sort(resources, new Comparator() {
      @Override
      public int compare(final Resource r1, final Resource r2) {
        String f1 = r1.getFilename();
        String f2 = r2.getFilename();
        return f1.compareToIgnoreCase(f2);
      }
    });
    return resources;
  }

  /**
   * Read the given resource as String.
   *
   * @param resource The resource.
   * @return The resource content.
   * @throws IOException If the resource content cannot be read it.
   */
  private static String toString(final Resource resource) throws IOException {
    InputStream in = null;
    try {
      in = resource.getInputStream();
      return IOUtils.toString(in, "UTF-8");
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  /**
   * Creates a Yaml instance and register all the given class's name for using
   * CompactObjectNotation.
   *
   * @param applicationContext The application's context.
   * @param metadata Keys are entity's name, values are Java classes.
   * @return A new Yaml instance.
   */
  private static Yaml newYaml(final ApplicationContext applicationContext,
      final Map metadata) {
    CompactConstructor constructor = new CompactConstructor() {
      @Override
      protected Class getClassForName(final String name) throws ClassNotFoundException {
        Collection classes = metadata.values();
        for (ClassMetadata classMetadata : classes) {
          Class mappedClass = classMetadata.getMappedClass();
          if (name.equals(mappedClass.getSimpleName())) {
            return mappedClass;
          }
        }
        return super.getClassForName(name);
      }

      @Override
      protected Object createInstance(final ScalarNode node, final CompactData data)
          throws Exception {
        Class clazz = getClassForName(data.getPrefix());
        Constructor[] constructors = clazz.getDeclaredConstructors();
        ConversionService conversionService = applicationContext.getBean(ConversionService.class);

        // find best matching constructor
        for (Constructor constructor : constructors) {
          Class[] parameterTypes = constructor.getParameterTypes();
          if (parameterTypes.length == data.getArguments().size()) {
            boolean found = true;
            int p = 0;
            Object[] args = new Object[parameterTypes.length];
            while (found && p < parameterTypes.length) {
              Class parameterType = parameterTypes[p];
              boolean canConvert = conversionService.canConvert(String.class, parameterType);
              if (canConvert) {
                args[p] = conversionService.convert(data.getArguments().get(p), parameterType);
              }
              found &= canConvert;
              p++;
            }
            if (found) {
              constructor.setAccessible(true);
              return BeanUtils.instantiateClass(constructor, args);
            }
          }
        }
        throw new IllegalArgumentException("Constructor not found: " + clazz.getName()
            + "(" + join(data.getArguments(), ",") + ")");
      }
    };
    // Add !tag
    for (Entry entry : metadata.entrySet()) {
      ClassMetadata cmetadata = entry.getValue();
      Class mappedClass = cmetadata.getMappedClass();
      String tag = "!!" + mappedClass.getSimpleName();
      constructor.addTypeDescription(new TypeDescription(mappedClass, tag));
    }
    return new Yaml(constructor);
  }
}