/*****************************************************************
 *   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.bridge;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.Iterator;

import org.apache.cayenne.jpa.JpaProviderException;
import org.apache.cayenne.jpa.conf.EntityMapLoaderContext;
import org.apache.cayenne.jpa.map.AccessType;
import org.apache.cayenne.jpa.map.JpaAttribute;
import org.apache.cayenne.jpa.map.JpaAttributes;
import org.apache.cayenne.jpa.map.JpaBasic;
import org.apache.cayenne.jpa.map.JpaColumn;
import org.apache.cayenne.jpa.map.JpaEntity;
import org.apache.cayenne.jpa.map.JpaEntityListener;
import org.apache.cayenne.jpa.map.JpaEntityListeners;
import org.apache.cayenne.jpa.map.JpaEntityMap;
import org.apache.cayenne.jpa.map.JpaId;
import org.apache.cayenne.jpa.map.JpaJoinColumn;
import org.apache.cayenne.jpa.map.JpaManagedClass;
import org.apache.cayenne.jpa.map.JpaManyToMany;
import org.apache.cayenne.jpa.map.JpaManyToOne;
import org.apache.cayenne.jpa.map.JpaNamedQuery;
import org.apache.cayenne.jpa.map.JpaOneToMany;
import org.apache.cayenne.jpa.map.JpaOneToOne;
import org.apache.cayenne.jpa.map.JpaPersistenceUnitDefaults;
import org.apache.cayenne.jpa.map.JpaPersistenceUnitMetadata;
import org.apache.cayenne.jpa.map.JpaQueryHint;
import org.apache.cayenne.jpa.map.JpaRelationship;
import org.apache.cayenne.jpa.map.JpaTable;
import org.apache.cayenne.jpa.map.JpaVersion;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.EntityListener;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.project.ProjectPath;
import org.apache.cayenne.util.BaseTreeVisitor;
import org.apache.cayenne.util.HierarchicalTreeVisitor;
import org.apache.cayenne.util.TraversalUtil;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.validation.SimpleValidationFailure;

/**
 * A converter between {@link org.apache.cayenne.jpa.map.JpaEntityMap} and Cayenne
 * {@link org.apache.cayenne.map.DataMap}.
 * 
 * @author Andrus Adamchik
 */
public class DataMapConverter {

    protected EntityMapLoaderContext context;

    protected ProjectPath targetPath;
    protected HierarchicalTreeVisitor visitor;

    public synchronized DataMap toDataMap(String name, EntityMapLoaderContext context) {
        this.context = context;

        // reset
        DataMap dataMap = new DataMap(name);
        dataMap.setDefaultPackage(context.getEntityMap().getPackageName());
        dataMap.setDefaultSchema(context.getEntityMap().getSchema());

        this.targetPath = new ProjectPath(dataMap);

        if (visitor == null) {
            visitor = createVisitor();
        }

        TraversalUtil.traverse(context.getEntityMap(), visitor);
        postprocess(dataMap);
        return dataMap;
    }

    /**
     * Connects missing reverse relationships.
     */
    protected void postprocess(DataMap dataMap) {

        // connect relationships paired via "mappedBy"; if any other reverse relationships
        // are missing, Cayenne runtime downstream should take care of it
        Iterator entities = dataMap.getDbEntities().iterator();
        while (entities.hasNext()) {

            DbEntity entity = (DbEntity) entities.next();
            Iterator it = entity.getRelationships().iterator();
            while (it.hasNext()) {
                JpaDbRelationship relationship = (JpaDbRelationship) it.next();
                if (relationship.getMappedBy() != null) {
                    DbRelationship owner = (DbRelationship) relationship
                            .getTargetEntity()
                            .getRelationship(relationship.getMappedBy());

                    if (owner != null) {
                        Iterator joins = owner.getJoins().iterator();
                        while (joins.hasNext()) {
                            DbJoin join = (DbJoin) joins.next();
                            DbJoin reverse = join.createReverseJoin();
                            reverse.setRelationship(relationship);
                            relationship.addJoin(reverse);
                        }
                    }
                }
            }
        }
    }

