// // EOEnterpriseObjectClazz.java // Project ERExtensions // // Created by ak on Fri Apr 12 2002 // package er.extensions.eof; import java.util.Enumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EODatabaseDataSource; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOModelGroup; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOArrayDataSource; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOKeyValueQualifier; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import er.extensions.foundation.ERXPatcher; /** * <h3>Adds class-level inheritance to EOF.</h3> * In Java, static methods are similar to class methods in Objective-C, but * one cannot use static methods in interfaces and static methods cannot be overridden * by a subclass. Using the clazz pattern removes those limitations. * <p> * Instead of using a static method, we can use a static inner class (a clazz) * instead. This allows for the methods on the clazz to be available statically to * the class. The advantage is that static utility methods don't need to be * generated for every subclass of an EOEnterpriseObject. It is generally sufficient to * simply use the utility methods available on the EOEnterpriseObjectClazz. * <p> * Every subclass of this class will get their own "ClazzObject" instance, so it's * OK to store things which might be different in superclasses. That is, the "User"'s * implementation can override the "Person"'s and because Person.clazz() will get * it's own instance, it will do only "Person" things. * <p> * Use subclasses of EOEnterpriseObjectClazz as inner classes in your EO subclasses * to work around the missing class object inheritance of java. They <b>must</b> * be named XX.XXClazz to work. * <p> * The methods from EOUtilities are mirrored here so you don't have to import EOAccess * in your subclasses, which is not legal for client-side classes. The implementation * for a client-side class could then be easily switched to use the server-side EOUtilites * implementation. * * @param <T> eo class to represent */ public class EOEnterpriseObjectClazz<T extends EOEnterpriseObject> { private static final Logger log = LoggerFactory.getLogger(EOEnterpriseObjectClazz.class); /** * caches the clazz objects */ private static NSMutableDictionary allClazzes = new NSMutableDictionary(); /** * caches the count attribute */ private static EOAttribute _objectCountAttribute = null; /** * Creates and caches an eo attribute that can be * used to return the number of objects that a given * fetch specification will return. * @return eo count attribute */ protected static EOAttribute objectCountAttribute() { if ( _objectCountAttribute == null ) { _objectCountAttribute = new EOAttribute(); _objectCountAttribute.setName("p_objectCountAttribute"); _objectCountAttribute.setColumnName("p_objectCountAttribute"); _objectCountAttribute.setClassName("java.lang.Number"); _objectCountAttribute.setValueType("i"); _objectCountAttribute.setReadFormat("count(*)"); } return _objectCountAttribute; } /** * @param foo */ protected static EOAttribute objectCountUniqueAttribute(EOAttribute foo) { EOAttribute tmp = new EOAttribute(); tmp.setName("p_objectCountUnique"+foo.name()); tmp.setColumnName("p_objectCountUnique"+foo.name()); tmp.setClassName("java.lang.Number"); tmp.setValueType("i"); tmp.setReadFormat("count( distinct t0."+foo.columnName()+")"); return tmp; } /** * caches the entity name */ private String _entityName; /** * Default public constructor. In case you let your code generate with a template, * you can simply call:<pre><code> * public static FooClazz clazz = new FooClazz(); * </code></pre> and the constructor will auto-discover your entity name. This only * works when you have a concrete subclass for the entity in question, though. */ public EOEnterpriseObjectClazz() { initialize(); } /** * Called by the constructor. * */ protected void initialize() { } protected void discoverEntityName() { // AK: If your class is enclosed by a EO subclass the constructor // will auto-discover the corresponding entity name. Not sure we need this, though. if(_entityName == null) { String className = getClass().getName(); int index = className.indexOf('$'); if(index > 0) { className = className.substring(0, index); Class c = ERXPatcher.classForName(className); if(c != null) { // we should use the class description, but it's too early for that when we // do this as a result of a static variable init. NSArray entities = (NSArray) EOModelGroup.defaultGroup().models().valueForKeyPath("entities.@flatten"); EOQualifier q = new EOKeyValueQualifier("className", EOQualifier.QualifierOperatorEqual, className); NSArray candidates = EOQualifier.filteredArrayWithQualifier(entities, q); if(candidates.count() > 1) { log.warn("More than one entity found: {}", candidates); } EOEntity entity = (EOEntity) candidates.lastObject(); if(entity != null) { String entityName = entity.name(); // HACK AK: this relies on you having set up your classes correctly, // meaning that you have exactly one final class var per EO class, with the correct // superclasses set up (so EOBase gets loaded before EOSubclass) if(allClazzes.containsKey(entityName)) { _entityName = entityName; } else { setEntityName(entityName); } } } } } } /** * Constructor that also supplies an entity name. * @param entityName entity name */ public EOEnterpriseObjectClazz(String entityName) { setEntityName(entityName); } /** * Convenience init so you can chain constructor calls:<pre><code> * public static FooClazz clazz = (FooClazz)new FooClazz().init("Foo"); * </code></pre> * without having to override the default constructor or the one * that takes an entity name. Also useful when you don't have a special * clazz defined for your entity, but would rather take one from a superclass. * @param entityName entity name */ public EOEnterpriseObjectClazz init(String entityName) { setEntityName(entityName); return this; } /** * Returns the class description for the entity. * * @return class description */ public EOClassDescription classDescription() { return entity().classDescriptionForInstances(); } /** * Utility to return a new array datasource * @param ec an editing context * @return array datasource */ public EOArrayDataSource newArrayDataSource(EOEditingContext ec) { return new EOArrayDataSource(classDescription(), ec); } /** * Utility to return a new database datasource * @param ec an editing context * @return database datasource */ public EODatabaseDataSource newDatabaseDataSource(EOEditingContext ec) { return new EODatabaseDataSource(ec, entityName()); } /** * Resets the clazz cache. */ public static void resetClazzCache() { allClazzes.removeAllObjects(); } /** * Method used to get a clazz object for a given entity name. * This method will cache the generated clazz object so that * for a given entity name only one clazz object will be created. * @param entityName name of the entity to get the Clazz object for * @return clazz object for the given entity */ public static EOEnterpriseObjectClazz clazzForEntityNamed(String entityName) { EOEnterpriseObjectClazz clazz = (EOEnterpriseObjectClazz)allClazzes.objectForKey(entityName); if(clazz == null) { clazz = factory().classFromEntity(ERXEOAccessUtilities.entityNamed(null, entityName)); clazz.setEntityName(entityName); } if(log.isDebugEnabled()) { log.debug("clazzForEntityNamed '{}': {}", entityName, clazz.getClass()); } return clazz; } /** * Creates and inserts an object of the type of * the clazz into the given editing context. * @param ec an editing context * @return newly created and inserted object */ public T createAndInsertObject(EOEditingContext ec) { T eo = (T) ERXEOControlUtilities.createAndInsertObject(ec,entityName()); return eo; } /** * Generates an array of primary key values for * the clazz's entity. Uses the database context * for the entity's model and the given editing context. * @param ec an editing context * @param i number of primary keys to generate * @return array of new primary keys */ public NSArray newPrimaryKeys(EOEditingContext ec, int i) { EOEntity entity = entity(ec); EODatabaseContext dbc = EODatabaseContext.registeredDatabaseContextForModel(entity.model(), ec); dbc.lock(); try { return dbc.availableChannel().adaptorChannel().primaryKeysForNewRowsWithEntity(i, entity); } finally { dbc.unlock(); } } /** * Gets all of the objects for the clazz's entity. * Just a cover method for the {@link com.webobjects.eoaccess.EOUtilities EOUtilities} * method <code>objectsForEntityNamed</code>. * @param ec editing context to fetch the objects into * @return array of all the objects for a given entity name. */ public NSArray<T> allObjects(EOEditingContext ec) { return EOUtilities.objectsForEntityNamed(ec, entityName()); } /** * Creates an enterprise object from a raw row * for the clazz's entity in the given editing context. * @param ec editing context to create the eo in * @param dict raw row dictionary * @return enterprise object for the raw row */ public T objectFromRawRow(EOEditingContext ec, NSDictionary dict) { return (T) EOUtilities.objectFromRawRow(ec, entityNameFromRawRow(ec, dict), dict); } /** * Utility method to get the entity name from a raw row dictionary, taking subclasses and restricting qualifiers into account. * @param ec an editing context * @param dict raw row dictionary * @return entity name, if any */ protected String entityNameFromRawRow(EOEditingContext ec, NSDictionary dict) { String entityName = entityName(); EOEntity entity = entity(ec); if(entity.isAbstractEntity() && entity.subEntities().count() > 0) { for(Enumeration e = entity.subEntities().objectEnumerator(); e.hasMoreElements();) { EOEntity sub = (EOEntity)e.nextElement(); if(sub.restrictingQualifier() != null) { if(sub.restrictingQualifier().evaluateWithObject(dict)) { return sub.name(); } } else { if(sub.isAbstractEntity()) { // do sth useful? } } } } return entityName; } /** * Fetches the enterprise object for the specified * primary key value and corresponding to the clazz's * entity name. * @param ec editing context to fetch into * @param pk primary key value. Compound primary keys are given as NSDictionaries. * @return enterprise object for the specified primary key value. */ public T objectWithPrimaryKeyValue(EOEditingContext ec, Object pk) { return (T) ERXEOControlUtilities.objectWithPrimaryKeyValue(ec, entityName(), pk, null); } /** * Fetches all of the objects matching the given qualifier * format corresponding to the clazz's entity using the * given editing context. * * @param ec editing context * @param qualifier qualifier string * @param args qualifier format arguments * * @return array of objects corresponding to the passed in parameters. */ public NSArray<T> objectsWithQualifierFormat(EOEditingContext ec, String qualifier, NSArray args) { return EOUtilities.objectsWithQualifierFormat(ec, entityName(), qualifier, args); } /** * Fetches all of the objects matching the given key and value * corresponding to the clazz's entity using the * given editing context. * @param ec editing context * @param key key string * @param value value * @return array of objects corresponding to the passed in parameters. */ public NSArray<T> objectsMatchingKeyAndValue(EOEditingContext ec, String key, Object value) { return EOUtilities.objectsMatchingKeyAndValue(ec, entityName(), key, value); } public NSArray<T> objectsMatchingQualifier(EOEditingContext ec, EOQualifier qualifier) { return objectsMatchingQualifier(ec, qualifier, null); } public NSArray<T> objectsMatchingQualifier(EOEditingContext ec, EOQualifier qualifier, NSArray<EOSortOrdering> sortOrdering) { return ec.objectsWithFetchSpecification(new EOFetchSpecification(entityName(), qualifier, sortOrdering)); } /** * Fetches the object matching the given key and value * corresponding to the clazz's entity using the * given editing context. If more than one matches, throws a EOMoreThanOneException, * otherwise returns null or the match. * @param ec editing context * @param key key string * @param value value * @return array of objects corresponding to the passed in parameters. */ public T objectMatchingKeyAndValue(EOEditingContext ec, String key, Object value) { NSArray<T> result = objectsMatchingKeyAndValue(ec, key, value); if(result.count() > 1) { throw new EOUtilities.MoreThanOneException("More than one: " + key + "->" + value); } return result.lastObject(); } /** * Fetches an array of objects for a given fetch specification * and an array of bindings. The fetch specification is resolved * off of the entity corresponding to the current clazz. * @param ec editing content to fetch into * @param name fetch specification name * @param bindings used to resolve binding keys within the fetch * specification * @return array of objects fetched using the given fetch specification */ public NSArray<T> objectsWithFetchSpecificationAndBindings(EOEditingContext ec, String name, NSDictionary bindings) { return EOUtilities.objectsWithFetchSpecificationAndBindings(ec, entityName(), name, bindings); } /** * Sets the entity name of the clazz. Also registers the clazz in the cache. * @param name of the entity */ protected void setEntityName(String name) { _entityName = name; allClazzes.setObjectForKey(this, _entityName); } /** * Gets the entity name of the clazz. * @return entity name of the clazz. */ public String entityName() { discoverEntityName(); return _entityName; } /** * Gets the entity corresponding to the entity * name of the clazz. * @return entity for the clazz */ public EOEntity entity() { return entity(null); } /** * Gets the entity corresponding to the entity * name of the clazz. * @param ec an editing context * @return entity for the clazz */ public EOEntity entity(EOEditingContext ec) { return ERXEOAccessUtilities.entityNamed(ec,entityName()); } /** * Gets a fetch specification for a given name. * @param name of the fetch specification * @return fetch specification for the given name and the clazz's entity * name */ public EOFetchSpecification fetchSpecificationNamed(String name) { return fetchSpecificationNamed(null,name); } /** * Gets a fetch specification for a given name. * @param ec editing context to use for finding the model group * @param name of the fetch specification * @return fetch specification for the given name and the clazz's entity * name */ public EOFetchSpecification fetchSpecificationNamed(EOEditingContext ec, String name) { return entity(ec).fetchSpecificationNamed(name); } /** * Creates a fetch spec for the entity. * @return fetch specification for the given name and the clazz's entity * name */ public ERXFetchSpecification<T> createFetchSpecification(EOQualifier qualifier, NSArray<EOSortOrdering> sortings) { return new ERXFetchSpecification(entityName(), qualifier, sortings); } /** * Filters an array with a given fetch spec. * @param array * @param spec * @param bindings */ public NSArray<T> filteredArray(NSArray<T> array, EOFetchSpecification spec, NSDictionary bindings) { EOQualifier qualifier; if (bindings != null) { spec = spec.fetchSpecificationWithQualifierBindings(bindings); } NSArray<T> result = new NSArray(array); qualifier = spec.qualifier(); if (qualifier != null) { result = EOQualifier.filteredArrayWithQualifier(result, qualifier); } NSArray sortOrderings = spec.sortOrderings(); if (sortOrderings != null) { result = EOSortOrdering.sortedArrayUsingKeyOrderArray(result,sortOrderings); } return result; } /** * Returns the number of objects matching the given * qualifier for the clazz's entity name. Implementation * wise this method will generate the correct sql to only * perform a count, i.e. all of the objects wouldn't be * pulled into memory. * @param ec editing context to use for the count qualification * @param qualifier to find the matching objects * @return number of matching objects */ public Number objectCountWithQualifier(EOEditingContext ec, EOQualifier qualifier) { return (Number) ERXEOControlUtilities._aggregateFunctionWithQualifierAndAggregateAttribute(ec, entityName(), qualifier, EOEnterpriseObjectClazz.objectCountAttribute()); } /** * Find the number of objects matching the given fetch * specification and bindings for the clazz's entity * name. Implementation wise the sql generated will * only return the count of the query, not all of the * rows matching the qualification. * @param ec ec used to perform the count in * @param fetchSpecName name of the fetch specification * @param bindings dictionary of bindings for the fetch * specification * @return number of objects matching the given fetch specification and * bindings */ public Number objectCountWithFetchSpecificationAndBindings(EOEditingContext ec, String fetchSpecName, NSDictionary bindings) { EOFetchSpecification unboundFetchSpec; EOFetchSpecification boundFetchSpec; unboundFetchSpec = fetchSpecificationNamed(ec, fetchSpecName); if (unboundFetchSpec == null) { return null; } boundFetchSpec = unboundFetchSpec.fetchSpecificationWithQualifierBindings(bindings); return objectCountWithQualifier(ec, boundFetchSpec.qualifier()); } /** * Constructs a fetch specification that will only fetch the primary * keys for a given qualifier. * * @param qualifier to construct the fetch spec with * @param sortOrderings array of sort orderings to sort the result * set with. * @param additionalKeys array of additional key paths to construct * the raw rows key paths to fetch. * @return fetch specification that can be used to fetch primary keys for * a given qualifier and sort orderings. */ public EOFetchSpecification primaryKeyFetchSpecificationForEntity(EOQualifier qualifier, NSArray<EOSortOrdering> sortOrderings, NSArray<String> additionalKeys) { String entityName = entityName(); EOFetchSpecification fs = new EOFetchSpecification(entityName, qualifier, sortOrderings); fs.setFetchesRawRows(true); EOEntity entity = entity(); NSMutableArray<String> keys = new NSMutableArray<>(entity.primaryKeyAttributeNames()); if(additionalKeys != null) { keys.addObjectsFromArray(additionalKeys); } if(entity.restrictingQualifier() != null) { NSArray restrict = entity.restrictingQualifier().allQualifierKeys().allObjects(); keys.addObjectsFromArray(restrict); } if(entity.isAbstractEntity()) { NSArray restrict = (NSArray)entity.subEntities().valueForKeyPath("restrictingQualifier.allQualifierKeys.allObjects.@flatten.@unique"); keys.addObjectsFromArray(restrict); } fs.setRawRowKeyPaths(keys); return fs; } /** * Fetches an array of primary keys matching a given qualifier * and sorted with a given array of sort orderings. * @param ec editing context to fetch into * @param eoqualifier to restrict matching primary keys * @param sortOrderings array of sort orders to sort result set * @return array of primary keys matching a given qualifier */ public NSArray primaryKeysMatchingQualifier(EOEditingContext ec, EOQualifier eoqualifier, NSArray sortOrderings) { EOFetchSpecification fs = primaryKeyFetchSpecificationForEntity(eoqualifier, sortOrderings, null); //NSArray nsarray = EOUtilities.rawRowsForQualifierFormat(ec, fs.qualifier(), ); NSArray nsarray = ec.objectsWithFetchSpecification(fs); return nsarray; } /** * Fetches an array of primary keys matching the values * in a given dictionary. * @param ec editing context to fetch into * @param nsdictionary dictionary of key value pairs to match * against. * @param sortOrderings array of sort orders to sort the result set * by. * @return array of primary keys matching the given criteria. */ public NSArray primaryKeysMatchingValues(EOEditingContext ec, NSDictionary nsdictionary, NSArray sortOrderings) { return primaryKeysMatchingQualifier(ec, EOQualifier.qualifierToMatchAllValues(nsdictionary), sortOrderings); } /** * Constructs an array of faults for a given array * of primary keys in a given editing context for * the clazz's entity. * @param ec editing context to construct the faults in * @param nsarray array of primary key dictionaries * @return array of faults for an array of primary key dictionaries. */ public NSArray<T> faultsFromRawRows(EOEditingContext ec, NSArray nsarray) { int count = nsarray.count(); NSMutableArray<T> faults = new NSMutableArray(count); for( int i = 0; i < count; i ++ ) { faults.addObject(objectFromRawRow(ec, (NSDictionary)nsarray.objectAtIndex(i))); } return faults; } /** * Fetches an array of faults matching a given qualifier. * @param ec editing context to use to fetch into * @param eoqualifier qualifier to match against * @return array of faults that match the given qualifier */ public NSArray<T> faultsMatchingQualifier(EOEditingContext ec, EOQualifier eoqualifier) { NSArray<T> nsarray = primaryKeysMatchingQualifier(ec, eoqualifier, null); return faultsFromRawRows(ec, nsarray); } /** * Fetches an array of faults matching a given qualifier * and sorted by an array of sort orderings. * @param ec editing context to use to fetch into * @param eoqualifier qualifier to match against * @param sortOrderings array of sort orderings to order the faults * @return array of faults that match the given qualifier */ public NSArray<T> faultsMatchingQualifier(EOEditingContext ec, EOQualifier eoqualifier, NSArray<EOSortOrdering> sortOrderings) { NSArray<T> nsarray = primaryKeysMatchingQualifier(ec, eoqualifier, sortOrderings); return faultsFromRawRows(ec, nsarray); } /** * Fetches an array of faults for a given set of criteria. * @param ec editing context to use to fetch into * @param nsdictionary key value criteria to match against * @param sortOrderings array of sort orderings to order the faults * @return array of faults that match the given criteria */ public NSArray<T> faultsMatchingValues(EOEditingContext ec, NSDictionary nsdictionary, NSArray<EOSortOrdering> sortOrderings) { NSArray nsarray = primaryKeysMatchingValues(ec, nsdictionary, sortOrderings); return faultsFromRawRows(ec, nsarray); } /** * Provides a hook to control how a clazz object is chosen from a given entity. */ public static interface ClazzFactory { public EOEnterpriseObjectClazz classFromEntity(EOEntity entity); } private static ClazzFactory _factory = new DefaultClazzFactory(); public static ClazzFactory factory() { return _factory; } public static void setFactory(ClazzFactory value) { _factory = value; } /** * Default factory implementation. * @author ak * */ public static class DefaultClazzFactory implements ClazzFactory { protected boolean classNameIsGenericRecord(final String className) { return className.equals("ERXGenericRecord"); } protected EOEnterpriseObjectClazz newInstanceOfDefaultClazz() { return new EOEnterpriseObjectClazz(); } protected EOEnterpriseObjectClazz newInstanceOfGenericRecordClazz() { return new ERXGenericRecord.ERXGenericRecordClazz(); } protected String clazzNameForEntity(EOEntity entity) { return entity.className() + "$" + entity.name() + "Clazz"; } /** * Creates a clazz object for a given entity. * Will look for a clazz object with the name: * <entity name>$<entity name>Clazz. * @param entity to generate the clazz for * @return clazz object for the given entity */ public EOEnterpriseObjectClazz classFromEntity(EOEntity entity) { EOEnterpriseObjectClazz clazz = null; if(entity == null) { clazz = newInstanceOfDefaultClazz(); } else { try { String className = entity.className(); if(classNameIsGenericRecord(className)) { clazz = newInstanceOfGenericRecordClazz(); } else { String clazzName = clazzNameForEntity(entity); clazz = (EOEnterpriseObjectClazz)Class.forName(clazzName).newInstance(); } } catch (InstantiationException ex) { } catch (ClassNotFoundException ex) { } catch (IllegalAccessException ex) { } } if(clazz == null) return classFromEntity(entity.parentEntity()); return clazz; } } }