/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.cayenne.jpa.conf;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import javax.persistence.spi.PersistenceUnitInfo;

import org.apache.cayenne.jpa.JpaProviderException;
import org.apache.cayenne.jpa.map.JpaClassDescriptor;
import org.apache.cayenne.jpa.map.JpaEntity;
import org.apache.cayenne.jpa.map.JpaEntityMap;

/**
 * Loads JPA mapping information from all sources per JPA specification.
 * <h3>Specification Documentation, Chapter 6.2.1.6</h3>
 * <p>
 * The set of managed persistence classes that are managed by a persistence unit is
 * defined by using one or more of the following:
 * <ul>
 * <li>One or more object/relational mapping XML files
 * <li>One or more jar files that will be searched for classes
 * <li>An explicit list of the classes
 * <li>The annotated managed persistence classes contained in the root of the persistence
 * unit (unless the exclude-unlisted-classes element is specified) [Java SE doesn't have
 * to support that].
 * </ul>
 * <p>
 * The result is undefined if multiple mapping files (including any orm.xml file)
 * referenced within a single persistence unit contain overlapping mapping information for
 * any given class.
 * </p>
 * <p>
 * The resulting set of entities managed by the persistence unit [and contained in the
 * returned entity map] is the union of these sources, with the mapping metadata
 * annotations (or annotation defaults) for any given class being overridden by the XML
 * mapping information file if there are both annotations as well as XML mappings for that
 * class. The minimum portable level of overriding is at the level of the persistent field
 * or property.
 * </p>
 * 
 * @author Andrus Adamchik
 */
public class EntityMapLoader {

    static final String DESCRIPTOR_LOCATION = "META-INF/orm.xml";

    protected EntityMapLoaderContext context;
    protected Map<String, JpaClassDescriptor> descriptors;

    /**
     * Creates an EntityMapLoader for the persistence unit, merging entity information
     * from all locations supported by the JPA specification.
     */
    public EntityMapLoader(PersistenceUnitInfo persistenceUnit) {
        loadEntityMap(persistenceUnit);
    }

    /**
     * Returns an entity map with entity
     */
    public JpaEntityMap getEntityMap() {
        return context.getEntityMap();
    }

    public EntityMapLoaderContext getContext() {
        return context;
    }

    /**
     * Loads a combined entity map including managed class descriptors from all supported
     * locations.
     */
    protected void loadEntityMap(PersistenceUnitInfo persistenceUnit)
            throws JpaProviderException {

        this.context = new EntityMapLoaderContext(persistenceUnit);

        try {
            loadFromAnnotations(persistenceUnit);
            updateFromXML(persistenceUnit);
            updateInheritanceHierarchy();
            updateFromDefaults();
        }
        catch (JpaProviderException e) {
            throw e;
        }
        catch (Exception e) {
            throw new JpaProviderException("Error loading ORM descriptors", e);
        }
    }

    protected void updateInheritanceHierarchy() {

        JpaEntityMap map = getEntityMap();

        for (JpaEntity entity : map.getEntities()) {

            Class<?> superclass = entity
                    .getClassDescriptor()
                    .getManagedClass()
                    .getSuperclass();

            while (superclass != null && !superclass.getName().equals("java.lang.Object")) {

                JpaEntity superEntity = map.getEntity(superclass.getName());
                if (superEntity != null) {
                    entity.setSuperEntity(superEntity);
                    break;
                }

                superclass = superclass.getSuperclass();
            }
        }
    }

    /**
     * Updates missing values with spec-compilant defaults.
     */
    protected void updateFromDefaults() {
        new EntityMapDefaultsProcessor().applyDefaults(context);
    }

    /**
     * Updates mapping with data loaded from XML. JPA specification gives some leeway in
     * processing conflicting mapping files. Cayenne provider strategy is "last mapping
     * file overrides all".
     * <h3>Specification Documentation, Chapter 6.2.1.6</h3>
     * <p>
     * An <em>orm.xml</em> file may be specified in the META-INF directory in the root
     * of the persistence unit or in the META-INF directory of any jar file referenced by
     * the persistence.xml. Alternatively, or in addition, other mapping files maybe
     * referenced by the mapping-file elements of the persistence-unit element, and may be
     * present anywhere on the classpath. An orm.xml file or other mapping file is loaded
     * as a resource by the persistence provider.
     */
    protected void updateFromXML(PersistenceUnitInfo unit) throws IOException {

        EntityMapMergeProcessor merger = new EntityMapMergeProcessor(context);

        Set<String> loadedLocations = new HashSet<String>();
        EntityMapXMLLoader loader = new EntityMapXMLLoader(
                context.getTempClassLoader(),
                false);

        // 1. load from the standard file called orm.xml
        loadedLocations.add(DESCRIPTOR_LOCATION);
        Enumeration<URL> standardDescriptors = context.getTempClassLoader().getResources(
                DESCRIPTOR_LOCATION);

        while (standardDescriptors.hasMoreElements()) {
            JpaEntityMap map = loader.getEntityMap(standardDescriptors.nextElement());
            merger.mergeOverride(map);
        }

        // 2. load from orm.xml within the jars
        // TODO: andrus, 4/20/2006 - load from the jar files

        // 3. load from explicitly specified descriptors
        for (String descriptor : unit.getMappingFileNames()) {

            // avoid loading duplicates, such as META-INF/orm.xml that is also explicitly
            // mentioned in the unit...
            if (loadedLocations.add(descriptor)) {

                Enumeration<URL> mappedDescriptors = context
                        .getTempClassLoader()
                        .getResources(descriptor);
                while (mappedDescriptors.hasMoreElements()) {
                    JpaEntityMap map = loader.getEntityMap(mappedDescriptors
                            .nextElement());
                    merger.mergeOverride(map);
                }
            }
        }
    }