    protected void recordConflict(ProjectPath path, String message) {
        context.recordConflict(new SimpleValidationFailure(path.getObject(), message));
    }

    /**
     * Creates a stateless instance of the JpaEntityMap traversal visitor. This method is
     * lazily invoked and cached by this object.
     */
    protected HierarchicalTreeVisitor createVisitor() {
        BaseTreeVisitor listenersVisitor = new BaseTreeVisitor();
        listenersVisitor.addChildVisitor(
                JpaEntityListener.class,
                new JpaDefaultEntityListenerVisitor());

        BaseTreeVisitor defaultsVisitor = new BaseTreeVisitor();
        defaultsVisitor.addChildVisitor(JpaEntityListeners.class, listenersVisitor);
        BaseTreeVisitor metadataVisitor = new BaseTreeVisitor();
        metadataVisitor
                .addChildVisitor(JpaPersistenceUnitDefaults.class, defaultsVisitor);

        BaseTreeVisitor visitor = new BaseTreeVisitor();
        visitor.addChildVisitor(JpaEntity.class, new JpaEntityVisitor());
        visitor.addChildVisitor(JpaNamedQuery.class, new JpaNamedQueryVisitor());
        visitor.addChildVisitor(JpaPersistenceUnitMetadata.class, metadataVisitor);
        return visitor;
    }

    private EntityListener makeEntityListener(JpaEntityListener jpaListener) {
        EntityListener listener = new EntityListener(jpaListener.getClassName());

        if (jpaListener.getPostLoad() != null) {
            listener.getCallbackMap().getPostLoad().addCallbackMethod(
                    jpaListener.getPostLoad().getMethodName());
        }

        if (jpaListener.getPostPersist() != null) {
            listener.getCallbackMap().getPostPersist().addCallbackMethod(
                    jpaListener.getPostPersist().getMethodName());
        }

        if (jpaListener.getPostRemove() != null) {
            listener.getCallbackMap().getPostRemove().addCallbackMethod(
                    jpaListener.getPostRemove().getMethodName());
        }

        if (jpaListener.getPostUpdate() != null) {
            listener.getCallbackMap().getPostUpdate().addCallbackMethod(
                    jpaListener.getPostUpdate().getMethodName());
        }

        if (jpaListener.getPrePersist() != null) {
            listener.getCallbackMap().getPrePersist().addCallbackMethod(
                    jpaListener.getPrePersist().getMethodName());
        }

        if (jpaListener.getPreRemove() != null) {
            listener.getCallbackMap().getPreRemove().addCallbackMethod(
                    jpaListener.getPreRemove().getMethodName());
        }

        if (jpaListener.getPreUpdate() != null) {
            listener.getCallbackMap().getPreUpdate().addCallbackMethod(
                    jpaListener.getPreUpdate().getMethodName());
        }
        return listener;
    }

    class JpaDefaultEntityListenerVisitor extends BaseTreeVisitor {

        @Override
        public boolean onStartNode(ProjectPath path) {
            JpaEntityListener jpaListener = (JpaEntityListener) path.getObject();

            DataMap map = (DataMap) targetPath.firstInstanceOf(DataMap.class);
            EntityListener listener = map.getEntityListener(jpaListener.getClassName());
            if (listener == null) {
                listener = makeEntityListener(jpaListener);
                map.addEntityListener(listener);
            }

            map.addDefaultEntityListener(listener);
            return false;
        }
    }

    class JpaEntityListenerVisitor extends BaseTreeVisitor {

        @Override
        public boolean onStartNode(ProjectPath path) {
            JpaEntityListener jpaListener = (JpaEntityListener) path.getObject();

            DataMap map = (DataMap) targetPath.firstInstanceOf(DataMap.class);
            EntityListener listener = map.getEntityListener(jpaListener.getClassName());
            if (listener == null) {
                listener = makeEntityListener(jpaListener);
                map.addEntityListener(listener);
            }

            ObjEntity entity = (ObjEntity) targetPath.firstInstanceOf(ObjEntity.class);
            entity.addEntityListener(listener);

            return false;
        }
    }

