content: /*****************************************************************
   *   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
   *
   *    https://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.wocompat;
  
  import org.apache.cayenne.dba.TypesMapping;
  import org.apache.cayenne.dbsync.naming.NameBuilder;
  import org.apache.cayenne.exp.ExpressionException;
  import org.apache.cayenne.map.DataMap;
  import org.apache.cayenne.map.DbEntity;
  import org.apache.cayenne.map.DbJoin;
  import org.apache.cayenne.map.DbRelationship;
  import org.apache.cayenne.map.ObjEntity;
  import org.apache.cayenne.map.ObjRelationship;
  import org.apache.cayenne.map.QueryDescriptor;
  import org.apache.cayenne.map.SQLTemplateDescriptor;
  import org.apache.cayenne.map.SelectQueryDescriptor;
  import org.apache.cayenne.query.Ordering;
  import org.apache.cayenne.query.PrefetchTreeNode;
  import org.apache.cayenne.query.QueryMetadata;
  import org.apache.cayenne.query.SortOrder;
  import org.apache.cayenne.wocompat.parser.Parser;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.io.File;
  import java.io.InputStream;
  import java.net.URL;
  import java.sql.Types;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.StringTokenizer;
  import java.util.function.Predicate;
  import java.util.stream.Collectors;
  
  /**
   * Class for converting stored Apple EOModel mapping files to Cayenne DataMaps.
   */
  public class EOModelProcessor {
  
  	private static final Logger logger = LoggerFactory.getLogger(EOModelProcessor.class);
  
  	protected Predicate<String> prototypeChecker;
  
  	public EOModelProcessor() {
  		prototypeChecker = entityName -> {
              if (entityName == null) {
                  return false;
              }
              return entityName.startsWith("EO") && entityName.endsWith("Prototypes");
          };
  	}
  
  	/**
  	 * Returns index.eomodeld contents as a Map.
  	 * 
  	 * @since 4.0
  	 */
  	// TODO: refactor EOModelHelper to provide a similar method without loading
  	// all entity files in memory... here we simply copied stuff from
  	// EOModelHelper
  	public Map loadModeIndex(URL url) throws Exception {
  
  		String urlString = url.toExternalForm();
  
  		if (!urlString.endsWith(".eomodeld")) {
  			url = new URL(urlString + ".eomodeld");
  		}
  
  		Parser plistParser = new Parser();
  
  		try (InputStream in = new URL(url, "index.eomodeld").openStream();) {
  			plistParser.ReInit(in);
  			return (Map) plistParser.propertyList();
  		}
  	}
  
  	/**
  	 * Performs EOModel loading.
  	 * 
  	 * @param url
  	 *            URL of ".eomodeld" directory.
  	 */
  	public DataMap loadEOModel(URL url) throws Exception {
  		return loadEOModel(url, false);
  	}
  
  	/**
  	 * Performs EOModel loading.
  	 * 
  	 * @param url
  	 *            URL of ".eomodeld" directory.
  	 * @param generateClientClass
  	 *            if true then loading of EOModel is java client classes aware
  	 *            and the following processing will work with Java client class
  	 *            settings of the EOModel.
  	 */
  	public DataMap loadEOModel(URL url, boolean generateClientClass) throws Exception {
  		EOModelHelper helper = makeHelper(url);
  
  		// create empty map
  		DataMap dataMap = helper.getDataMap();
  
  		// process enitities ... throw out prototypes ... for now
  		List<String> modelNames = new ArrayList<>(helper.modelNamesAsList());
  		modelNames = modelNames.stream().filter(prototypeChecker.negate()).collect(Collectors.toList());
  		modelNames.forEach( name -> {
  			// create and register entity
  			makeEntity(helper, name, generateClientClass);
  		});
  
  		// now sort following inheritance hierarchy
  		modelNames.sort(new InheritanceComparator(dataMap));
  
  		// after all entities are loaded, process attributes
  		Iterator<String> it = modelNames.iterator();
  		while (it.hasNext()) {
  			String name = it.next();
  
  			EOObjEntity e = (EOObjEntity) dataMap.getObjEntity(name);
  			// process entity attributes
  			makeAttributes(helper, e);
  		}
  
  		// after all entities are loaded, process relationships
  		it = modelNames.iterator();
  		while (it.hasNext()) {
  			String name = (String) it.next();
  			makeRelationships(helper, dataMap.getObjEntity(name));
  		}
  
  		// after all normal relationships are loaded, process flattened
  		// relationships
  		it = modelNames.iterator();
  		while (it.hasNext()) {
  			String name = (String) it.next();
  			makeFlatRelationships(helper, dataMap.getObjEntity(name));
  		}
  
  		// now create missing reverse DB (but not OBJ) relationships
  		// since Cayenne requires them
  		it = modelNames.iterator();
  		while (it.hasNext()) {
  			String name = (String) it.next();
  			DbEntity dbEntity = dataMap.getObjEntity(name).getDbEntity();
  
  			if (dbEntity != null) {
  				makeReverseDbRelationships(dbEntity);
  			}
  		}
  
  		// build SelectQueries out of EOFetchSpecifications...
  		it = modelNames.iterator();
  		while (it.hasNext()) {
  			String name = (String) it.next();
  			Iterator queries = helper.queryNames(name);
  			while (queries.hasNext()) {
  				String queryName = (String) queries.next();
  				EOObjEntity entity = (EOObjEntity) dataMap.getObjEntity(name);
  				makeQuery(helper, entity, queryName);
  			}
  		}
  
  		return dataMap;
  	}
  
  	/**
  	 * Returns whether an Entity is an EOF EOPrototypes entity. According to EOF
  	 * conventions EOPrototypes and EO[Adapter]Prototypes entities are
  	 * considered to be prototypes.
  	 * 
  	 * @since 1.1
  	 */
  	protected boolean isPrototypesEntity(String entityName) {
  		return prototypeChecker.test(entityName);
  	}
  
  	/**
  	 * Creates an returns new EOModelHelper to process EOModel. Exists mostly
  	 * for the benefit of subclasses.
  	 */
  	protected EOModelHelper makeHelper(URL url) throws Exception {
  		return new EOModelHelper(url);
  	}
  
  	/**
  	 * Creates a Cayenne query out of EOFetchSpecification data.
  	 * 
  	 * @since 1.1
  	 */
  	protected QueryDescriptor makeQuery(EOModelHelper helper, EOObjEntity entity, String queryName) {
  
  		DataMap dataMap = helper.getDataMap();
  		Map queryPlist = helper.queryPListMap(entity.getName(), queryName);
  		if (queryPlist == null) {
  			return null;
  		}
  
  		QueryDescriptor query;
  		if (queryPlist.containsKey("hints")) { // just a predefined SQL query
  			query = makeEOSQLQueryDescriptor(entity, queryPlist);
  		} else {
  			query = makeEOQueryDescriptor(entity, queryPlist);
  		}
  		query.setName(entity.qualifiedQueryName(queryName));
  		dataMap.addQueryDescriptor(query);
  
  		return query;
  	}
  
  	protected QueryDescriptor makeEOQueryDescriptor(ObjEntity root, Map plistMap) {
  		SelectQueryDescriptor descriptor = QueryDescriptor.selectQueryDescriptor();
  		descriptor.setRoot(root);
  
  		descriptor.setDistinct("YES".equalsIgnoreCase((String) plistMap.get("usesDistinct")));
  
  		Object fetchLimit = plistMap.get("fetchLimit");
  		if (fetchLimit != null) {
  			try {
  				if (fetchLimit instanceof Number) {
  					descriptor.setProperty(QueryMetadata.FETCH_LIMIT_PROPERTY,
  							String.valueOf(((Number) fetchLimit).intValue()));
  				} else if (isNumeric(fetchLimit.toString())) {
  					descriptor.setProperty(QueryMetadata.FETCH_LIMIT_PROPERTY, fetchLimit.toString());
  				}
  			} catch (NumberFormatException nfex) {
  				// ignoring...
  			}
  		}
  
  		// sort orderings
  		List<Map<String, String>> orderings = (List<Map<String, String>>) plistMap.get("sortOrderings");
  		if (orderings != null && !orderings.isEmpty()) {
  			for (Map<String, String> ordering : orderings) {
  				boolean asc = !"compareDescending:".equals(ordering.get("selectorName"));
  				String key = ordering.get("key");
  				if (key != null) {
  					descriptor.addOrdering(new Ordering(key, asc ? SortOrder.ASCENDING : SortOrder.DESCENDING));
  				}
  			}
  		}
  
  		// qualifiers
  		Map<String, ?> qualifierMap = (Map<String, ?>) plistMap.get("qualifier");
  		if (qualifierMap != null && !qualifierMap.isEmpty()) {
  			descriptor.setQualifier(EOQuery.EOFetchSpecificationParser.makeQualifier((EOObjEntity) root, qualifierMap));
  		}
  
  		// prefetches
  		List prefetches = (List) plistMap.get("prefetchingRelationshipKeyPaths");
  		if (prefetches != null && !prefetches.isEmpty()) {
  			Iterator it = prefetches.iterator();
  			while (it.hasNext()) {
  				descriptor.addPrefetch((String) it.next(), PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
  			}
  		}
  
  		// data rows - note that we do not support fetching individual columns
  		// in the
  		// modeler...
  		if (plistMap.containsKey("rawRowKeyPaths")) {
  			descriptor.setProperty(QueryMetadata.FETCHING_DATA_ROWS_PROPERTY, String.valueOf(true));
  		}
  
  		return descriptor;
  	}
  
  	protected QueryDescriptor makeEOSQLQueryDescriptor(ObjEntity root, Map plistMap) {
  		SQLTemplateDescriptor descriptor = QueryDescriptor.sqlTemplateDescriptor();
  		descriptor.setRoot(root);
  
  		Object fetchLimit = plistMap.get("fetchLimit");
  		if (fetchLimit != null) {
  			try {
  				if (fetchLimit instanceof Number) {
  					descriptor.setProperty(QueryMetadata.FETCH_LIMIT_PROPERTY,
  							String.valueOf(((Number) fetchLimit).intValue()));
  				} else if (isNumeric(fetchLimit.toString())) {
  					descriptor.setProperty(QueryMetadata.FETCH_LIMIT_PROPERTY, fetchLimit.toString());
  				}
  			} catch (NumberFormatException nfex) {
  				// ignoring...
  			}
  		}
  
  		//query
  		// TODO: doesn't work with Stored Procedures.
  		Map hints = (Map) plistMap.get("hints");
  		if (hints != null && !hints.isEmpty()) {
  			String sqlExpression = (String) hints.get("EOCustomQueryExpressionHintKey");
  			if (sqlExpression != null) {
  				descriptor.setSql(sqlExpression);
  			}
  		}
  
  		return descriptor;
  	}
  
  	/**
  	 * Creates and returns a new ObjEntity linked to a corresponding DbEntity.
  	 */
  	protected EOObjEntity makeEntity(EOModelHelper helper, String name, boolean generateClientClass) {
  
  		DataMap dataMap = helper.getDataMap();
  		Map entityPlist = helper.entityPListMap(name);
  
  		// create ObjEntity
  		EOObjEntity objEntity = new EOObjEntity(name);
  		objEntity.setEoMap(entityPlist);
  		String parent = (String) entityPlist.get("parent");
  		objEntity.setClassName(helper.entityClass(name, generateClientClass));
  
  		if (parent != null) {
  			objEntity.setSubclass(true);
  			objEntity.setSuperClassName(helper.entityClass(parent, generateClientClass));
  		}
  
  		// add flag whether this entity is set as abstract in the model
  		objEntity.setAbstractEntity("Y".equals(entityPlist.get("isAbstractEntity")));
  
  		// create DbEntity...since EOF allows the same table to be
  		// associated with multiple EOEntities, check for name duplicates
  		String dbEntityName = (String) entityPlist.get("externalName");
  		if (dbEntityName != null) {
  
  			// ... if inheritance is involved and parent hierarchy uses the same
  			// DBEntity,
  			// do not create a DbEntity...
  			boolean createDbEntity = true;
  			if (parent != null) {
  				String parentName = parent;
  				while (parentName != null) {
  					Map parentData = helper.entityPListMap(parentName);
  					if (parentData == null) {
  						break;
  					}
  
  					String parentExternalName = (String) parentData.get("externalName");
  					if (parentExternalName == null) {
  						parentName = (String) parentData.get("parent");
  						continue;
  					}
  
  					if (dbEntityName.equals(parentExternalName)) {
  						createDbEntity = false;
  					}
  
  					break;
  				}
  			}
  
  			if (createDbEntity) {
  				int i = 0;
  				String dbEntityBaseName = dbEntityName;
  				while (dataMap.getDbEntity(dbEntityName) != null) {
  					dbEntityName = dbEntityBaseName + i++;
  				}
  
  				objEntity.setDbEntityName(dbEntityName);
  				DbEntity de = new DbEntity(dbEntityName);
  				dataMap.addDbEntity(de);
  			}
  		}
  
  		// set various flags
  		objEntity.setReadOnly("Y".equals(entityPlist.get("isReadOnly")));
  		objEntity.setSuperEntityName((String) entityPlist.get("parent"));
  
  		dataMap.addObjEntity(objEntity);
  
  		return objEntity;
  	}
  	
  	private static boolean externalTypeIsClob(String type) {
  		if( type == null ) return false;
  		return "CLOB".equalsIgnoreCase(type) || "TEXT".equalsIgnoreCase(type);
  	}
  
  	/**
  	 * Create ObjAttributes of the specified entity, as well as DbAttributes of
  	 * the corresponding DbEntity.
  	 */
  	protected void makeAttributes(EOModelHelper helper, EOObjEntity objEntity) {
  		Map entityPlistMap = helper.entityPListMap(objEntity.getName());
  		List primaryKeys = (List) entityPlistMap.get("primaryKeyAttributes");
  
  		List classProperties;
  		classProperties = (List) entityPlistMap.get("classProperties");
  
  		List attributes = (List) entityPlistMap.get("attributes");
  		DbEntity dbEntity = objEntity.getDbEntity();
  
  		if (primaryKeys == null) {
  			primaryKeys = Collections.EMPTY_LIST;
  		}
  
  		if (classProperties == null) {
  			classProperties = Collections.EMPTY_LIST;
  		}
  
  		if (attributes == null) {
  			attributes = Collections.EMPTY_LIST;
  		}
  
  		// detect single table inheritance
  		boolean singleTableInheritance = false;
  		String parentName = (String) entityPlistMap.get("parent");
  		while (parentName != null) {
  			Map parentData = helper.entityPListMap(parentName);
  			if (parentData == null) {
  				break;
  			}
  
  			String parentExternalName = (String) parentData.get("externalName");
  			if (parentExternalName == null) {
  				parentName = (String) parentData.get("parent");
  				continue;
  			}
  
  			if (dbEntity.getName() != null && dbEntity.getName().equals(parentExternalName)) {
  				singleTableInheritance = true;
  			}
  
  			break;
  		}
  
  		Iterator it = attributes.iterator();
  		while (it.hasNext()) {
  			Map attrMap = (Map) it.next();
  
  			String prototypeName = (String) attrMap.get("prototypeName");
  			Map prototypeAttrMap = helper.getPrototypeAttributeMapFor(prototypeName);
  
  			String dbAttrName = getStringValueFromMap( "columnName", attrMap, prototypeAttrMap );
  			String attrName = getStringValueFromMap( "name", attrMap, prototypeAttrMap );
  			String attrType = getStringValueFromMap( "valueClassName", attrMap, prototypeAttrMap );
  			String valueType = getStringValueFromMap( "valueType", attrMap, prototypeAttrMap );
  			String externalType = getStringValueFromMap( "externalType", attrMap, prototypeAttrMap );
  
  			String javaType = helper.javaTypeForEOModelerType(attrType, valueType);
  			EODbAttribute dbAttr = null;
  
  			if (dbAttrName != null && dbEntity != null) {
  
  				// if inherited attribute, skip it for DbEntity...
  				if (!singleTableInheritance || dbEntity.getAttribute(dbAttrName) == null) {
  
  					// create DbAttribute...since EOF allows the same column name for
  					// more than one Java attribute, we need to check for name duplicates
  					int i = 0;
  					String dbAttributeBaseName = dbAttrName;
  					while (dbEntity.getAttribute(dbAttrName) != null) {
  						dbAttrName = dbAttributeBaseName + i++;
  					}
  
  					int sqlType = TypesMapping.getSqlTypeByJava(javaType);
  					int width = getInt("width", attrMap, prototypeAttrMap, -1);
  					if (sqlType == Types.VARCHAR && width < 0 && externalTypeIsClob(externalType)) {
  						// CLOB, or TEXT as PostgreSQL calls it, is usally noted as having no width. In order to
  						// not mistake any VARCHAR columns that just happen to have no width set in the model
  						// for CLOB columns, use externalType as an additional check.
  						sqlType = Types.CLOB;
  					} else if(sqlType == TypesMapping.NOT_DEFINED && externalType != null) {
  						// At this point we usually hit a custom Java class through a prototype, which isn't resolvable
  						// with the model alone. But we can use the externalType as a hint. If that still doesn't match
  						// anything, sqlType will still be NOT_DEFINED.
  						sqlType = TypesMapping.getSqlTypeByName(externalType.toUpperCase());
  					}
  					dbAttr = new EODbAttribute(dbAttrName, sqlType, dbEntity);
  					dbAttr.setEoAttributeName(attrName);
  					dbEntity.addAttribute(dbAttr);
  
  					if (width >= 0) {
  						dbAttr.setMaxLength(width);
  					}
  
  					int scale = getInt("scale", attrMap, prototypeAttrMap, -1);
  					if (scale >= 0) {
  						dbAttr.setScale(scale);
  					}
  
  					if (primaryKeys.contains(attrName))
  						dbAttr.setPrimaryKey(true);
  
  					Object allowsNull = attrMap.get("allowsNull");
  					// TODO: Unclear that allowsNull should be inherited from EOPrototypes
  					// if (null == allowsNull) allowsNull = prototypeAttrMap.get("allowsNull");;
  
  					dbAttr.setMandatory(!"Y".equals(allowsNull));
  				}
  			}
  
  			if (classProperties.contains(attrName)) {
  				EOObjAttribute attr = new EOObjAttribute(attrName, javaType, objEntity);
  
  				// set readOnly flag of Attribute if either attribute is read or
  				// if entity is readOnly
  				String entityReadOnlyString = (String) entityPlistMap.get("isReadOnly");
  				String attributeReadOnlyString = (String) attrMap.get("isReadOnly");
  				if ("Y".equals(entityReadOnlyString) || "Y".equals(attributeReadOnlyString)) {
  					attr.setReadOnly(true);
  				}
  
  				// set name instead of the actual attribute, as it may be
  				// inherited....
  				attr.setDbAttributePath(dbAttrName);
  				objEntity.addAttribute(attr);
  			}
  		}
  	}
  
  	private String getStringValueFromMap( String key, Map attrMap, Map prototypeAttrMap ) {
  		String dbAttrName = (String) attrMap.get(key);
  		if (null == dbAttrName) {
  			dbAttrName = (String) prototypeAttrMap.get(key);
  		}
  		return dbAttrName;
  	}
  
  	int getInt(String key, Map map, Map prototypes, int defaultValue) {
  
  		Object value = map.get(key);
  		if (value == null) {
  			value = prototypes.get(key);
  		}
  
  		if (value == null) {
  			return defaultValue;
  		}
  
  		// per CAY-752, value can be a String or a Number, so handle both
  		if (value instanceof Number) {
  			return ((Number) value).intValue();
  		} else {
  			try {
  				return Integer.parseInt(value.toString());
  			} catch (NumberFormatException nfex) {
  				return defaultValue;
  			}
  		}
  	}
  
  	/**
  	 * Create ObjRelationships of the specified entity, as well as
  	 * DbRelationships of the corresponding DbEntity.
  	 */
  	protected void makeRelationships(EOModelHelper helper, ObjEntity objEntity) {
  		Map entityPlistMap = helper.entityPListMap(objEntity.getName());
  		List classProps = (List) entityPlistMap.get("classProperties");
  		List rinfo = (List) entityPlistMap.get("relationships");
  
  		Collection attributes = (Collection) entityPlistMap.get("attributes");
  
  		if (rinfo == null) {
  			return;
  		}
  
  		if (classProps == null) {
  			classProps = Collections.EMPTY_LIST;
  		}
  
  		if (attributes == null) {
  			attributes = Collections.EMPTY_LIST;
  		}
  
  		DbEntity dbSrc = objEntity.getDbEntity();
  		Iterator it = rinfo.iterator();
  		while (it.hasNext()) {
  			Map relMap = (Map) it.next();
  			String targetName = (String) relMap.get("destination");
  
  			// ignore flattened relationships for now
  			if (targetName == null) {
  				continue;
  			}
  
  			String relName = (String) relMap.get("name");
  			boolean toMany = "Y".equals(relMap.get("isToMany"));
  			boolean toDependentPK = "Y".equals(relMap.get("propagatesPrimaryKey"));
  			ObjEntity target = helper.getDataMap().getObjEntity(targetName);
  
  			// target maybe null for cross-EOModel relationships
  			// ignoring those now.
  			if (target == null) {
  				continue;
  			}
  
  			DbEntity dbTarget = target.getDbEntity();
  			Map targetPlistMap = helper.entityPListMap(targetName);
  			Collection targetAttributes = (Collection) targetPlistMap.get("attributes");
  			DbRelationship dbRel = null;
  
  			// process underlying DbRelationship
  			// Note: there is no flattened rel. support here....
  			// Note: source maybe null, e.g. an abstract entity.
  			if (dbSrc != null && dbTarget != null) {
  
  				// in case of inheritance EOF stores duplicates of all inherited
  				// relationships, so we must skip this relationship in DB entity
  				// if it is
  				// already there...
  
  				dbRel = dbSrc.getRelationship(relName);
  				if (dbRel == null) {
  
  					dbRel = new DbRelationship();
  					dbRel.setSourceEntity(dbSrc);
  					dbRel.setTargetEntityName(dbTarget);
  					dbRel.setToMany(toMany);
  					dbRel.setName(relName);
  					dbRel.setToDependentPK(toDependentPK);
  					dbSrc.addRelationship(dbRel);
  
  					List joins = (List) relMap.get("joins");
  					Iterator jIt = joins.iterator();
  					while (jIt.hasNext()) {
  						Map joinMap = (Map) jIt.next();
  
  						DbJoin join = new DbJoin(dbRel);
  
  						// find source attribute dictionary and extract the
  						// column name
  						String sourceAttributeName = (String) joinMap.get("sourceAttribute");
  						join.setSourceName(columnName(attributes, sourceAttributeName));
  
  						String targetAttributeName = (String) joinMap.get("destinationAttribute");
  
  						join.setTargetName(columnName(targetAttributes, targetAttributeName));
  						dbRel.addJoin(join);
  					}
  				}
  			}
  
  			// only create obj relationship if it is a class property
  			if (classProps.contains(relName)) {
  				ObjRelationship rel = new ObjRelationship();
  				rel.setName(relName);
  				rel.setSourceEntity(objEntity);
  				rel.setTargetEntityName(target);
  				objEntity.addRelationship(rel);
  
  				if (dbRel != null) {
  					rel.addDbRelationship(dbRel);
  				}
  			}
  		}
  	}
  
  	/**
  	 * Create reverse DbRelationships that were not created so far, since
  	 * Cayenne requires them.
  	 * 
  	 * @since 1.0.5
  	 */
  	protected void makeReverseDbRelationships(DbEntity dbEntity) {
  		if (dbEntity == null) {
  			throw new NullPointerException("Attempt to create reverse relationships for the null DbEntity.");
  		}
  
  		// iterate over a copy of the collection, since in case of
  		// reflexive relationships, we may modify source entity relationship map
  
  		for (DbRelationship relationship : new ArrayList<>(dbEntity.getRelationships())) {
  
  			if (relationship.getReverseRelationship() == null) {
  				DbRelationship reverse = relationship.createReverseRelationship();
  				reverse.setName(NameBuilder.builder(reverse, reverse.getSourceEntity())
  						// TODO: we can do better with ObjectNameGenerator
  						.baseName(relationship.getName() + "Reverse")
  						.name());
  				relationship.getTargetEntity().addRelationship(reverse);
  			}
  		}
  	}
  
  	/**
  	 * Create Flattened ObjRelationships of the specified entity.
  	 */
  	protected void makeFlatRelationships(EOModelHelper helper, ObjEntity e) {
  		Map info = helper.entityPListMap(e.getName());
  		List rinfo = (List) info.get("relationships");
  		if (rinfo == null) {
  			return;
  		}
  
  		Iterator it = rinfo.iterator();
  		while (it.hasNext()) {
  			Map relMap = (Map) it.next();
  			String targetPath = (String) relMap.get("definition");
  
  			// ignore normal relationships
  			if (targetPath == null) {
  				continue;
  			}
  
  			ObjRelationship flatRel = new ObjRelationship();
  			flatRel.setName((String) relMap.get("name"));
  			flatRel.setSourceEntity(e);
  
  			try {
  				flatRel.setDbRelationshipPath(targetPath);
  			} catch (ExpressionException ex) {
  				logger.warn("Invalid relationship: " + targetPath);
  				continue;
  			}
  
  			// find target entity
  			Map entityInfo = info;
  			StringTokenizer toks = new StringTokenizer(targetPath, ".");
  			while (toks.hasMoreTokens() && entityInfo != null) {
  				String pathComponent = toks.nextToken();
  
  				// get relationship info and reset entityInfo, so that we could
  				// use
  				// entityInfo state as an indicator of valid flat relationship
  				// enpoint
  				// outside the loop
  				Collection relationshipInfo = (Collection) entityInfo.get("relationships");
  				entityInfo = null;
  
  				if (relationshipInfo == null) {
  					break;
  				}
  
  				Iterator rit = relationshipInfo.iterator();
  				while (rit.hasNext()) {
  					Map pathRelationship = (Map) rit.next();
  					if (pathComponent.equals(pathRelationship.get("name"))) {
  						String targetName = (String) pathRelationship.get("destination");
  						entityInfo = helper.entityPListMap(targetName);
  						break;
  					}
  				}
  			}
  
  			if (entityInfo != null) {
  				flatRel.setTargetEntityName((String) entityInfo.get("name"));
  			}
  
  			e.addRelationship(flatRel);
  		}
  	}
  
  	/**
  	 * Locates an attribute map matching the name and returns column name for
  	 * this attribute.
  	 * 
  	 * @since 1.1
  	 */
  	String columnName(Collection entityAttributes, String attributeName) {
  		if (attributeName == null) {
  			return null;
  		}
  
  		Iterator it = entityAttributes.iterator();
  		while (it.hasNext()) {
  			Map map = (Map) it.next();
  			if (attributeName.equals(map.get("name"))) {
  				return (String) map.get("columnName");
  			}
  		}
  
  		return null;
  	}
  
  	static boolean isNumeric(String str) {
  		if (str == null) {
  			return false;
  		}
  
  		for(int i = 0; i < str.length(); ++i) {
  			if (!Character.isDigit(str.charAt(i))) {
  				return false;
  			}
  		}
  		return true;
  	}
  
  	// sorts ObjEntities so that subentities in inheritance hierarchy are shown
  	// last
  	final class InheritanceComparator implements Comparator {
  
  		DataMap dataMap;
  
  		InheritanceComparator(DataMap dataMap) {
  			this.dataMap = dataMap;
  		}
  
  		@Override
  		public int compare(Object o1, Object o2) {
  			if (o1 == null) {
  				return o2 != null ? -1 : 0;
  			} else if (o2 == null) {
  				return 1;
  			}
  
  			String name1 = o1.toString();
  			String name2 = o2.toString();
  
  			ObjEntity e1 = dataMap.getObjEntity(name1);
  			ObjEntity e2 = dataMap.getObjEntity(name2);
  
  			return compareEntities(e1, e2);
  		}
  
  		int compareEntities(ObjEntity e1, ObjEntity e2) {
  			if (e1 == null) {
  				return e2 != null ? -1 : 0;
  			} else if (e2 == null) {
  				return 1;
  			}
  
  			// entity goes first if it is a direct or indirect superentity of
  			// another
  			// one
  			if (e1.isSubentityOf(e2)) {
  				return 1;
  			}
  
  			if (e2.isSubentityOf(e1)) {
  				return -1;
  			}
  
  			// sort alphabetically
  			return e1.getName().compareTo(e2.getName());
  		}
  	}
  }
  