// // ERXEOAccessUtilities.java // ERExtensions // // Created by Max Muller on Sat Feb 22 2003. // package er.extensions.eof; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.apache.log4j.Level; import org.apache.log4j.Logger; import com.webobjects.appserver.WOSession; import com.webobjects.eoaccess.EOAdaptorChannel; import com.webobjects.eoaccess.EOAdaptorOperation; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EODatabase; import com.webobjects.eoaccess.EODatabaseChannel; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EODatabaseOperation; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOEntityClassDescription; import com.webobjects.eoaccess.EOGeneralAdaptorException; import com.webobjects.eoaccess.EOJoin; import com.webobjects.eoaccess.EOModel; import com.webobjects.eoaccess.EOModelGroup; import com.webobjects.eoaccess.EOObjectNotAvailableException; import com.webobjects.eoaccess.EOProperty; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOSQLExpression; import com.webobjects.eoaccess.EOSQLExpressionFactory; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOAndQualifier; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOFaultHandler; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOGlobalID; import com.webobjects.eocontrol.EOKeyGlobalID; import com.webobjects.eocontrol.EOKeyValueQualifier; import com.webobjects.eocontrol.EOObjectStoreCoordinator; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSLog; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSSet; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation._NSDelegate; import com.webobjects.jdbcadaptor.JDBCPlugIn; import er.extensions.appserver.ERXSession; import er.extensions.eof.listener.ERXEOExecutionListenerDumbImpl; import er.extensions.eof.listener.IERXEOExecutionListener; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXDictionaryUtilities; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXThreadStorage; import er.extensions.foundation.ERXUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.jdbc.ERXSQLHelper; import er.extensions.statistics.ERXStats; import er.extensions.statistics.ERXStats.Group; /** * Collection of EOAccess related utilities. * * EOAccess provides the data access mechanisms for the Enterprise Objects technology. */ public class ERXEOAccessUtilities { /** logging support */ public static final Logger log = Logger.getLogger(ERXEOAccessUtilities.class); /** SQL logger */ private static Logger sqlLoggingLogger = null; private static final AtomicReference<IERXEOExecutionListener> listener = new AtomicReference<>(new ERXEOExecutionListenerDumbImpl()); public static void setListener(IERXEOExecutionListener aListener) { listener.set(aListener); } /** * Finds an entity that is contained in a string. This is used a lot in * DirectToWeb. Example: "ListAllStudios" => Studio * * @param ec * editing context * @param string * string to look into * @return found entity or <code>null</code> */ public static EOEntity entityMatchingString(EOEditingContext ec, String string) { EOEntity result = null; if (string != null) { String lowerCaseName = string.toLowerCase(); EOModelGroup group = modelGroup(ec); NSArray<String> entityNames = (NSArray<String>) ERXUtilities.entitiesForModelGroup(group).valueForKeyPath("name.toLowerCase"); NSMutableArray<String> possibleEntities = new NSMutableArray<>(); for (String lowercaseEntityName : entityNames) { if (lowerCaseName.indexOf(lowercaseEntityName) != -1) possibleEntities.addObject(lowercaseEntityName); } if (possibleEntities.count() == 1) { result = ERXUtilities.caseInsensitiveEntityNamed(possibleEntities.lastObject()); } else if (possibleEntities.count() > 1) { ERXArrayUtilities.sortArrayWithKey(possibleEntities, "length"); if (possibleEntities.objectAtIndex(0).length() == possibleEntities.lastObject().length()) log.warn("Found multiple entities of the same length for string: " + string + " possible entities: " + possibleEntities); result = ERXUtilities.caseInsensitiveEntityNamed(possibleEntities.lastObject()); } if (log.isDebugEnabled()) log.debug("Found possible entities: " + possibleEntities + " for string: " + string + " result: " + result); } return result; } /** * Finds an entity that is associated with the table name. When inheritance is used, * will return the least derived entity using that table. This can be used to deal * with database exceptions where you only have the table name to go on. As multiple * entities can map to a single table, the results of this method are inexact. * * @param ec * editing context * @param tableName * table (external) name to find an entity for * @return found entity or <code>null</code> */ public static EOEntity entityUsingTable(EOEditingContext ec, String tableName) { EOEntity result = null; NSMutableArray possibleEntities = new NSMutableArray(); if (tableName != null) { NSArray entities = ERXUtilities.entitiesForModelGroup(modelGroup(ec)); tableName = tableName.toLowerCase(); for (Enumeration e = entities.objectEnumerator(); e.hasMoreElements();) { EOEntity entity = (EOEntity)e.nextElement(); if (entity.externalName() != null) { String lowercaseTableName = entity.externalName().toLowerCase(); if (tableName.equals(lowercaseTableName)) { // Prefer the parent entity as long as it is using the same table EOEntity root = entity; while (root.parentEntity() != null && lowercaseTableName.equalsIgnoreCase(root.parentEntity().externalName())) root = root.parentEntity(); if (! possibleEntities.containsObject(root)) possibleEntities.addObject(root); } } } if (possibleEntities.count() > 0) { result = (EOEntity) possibleEntities.lastObject(); } if (log.isEnabledFor(Level.WARN) && possibleEntities.count() > 1) log.warn("Found multiple entities: " + possibleEntities.valueForKey("name") + " for table name: " + tableName); if (log.isDebugEnabled()) log.debug("Found possible entities: " + possibleEntities.valueForKey("name") + " for table name: " + tableName + " result: " + result); } return result; } /** * Method used to determine if a given entity is a shared entity. * * @param ec * editing context * @param entityName * name of the entity * @return if the entity is a shared entity * @throws IllegalStateException if the entityName provided is null */ public static boolean entityWithNamedIsShared(EOEditingContext ec, String entityName) { if (entityName == null) throw new IllegalStateException("Entity name argument is null for method: entityWithNamedIsShared"); EOEntity entity = entityNamed(ec, entityName); return entity != null && entity.sharedObjectFetchSpecificationNames().count() > 0; } /** * Convenience method to get the next unique ID from a sequence. * * @param ec * editing context * @param modelName * name of the model which connects to the database that has the * sequence in it * @param sequenceName * name of the sequence * @return next value in the sequence * @deprecated */ // ENHANCEME: Need a non-oracle specific way of doing this. Should poke // around at // the adaptor level and see if we can't find something better. @Deprecated public static Number getNextValFromSequenceNamed(EOEditingContext ec, String modelName, String sequenceName) { EODatabaseContext dbContext = EOUtilities.databaseContextForModelNamed(ec, modelName); return ERXSQLHelper.newSQLHelper(dbContext).getNextValFromSequenceNamed(ec, modelName, sequenceName); } /** * Utility method used to execute arbitrary SQL. This has the advantage over * the {@link com.webobjects.eoaccess.EOUtilities EOUtilities} * <code>rawRowsForSQL</code> in that it can be used with other statements * besides just SELECT without throwing exceptions. * * @param ec * editing context that determines which model group and database * context to use. * @param entityName * name of an entity in the model connected to the database you * wish to execute SQL against * @param exp * SQL expression */ // ENHANCEME: Should support the use of bindings // ENHANCEME: Could also support the option of using a seperate EOF stack so // as to execute // sql in a non-blocking fashion. public static void evaluateSQLWithEntityNamed(EOEditingContext ec, String entityName, String exp) { EOEntity entity = EOUtilities.entityNamed(ec, entityName); evaluateSQLWithEntity(ec, entity, exp); } /** * Utility method used to execute arbitrary SQL. This has the advantage over * the {@link com.webobjects.eoaccess.EOUtilities EOUtilities} * <code>rawRowsForSQL</code> in that it can be used with other statements * besides just SELECT without throwing exceptions. * * @param ec * editing context that determines which model group and database * context to use. * @param entity * an entity in the model connected to the database you wish to * execute SQL against * @param exp * SQL expression */ // ENHANCEME: Should support the use of bindings // ENHANCEME: Could also support the option of using a seperate EOF stack so // as to execute // sql in a non-blocking fashion. public static void evaluateSQLWithEntity(EOEditingContext ec, EOEntity entity, String exp) { EODatabaseContext dbContext = EODatabaseContext.registeredDatabaseContextForModel(entity.model(), ec); dbContext.lock(); try { EOAdaptorChannel adaptorChannel = dbContext.availableChannel().adaptorChannel(); if (!adaptorChannel.isOpen()) { adaptorChannel.openChannel(); } EOSQLExpressionFactory factory = adaptorChannel.adaptorContext().adaptor().expressionFactory(); if (ERXEOAccessUtilities.log.isInfoEnabled()) { ERXEOAccessUtilities.log.info("Executing " + exp); } // If channel.evaluateExpression throws when committing, it won't close the JDBC transaction // Probably a bug in JDBCChannel, but we must take care of it boolean contextHadOpenTransaction = adaptorChannel.adaptorContext().hasOpenTransaction(); try { adaptorChannel.evaluateExpression(factory.expressionForString(exp)); } catch (EOGeneralAdaptorException e) { if (adaptorChannel.adaptorContext().hasOpenTransaction() && ! contextHadOpenTransaction) { adaptorChannel.adaptorContext().rollbackTransaction(); } throw e; } } finally { dbContext.unlock(); } } /** * Creates the SQL which is used by the provides EOFetchSpecification. * * @param ec * the EOEditingContext * @param spec * the EOFetchSpecification in question * * @return the SQL which the EOFetchSpecification would use */ public static String sqlForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec) { return sqlExpressionForFetchSpecification(ec, spec, 0, -1).statement(); } /** * Returns the raw rows for the given EOSQLExpression. When possible, * you should use the variant of this method that requires you to pass * in the array of EOAttributes you are fetching. If you do not pass in * attributes, this will use channel.describeResults(), which can produce * attributes that may not be able to be faulted back into EO's because * of case mismatches. * * @param ec * the EOEditingContext * @param modelName * the name of the model in question * @param expression * the EOSQLExpression in question * * @return array of dictionaries */ public static NSArray<NSDictionary> rawRowsForSQLExpression(EOEditingContext ec, String modelName, EOSQLExpression expression) { EOModelGroup modelGroup = EOUtilities.modelGroup(ec); EOModel model = modelGroup.modelNamed(modelName); return ERXEOAccessUtilities.rawRowsForSQLExpression(ec, model, expression, null); } /** * Returns the raw rows for the given EOSQLExpression. * * @param ec * the EOEditingContext * @param model * the model in question * @param expression * the EOSQLExpression to fetch with * @param attributes the attributes to fetch * * @return array of dictionaries */ public static NSArray rawRowsForSQLExpression(EOEditingContext ec, EOModel model, EOSQLExpression expression, NSArray<EOAttribute> attributes) { NSArray results = NSArray.EmptyArray; EODatabaseContext dbc = EODatabaseContext.registeredDatabaseContextForModel(model, ec); dbc.lock(); try { results = _rawRowsForSQLExpression(dbc, expression, attributes); } catch (Exception localException) { if (dbc._isDroppedConnectionException(localException)) { try { dbc.database().handleDroppedConnection(); results = _rawRowsForSQLExpression(dbc, expression, attributes); } catch(Exception ex) { throw NSForwardException._runtimeExceptionForThrowable(ex); } } else { throw NSForwardException._runtimeExceptionForThrowable(localException); } } finally { dbc.unlock(); } return results; } private static NSArray _rawRowsForSQLExpression(EODatabaseContext dbc, EOSQLExpression expression, NSArray<EOAttribute> attributes) { NSMutableArray results = null; EOAdaptorChannel channel = dbc.availableChannel().adaptorChannel(); if (!channel.isOpen()) channel.openChannel(); // If channel.evaluateExpression throws when committing, it won't close the JDBC transaction // Probably a bug in JDBCChannel, but we must take care of it boolean contextHadOpenTransaction = channel.adaptorContext().hasOpenTransaction(); try { channel.evaluateExpression(expression); } catch (EOGeneralAdaptorException e) { if (channel.adaptorContext().hasOpenTransaction() && ! contextHadOpenTransaction) { channel.adaptorContext().rollbackTransaction(); } throw e; } if (attributes == null) { channel.setAttributesToFetch(channel.describeResults()); } else { channel.setAttributesToFetch(attributes); } try { results = new NSMutableArray(); NSDictionary row; while ((row = channel.fetchRow()) != null) results.addObject(row); } catch (EOGeneralAdaptorException ex) { channel.cancelFetch(); throw ex; } return results; } /** * Creates the SQL which is used by the provided EOFetchSpecification, * limited by the given range. * * @param ec * the EOEditingContext * @param spec * the EOFetchSpecification in question * @param start * start of rows to fetch * @param end * end of rows to fetch (-1 if not used) * * @return the EOSQLExpression which the EOFetchSpecification would use */ public static EOSQLExpression sqlExpressionForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec, long start, long end) { EOSQLExpression expression = null; EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, spec.entityName()); EOModel model = entity.model(); EODatabaseContext dbc = EODatabaseContext.registeredDatabaseContextForModel(model, ec); dbc.lock(); try { ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(ec, model.name()); expression = sqlHelper.sqlExpressionForFetchSpecification(ec, spec, start, end); } catch (Exception e) { throw NSForwardException._runtimeExceptionForThrowable(e); } finally { dbc.unlock(); } return expression; } /** * Returns the number of rows the supplied EOFetchSpecification would * return. * * @param ec * the EOEditingContext * @param spec * the EOFetchSpecification in question * @return the number of rows */ public static int rowCountForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec) { EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, spec.entityName()); if (entity == null) throw new java.lang.IllegalStateException("entity could not be found for name \""+spec.entityName()+"\". Checked EOModelGroup for loaded models."); EOModel model = entity.model(); EODatabaseContext dbc = EODatabaseContext.registeredDatabaseContextForModel(model, ec); int results = 0; dbc.lock(); try { results = ERXSQLHelper.newSQLHelper(ec, model.name()).rowCountForFetchSpecification(ec, spec); } catch (Exception localException) { if (dbc._isDroppedConnectionException(localException)) { try { dbc.database().handleDroppedConnection(); results = ERXSQLHelper.newSQLHelper(ec, model.name()).rowCountForFetchSpecification(ec, spec); } catch (Exception ex) { throw NSForwardException._runtimeExceptionForThrowable(ex); } } else { throw NSForwardException._runtimeExceptionForThrowable(localException); } } finally { dbc.unlock(); } return results; } /** * Similar to the helper in EOUtilities, but allows for <code>null</code> editingContext. * If ec is <code>null</code>, it will try to get at the session via thread storage and * use its defaultEditingContext. This is here now so we can remove the * delegate in ERXApplication. * * @param ec * editing context used to locate the model group (can be <code>null</code>) * @return the model group associated with the editing context's root object store */ public static EOModelGroup modelGroup(EOEditingContext ec) { if (ec == null && !ERXThreadStorage.wasInheritedFromParentThread()) { // this can be problematic, if called from a background thread with ERXThreadStorage.useInheritableThreadLocal=true, // which is the default. In my case it was called indirectly from ERXEntityClassDescription.Factory.classDescriptionNeededForEntityName // resulting in locking problems WOSession s = ERXSession.anySession(); if (s != null) { ec = s.defaultEditingContext(); } } EOModelGroup group; if (ec == null) { group = EOModelGroup.defaultGroup(); } else { group = EOModelGroup.modelGroupForObjectStoreCoordinator((EOObjectStoreCoordinator) ec.rootObjectStore()); } return group; } /** * Similar to the helper in EOUtilities, but allows for <code>null</code> editingContext. * * @param ec * editing context used to locate the model group (can be <code>null</code>) * @param entityName * entity name * @return the entity with the specified name */ public static EOEntity entityNamed(EOEditingContext ec, String entityName) { EOModelGroup modelGroup = modelGroup(ec); return modelGroup.entityNamed(entityName); } /** * Creates an aggregate integer attribute for a given function name. These can then * be used to query on when using raw rows. * * @param ec * editing context used to locate the model group * @param function * name of the function MAX, MIN, etc * @param attributeName * name of the attribute * @param entityName * name of the entity * @return aggregate function attribute */ public static EOAttribute createAggregateAttribute(EOEditingContext ec, String function, String attributeName, String entityName) { return ERXEOAccessUtilities.createAggregateAttribute(ec, function, attributeName, entityName, Number.class, "i"); } /** * Creates an aggregate attribute for a given function name. These can then * be used to query on when using raw rows. * * @param ec * editing context used to locate the model group * @param function * name of the function MAX, MIN, etc * @param attributeName * name of the attribute * @param entityName * name of the entity * @param valueClass the java class of this attribute's values * @param valueType the EOAttribute value type * @return aggregate function attribute */ public static EOAttribute createAggregateAttribute(EOEditingContext ec, String function, String attributeName, String entityName, Class valueClass, String valueType) { return ERXEOAccessUtilities.createAggregateAttribute(ec, function, attributeName, entityName, valueClass, valueType, null, null); } /** * Creates an aggregate attribute for a given function name. These can then * be used to query on when using raw rows. * * @param ec * editing context used to locate the model group * @param function * name of the function MAX, MIN, etc * @param attributeName * name of the attribute * @param entityName * name of the entity * @param aggregateName the name to assign to the aggregate column in the query * @param valueClass the java class of this attribute's values * @param valueType the EOAttribute value type * @return aggregate function attribute */ public static EOAttribute createAggregateAttribute(EOEditingContext ec, String function, String attributeName, String entityName, Class valueClass, String valueType, String aggregateName) { return ERXEOAccessUtilities.createAggregateAttribute(ec, function, attributeName, entityName, valueClass, valueType, aggregateName, null); } /** * Creates an aggregate attribute for a given function name. These can then * be used to query on when using raw rows. * * @param ec * editing context used to locate the model group * @param function * name of the function MAX, MIN, etc * @param attributeName * name of the attribute * @param entityName * name of the entity * @param aggregateName the name to assign to the aggregate column in the query * @param valueClass the java class of this attribute's values * @param valueType the EOAttribute value type * @param entityTableAlias the "t0"-style name of the attribute in this query (or null for "t0") * @return aggregate function attribute */ public static EOAttribute createAggregateAttribute(EOEditingContext ec, String function, String attributeName, String entityName, Class valueClass, String valueType, String aggregateName, String entityTableAlias) { return createAggregateAttribute(ec, function, attributeName, entityName, valueClass, valueType, aggregateName, entityTableAlias, false); } /** * Creates an aggregate attribute for a given function name. These can then * be used to query on when using raw rows. * * @param ec * editing context used to locate the model group * @param function * name of the function MAX, MIN, etc * @param attributeName * name of the attribute * @param entityName * name of the entity * @param aggregateName * the name to assign to the aggregate column in the query * @param valueClass * the java class of this attribute's values * @param valueType * the EOAttribute value type * @param entityTableAlias * the "t0"-style name of the attribute in this query (or null for "t0") * @param usesDistinct * <code>true</code> if function should be used on distinct values * @return aggregate function attribute */ public static EOAttribute createAggregateAttribute(EOEditingContext ec, String function, String attributeName, String entityName, Class valueClass, String valueType, String aggregateName, String entityTableAlias, boolean usesDistinct) { if (function == null) { throw new IllegalStateException("Function is null."); } if (attributeName == null) { throw new IllegalStateException("Attribute name is null."); } if (entityName == null) { throw new IllegalStateException("Entity name is null."); } EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); if (entity == null) { throw new IllegalStateException("Unable find entity named: " + entityName); } EOAttribute attribute = entity.attributeNamed(attributeName); if (attribute == null) { throw new IllegalStateException("Unable find attribute named: " + attributeName + " for entity: " + entityName); } EOAttribute aggregate = new EOAttribute(); if (aggregateName != null) { aggregate.setName(aggregateName); aggregate.setColumnName(aggregateName); } else { aggregate.setName("p_object" + function + "Attribute"); aggregate.setColumnName("p_object" + function + "Attribute"); } aggregate.setClassName(valueClass.getName()); if (valueType != null) { aggregate.setValueType(valueType); } else { aggregate.setValueType(attribute.valueType()); } // MS: This "t0." is totally wrong, but it is required. It should be dynamically // generated, but this function doesn't have an EOSQLExpression to operate on if (entityTableAlias == null) { entityTableAlias = "t0"; } aggregate.setReadFormat(ERXSQLHelper.newSQLHelper(entity.model()).readFormatForAggregateFunction(function, entityTableAlias + "." + attribute.columnName(), aggregateName, usesDistinct)); return aggregate; } /** * Oracle 9 has a maximum length of 30 characters for table names, column names and constraint names. * Foreign key constraint names are defined like this from the plugin: * <p> * TABLENAME_FOEREIGNKEYNAME_FK * <p> * The whole statement looks like this: * <p> * ALTER TABLE [TABLENAME] ADD CONSTRAINT [CONSTRAINTNAME] FOREIGN KEY ([FK]) REFERENCES [DESTINATION_TABLE] ([PK]) DEFERRABLE INITIALLY DEFERRED * <p> * THIS means that the tablename and the columnname together cannot * be longer than 26 characters. * <p> * This method checks each foreign key constraint name and if it is longer than 30 characters it is replaced * with a unique name. * * @param entities * a NSArray containing the entities for which create table * statements should be generated or <code>null</code> if all entities in the * model should be used. * @param modelName * the name of the EOModel * @param optionsCreate * @return a <code>String</code> containing SQL statements to create * tables * * @see er.extensions.jdbc.ERXSQLHelper#createSchemaSQLForEntitiesInModelWithNameAndOptions(NSArray, String, NSDictionary) */ public static String createSchemaSQLForEntitiesInModelWithNameAndOptionsForOracle9(NSArray entities, String modelName, NSDictionary optionsCreate) { EODatabaseContext dc = EOUtilities.databaseContextForModelNamed(ERXEC.newEditingContext(), modelName); return ERXSQLHelper.newSQLHelper(dc).createSchemaSQLForEntitiesInModelWithNameAndOptions(entities, modelName, optionsCreate); } /** * Creates SQL to create tables for the specified Entities. This can be used * with EOUtilities rawRowsForSQL method to create the tables. * * @param entities * a NSArray containing the entities for which create table * statements should be generated or null if all entities in the * model should be used. * @param modelName * the name of the EOModel * @param optionsCreate * a NSDictionary containing the different options. Possible keys * are * <ul> * <li>EOSchemaGeneration.DropTablesKey</li> * <li>EOSchemaGeneration.DropPrimaryKeySupportKey</li> * <li>EOSchemaGeneration.CreateTablesKey</li> * <li>EOSchemaGeneration.CreatePrimaryKeySupportKey</li> * <li>EOSchemaGeneration.PrimaryKeyConstraintsKey</li> * <li>EOSchemaGeneration.ForeignKeyConstraintsKey</li> * <li>EOSchemaGeneration.CreateDatabaseKey</li> * <li>EOSchemaGeneration.DropDatabaseKey</li> * </ul> * Possible values are <code>YES</code> and <code>NO</code> * * @return a <code>String</code> containing SQL statements to create * tables * @deprecated */ @Deprecated public static String createSchemaSQLForEntitiesInModelWithNameAndOptions(NSArray entities, String modelName, NSDictionary optionsCreate) { EODatabaseContext dc = EOUtilities.databaseContextForModelNamed(ERXEC.newEditingContext(), modelName); return ERXSQLHelper.newSQLHelper(dc).createSchemaSQLForEntitiesInModelWithNameAndOptions(entities, modelName, optionsCreate); } /** * Creates the schema sql for a set of entities. * * @param entities the entities to create sql for * @param databaseContext the database context to use * @param optionsCreate the options (@see createSchemaSQLForEntitiesInModelWithNameAndOptions) * @return a sql script * @deprecated */ @Deprecated public static String createSchemaSQLForEntitiesWithOptions(NSArray entities, EODatabaseContext databaseContext, NSDictionary optionsCreate) { return ERXSQLHelper.newSQLHelper(databaseContext).createSchemaSQLForEntitiesWithOptions(entities, databaseContext, optionsCreate); } /** * creates SQL to create tables for the specified Entities. This can be used * with EOUtilities rawRowsForSQL method to create the tables. * * @param entities * a NSArray containing the entities for which create table * statements should be generated or null if all entities in the * model should be used. * @param modelName * the name of the EOModel<p>This method uses the * following defaults options: * <ul> * <li>EOSchemaGeneration.DropTablesKey=YES</li> * <li>EOSchemaGeneration.DropPrimaryKeySupportKey=YES</li> * <li>EOSchemaGeneration.CreateTablesKey=YES</li> * <li>EOSchemaGeneration.CreatePrimaryKeySupportKey=YES</li> * <li>EOSchemaGeneration.PrimaryKeyConstraintsKey=YES</li> * <li>EOSchemaGeneration.ForeignKeyConstraintsKey=YES</li> * <li>EOSchemaGeneration.CreateDatabaseKey=NO</li> * <li>EOSchemaGeneration.DropDatabaseKey=NO</li> * </ul> * Possible values are <code>YES</code> and <code>NO</code> * * @return a <code>String</code> containing SQL statements to create * tables * @deprecated */ @Deprecated public static String createSchemaSQLForEntitiesInModelWithName(NSArray entities, String modelName) { EODatabaseContext databaseContext = EOUtilities.databaseContextForModelNamed(ERXEC.newEditingContext(), modelName); return ERXSQLHelper.newSQLHelper(databaseContext).createSchemaSQLForEntitiesInModelWithName(entities, modelName); } /** * creates SQL to create tables for the specified Entities. This can be used * with EOUtilities rawRowsForSQL method to create the tables. * * @param entities * a NSArray containing the entities for which create table * statements should be generated or null if all entities in the * model should be used. * @param databaseContext * the databaseContext * * @param create if true, tables and keys are created * @param drop if true, tables and keys are dropped * @return a <code>String</code> containing SQL statements to create * tables * @deprecated */ @Deprecated public static String createSchemaSQLForEntitiesInDatabaseContext(NSArray entities, EODatabaseContext databaseContext, boolean create, boolean drop) { return ERXSQLHelper.newSQLHelper(databaseContext).createSchemaSQLForEntitiesInDatabaseContext(entities, databaseContext, create, drop); } /** * @deprecated */ @Deprecated public static String createIndexSQLForEntitiesForOracle(NSArray entities) { NSMutableArray a = new NSMutableArray(); a.addObject("BLOB"); a.addObject("CLOB"); return createIndexSQLForEntities(entities, a); } /** * @deprecated */ @Deprecated public static String createIndexSQLForEntities(NSArray entities) { return createIndexSQLForEntities(entities, null); } /** * @deprecated */ @Deprecated public static String createIndexSQLForEntities(NSArray entities, NSArray externalTypesToIgnore) { EOEntity ent = (EOEntity)entities.objectAtIndex(0); String modelName = ent.model().name(); return ERXSQLHelper.newSQLHelper(ERXEC.newEditingContext(), modelName).createIndexSQLForEntities(entities, externalTypesToIgnore); } public static boolean entityUsesSeparateTable(EOEntity entity) { if (entity.parentEntity() == null) return true; EOEntity parent = entity.parentEntity(); while (parent != null) { if (!entity.externalName().equals(parent.externalName())) return true; entity = parent; parent = entity.parentEntity(); } return false; } public static EOAttribute attributeWithColumnNameFromEntity(String columnName, EOEntity entity) { for (Enumeration e = entity.attributes().objectEnumerator(); e.hasMoreElements();) { EOAttribute att = (EOAttribute)e.nextElement(); if (columnName.equalsIgnoreCase(att.columnName())) { return att; } } return null; } /** * Returns true if the exception is an optimistic locking exception. * * @param e * the exception as recieved from saveChanges() * @return true if the error could be handled. */ public static boolean isOptimisticLockingFailure(EOGeneralAdaptorException e) { boolean wasHandled = false; NSDictionary userInfo = e.userInfo(); if(userInfo != null) { String eType = (String)userInfo.objectForKey(EOAdaptorChannel.AdaptorFailureKey); if (EOAdaptorChannel.AdaptorOptimisticLockingFailure.equals(eType)) { EOAdaptorOperation adaptorOp = (EOAdaptorOperation) userInfo.objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey); EODatabaseOperation databaseOp = (EODatabaseOperation) userInfo.objectForKey(EODatabaseContext.FailedDatabaseOperationKey); wasHandled = (adaptorOp != null && databaseOp != null); } else { log.error("Missing EOFailedAdaptorOperationKey or EOFailedDatabaseOperationKey in " + e + ": " + userInfo); } } return wasHandled; } /** * <span class="ja"> * 例外エラーが重複エラーの場合は true を戻します。 * * @param e - saveChanges() から受けた例外エラーそのまま * * @return エラーが処理できた場合は true * </span> */ public static boolean isUniqueFailure(EOGeneralAdaptorException e) { boolean wasHandled = false; NSDictionary userInfo = e.userInfo(); if(userInfo != null) { EOAdaptorOperation adaptorOp = (EOAdaptorOperation) userInfo.objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey); wasHandled = adaptorOp.toString().contains("UNIQUE"); if (!wasHandled) { log.error("UNIQUE Integrity constraint violation " + e + ": " + userInfo); } } return wasHandled; } /** * Given an array of EOs, returns snapshot dictionaries for the given * related objects. */ //CHECKME ak is this description correct? public static NSArray snapshotsForObjectsFromRelationshipNamed(NSArray eos, String relKey) { NSMutableArray result = new NSMutableArray(); if (eos.count() > 0) { EOEnterpriseObject eo = (EOEnterpriseObject)eos.lastObject(); String entityName = eo.entityName(); EOEditingContext ec = eo.editingContext(); EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); EORelationship relationship = entity.relationshipNamed(relKey); if(relationship.sourceAttributes().count() == 1) { EOAttribute attribute = relationship.sourceAttributes().lastObject(); EODatabaseContext context = EOUtilities.databaseContextForModelNamed(ec, entity.model().name()); String name = attribute.name(); for (Enumeration e = eos.objectEnumerator(); e.hasMoreElements();) { EOEnterpriseObject target = (EOEnterpriseObject) e.nextElement(); Object value = (context.snapshotForGlobalID(ec.globalIDForObject(target))).valueForKey(name); result.addObject(value); } } else { throw new IllegalArgumentException("Has more than one relationship attribute: " + relKey); } } return result; } /** * Utility method to generate a new primary key dictionary using the adaptor * for a given entity. This is can be handy if you need to have a primary * key for an object before it is saved to the database. This method uses * the same method that EOF uses by default for generating primary keys. See * {@link ERXGeneratesPrimaryKeyInterface}for more information about using * a newly created dictionary as the primary key for an enterprise object. * * @param ec * editing context * @param entityName * name of the entity to generate the primary key dictionary for. * @return a dictionary containing a new primary key for the given entity. */ public static NSDictionary primaryKeyDictionaryForEntity(EOEditingContext ec, String entityName) { EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName); EODatabaseContext dbContext = EODatabaseContext.registeredDatabaseContextForModel(entity.model(), ec); NSDictionary primaryKey = null; dbContext.lock(); try { EOAdaptorChannel adaptorChannel = dbContext.availableChannel().adaptorChannel(); if (!adaptorChannel.isOpen()) { adaptorChannel.openChannel(); } NSArray arr = adaptorChannel.primaryKeysForNewRowsWithEntity(1, entity); if (arr != null) { primaryKey = (NSDictionary) arr.lastObject(); } else { log.warn("Could not get primary key for entity: " + entityName + " exception"); } } catch (Exception e) { log.error("Caught exception when generating primary key for entity: " + entityName + " exception: " + e, e); } finally { dbContext.unlock(); } return primaryKey; } /** * Creates an array containing all of the primary keys of the given objects. * * @param eos * array of enterprise objects * @return array of primary keys */ public static NSArray primaryKeysForObjects(NSArray<? extends EOEnterpriseObject> eos) { NSMutableArray result = new NSMutableArray(); if (eos != null) { for (EOEnterpriseObject target : eos) { NSDictionary pKey = EOUtilities.primaryKeyForObject(target.editingContext(), target); result.addObject(pKey.allValues().objectAtIndex(0)); } } return result; } /** * Crude hack to get at the end of a relationship path. * @param relationship */ public static EORelationship lastRelationship(EORelationship relationship) { return (EORelationship) NSKeyValueCoding.Utility.valueForKey(relationship, "lastRelationship"); } /** * Creates an array of relationships and attributes from the given keypath to give to the * EOSQLExpression method <code>sqlStringForAttributePath</code>. If the last element is a * relationship, then the relationship's source attribute will get chosen. As such, this can only * work for single-value relationships in the last element. * @param entity base entity * @param keyPath key path * @return array of EOProperties that make up the given key path */ public static NSArray<EOProperty> attributePathForKeyPath(EOEntity entity, String keyPath) { NSMutableArray<EOProperty> result = new NSMutableArray<>(); String[] parts = keyPath.split("\\."); String part; for (int i = 0; i < parts.length - 1; i++) { part = parts[i]; EORelationship relationship = entity.anyRelationshipNamed(part); if(relationship == null) { // CHECKME AK: it would probably be better to return null // to indicate that this is not a valid path? return NSArray.EmptyArray; } entity = relationship.destinationEntity(); result.addObject(relationship); } part = parts[parts.length-1]; EOAttribute attribute = entity.anyAttributeNamed(part); if(attribute == null) { EORelationship relationship = entity.anyRelationshipNamed(part); if(relationship == null) { throw new IllegalArgumentException("Last element is not an attribute nor a relationship: " + keyPath); } if (relationship.isFlattened()) { NSArray<EOProperty> path = attributePathForKeyPath(entity, relationship.definition()); result.addObjectsFromArray(path); return result; } attribute = relationship.joins().lastObject().sourceAttribute(); } result.addObject(attribute); return result; } /** * Creates a where clause string " someKey IN ( someValue1,...)". Can migrate keyPaths. * @deprecated */ @Deprecated public static String sqlWhereClauseStringForKey(EOSQLExpression e, String key, NSArray valueArray) { return ERXSQLHelper.newSQLHelper(e).sqlWhereClauseStringForKey(e, key, valueArray); } /** * Returns the database context for an EO. * * @param eo the EO to get a database context for * @return the eo's database context */ public static EODatabaseContext databaseContextForObject(EOEnterpriseObject eo) { EOEditingContext editingContext = eo.editingContext(); EOObjectStoreCoordinator osc = (EOObjectStoreCoordinator) editingContext.rootObjectStore(); EODatabaseContext databaseContext = (EODatabaseContext) osc.objectStoreForObject(eo); return databaseContext; } /** * Returns the database context for the given entity in the given * EOObjectStoreCoordinator * * @param entityName entity name * @param osc the object store coordinator * @return database context */ public static EODatabaseContext databaseContextForEntityNamed(EOObjectStoreCoordinator osc, String entityName) { EOModel model = EOModelGroup.modelGroupForObjectStoreCoordinator(osc).entityNamed(entityName).model(); EODatabaseContext dbc = EODatabaseContext.registeredDatabaseContextForModel(model, osc); return dbc; } /** * Closes the (JDBC) Connection from all database channels for the specified * EOObjectStoreCoordinator * * @param osc * the EOObjectStoreCoordinator from which the (JDBC)Connections * should be closed * @return <code>true</code> if all connections have been closed */ public static boolean closeDatabaseConnections(EOObjectStoreCoordinator osc) { boolean couldClose = true; try { int i, contextCount, j, channelCount; NSArray databaseContexts; databaseContexts = osc.cooperatingObjectStores(); contextCount = databaseContexts.count(); for (i = contextCount; i-- > 0;) { NSArray channels = ((EODatabaseContext) databaseContexts.objectAtIndex(i)).registeredChannels(); channelCount = channels.count(); for (j = channelCount; j-- > 0;) { EODatabaseChannel dbch = (EODatabaseChannel) channels.objectAtIndex(j); if (!dbch.adaptorChannel().adaptorContext().hasOpenTransaction()) { dbch.adaptorChannel().closeChannel(); } else { log.warn("could not close Connection from " + dbch + " because its EOAdaptorContext " + dbch.adaptorChannel().adaptorContext() + " had open Transactions"); couldClose = false; } } } } catch (Exception e) { log.error("could not close all Connections, reason:", e); couldClose = false; } return couldClose; } private static Set<String> _keysWithWarning = Collections.synchronizedSet(new HashSet<String>()); /** * Returns the last entity for the given key path. If the path is empty or <code>null</code>, returns the given entity. * @param entity * @param keyPath * @return entity object */ public static EOEntity destinationEntityForKeyPath(EOEntity entity, String keyPath) { if(keyPath == null || keyPath.length() == 0) { return entity; } NSArray<String> keyArray = NSArray.componentsSeparatedByString(keyPath, "."); for(String key : keyArray) { EORelationship rel = entity.anyRelationshipNamed(key); if(rel == null) { if(entity.anyAttributeNamed(key) == null) { if(key.indexOf("@") != 0) { if(!_keysWithWarning.contains(key + "-" + entity)) { _keysWithWarning.add(key + "-" + entity); log.warn("No relationship or attribute <" + key + "> in entity: " + entity); } } } return null; } entity = rel.destinationEntity(); } return entity; } /** Returns the EOEntity for the provided EOEnterpriseObject if one exists * * @param eo the EOEnterpriseObject * @return the EOEntity from the EOEnterpriseObject */ public static EOEntity entityForEo(EOEnterpriseObject eo) { EOClassDescription classDesc = eo.classDescription(); if (classDesc instanceof EOEntityClassDescription) return ((EOEntityClassDescription)classDesc).entity(); return null; } public static NSArray classPropertiesNotInParent(EOEntity entity, boolean includeAttributes, boolean includeToOneRelationships, boolean includeToManyRelationships) { Object parent = entity.parentEntity(); if (parent == null) { return NSArray.EmptyArray; } NSMutableArray ret = new NSMutableArray(); NSArray parentAttributeNames = (NSArray) entity.parentEntity().attributes().valueForKey("name"); NSArray attributes = entity.attributes(); NSArray cpNames = entity.classPropertyNames(); if (includeAttributes) { for (int i = attributes.count(); i-- > 0;) { EOAttribute att = (EOAttribute) attributes.objectAtIndex(i); String name = att.name(); if (cpNames.containsObject(name) && !parentAttributeNames.containsObject(name)) { ret.addObject(att); } } } NSArray parentRelationships = (NSArray) entity.parentEntity().relationships().valueForKey("name"); NSArray relationships = entity.relationships(); for (int i = relationships.count(); i-- > 0;) { EORelationship element = (EORelationship) relationships.objectAtIndex(i); if ((element.isToMany() && includeToManyRelationships) || (!element.isToMany() && includeToOneRelationships)) { String name = element.name(); if (cpNames.containsObject(name) && !parentRelationships.containsObject(name)) { ret.addObject(element); } } } return ret; } public static NSArray externalNamesForEntity(EOEntity entity, boolean includeParentEntities) { if (includeParentEntities) { entity = rootEntityForEntity(entity); } NSMutableArray entityNames = new NSMutableArray(); if (entity.subEntities().count() > 0) { for (Enumeration it = entity.subEntities().objectEnumerator(); it.hasMoreElements();) { EOEntity entity1 = (EOEntity) it.nextElement(); NSArray names = externalNamesForEntity(entity1, includeParentEntities); entityNames.addObjectsFromArray(names); } } entityNames.addObject(entity.externalName()); return ERXArrayUtilities.arrayWithoutDuplicates(entityNames); } public static NSArray externalNamesForEntityNamed(String entityName, boolean includeParentEntities) { return externalNamesForEntity(EOModelGroup.defaultGroup().entityNamed(entityName), includeParentEntities); } /** * Walks all of the parentEntity relationships to * find the root entity. * @param entity to find the root parent * @return root parent entity */ public static EOEntity rootEntityForEntity(EOEntity entity) { while (entity.parentEntity() != null) { entity = entity.parentEntity(); } return entity; } /** * Walks all of the parentEntity relationships to * find the root entity. * @param entityName to find the root parent * @return root parent entity */ public static EOEntity rootEntityForEntityNamed(String entityName) { return rootEntityForEntity(EOModelGroup.defaultGroup().entityNamed(entityName)); } public static void logExpression(EOAdaptorChannel channel, EOSQLExpression expression, long startTime) { if (sqlLoggingLogger == null) { sqlLoggingLogger = Logger.getLogger("er.extensions.ERXAdaptorChannelDelegate.sqlLogging"); } // sqlLoggingLogger.setLevel(Level.DEBUG); String entityMatchPattern = ERXProperties.stringForKeyWithDefault( "er.extensions.ERXAdaptorChannelDelegate.trace.entityMatchPattern", ".*"); long millisecondsNeeded = System.currentTimeMillis() - startTime; String entityName = (expression.entity() != null ? expression.entity().name() : "Unknown"); if (entityName.matches(entityMatchPattern)) { long debugMilliseconds = ERXProperties.longForKeyWithDefault( "er.extensions.ERXAdaptorChannelDelegate.trace.milliSeconds.debug", 0); long infoMilliseconds = ERXProperties.longForKeyWithDefault("er.extensions.ERXAdaptorChannelDelegate.trace.milliSeconds.info", 100); long warnMilliseconds = ERXProperties.longForKeyWithDefault("er.extensions.ERXAdaptorChannelDelegate.trace.milliSeconds.warn", 1000); long errorMilliseconds = ERXProperties.longForKeyWithDefault( "er.extensions.ERXAdaptorChannelDelegate.trace.milliSeconds.error", 5000); int maxLength = ERXProperties.intForKeyWithDefault("er.extensions.ERXAdaptorChannelDelegate.trace.maxLength", 3000); boolean needsLog = false; if (millisecondsNeeded > errorMilliseconds) { needsLog = true; } else if (millisecondsNeeded > warnMilliseconds) { needsLog = true; } else if (millisecondsNeeded > infoMilliseconds) { if (sqlLoggingLogger.isInfoEnabled()) { needsLog = true; } } else if (millisecondsNeeded > debugMilliseconds) { if (sqlLoggingLogger.isDebugEnabled()) { needsLog = true; } } if (ERXStats.isTrackingStatistics()) { String statement = expression.statement(); // AK: special postgres data PK handling statement = statement.replaceAll("decode\\(.*?\\)", "?"); // IN's can be quite long and are normally not bound statement = statement.replaceAll(" IN \\(.*?\\)", " IN ([removed])"); statement = statement.replaceAll("([a-zA-Z0-9_\\.]+)\\s+IN \\(.*?\\)(\\s+OR\\s+\\1\\s+IN \\(.*?\\))+", " IN ([multi removed])"); // the cols are always the same, replace with * statement = statement.replaceAll("((t0|T0)\\.[a-zA-Z0-9_]+\\,\\s*)*(t0|T0)\\.[a-zA-Z0-9_\\.]+\\s+FROM\\s+", "t0.* FROM "); ERXStats.addDurationForKey(millisecondsNeeded, Group.SQL, entityName + ": " +statement); } listener.get().log(millisecondsNeeded, entityName); if (needsLog) { String logString = createLogString(channel, expression, millisecondsNeeded); if (logString.length() > maxLength) { logString = logString.substring(0, maxLength); } if (millisecondsNeeded > errorMilliseconds) { sqlLoggingLogger.error(logString, new RuntimeException("Statement running too long")); } else if (millisecondsNeeded > warnMilliseconds) { sqlLoggingLogger.warn(logString); } else if (millisecondsNeeded > infoMilliseconds) { if (sqlLoggingLogger.isInfoEnabled()) { sqlLoggingLogger.info(logString); } } else if (millisecondsNeeded > debugMilliseconds) { if (sqlLoggingLogger.isDebugEnabled()) { sqlLoggingLogger.debug(logString); } } } } } public static String createLogString(EOAdaptorChannel channel, EOSQLExpression expression, long millisecondsNeeded) { StringBuilder sb = new StringBuilder(); String entityName = (expression.entity() != null ? expression.entity().name() : "Unknown"); sb.append("\"").append(entityName).append("\"@").append(channel.adaptorContext().hashCode()); sb.append(" expression took ").append(millisecondsNeeded).append(" ms: ").append(expression.statement()); NSArray<NSDictionary<String, ? extends Object>> variables = expression.bindVariableDictionaries(); int cnt = variables != null ? variables.count() : 0; if (cnt > 0) { sb.append(" withBindings: "); for (int i = 0; i < cnt; i++) { NSDictionary<String, ? extends Object> nsdictionary = variables.objectAtIndex(i); Object obj = nsdictionary.valueForKey("BindVariableValue"); String attributeName = (String) nsdictionary.valueForKey("BindVariableName"); if (obj instanceof String) { obj = EOSQLExpression.sqlStringForString((String) obj); } else if (obj instanceof Number) { obj = EOSQLExpression.sqlStringForNumber((Number) obj); } else if (obj instanceof NSTimestamp) { obj = obj.toString(); } else if (obj instanceof NSData) { // ak: this is just for logging, however we would // like to get readable data // in particular for PKs and with postgres this // works. // plain EOF is broken, though try { if (((NSData) obj).length() < 50) { obj = expression.sqlStringForData((NSData) obj); } } catch (ArrayIndexOutOfBoundsException ex) { // ignore, this is a bug in EOF } if (obj instanceof NSData) { // produces very yucky output obj = obj.toString(); } } else { if (expression.entity() != null) { EOAttribute attribute = expression.entity().anyAttributeNamed(attributeName); if (attribute != null) { obj = expression.formatValueForAttribute(obj, attribute); } } } if (i != 0) sb.append(", "); sb.append(i + 1); sb.append(':'); sb.append(obj); sb.append('['); sb.append(attributeName); sb.append(']'); } } return sb.toString(); } /** * Creates an AND qualifier of EOKeyValueQualifiers for every keypath in the given array of attributes. * * @author ak * @param attributes * @param values * @return qualifier. EOAndQualifier for 2 or more elements in <code>attributes</code>, EOKeyValueQualifier for a single element <code>attributes</code> array, <code>null</code> when <code>attributes</code> is null or empty. */ public static EOQualifier qualifierFromAttributes(NSArray<EOAttribute> attributes, NSDictionary values) { EOQualifier result = null; if (attributes != null && attributes.count() > 0) { NSMutableArray<EOQualifier> qualifiers = new NSMutableArray<>(); for (EOAttribute key : attributes) { Object value = values.objectForKey(key.name()); qualifiers.addObject(new EOKeyValueQualifier(key.name(), EOQualifier.QualifierOperatorEqual, value)); } // Don't wrap in an AND qualifier if there is only one qualifier result = (qualifiers.count() == 1 ? qualifiers.objectAtIndex(0) : new EOAndQualifier(qualifiers)); } return result; } /** * Filters a list of relationships for only the ones that * have a given EOAttribute as a source attribute. * @param entity * @param attrib EOAttribute to filter source attributes of * relationships. * @return filtered array of EORelationship objects that have * the given attribute as the source attribute. */ public static NSArray<EORelationship> relationshipsForAttribute(EOEntity entity, EOAttribute attrib) { NSMutableArray<EORelationship> arr = new NSMutableArray<>(); for (EORelationship rel : entity.relationships()) { NSArray<EOAttribute> attribs = rel.sourceAttributes(); if (attribs.containsObject(attrib)) { arr.addObject(rel); } } return arr; } /** * Returns the source attribute for the relationship named relationshipName on entity. Assumes there is only one join. * * @param entity EOEntity to find relationship on * @param relationshipName name of relationship on entity * @return source attribute for the relationship */ public static EOAttribute sourceAttributeForRelationship(EOEntity entity, String relationshipName) { EORelationship relationship = entity.relationshipNamed(relationshipName); return sourceAttributeForRelationship(relationship); } /** * Returns the source attribute for the relationship. Assumes there is only one join. * * @param relationship relationship on entity to return source attribute for * @return source attribute for the relationship */ public static EOAttribute sourceAttributeForRelationship(EORelationship relationship) { EOJoin join = relationship.joins().objectAtIndex(0); return join.sourceAttribute(); } /** * Returns the external column name for the source attribute for the relationship named relationshipName on entity. Assumes there is only one join. * This is intend to support the *RowsDescribedByQualifier methods when used with relationships. * * @param entity EOEntity to find relationship on * @param relationshipName name of relationship on entity * @return the external column name for the source attribute for the relationship */ public static String sourceColumnForRelationship(EOEntity entity, String relationshipName) { EORelationship relationship = entity.relationshipNamed(relationshipName); return sourceColumnForRelationship(relationship); } /** * Returns the external column name for the source attribute for the relationship named relationshipName on entity. Assumes there is only one join. * This is intend to support the *RowsDescribedByQualifier methods when used with relationships. * * @param relationship relationship on entity to return the external column name for the source attribute for * @return the external column name for the source attribute for the relationship */ public static String sourceColumnForRelationship(EORelationship relationship) { return sourceAttributeForRelationship(relationship).columnName(); } public static EOEnterpriseObject refetchFailedObject(EOEditingContext ec, EOGeneralAdaptorException e) { EOAdaptorOperation adaptorOp = (EOAdaptorOperation) e.userInfo().objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey); EODatabaseOperation databaseOp = (EODatabaseOperation) e.userInfo().objectForKey(EODatabaseContext.FailedDatabaseOperationKey); NSDictionary dbSnapshot = databaseOp.dbSnapshot(); EOEntity entity = adaptorOp.entity(); String entityName = entity.name(); EOGlobalID gid = entity.globalIDForRow(dbSnapshot); EOEnterpriseObject eo = ec.faultForGlobalID(gid, ec); // EOUtilities.databaseContextForModelNamed(ec, eo.entityName()).forgetSnapshotForGlobalID(gid); ec.refaultObject(eo); // NOTE AK: I think we can just return the object here, // as the next time it is accessed the fault will get NSArray primaryKeyAttributes = entity.primaryKeyAttributes(); EOQualifier qualifier = ERXEOAccessUtilities.qualifierFromAttributes(primaryKeyAttributes, dbSnapshot); EOFetchSpecification fs = new EOFetchSpecification(entityName, qualifier, null); fs.setRefreshesRefetchedObjects(true); NSArray objs = ec.objectsWithFetchSpecification(fs); eo = null; if (objs.count() == 1) { eo = (EOEnterpriseObject) objs.objectAtIndex(0); if (log.isDebugEnabled()) { log.debug("failedEO: "+ eo); } } else if(objs.count() == 0) { throw new EOObjectNotAvailableException("Can't recover: Object was deleted: " + fs); } else { throw new EOUtilities.MoreThanOneException("Can't recover: More than one object found: " + objs); } return eo; } /** * Method used to apply a set of changes to a re-fetched eo. * This method is used to re-apply changes to a given eo after * it has been refetched. * * @param eo enterprise object to have the changes re-applied to. * @param e */ public static void reapplyChanges(EOEnterpriseObject eo, EOGeneralAdaptorException e) { EOAdaptorOperation adaptorOp = (EOAdaptorOperation) e.userInfo().objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey); NSDictionary changedValues = adaptorOp.changedValues(); EOEntity entity = ERXEOAccessUtilities.entityForEo(eo); EOEditingContext ec = eo.editingContext(); NSArray keys = changedValues.allKeys(); NSMutableSet relationships = new NSMutableSet(); for (int i=0; i<keys.count(); i++) { String key = (String)keys.objectAtIndex(i); EOAttribute attrib = entity.attributeNamed(key); if (attrib != null) { Object val = changedValues.objectForKey(key); if(ERXValueUtilities.isNull(val)) { val = null; } if (entity.classProperties().containsObject(attrib)) { eo.takeValueForKey(val, key); } NSArray relsUsingAttrib = ERXEOAccessUtilities.relationshipsForAttribute(entity, attrib); relationships.addObjectsFromArray(relsUsingAttrib); } else { log.error("Changed value found that isn't an attribute: " + key + "->" + changedValues.objectForKey(key)); } } for (Enumeration enumerator = relationships.objectEnumerator(); enumerator.hasMoreElements();) { EORelationship relationship = (EORelationship) enumerator.nextElement(); NSMutableDictionary pk = EOUtilities.destinationKeyForSourceObject(ec, eo, relationship.name()).mutableClone(); for (int i=0; i<keys.count(); i++) { String key = (String)keys.objectAtIndex(i); if(pk.objectForKey(key) != null) { Object val = changedValues.objectForKey(key); pk.setObjectForKey(val, key); } } EOEntity destEnt = relationship.destinationEntity(); EOGlobalID gid = destEnt.globalIDForRow(pk); if(gid != null) { EOEnterpriseObject destEO = ec.faultForGlobalID(gid, ec); eo.takeValueForKey(destEO, relationship.name()); } else { throw new NullPointerException("Gid is null: " + pk); } } } /** * Deals with the nitty-gritty of direct row manipulation by * correctly opening, closing, locking and unlocking the * needed EOF objects for direct row manipulation. Wraps the * actions in a transaction. * The API is not really finalized, if someone has a better idea * he's welcome to change it. * @author ak */ public static abstract class ChannelAction { protected abstract int doPerform(EOAdaptorChannel channel); public int perform(EOEditingContext ec, String modelName) { boolean wasOpen = true; EOAdaptorChannel channel = null; int rows = 0; ec.lock(); try { EODatabaseContext dbc = EOUtilities.databaseContextForModelNamed(ec, modelName); dbc.lock(); try { channel = dbc.availableChannel().adaptorChannel(); wasOpen = channel.isOpen(); if(!wasOpen) { channel.openChannel(); } channel.adaptorContext().beginTransaction(); try { rows = doPerform(channel); channel.adaptorContext().commitTransaction(); } catch(RuntimeException ex) { channel.adaptorContext().rollbackTransaction(); throw ex; } } finally { if (!wasOpen && channel != null) { channel.closeChannel(); } dbc.unlock(); } } finally { ec.unlock(); } return rows; } } /** * Deletes rows described by the qualifier. Note that the values and the qualifier need to be on an attribute * and not on a relationship level. I.e. you need to give relationshipForeignKey = pk of object instead of * relatedObject = object * @param ec * @param entityName * @param qualifier */ public static int deleteRowsDescribedByQualifier(EOEditingContext ec, String entityName, final EOQualifier qualifier) { final EOEntity entity = entityNamed(ec, entityName); ChannelAction action = new ChannelAction() { @Override protected int doPerform(EOAdaptorChannel channel) { return channel.deleteRowsDescribedByQualifier(qualifier, entity); } }; return action.perform(ec, entity.model().name()); } /** * Updates rows described by the qualifier. Note that the values and the qualifier need to be on an attribute * and not on a relationship level. I.e. you need to give relationshipForeignKey = pk of object instead of * relatedObject = object. The newValues dictionaries also holds foreign keys, not objects. * @param ec * @param entityName * @param qualifier * @param newValues */ public static int updateRowsDescribedByQualifier(EOEditingContext ec, String entityName, final EOQualifier qualifier, final NSDictionary newValues) { final EOEntity entity = entityNamed(ec, entityName); ChannelAction action = new ChannelAction() { @Override protected int doPerform(EOAdaptorChannel channel) { return channel.updateValuesInRowsDescribedByQualifier(newValues, qualifier, entity); } }; return action.perform(ec, entity.model().name()); } /** * Insert row described dictionary. * @param ec * @param entityName * @param newValues */ public static int insertRow(EOEditingContext ec, String entityName, final NSDictionary newValues) { final EOEntity entity = entityNamed(ec, entityName); ChannelAction action = new ChannelAction() { @Override protected int doPerform(EOAdaptorChannel channel) { channel.insertRow(newValues, entity); return 1; } }; return action.perform(ec, entity.model().name()); } /** * Insert rows described the array of dictionaries. * @param ec * @param entityName * @param newValues */ public static int insertRows(EOEditingContext ec, String entityName, final Collection<? extends NSDictionary<String, ?>> newValues) { final EOEntity entity = entityNamed(ec, entityName); ChannelAction action = new ChannelAction() { @Override protected int doPerform(EOAdaptorChannel channel) { int insert = 0; for (NSDictionary dictionary : newValues) { channel.insertRow(dictionary, entity); insert++; } return insert; } }; return action.perform(ec, entity.model().name()); } /** * Creates count new primary keys for the entity. * @param ec * @param entityName * @param count */ public static NSArray primaryKeysForNewRows(EOEditingContext ec, String entityName, final int count) { final NSMutableArray result = new NSMutableArray(); final EOEntity entity = entityNamed(ec, entityName); ChannelAction action = new ChannelAction() { @Override protected int doPerform(EOAdaptorChannel channel) { NSArray keys = channel.primaryKeysForNewRowsWithEntity(count, entity); result.addObjectsFromArray(keys); return count; } }; action.perform(ec, entity.model().name()); return result; } /** * Tries to get the plugin name for a JDBC based model. * * @param model model name * @return name of the JDBC plugin or <code>null</code> */ public static String guessPluginName(EOModel model) { String pluginName = null; // If you don't explicitly set a prototype name, and you don't // declare a preferred databaseConfig, // then attempt to load Wonder-style prototypes with the name // EOJDBC(driverName)Prototypes. if ("JDBC".equals(model.adaptorName())) { NSDictionary connectionDictionary = model.connectionDictionary(); if (connectionDictionary != null) { pluginName = guessPluginNameForConnectionDictionary(connectionDictionary); } } return pluginName; } /** * Tries to get the plugin name for a connection dictionary. * @param connectionDictionary the connectionDictionary to guess a plugin name for * @return the plugin name */ public static String guessPluginNameForConnectionDictionary(NSDictionary connectionDictionary) { String pluginName = null; String jdbcUrl = (String) connectionDictionary.objectForKey("URL"); if (jdbcUrl != null) { pluginName = (String) connectionDictionary.objectForKey("plugin"); if (pluginName == null || pluginName.trim().length() == 0) { pluginName = JDBCPlugIn.plugInNameForURL(jdbcUrl); if (pluginName == null) { // AK: this is a hack that is totally bogus.... int firstColon = jdbcUrl.indexOf(':'); int secondColon = jdbcUrl.indexOf(':', firstColon + 1); if (firstColon != -1 && secondColon != -1) { pluginName = jdbcUrl.substring(firstColon + 1, secondColon); } } else { pluginName = ERXStringUtilities.lastPropertyKeyInKeyPath(pluginName); pluginName = pluginName.replaceFirst("PlugIn", ""); if (pluginName.startsWith("_")) { pluginName = pluginName.substring(1); } } } } if (pluginName != null && pluginName.trim().length() == 0) { pluginName = null; } return pluginName; } /** * Returns a new fetch spec by morphing sort orderings containing the keys <code>foo.name</code> * returning <code>foo.name_de</code> where appropriate. * @param ec * @param fetchSpecification * @return localized fetch specification */ public static EOFetchSpecification localizeFetchSpecification(EOEditingContext ec, EOFetchSpecification fetchSpecification) { if(fetchSpecification != null && fetchSpecification.sortOrderings().count() > 0) { fetchSpecification = (EOFetchSpecification) fetchSpecification.clone(); NSMutableArray sortOrderings = new NSMutableArray(fetchSpecification.sortOrderings().count()); for (Enumeration enumerator = fetchSpecification.sortOrderings().objectEnumerator(); enumerator.hasMoreElements();) { EOSortOrdering item = (EOSortOrdering) enumerator.nextElement(); String key = item.key(); NSArray path = NSArray.componentsSeparatedByString(key, "."); EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, fetchSpecification.entityName()); String attributeName = (String) path.lastObject(); String prefix = ""; if(path.count() > 1) { for (int i = 0; i < path.count() - 1; i++) { String part = (String) path.objectAtIndex(i); EORelationship rel = entity.anyRelationshipNamed(part); entity = rel.destinationEntity(); prefix += part + "."; } } if(entity.attributeNamed(attributeName) == null) { EOClassDescription cd = entity.classDescriptionForInstances(); if(cd instanceof ERXEntityClassDescription) { String localizedKey = ((ERXEntityClassDescription)cd).localizedKey(attributeName); if(localizedKey != null) { item = new EOSortOrdering(prefix + localizedKey, item.selector()); } } } sortOrderings.addObject(item); } fetchSpecification.setSortOrderings(sortOrderings); } return fetchSpecification; } /** * Batch fetch a relationship, optionally skipping any relationship that has already faulted in its to-many relationship. * * @param databaseContext the database context to fetch in * @param entityName the name of the entity to fetch on * @param relationshipName the name of the relationship in that entity to fetch * @param objects the objects to fetch the relationship on * @param editingContext the editingContext to fetch in * @param skipFaultedRelationships if true, skip any object whose relationship has already been faulted */ public static void batchFetchRelationship(EODatabaseContext databaseContext, String entityName, String relationshipName, NSArray objects, EOEditingContext editingContext, boolean skipFaultedRelationships) { EOEntity entity = ERXEOAccessUtilities.entityNamed(editingContext, entityName); EORelationship relationship = entity.relationshipNamed(relationshipName); ERXEOAccessUtilities.batchFetchRelationship(databaseContext, relationship, objects, editingContext, skipFaultedRelationships); } /** * Batch fetch a relationship, optionally skipping any relationship that has * already faulted in its to-many relationship. * * @param databaseContext * the database context to fetch in * @param relationship * the relationship to fetch * @param objects * the objects to fetch the relationship on * @param editingContext * the editingContext to fetch in * @param skipFaultedRelationships * if true, skip any object whose relationship has already been * faulted */ public static void batchFetchRelationship(EODatabaseContext databaseContext, EORelationship relationship, NSArray objects, EOEditingContext editingContext, boolean skipFaultedRelationships) { NSArray objectsToBatchFetch; if (relationship.isToMany()) { if (skipFaultedRelationships) { NSMutableArray<EOEnterpriseObject> objectsWithUnfaultedRelationships = new NSMutableArray<>(); String relationshipName = relationship.name(); Enumeration objectsEnum = objects.objectEnumerator(); while (objectsEnum.hasMoreElements()) { EOEnterpriseObject object = (EOEnterpriseObject) objectsEnum.nextElement(); Object relationshipValue = object.storedValueForKey(relationshipName); if (EOFaultHandler.isFault(relationshipValue)) { objectsWithUnfaultedRelationships.addObject(object); } } objectsToBatchFetch = objectsWithUnfaultedRelationships; } else { objectsToBatchFetch = objects; } } else { NSMutableSet<EOGlobalID> gids = new NSMutableSet<>(); NSMutableArray<EOEnterpriseObject> objectsWithUnfaultedRelationships = new NSMutableArray<>(); EOEntity destinationEntity = relationship.destinationEntity(); String relationshipName = relationship.name(); Enumeration objectsEnum = objects.objectEnumerator(); while (objectsEnum.hasMoreElements()) { EOEnterpriseObject object = (EOEnterpriseObject) objectsEnum.nextElement(); // This seems a bit odd. Why not just use object.isFault()? NSDictionary sourceSnapshot = databaseContext.snapshotForGlobalID(editingContext.globalIDForObject(object)); if (sourceSnapshot == null) { // object here is an unfaulted object, not an object with an unfaulted relationship objectsWithUnfaultedRelationships.addObject(object); } else { NSDictionary destinationPK = relationship._foreignKeyForSourceRow(sourceSnapshot); EOGlobalID destinationGID = destinationEntity.globalIDForRow(destinationPK); if (destinationGID != null) { NSDictionary destinationSnapshot = databaseContext.snapshotForGlobalID(destinationGID, editingContext.fetchTimestamp()); if (destinationSnapshot == null || ! skipFaultedRelationships) { gids.addObject(destinationGID); // The ERXEOGlobalIDUtilities.fetchObjectsWithGlobalIDs below will fetch the row and register // the snapshot. BUT as the fault is deferred, it will not get fired. This means there is no // strong reference to the fetched object in the EC. If the garbage collector runs, it will detect // this and place the EO on the EC's cleanup queue (_referenceQueue()). This is processed from // _processReferenceQueue() which is called at various points in EOEditingContext, most problematically // from unlockObjectStore(). If this happens before the deferred fault is fired, the snapshot will be discarded. // The object will get re-fetched when next referenced. This makes batch fetching of to-one relationships of // little use. To avoid this, the call to storedValueForKey below will call (via the _LazyGenericRecordBinding) // willReadRelationship() on the deferred fault. This fires the deferred fault (calls createFaultForDeferredFault() // defined on EOAccessDeferredFaultHandler), making it a regular fault. When the EO is batch fetched below, // the regular fault resolves and there is a strong reference to the EO. This is what EOFetchSpecification // pre-fetching does for to-one relationships. // References: // http://developer.apple.com/documentation/developertools/reference/wo541reference/com/webobjects/eocontrol/EODeferredFaulting.html // http://developer.apple.com/DOCUMENTATION/WebObjects/Enterprise_Objects/Fetching/chapter_6_section_8.html object.storedValueForKey(relationshipName); } } } } objectsToBatchFetch = objectsWithUnfaultedRelationships; // MS: This is split out into a separate fetch because EOF does not handle batch // fetching of abstract entities very effectively. We instead want to create // our own GID and batch fetch the GIDs ourselves. if (gids.count() > 0) { ERXEOGlobalIDUtilities.fetchObjectsWithGlobalIDs(editingContext, gids.allObjects(), ! skipFaultedRelationships); } } if (objectsToBatchFetch.count() > 0) { databaseContext.batchFetchRelationship(relationship, objectsToBatchFetch, editingContext); } } /** * In a multi-OSC or multi-instance scenario, when there are bugs, it is possible for the snapshots to become out of * sync with the database. It is useful to have some way to determine when exactly this condition occurs for writing * test cases. * * @return a set of strings that describe the mismatches that occurred */ public static NSSet verifyAllSnapshots() { NSMutableSet mismatches = new NSMutableSet(); NSMutableSet verifiedDatabases = new NSMutableSet(); EOEditingContext editingContext = ERXEC.newEditingContext(); EOModelGroup modelGroup = EOModelGroup.defaultGroup(); Enumeration modelsEnum = modelGroup.models().objectEnumerator(); while (modelsEnum.hasMoreElements()) { EOModel model = (EOModel) modelsEnum.nextElement(); EODatabaseContext databaseContext = null; try { databaseContext = EODatabaseContext.registeredDatabaseContextForModel(model, editingContext); } catch (IllegalStateException e) { log.warn("Model " + model.name() + " failed: " + e.getMessage()); } if (databaseContext != null) { databaseContext.lock(); try { EODatabase database = databaseContext.database(); if (!verifiedDatabases.containsObject(database)) { Enumeration gidEnum = database.snapshots().keyEnumerator(); while (gidEnum.hasMoreElements()) { EOGlobalID gid = (EOGlobalID) gidEnum.nextElement(); if (gid instanceof EOKeyGlobalID) { EOEnterpriseObject eo = null; EOKeyGlobalID keyGID = (EOKeyGlobalID) gid; String entityName = keyGID.entityName(); EOEntity entity = modelGroup.entityNamed(entityName); NSDictionary snapshot = database.snapshotForGlobalID(gid); if (snapshot != null) { EOQualifier gidQualifier = entity.qualifierForPrimaryKey(entity.primaryKeyForGlobalID(gid)); EOFetchSpecification gidFetchSpec = new EOFetchSpecification(entityName, gidQualifier, null); NSMutableDictionary databaseSnapshotClone; NSMutableDictionary memorySnapshotClone = snapshot.mutableClone(); EOAdaptorChannel channel = databaseContext.availableChannel().adaptorChannel(); channel.openChannel(); channel.selectAttributes(entity.attributesToFetch(), gidFetchSpec, false, entity); try { databaseSnapshotClone = channel.fetchRow().mutableClone(); } finally { channel.cancelFetch(); } // gidFetchSpec.setRefreshesRefetchedObjects(true); // NSArray databaseEOs = editingContext.objectsWithFetchSpecification(gidFetchSpec); if (databaseSnapshotClone == null) { mismatches.addObject(gid + " was deleted in the database, but the snapshot still exists: " + memorySnapshotClone); } else { // NSMutableDictionary refreshedSnapshotClone = // database.snapshotForGlobalID(gid).mutableClone(); ERXDictionaryUtilities.removeMatchingEntries(memorySnapshotClone, databaseSnapshotClone); if (databaseSnapshotClone.count() > 0 || memorySnapshotClone.count() > 0) { mismatches.addObject(gid + " doesn't match the database: original = " + memorySnapshotClone + "; database = " + databaseSnapshotClone); } eo = (EOEnterpriseObject) editingContext.objectsWithFetchSpecification(gidFetchSpec).objectAtIndex(0); } } if (eo != null) { Enumeration relationshipsEnum = entity.relationships().objectEnumerator(); while (relationshipsEnum.hasMoreElements()) { EORelationship relationship = (EORelationship) relationshipsEnum.nextElement(); String relationshipName = relationship.name(); NSArray originalDestinationGIDs = database.snapshotForSourceGlobalID(keyGID, relationshipName); if (originalDestinationGIDs != null) { NSMutableArray newDestinationGIDs = new NSMutableArray(); EOQualifier qualifier = relationship.qualifierWithSourceRow(database.snapshotForGlobalID(keyGID)); EOFetchSpecification relationshipFetchSpec = new EOFetchSpecification(entityName, qualifier, null); EOAdaptorChannel channel = databaseContext.availableChannel().adaptorChannel(); channel.openChannel(); try { channel.selectAttributes(relationship.destinationEntity().attributesToFetch(), relationshipFetchSpec, false, relationship.destinationEntity()); NSDictionary destinationSnapshot = null; do { destinationSnapshot = channel.fetchRow(); if (destinationSnapshot != null) { EOGlobalID destinationGID = relationship.destinationEntity().globalIDForRow(destinationSnapshot); newDestinationGIDs.addObject(destinationGID); } } while (destinationSnapshot != null); } finally { channel.cancelFetch(); } NSArray objectsNotInDatabase = ERXArrayUtilities.arrayMinusArray(originalDestinationGIDs, newDestinationGIDs); if (objectsNotInDatabase.count() > 0) { mismatches.addObject(gid + "." + relationshipName + " has entries not in the database: " + objectsNotInDatabase); } NSArray objectsNotInMemory = ERXArrayUtilities.arrayMinusArray(newDestinationGIDs, originalDestinationGIDs); if (objectsNotInMemory.count() > 0) { mismatches.addObject(gid + "." + relationshipName + " is missing entries in the database: " + objectsNotInMemory); } } } } } } verifiedDatabases.addObject(database); } } finally { databaseContext.unlock(); } } } return mismatches; } /** * Utility method to make a shared entity editable. This * can be useful if you want to have an administration * application that can edit shared enterprise objects * and need a way at start up to disable the sharing * constraints. * @param entityName name of the shared entity to make * shareable. */ // FIXME: Should have to pass in an editing context so that the // correct model group and shared ec will be used. // FIXME: Should also dump all of the currently shared eos from // the shared context. public static void makeEditableSharedEntityNamed(String entityName) { EOEntity e = ERXEOAccessUtilities.entityNamed(null, entityName); if (e != null && e.isReadOnly()) { e.setReadOnly(false); e.setCachesObjects(false); // Remove all of the shared fetch specs for (Enumeration fetchSpecNameObjectEnumerator = e.sharedObjectFetchSpecificationNames().objectEnumerator(); fetchSpecNameObjectEnumerator.hasMoreElements();) { e.removeSharedObjectFetchSpecificationByName((String)fetchSpecNameObjectEnumerator.nextElement()); } } else if (e == null) { log.warn("makeEditableSharedEntityNamed: unable to find entity named: " + entityName); } else { log.warn("makeEditableSharedEntityNamed: entity already editable: " + entityName); } } /** * Programatically adds a toOne or toMany relationship to an entity at runtime. * * @param relationshipName name of the relationship to be created on the source entity * @param sourceEntityName name of the source entity * @param sourceAttributeName name of the attribute in the source entity to be used by the join * @param destinationEntityName name of the destination entity * @param destinationAttributeName name of the attribute in the destination entity to be used by the join * @param toMany if true, the relationship will be toMany, otherwise it will be toOne * @param deleteRule * EOClassDescription.DeleteRuleCascade || * EOClassDescription.DeleteRuleDeny || * EOClassDescription.DeleteRuleNoAction || * EOClassDescription.DeleteRuleNullify * @param isMandatory mandatory or not * @param isClassProperty class property or not * @param shouldPropagatePrimaryKey propagate primary key or not * @return the newly created relationship * * @author th */ public static EORelationship createRelationship(String relationshipName, String sourceEntityName, String sourceAttributeName, String destinationEntityName, String destinationAttributeName, boolean toMany, int deleteRule, boolean isMandatory, boolean isClassProperty, boolean shouldPropagatePrimaryKey) { EOEntity sourceEntity = EOModelGroup.defaultGroup().entityNamed(sourceEntityName); if(sourceEntity.isAbstractEntity()) log.warn("If you programatically add relationships to an abstract entity, make sure you also add it to child entities"); EOEntity destinationEntity = EOModelGroup.defaultGroup().entityNamed(destinationEntityName); EOAttribute sourceAttribute = sourceEntity.attributeNamed(sourceAttributeName); EOAttribute destinationAttribute = destinationEntity.attributeNamed(destinationAttributeName); EOJoin join = new EOJoin(sourceAttribute, destinationAttribute); EORelationship relationship = new EORelationship(); relationship.setName(relationshipName); sourceEntity.addRelationship(relationship); relationship.addJoin(join); relationship.setToMany(toMany); relationship.setJoinSemantic(EORelationship.InnerJoin); relationship.setDeleteRule(deleteRule); relationship.setIsMandatory(isMandatory); relationship.setPropagatesPrimaryKey(shouldPropagatePrimaryKey); NSMutableArray<EOProperty> classProperties = sourceEntity.classProperties().mutableClone(); if (isClassProperty && !classProperties.containsObject(relationship)) { classProperties.addObject(relationship); sourceEntity.setClassProperties(classProperties); } else if (!isClassProperty && classProperties.containsObject(relationship)) { classProperties.removeObject(relationship); sourceEntity.setClassProperties(classProperties); } if(log.isDebugEnabled()) log.debug(relationship); return relationship; } /** * Programatically adds a flattened relationship to an entity at runtime. * * @param relationshipName name of the relationship to be created on the source entity * @param sourceEntityName name of the source entity * @param definition data path of the relationship (e.g. department.facility) * @param deleteRule * EOClassDescription.DeleteRuleCascade || * EOClassDescription.DeleteRuleDeny || * EOClassDescription.DeleteRuleNoAction || * EOClassDescription.DeleteRuleNullify * @param isMandatory mandatory or not * @param isClassProperty class property or not * @return the newly created relationship * * @author th */ public static EORelationship createFlattenedRelationship(String relationshipName, String sourceEntityName, String definition, int deleteRule, boolean isMandatory, boolean isClassProperty) { EOEntity sourceEntity = EOModelGroup.defaultGroup().entityNamed(sourceEntityName); if(sourceEntity.isAbstractEntity()) log.warn("If you programatically add relationships to an abstract entity, make sure you also add it to child entities"); // order is important here! // first create relationship and set name EORelationship relationship = new EORelationship(); relationship.setName(relationshipName); // then add to entity (will only work if name is set!) sourceEntity.addRelationship(relationship); relationship.setDeleteRule(deleteRule); relationship.setIsMandatory(isMandatory); // finally set definition (only will work if assigned to entity!) relationship.setDefinition(definition); if (isClassProperty) { NSMutableArray<EOProperty> classProperties = sourceEntity.classProperties().mutableClone(); classProperties.addObject(relationship); sourceEntity.setClassProperties(classProperties); } if(log.isDebugEnabled()) log.debug(relationship + ", flattened:" + relationship.isFlattened() + ", definition:" + relationship.definition() + ", relationshipPath:" + relationship.relationshipPath()); return relationship; } /** * Executes the given database context operation within a database context lock, retrying up to maxRetryCount times if the database connection dropped. * * @param <T> the type of the results of this operation * @param databaseContext the (possibly unlocked) database context to operate on * @param maxAttempts the maximum number of times to reattempt the operation before failing * @param operation the database context operation to perform */ public static <T> T executeDatabaseContextOperation(final EODatabaseContext databaseContext, final int maxAttempts, final DatabaseContextOperation<T> operation) { T results = null; boolean succeeded = false; for (int attempt = 0; !succeeded && attempt < maxAttempts; attempt ++) { databaseContext.lock(); try { results = operation.execute(databaseContext); succeeded = true; } catch (Exception e) { boolean didHandleException = false; Object delegate = databaseContext.delegate(); if (delegate instanceof _NSDelegate) { if (((_NSDelegate)delegate).respondsTo("databaseContextShouldHandleDatabaseException")) { didHandleException = ((Boolean) ((_NSDelegate)delegate).perform("databaseContextShouldHandleDatabaseException", new Object[] { databaseContext, e })).booleanValue() == false; } } else if (delegate instanceof ERXDatabaseContextDelegate) { try { didHandleException = (((ERXDatabaseContextDelegate)delegate).databaseContextShouldHandleDatabaseException(databaseContext, e) == false); } catch (Throwable t) { // fucking pointless. ERXEOAccessUtilities.log.error("caught an exception while trying to handle a database exception.", t); } } if (didHandleException) { // delegate handled exception and clean up, just try again succeeded = false; } else if (databaseContext._isDroppedConnectionException(e)) { // start clean up at the database and let it work down the stack, then try again try { databaseContext.database().handleDroppedConnection(); succeeded = false; } catch (Exception ex) { throw NSForwardException._runtimeExceptionForThrowable(ex); } } else { throw NSForwardException._runtimeExceptionForThrowable(e); } NSLog._conditionallyLogPrivateException(e); } finally { databaseContext.unlock(); } } return results; } /** * Executes the given adaptor channel operation within a database context lock and with a requested open channel, retrying up to maxRetryCount times if the database connection dropped. * * @param <T> the type of the results of this operation * @param databaseContext the (possibly unlocked) database context to operate on * @param maxAttempts the maximum number of times to reattempt the operation before failing * @param operation the adaptor channel operation to perform */ public static <T> T executeAdaptorChannelOperation(final EODatabaseContext databaseContext, final int maxAttempts, final AdaptorChannelOperation<T> operation) { return ERXEOAccessUtilities.executeDatabaseContextOperation(databaseContext, maxAttempts, new DatabaseContextOperation<T>() { public T execute(EODatabaseContext databaseContext) throws Exception { EOAdaptorChannel adaptorChannel = databaseContext.availableChannel().adaptorChannel(); if (!adaptorChannel.isOpen()) { adaptorChannel.openChannel(); } return operation.execute(databaseContext, adaptorChannel); } }); } /** * Executes the given adaptor channel operation within a database context lock and with a requested open channel, retrying up to maxRetryCount times if the database connection dropped. * * @param <T> the type of the results of this operation * @param modelName the name of the model to lookup a database context for * @param maxAttempts the maximum number of times to reattempt the operation before failing * @param operation the adaptor channel operation to perform */ public static <T> T executeAdaptorChannelOperation(final String modelName, final int maxAttempts, final AdaptorChannelOperation<T> operation) { EOEditingContext editingContext = ERXEC.newEditingContext(); EODatabaseContext databaseContext; try { databaseContext = EOUtilities.databaseContextForModelNamed(editingContext, modelName); } finally { editingContext.dispose(); } return ERXEOAccessUtilities.executeAdaptorChannelOperation(databaseContext, maxAttempts, operation); } /** * Implemented by any database operation that needs to run inside of a database context lock. * * @author mschrag * * @param <T> the type of the results of this operation */ public static interface DatabaseContextOperation<T> { /** * Executes the given operation. * * @param databaseContext the database context to operate on * @throws Exception if anything goes wrong */ public T execute(EODatabaseContext databaseContext) throws Exception; } /** * Implemented by any adaptor channel operation that needs to run with an open adaptor channel. * * @author mschrag * * @param <T> the type of the results of this operation */ public static interface AdaptorChannelOperation<T> { /** * Executes the given operation with the given channel. * * @param databaseContext the locked database context to operate on * @param channel the open adaptor channel to operate on * @throws Exception if anything goes wrong */ public T execute(EODatabaseContext databaseContext, EOAdaptorChannel channel) throws Exception; } /** * Add or remove a class property from its entity. * @param property The property to add or remove from the class properties array * @param isClassProperty true adds the property to the list of class properties, * false removes it. */ public static void setIsClassProperty(EOProperty property, boolean isClassProperty) { EOEntity entity = null; if(property instanceof EOAttribute) { entity = ((EOAttribute)property).entity(); } else if(property instanceof EORelationship) { entity = ((EORelationship)property).entity(); } else { throw new IllegalArgumentException("property must be an EOAttribute or an EORelationship."); } NSArray<EOProperty> classProperties = entity.classProperties(); if(isClassProperty && !classProperties.contains(property)) { classProperties = classProperties.arrayByAddingObject(property); entity.setClassProperties(classProperties); } else if(!isClassProperty && classProperties.contains(property)) { classProperties = ERXArrayUtilities.arrayMinusObject(classProperties, property); entity.setClassProperties(classProperties); } } /** * Add or remove an attribute from the entities list of attributes used for locking * @param attr the attribute to add or remove * @param isUsedForLocking true adds the attribute, false removes it */ public static void setIsAttributeUsedForLocking(EOAttribute attr, boolean isUsedForLocking) { EOEntity entity = attr.entity(); NSArray<EOAttribute> atts = entity.attributesUsedForLocking(); if(isUsedForLocking && !atts.contains(attr)) { atts = atts.arrayByAddingObject(attr); entity.setAttributesUsedForLocking(atts); } else if(!isUsedForLocking && atts.contains(attr)) { atts = ERXArrayUtilities.arrayMinusObject(atts, attr); entity.setAttributesUsedForLocking(atts); } } /** * Utility method used to find all of the non-abstract sub entity names * for a given entity including itself. * @param ec editing context * @param rootEntityName name of entity to walk all of the <code>subEntities</code> * relationships * @return a list of all concrete entity names that inherit from * rootEntityName, including rootEntityName itself if it is * concrete. */ public static NSArray<String> entityHierarchyNamesForEntityNamed(EOEditingContext ec, String rootEntityName) { NSMutableArray<String> names = new NSMutableArray<>(); EOEntity rootEntity = entityNamed(ec, rootEntityName); NSArray<EOEntity> entities = entityHierarchyForEntity(ec, rootEntity); for (EOEntity entity : entities) { names.add(entity.name()); } return names.immutableClone(); } /** * Utility method used to find all of the non-abstract sub entities * for a given entity including itself. * @param ec editing context * @param rootEntity to walk all of the <code>subEntities</code> * relationships * @return a list of all concrete entities that inherit from rootEntity, * including rootEntity itself if it is concrete. */ public static NSArray<EOEntity> entityHierarchyForEntity(EOEditingContext ec, EOEntity rootEntity) { NSMutableArray<EOEntity> entities = new NSMutableArray<>(); if (!rootEntity.isAbstractEntity()) { entities.add(rootEntity); } @SuppressWarnings("unchecked") NSArray<EOEntity> subEntities = rootEntity.subEntities(); for (EOEntity subEntity : subEntities) { entities.addAll(entityHierarchyForEntity(ec, subEntity)); } return entities.immutableClone(); } /** * Utility method used to find all of the sub entities * for a given entity. * @param rootEntity to walk all of the <code>subEntities</code> * relationships * @param includeAbstracts determines if abstract entities should * be included in the returned array * @return all of the sub-entities for a given entity. */ public static NSArray<EOEntity> allSubEntitiesForEntity(EOEntity rootEntity, boolean includeAbstracts) { NSMutableArray<EOEntity> entities = new NSMutableArray<>(); if (rootEntity != null) { for (EOEntity subEntity : rootEntity.subEntities()) { if (!subEntity.isAbstractEntity() || includeAbstracts) { entities.addObject(subEntity); } if (subEntity.subEntities().count() > 0) { entities.addAll(allSubEntitiesForEntity(subEntity, includeAbstracts)); } } } return entities.immutableClone(); } }