    class JpaBasicVisitor extends NestedVisitor {

        @Override
        Object createObject(ProjectPath path) {

            JpaBasic jpaBasic = (JpaBasic) path.getObject();

            ObjEntity parentCayenneEntity = (ObjEntity) targetPath.getObject();

            ObjAttribute cayenneAttribute = new ObjAttribute(jpaBasic.getName());
            cayenneAttribute
                    .setType(getAttributeType(path, jpaBasic.getName()).getName());
            cayenneAttribute.setDbAttributeName(jpaBasic.getColumn().getName());

            parentCayenneEntity.addAttribute(cayenneAttribute);
            return cayenneAttribute;
        }

        Class getAttributeType(ProjectPath path, String name) {
            AccessType access = null;

            JpaManagedClass entity = (JpaManagedClass) path
                    .firstInstanceOf(JpaManagedClass.class);
            access = entity.getAccess();

            if (access == null) {
                JpaEntityMap map = (JpaEntityMap) path
                        .firstInstanceOf(JpaEntityMap.class);
                access = map.getAccess();
            }

            Class objectClass = ((ObjEntity) targetPath.firstInstanceOf(ObjEntity.class))
                    .getJavaClass();

            try {
                if (access == AccessType.FIELD) {
                    return lookupFieldInHierarchy(objectClass, name).getType();
                }
                else {
                    return new PropertyDescriptor(name, objectClass).getPropertyType();
                }
            }
            catch (Exception e) {
                throw new JpaProviderException("Error resolving attribute '"
                        + name
                        + "', access type:"
                        + access
                        + ", class: "
                        + objectClass.getName(), e);
            }
        }

        Field lookupFieldInHierarchy(Class beanClass, String fieldName)
                throws SecurityException, NoSuchFieldException {

            try {
                return beanClass.getDeclaredField(fieldName);
            }
            catch (NoSuchFieldException e) {

                Class superClass = beanClass.getSuperclass();
                if (superClass == null
                        || superClass.getName().equals(Object.class.getName())) {
                    throw e;
                }

                return lookupFieldInHierarchy(superClass, fieldName);
            }
        }
    }

    class JpaVersionVisitor extends JpaBasicVisitor {

        @Override
        Object createObject(ProjectPath path) {

            JpaVersion version = (JpaVersion) path.getObject();

            ObjEntity parentCayenneEntity = (ObjEntity) targetPath.getObject();

            ObjAttribute cayenneAttribute = new ObjAttribute(version.getName());
            cayenneAttribute.setType(getAttributeType(path, version.getName()).getName());
            cayenneAttribute.setDbAttributeName(version.getColumn().getName());

            parentCayenneEntity.addAttribute(cayenneAttribute);
            return cayenneAttribute;
        }
    }

    class JpaColumnVisitor extends BaseTreeVisitor {

        @Override
        public boolean onStartNode(ProjectPath path) {
            JpaColumn jpaColumn = (JpaColumn) path.getObject();
            JpaAttribute attribute = (JpaAttribute) path.getObjectParent();

            DbAttribute dbAttribute = new DbAttribute(jpaColumn.getName());

            if (attribute instanceof JpaBasic) {
                JpaBasic basic = (JpaBasic) attribute;
                dbAttribute.setType(basic.getDefaultJdbcType());
            }
            else if (attribute instanceof JpaVersion) {
                JpaVersion version = (JpaVersion) attribute;
                dbAttribute.setType(version.getDefaultJdbcType());
            }

            dbAttribute.setMandatory(!jpaColumn.isNullable());
            dbAttribute.setMaxLength(jpaColumn.getLength());

            // DbAttribute "no scale" means -1, not 0 like in JPA.
            if (jpaColumn.getScale() > 0) {
                dbAttribute.setScale(jpaColumn.getScale());
            }

            // DbAttribute "no precision" means -1, not 0 like in JPA.
            if (jpaColumn.getPrecision() > 0) {
                dbAttribute.setAttributePrecision(jpaColumn.getPrecision());
            }

            if (jpaColumn.getTable() == null) {
                throw new JpaProviderException("No default table defined for JpaColumn "
                        + jpaColumn.getName());
            }

            DbEntity entity = ((DataMap) targetPath.firstInstanceOf(DataMap.class))
                    .getDbEntity(jpaColumn.getTable());

            if (entity == null) {
                throw new JpaProviderException("No DbEntity defined for table  "
                        + jpaColumn.getTable());
            }

            entity.addAttribute(dbAttribute);

            return false;
        }
    }