    /**
     * Loads JpaEntityMap based on metadata annotations of persistent classes.
     */
    protected void loadFromAnnotations(PersistenceUnitInfo persistenceUnit) {

        Map<String, Class<?>> managedClassMap = new HashMap<String, Class<?>>();

        // must use Unit class loader to prevent loading an un-enahnced class in the
        // app ClassLoader
        ClassLoader loader = context.getTempClassLoader();

        // load explicitly mentioned classes
        Collection<String> explicitClasses = persistenceUnit.getManagedClassNames();
        if (explicitClasses != null) {
            for (String className : explicitClasses) {

                Class<?> managedClass;
                try {
                    managedClass = Class.forName(className, true, loader);
                }
                catch (ClassNotFoundException e) {
                    throw new JpaProviderException("Class not found: " + className, e);
                }

                managedClassMap.put(className, managedClass);
            }
        }

        // now detect potential managed classes from PU root and extra jars
        if (!persistenceUnit.excludeUnlistedClasses()) {
            Collection<String> implicitClasses = listImplicitClasses(persistenceUnit);
            for (String className : implicitClasses) {

                if (managedClassMap.containsKey(className)) {
                    continue;
                }

                Class<?> managedClass;
                try {
                    managedClass = Class.forName(className, true, loader);
                }
                catch (ClassNotFoundException e) {
                    throw new JpaProviderException("Class not found: " + className, e);
                }

                if (managedClass.getAnnotation(Entity.class) != null
                        || managedClass.getAnnotation(MappedSuperclass.class) != null
                        || managedClass.getAnnotation(Embeddable.class) != null) {
                    managedClassMap.put(className, managedClass);
                }
            }
        }

        if (!managedClassMap.isEmpty()) {

            EntityMapAnnotationLoader annotationLoader = new EntityMapAnnotationLoader(
                    context);
            for (Class<?> managedClass : managedClassMap.values()) {
                annotationLoader.loadClassMapping(managedClass);
            }
        }
    }

    /**
     * Returns a collection of all classes that are located in the unit root and unit
     * extra jars.
     */
    protected Collection<String> listImplicitClasses(PersistenceUnitInfo persistenceUnit) {

        Collection<String> classes = new ArrayList<String>();
        URL rootURL = persistenceUnit.getPersistenceUnitRootUrl();
        if (rootURL != null) {
            if ("file".equals(rootURL.getProtocol())) {
                locateClassesInFolder(rootURL, classes);
            }
            else {
                locateClassesInJar(rootURL, classes);
            }
        }

        for (URL jarURL : persistenceUnit.getJarFileUrls()) {
            if (jarURL != null) {
                // that's unlikely ... but we can handle it just in case...
                if ("file".equals(jarURL.getProtocol())) {
                    locateClassesInFolder(jarURL, classes);
                }
                else {
                    locateClassesInJar(jarURL, classes);
                }
            }
        }

        return classes;
    }

    private void locateClassesInFolder(URL dirURL, Collection<String> classes) {
        File root;
        try {
            root = new File(dirURL.toURI());
        }
        catch (URISyntaxException e) {
            throw new JpaProviderException("Error converting url to file: " + dirURL, e);
        }

        locateClassesInFolder(root, root.getAbsolutePath().length() + 1, classes);
    }

    private void locateClassesInFolder(
            File folder,
            int rootPathLength,
            Collection<String> classes) {

        File[] files = folder.listFiles();
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                if (files[i].isDirectory()) {
                    locateClassesInFolder(files[i], rootPathLength, classes);
                }
                else {
                    String name = files[i].getName();
                    if (name.endsWith(".class")) {

                        int suffixLen = ".class".length();

                        String absPath = files[i].getAbsolutePath();
                        if (absPath.length() > rootPathLength + suffixLen) {
                            classes.add(absPath.substring(
                                    rootPathLength,
                                    absPath.length() - suffixLen).replace('/', '.'));
                        }
                    }
                }
            }
        }
    }

    private void locateClassesInJar(URL jarURL, Collection<String> classes) {

        try {
            JarURLConnection connection = (JarURLConnection) jarURL.openConnection();
            JarFile jar = connection.getJarFile();
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry e = entries.nextElement();
                if (e.isDirectory()) {
                    continue;
                }

                String name = e.getName();
                if (name.endsWith(".class")) {
                    int suffixLen = ".class".length();
                    classes.add(name.substring(0, name.length() - suffixLen).replace(
                            '/',
                            '.'));
                }
            }
        }
        catch (Exception e) {
            throw new JpaProviderException("Error reading jar contents: " + jarURL, e);
        }
    }
}
