/***************************************************************** * 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.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.QueryMetadata; import org.apache.cayenne.query.SortOrder; import org.apache.cayenne.wocompat.parser.Parser; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.collections.PredicateUtils; import org.apache.commons.lang.StringUtils; 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; /** * 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 prototypeChecker; public EOModelProcessor() { prototypeChecker = new Predicate() { @Override public boolean evaluate(Object object) { if (object == null) { return false; } String entityName = object.toString(); return entityName.startsWith("EO") && entityName.endsWith("Prototypes"); } }; } /** * @deprecated since 4.0 in favor of {@link #loadModeIndex(URL)}. */ @Deprecated public Map loadModeIndex(String path) throws Exception { return loadModeIndex(new File(path).toURI().toURL()); } /** * 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(); } } /** * @deprecated since 4.0 in favor of {@link #loadEOModel(URL)}. */ @Deprecated public DataMap loadEOModel(String path) throws Exception { return loadEOModel(path, false); } /** * @deprecated since 4.0 in favor of {@link #loadEOModel(URL, boolean)}. */ @Deprecated public DataMap loadEOModel(String path, boolean generateClientClass) throws Exception { return loadEOModel(new File(path).toURI().toURL(), generateClientClass); } /** * 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 modelNames = new ArrayList(helper.modelNamesAsList()); CollectionUtils.filter(modelNames, PredicateUtils.notPredicate(prototypeChecker)); Iterator it = modelNames.iterator(); while (it.hasNext()) { String name = (String) it.next(); // create and register entity makeEntity(helper, name, generateClientClass); } // now sort following inheritance hierarchy Collections.sort(modelNames, new InheritanceComparator(dataMap)); // after all entities are loaded, process attributes it = modelNames.iterator(); while (it.hasNext()) { String name = (String) 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.evaluate(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 (StringUtils.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()); } } // 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 (StringUtils.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); objEntity.setServerOnly(!generateClientClass); 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; if (objEntity.isServerOnly()) { classProperties = (List) entityPlistMap.get("classProperties"); } else { classProperties = (List) entityPlistMap.get("clientClassProperties"); } 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; } // 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()); } } }