    class JpaIdVisitor extends JpaBasicVisitor {

        @Override
        Object createObject(ProjectPath path) {

            JpaId id = (JpaId) path.getObject();

            ObjEntity parentCayenneEntity = (ObjEntity) targetPath.getObject();

            ObjAttribute cayenneAttribute = new ObjAttribute(id.getName());
            cayenneAttribute.setType(getAttributeType(path, id.getName()).getName());
            cayenneAttribute.setDbAttributeName(id.getColumn().getName());

            parentCayenneEntity.addAttribute(cayenneAttribute);
            return cayenneAttribute;
        }
    }

    class JpaIdColumnVisitor extends BaseTreeVisitor {

        @Override
        public boolean onStartNode(ProjectPath path) {
            JpaColumn jpaColumn = (JpaColumn) path.getObject();

            DbAttribute dbAttribute = new DbAttribute(jpaColumn.getName());

            JpaId jpaId = (JpaId) path.firstInstanceOf(JpaId.class);

            dbAttribute.setType(jpaId.getDefaultJdbcType());

            dbAttribute.setMaxLength(jpaColumn.getLength());
            dbAttribute.setMandatory(true);
            dbAttribute.setPrimaryKey(true);

            if (jpaColumn.getScale() > 0) {
                dbAttribute.setScale(jpaColumn.getScale());
            }

            if (jpaColumn.getPrecision() > 0) {
                dbAttribute.setAttributePrecision(jpaColumn.getPrecision());
            }

            if (jpaColumn.getTable() == null) {
                recordConflict(path, "No table defined for JpaColumn '"
                        + jpaColumn.getName()
                        + "'");
                return false;
            }

            DbEntity entity = ((DataMap) targetPath.firstInstanceOf(DataMap.class))
                    .getDbEntity(jpaColumn.getTable());

            if (entity == null) {
                recordConflict(path, "Invalid table definition for JpaColumn: "
                        + jpaColumn.getTable()
                        + "'");
                return false;
            }

            entity.addAttribute(dbAttribute);
            return false;
        }
    }

    class JpaJoinColumnVisitor extends BaseTreeVisitor {

        @Override
        public boolean onStartNode(ProjectPath path) {

            JpaJoinColumn jpaJoin = (JpaJoinColumn) path.getObject();
            JpaRelationship jpaRelationship = (JpaRelationship) path.getObjectParent();
            JpaEntity targetEntity = context.getEntityMap().entityForClass(
                    jpaRelationship.getTargetEntityName());
            JpaId jpaTargetId = targetEntity.getAttributes().getId(
                    jpaJoin.getReferencedColumnName());

            if (jpaTargetId == null) {
                throw new IllegalArgumentException("Null id "
                        + targetEntity.getName()
                        + "."
                        + jpaJoin.getReferencedColumnName());
            }

            DbRelationship dbRelationship = (DbRelationship) targetPath.getObject();

            // add FK
            DbAttribute src = new DbAttribute(jpaJoin.getName());

            // TODO: andrus, 5/2/2006 - infer this from Jpa relationship
            src.setMandatory(false);
            src.setMaxLength(jpaTargetId.getColumn().getLength());
            src.setType(jpaTargetId.getDefaultJdbcType());

            Entity srcEntity = dbRelationship.getSourceEntity();
            srcEntity.addAttribute(src);

            // add join
            DbJoin join = new DbJoin(dbRelationship, src.getName(), jpaTargetId
                    .getColumn()
                    .getName());
            dbRelationship.addJoin(join);

            return false;
        }
    }

    class JpaEntityVisitor extends NestedVisitor {

        JpaEntityVisitor() {

            BaseTreeVisitor listenersVisitor = new BaseTreeVisitor();
            listenersVisitor.addChildVisitor(
                    JpaEntityListener.class,
                    new JpaEntityListenerVisitor());

            BaseTreeVisitor attributeVisitor = new BaseTreeVisitor();
            attributeVisitor.addChildVisitor(
                    JpaManyToOne.class,
                    new JpaManyToOneVisitor());
            attributeVisitor.addChildVisitor(JpaOneToOne.class, new JpaOneToOneVisitor());
            attributeVisitor.addChildVisitor(
                    JpaOneToMany.class,
                    new JpaOneToManyVisitor());
            attributeVisitor.addChildVisitor(
                    JpaManyToMany.class,
                    new JpaManyToManyVisitor());

            JpaBasicVisitor basicVisitor = new JpaBasicVisitor();
            basicVisitor.addChildVisitor(JpaColumn.class, new JpaColumnVisitor());
            attributeVisitor.addChildVisitor(JpaBasic.class, basicVisitor);

            JpaVersionVisitor versionVisitor = new JpaVersionVisitor();
            versionVisitor.addChildVisitor(JpaColumn.class, new JpaColumnVisitor());
            attributeVisitor.addChildVisitor(JpaVersion.class, versionVisitor);

            JpaIdVisitor idVisitor = new JpaIdVisitor();
            idVisitor.addChildVisitor(JpaColumn.class, new JpaIdColumnVisitor());
            attributeVisitor.addChildVisitor(JpaId.class, idVisitor);

            // TODO: andrus 8/6/2006 - handle Embedded, EmbeddedId, AttributeOverride

            addChildVisitor(JpaAttributes.class, attributeVisitor);
            addChildVisitor(JpaTable.class, new JpaTableVisitor());
            addChildVisitor(JpaNamedQuery.class, new JpaNamedQueryVisitor());
            addChildVisitor(JpaEntityListeners.class, listenersVisitor);
        }

        @Override
        Object createObject(ProjectPath path) {
            JpaEntity jpaEntity = (JpaEntity) path.getObject();
            ObjEntity cayenneEntity = new ObjEntity(jpaEntity.getName());
            cayenneEntity.setClassName(jpaEntity.getClassName());
            initCallbacks(jpaEntity, cayenneEntity);

            ((DataMap) targetPath.getObject()).addObjEntity(cayenneEntity);

            return cayenneEntity;
        }

        private void initCallbacks(JpaEntity jpaEntity, ObjEntity cayenneEntity) {
            if (jpaEntity.getPostLoad() != null) {
                cayenneEntity.getCallbackMap().getPostLoad().addCallbackMethod(
                        jpaEntity.getPostLoad().getMethodName());
            }

            if (jpaEntity.getPostPersist() != null) {
                cayenneEntity.getCallbackMap().getPostPersist().addCallbackMethod(
                        jpaEntity.getPostPersist().getMethodName());
            }

            if (jpaEntity.getPostRemove() != null) {
                cayenneEntity.getCallbackMap().getPostRemove().addCallbackMethod(
                        jpaEntity.getPostRemove().getMethodName());
            }

            if (jpaEntity.getPostUpdate() != null) {
                cayenneEntity.getCallbackMap().getPostUpdate().addCallbackMethod(
                        jpaEntity.getPostUpdate().getMethodName());
            }

            if (jpaEntity.getPrePersist() != null) {
                cayenneEntity.getCallbackMap().getPrePersist().addCallbackMethod(
                        jpaEntity.getPrePersist().getMethodName());
            }

            if (jpaEntity.getPreRemove() != null) {
                cayenneEntity.getCallbackMap().getPreRemove().addCallbackMethod(
                        jpaEntity.getPreRemove().getMethodName());
            }

            if (jpaEntity.getPreUpdate() != null) {
                cayenneEntity.getCallbackMap().getPreUpdate().addCallbackMethod(
                        jpaEntity.getPreUpdate().getMethodName());
            }
        }
    }

    class JpaManyToOneVisitor extends JpaRelationshipVisitor {

        @Override
        Object createObject(ProjectPath path) {
            ObjRelationship objRelationship = (ObjRelationship) super.createObject(path);
            return objRelationship.getDbRelationships().get(0);
        }
    }

    class JpaOneToManyVisitor extends JpaRelationshipVisitor {

        @Override
        Object createObject(ProjectPath path) {
            ObjRelationship objRelationship = (ObjRelationship) super.createObject(path);
            JpaDbRelationship relationship = (JpaDbRelationship) objRelationship
                    .getDbRelationships()
                    .get(0);

            if (relationship != null) {
                JpaOneToMany jpaRelationship = (JpaOneToMany) path.getObject();
                relationship.setMappedBy(jpaRelationship.getMappedBy());
                objRelationship.setMapKey(jpaRelationship.getMapKey());

                objRelationship.setCollectionType(jpaRelationship
                        .getPropertyDescriptor()
                        .getType().getName());
            }
            return relationship;
        }
    }

    class JpaOneToOneVisitor extends JpaRelationshipVisitor {

        @Override
        Object createObject(ProjectPath path) {
            ObjRelationship objRelationship = (ObjRelationship) super.createObject(path);
            JpaDbRelationship relationship = (JpaDbRelationship) objRelationship
                    .getDbRelationships()
                    .get(0);

            if (relationship != null) {
                JpaOneToOne jpaRelationship = (JpaOneToOne) path.getObject();
                relationship.setMappedBy(jpaRelationship.getMappedBy());
            }
            return relationship;
        }
    }

    class JpaManyToManyVisitor extends JpaRelationshipVisitor {

        @Override
        Object createObject(ProjectPath path) {
            ObjRelationship objRelationship = (ObjRelationship) super.createObject(path);
            JpaDbRelationship relationship = (JpaDbRelationship) objRelationship
                    .getDbRelationships()
                    .get(0);

            if (relationship != null) {
                JpaManyToMany jpaRelationship = (JpaManyToMany) path.getObject();
                relationship.setMappedBy(jpaRelationship.getMappedBy());
            }
            return relationship;
        }
    }

    abstract class JpaRelationshipVisitor extends NestedVisitor {

        JpaRelationshipVisitor() {
            addChildVisitor(JpaJoinColumn.class, new JpaJoinColumnVisitor());
        }

        @Override
        Object createObject(ProjectPath path) {

            JpaRelationship relationship = (JpaRelationship) path.getObject();

            ObjEntity cayenneSrcEntity = (ObjEntity) targetPath.getObject();
            ObjRelationship cayenneRelationship = new ObjRelationship(relationship
                    .getName());

            cayenneSrcEntity.addRelationship(cayenneRelationship);

            JpaEntity jpaTargetEntity = ((JpaEntityMap) path.getRoot())
                    .entityForClass(relationship.getTargetEntityName());

            if (jpaTargetEntity == null) {
                recordConflict(path, "Unknown target entity '"
                        + relationship.getTargetEntityName());
                return null;
            }

            cayenneRelationship.setTargetEntityName(jpaTargetEntity.getName());

            DbEntity cayenneSrcDbEntity = cayenneSrcEntity.getDbEntity();
            DbEntity cayenneTargetDbEntity = cayenneSrcEntity.getDataMap().getDbEntity(
                    jpaTargetEntity.getTable().getName());
            if (cayenneTargetDbEntity == null) {
                cayenneTargetDbEntity = new DbEntity(jpaTargetEntity.getTable().getName());
                cayenneSrcEntity.getDataMap().addDbEntity(cayenneTargetDbEntity);
            }

            JpaDbRelationship dbRelationship = new JpaDbRelationship(cayenneRelationship
                    .getName());
            dbRelationship.setTargetEntity(cayenneTargetDbEntity);
            dbRelationship.setToMany(relationship.isToMany());

            cayenneSrcDbEntity.addRelationship(dbRelationship);
            cayenneRelationship.addDbRelationship(dbRelationship);

            return cayenneRelationship;
        }
    }

    class JpaNamedQueryVisitor extends NestedVisitor {

        @Override
        Object createObject(ProjectPath path) {
            JpaNamedQuery jpaQuery = (JpaNamedQuery) path.getObject();
            JpaIndirectQuery cayenneQuery;

            JpaQueryHint hint = jpaQuery.getHint(QueryHints.QUERY_TYPE_HINT);
            if (hint != null && !Util.isEmptyString(hint.getValue())) {
                try {

                    // query class is not enhanced, so use normal class loader
                    Class cayenneQueryClass = Class.forName(hint.getValue(), true, Thread
                            .currentThread()
                            .getContextClassLoader());

                    if (!JpaIndirectQuery.class.isAssignableFrom(cayenneQueryClass)) {
                        recordConflict(path, "Unknown type for Cayenne query '"
                                + jpaQuery.getName()
                                + "': "
                                + cayenneQueryClass.getName());
                        return null;
                    }

                    cayenneQuery = (JpaIndirectQuery) cayenneQueryClass.newInstance();
                }
                catch (Exception e) {
                    recordConflict(path, "Problem while creating Cayenne query '"
                            + jpaQuery.getName()
                            + "', exception"
                            + e.getMessage());
                    return null;
                }
            }
            else {
                // by default use EJBQL query...
                cayenneQuery = new JpaEjbQLQuery();
            }

            cayenneQuery.setName(jpaQuery.getName());
            cayenneQuery.setJpaQuery(jpaQuery);

            DataMap parentMap = (DataMap) targetPath.firstInstanceOf(DataMap.class);

            ObjEntity parentEntity = (ObjEntity) targetPath
                    .firstInstanceOf(ObjEntity.class);
            if (parentEntity != null) {
                cayenneQuery.setParentEntity(parentEntity);
            }
            else {
                cayenneQuery.setParentMap(parentMap);
            }

            parentMap.addQuery(cayenneQuery);

            return cayenneQuery;
        }
    }

    class JpaTableVisitor extends NestedVisitor {

        @Override
        Object createObject(ProjectPath path) {

            JpaTable jpaTable = (JpaTable) path.getObject();
            ObjEntity parentCayenneEntity = (ObjEntity) targetPath.getObject();

            DbEntity cayenneEntity = parentCayenneEntity.getDataMap().getDbEntity(
                    jpaTable.getName());
            if (cayenneEntity == null) {
                cayenneEntity = new DbEntity(jpaTable.getName());
                parentCayenneEntity.getDataMap().addDbEntity(cayenneEntity);
            }

            cayenneEntity.setCatalog(jpaTable.getCatalog());
            cayenneEntity.setSchema(jpaTable.getSchema());

            parentCayenneEntity.setDbEntity(cayenneEntity);
            return cayenneEntity;
        }
    }

    /**
     * A superclass of visitors that need to push/pop processed object from the stack.
     */
    abstract class NestedVisitor extends BaseTreeVisitor {

        abstract Object createObject(ProjectPath path);

        @Override
        public boolean onStartNode(ProjectPath path) {
            Object object = createObject(path);

            if (object != null) {
                targetPath = targetPath.appendToPath(object);
                return true;
            }
            else {
                return false;
            }
        }

        @Override
        public void onFinishNode(ProjectPath path) {
            targetPath = targetPath.subpathWithSize(targetPath.getPath().length - 1);
        }
    }
}
