package er.taggable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOGeneralAdaptorException;
import com.webobjects.eoaccess.EOJoin;
import com.webobjects.eoaccess.EOModel;
import com.webobjects.eoaccess.EOModelGroup;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eoaccess.EOSQLExpression;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSSelector;
import er.extensions.eof.ERXEC;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.eof.ERXEOAttribute;
import er.extensions.eof.ERXEOControlUtilities;
import er.extensions.eof.ERXGenericRecord;
import er.extensions.eof.ERXKey;
import er.extensions.eof.ERXQ;
import er.extensions.foundation.ERXCommandLineTokenizer;
import er.extensions.jdbc.ERXSQLHelper;
import er.taggable.model.ERTag;
/**
* ERTaggableEntity provides entity-level tag management and fetching methods.
*
* Typically you would provide a cover method from your entity class to an
* instance of an ERTaggableEntity:
*
* <code>
* public class Person extends _Person {
* ...
* public static ERTaggableEntity<Person> taggableEntity() {
* return ERTaggableEntity.taggableEntity(Person.ENTITY_NAME);
* }
* }
* </code>
* @author mschrag
*
* @param <T> the java class of the entity that this ERTaggableEntity is associated with
*/
public class ERTaggableEntity<T extends ERXGenericRecord> {
/**
* Default is white-space and/or comma(s). Multiple string of separators treated as one.
*/
private static final String DEFAULT_SEPARATOR = "[\\s,]+";
/**
* The key stored in entity userInfo that flags an entity as taggable.
*/
public static final String ERTAGGABLE_KEY = "_ERTaggable";
/**
* The key stored in entity userInfo that specifies the name of the tag entity.
*/
public static final String ERTAGGABLE_TAG_ENTITY_KEY = "_ERTaggableTagEntity";
/**
* The key stored in entity userInfo that specifies the name of the tag relationship.
*/
public static final String ERTAGGABLE_TAG_RELATIONSHIP_KEY = "_ERTaggableTagRelationship";
/**
* The default name of the flattened to-many relationship to the tag entity.
*/
public static final String DEFAULT_TAGS_RELATIONSHIP_NAME = "tags";
private static final NSMutableDictionary<String, Class<? extends ERTaggableEntity<?>>> _taggableEntities = new NSMutableDictionary<String, Class<? extends ERTaggableEntity<?>>>();
private final EOEntity _tagEntity;
private final EOEntity _entity;
private final EORelationship _tagsRelationship;
private final String _separator = ERTaggableEntity.DEFAULT_SEPARATOR;
private ERTagNormalizer _normalizer = new ERDefaultTagNormalizer();
/**
* Constructs an ERTaggableEntity.
*
* @param entity the entity to tag
*/
protected ERTaggableEntity(EOEntity entity) {
if (!ERTaggableEntity.isTaggable(entity)) {
throw new IllegalArgumentException("The entity '" + entity.name() + "' has not been registered as taggable.");
}
_entity = entity;
String tagEntityName = (String) entity.userInfo().objectForKey(ERTaggableEntity.ERTAGGABLE_TAG_ENTITY_KEY);
_tagEntity = _entity.model().modelGroup().entityNamed(tagEntityName);
String tagsRelationshipName = (String) entity.userInfo().objectForKey(ERTaggableEntity.ERTAGGABLE_TAG_RELATIONSHIP_KEY);
_tagsRelationship = _entity.relationshipNamed(tagsRelationshipName);
}
@Override
public int hashCode() {
return _entity.hashCode();
}
@Override
public boolean equals(Object obj) {
return (obj instanceof ERTaggableEntity && ((ERTaggableEntity<?>) obj)._entity.equals(_entity));
}
/**
* Sets the taggable entity class for the given entity name. This allows you to override the
* taggable entity that will be used throughout the framework for any particular entity.
*
* @param taggableEntity the taggable entity class
* @param entityName the name of the entity to associate with
*/
public static void setTaggableEntityForEntityNamed(Class<? extends ERTaggableEntity<?>> taggableEntity, String entityName) {
ERTaggableEntity._taggableEntities.setObjectForKey(taggableEntity, entityName);
}
/**
* Constructs an ERTaggableEntity.
*
* @param entity the entity to tag
*/
@SuppressWarnings("unchecked")
public static <T extends ERXGenericRecord> ERTaggableEntity<T> taggableEntity(EOEntity entity) {
Class<? extends ERTaggableEntity> taggableEntityClass = ERTaggableEntity._taggableEntities.objectForKey(entity.name());
ERTaggableEntity<T> taggableEntity;
if (taggableEntityClass == null) {
taggableEntity = new ERTaggableEntity<>(entity);
}
else {
try {
taggableEntity = taggableEntityClass.getConstructor(EOEntity.class).newInstance(entity);
}
catch (Exception e) {
throw new RuntimeException("Failed to create ERTaggableEntity for entity '" + entity + "'.", e);
}
}
return taggableEntity;
}
/**
* Constructs an ERTaggableEntity.
*
* @param entityName the name of the entity to tag
*/
public static <T extends ERXGenericRecord> ERTaggableEntity<T> taggableEntity(String entityName) {
return ERTaggableEntity.taggableEntity(EOModelGroup.defaultGroup().entityNamed(entityName));
}
/**
* Shortcut for getting an ERTaggableEntity for an EO.
*
* @param <T> the type of the entity
* @param eo the EO
* @return an ERTaggableEntity corresponding to the entity of the EO
*/
public static <T extends ERXGenericRecord> ERTaggableEntity<T> taggableEntity(T eo) {
return ERTaggableEntity.taggableEntity(eo.entity());
}
/**
* Fetches all the EOs of all taggable entities that are associated with all of the given tags (unlimited).
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @return a dictionary mapping entities to an array of matching EO's
*/
public static NSDictionary<EOEntity, NSArray<? extends ERXGenericRecord>> fetchAllTaggedWith(EOEditingContext editingContext, Object tags) {
return ERTaggableEntity.fetchAllTaggedWith(editingContext, ERTag.Inclusion.ALL, -1, tags);
}
/**
* Fetches all the EOs of all taggable entities that are associated with the given tags (unlimited).
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @return a dictionary mapping entities to an array of matching EO's
*/
public static NSDictionary<EOEntity, NSArray<? extends ERXGenericRecord>> fetchAllTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, Object tags) {
return ERTaggableEntity.fetchAllTaggedWith(editingContext, inclusion, -1, tags);
}
/**
* Fetches all the EOs of all taggable entities that are associated with the given tags.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @param limit the limit of the number of objects to return (or -1 for unlimited)
* @return a dictionary mapping entities to an array of matching EO's
*/
public static NSDictionary<EOEntity, NSArray<? extends ERXGenericRecord>> fetchAllTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, int limit, Object tags) {
NSMutableDictionary<EOEntity, NSArray<? extends ERXGenericRecord>> taggedEntities = new NSMutableDictionary<EOEntity, NSArray<? extends ERXGenericRecord>>();
for (EOEntity taggableEntity : ERTaggableEntity.taggableEntities()) {
NSArray<ERXGenericRecord> taggedItems = ERTaggableEntity.taggableEntity(taggableEntity).fetchTaggedWith(editingContext, inclusion, limit, tags);
taggedEntities.setObjectForKey(taggedItems, taggableEntity);
}
return taggedEntities;
}
/**
* Returns whether or not the given entity has been registered as taggable.
*
* @param entity the entity to check
* @return true if the entity is taggable, false if not
*/
public static boolean isTaggable(EOEntity entity) {
return Boolean.TRUE.equals(entity.userInfo().objectForKey(ERTaggableEntity.ERTAGGABLE_KEY));
}
/**
* Returns an array of taggable entities.
*
* @return an array of taggable entities
*/
@SuppressWarnings("unchecked")
public static NSArray<EOEntity> taggableEntities() {
NSMutableArray<EOEntity> taggableEntities = new NSMutableArray<>();
for (EOModel model : EOModelGroup.defaultGroup().models()) {
for (EOEntity entity : model.entities()) {
if (ERTaggableEntity.isTaggable(entity)) {
taggableEntities.addObject(entity);
}
}
}
return taggableEntities;
}
/**
* Returns the flattened to-many relationship from the taggable entity to the given tag entity.
*
* @param entity the taggable entity
* @param tagEntity the tag entity
* @return the flattened to-many relationship between them (or null if there isn't one)
*/
@SuppressWarnings("unchecked")
public static EORelationship tagsRelationshipForEntity(EOEntity entity, EOEntity tagEntity) {
EORelationship tagsRelationship = null;
for (EORelationship relationship : entity.relationships()) {
if (relationship.isFlattened() && tagEntity.name().equals(relationship.destinationEntity().name())) {
tagsRelationship = relationship;
break;
}
}
return tagsRelationship;
}
/**
* Registers the given entity name in the default model group as taggable. An entity must
* be registered as taggable prior to attempting any tagging operations on it. The application
* constructor is an obvious place to register an entity as taggable. If the entity does not
* contain a flattened to-many tags relationship, a join entity (between your entity and "ERTag")
* and a flattened tags relationship (named "tags") will be automatically generated.
*
* @param entityName the name of the entity to lookup
* @param taggableEntity the taggable entity to associate with this taggable
* @return the join entity (you can probably ignore this)
*/
public static EOEntity registerTaggable(String entityName, Class<? extends ERTaggableEntity<?>> taggableEntity) {
EOEntity joinEntity = ERTaggableEntity.registerTaggable(entityName);
ERTaggableEntity.setTaggableEntityForEntityNamed(taggableEntity, entityName);
return joinEntity;
}
/**
* Registers the given entity name in the default model group as taggable. An entity must
* be registered as taggable prior to attempting any tagging operations on it. The application
* constructor is an obvious place to register an entity as taggable. If the entity does not
* contain a flattened to-many tags relationship, a join entity (between your entity and "ERTag")
* and a flattened tags relationship (named "tags") will be automatically generated.
*
* @param entityName the name of the entity to lookup
* @return the join entity (you can probably ignore this)
*/
public static EOEntity registerTaggable(String entityName) {
EOEntity entity = EOModelGroup.defaultGroup().entityNamed(entityName);
if (entity == null) {
throw new IllegalArgumentException("There is no entity named '" + entityName + "' in this model group.");
}
return ERTaggableEntity.registerTaggable(entity);
}
/**
* Registers the given entity as taggable. An entity must be registered as taggable prior
* to attempting any tagging operations on it. The application constructor is an obvious
* place to register an entity as taggable. If the entity does not contain a flattened
* to-many tags relationship, a join entity (between your entity and "ERTag") and a
* flattened tags relationship (named "tags") will be automatically generated.
*
* @param entity the entity to register
* @return the join entity (you can probably ignore this)
*/
public static EOEntity registerTaggable(EOEntity entity) {
return ERTaggableEntity.registerTaggable(entity, ERTaggableEntity.DEFAULT_TAGS_RELATIONSHIP_NAME);
}
/**
* Registers the given entity as taggable. An entity must be registered as taggable prior
* to attempting any tagging operations on it. The application constructor is an obvious
* place to register an entity as taggable. If the entity does not contain a flattened
* to-many tags relationship, a join entity and a flattened tags relationship will be
* automatically generated.
*
* @param entity the entity to register
* @param tagsRelationshipName the name of the flattened to-many tags relationship
* @return the join entity (you can probably ignore this)
*/
public static EOEntity registerTaggable(EOEntity entity, String tagsRelationshipName) {
EOEntity tagEntity = entity.model().modelGroup().entityNamed(ERTag.ENTITY_NAME);
if (tagEntity == null) {
throw new IllegalArgumentException("There is no entity named '" + ERTag.ENTITY_NAME + "' in this model group.");
}
return ERTaggableEntity.registerTaggable(entity, tagsRelationshipName, tagEntity, null);
}
/**
* Registers the given entity as taggable. An entity must be registered as taggable prior
* to attempting any tagging operations on it. The application constructor is an obvious
* place to register an entity as taggable. If the entity does not contain a flattened
* to-many tags relationship, a join entity and a flattened tags relationship will be
* automatically generated.
*
* @param entity the entity to register
* @param tagsRelationshipName the name of the flattened to-many tags relationship
* @param tagEntity the ERTag entity that contains the tags for this entity
* @param taggableEntity the taggable entity to associate with this taggable
* @return the join entity (you can probably ignore this)
*/
@SuppressWarnings("unchecked")
public static EOEntity registerTaggable(EOEntity entity, String tagsRelationshipName, EOEntity tagEntity, Class<? extends ERTaggableEntity<?>> taggableEntity) {
EORelationship tagsRelationship;
if (tagsRelationshipName == null) {
tagsRelationship = ERTaggableEntity.tagsRelationshipForEntity(entity, tagEntity);
}
else {
tagsRelationship = entity.relationshipNamed(tagsRelationshipName);
}
EOEntity joinEntity = null;
if (tagsRelationship == null) {
joinEntity = new EOEntity();
joinEntity.setName(entity.name() + "Tag");
joinEntity.setExternalName(joinEntity.name());
EORelationship joinToItemRelationship = new EORelationship();
joinToItemRelationship.setName(entity.name());
joinToItemRelationship.setIsMandatory(true);
joinToItemRelationship.setToMany(false);
joinToItemRelationship.setJoinSemantic(EORelationship.InnerJoin);
joinEntity.addRelationship(joinToItemRelationship);
for (EOAttribute itemPrimaryKey : entity.primaryKeyAttributes()) {
EOAttribute itemFKAttribute = new EOAttribute();
itemFKAttribute.setExternalType(itemPrimaryKey.externalType());
itemFKAttribute.setValueType(itemPrimaryKey.valueType());
itemFKAttribute.setName("item_" + itemPrimaryKey.name());
itemFKAttribute.setColumnName("item_" + itemPrimaryKey.columnName());
itemFKAttribute.setClassName(itemPrimaryKey.className());
itemFKAttribute.setWidth(itemPrimaryKey.width());
itemFKAttribute.setPrecision(itemPrimaryKey.precision());
itemFKAttribute.setScale(itemPrimaryKey.scale());
itemFKAttribute.setAllowsNull(false);
joinEntity.addAttribute(itemFKAttribute);
EOJoin join = new EOJoin(itemFKAttribute, itemPrimaryKey);
joinToItemRelationship.addJoin(join);
}
EORelationship joinToTagRelationship = new EORelationship();
joinToTagRelationship.setName(tagEntity.name());
joinToTagRelationship.setIsMandatory(true);
joinToTagRelationship.setToMany(false);
joinToTagRelationship.setJoinSemantic(EORelationship.InnerJoin);
joinEntity.addRelationship(joinToTagRelationship);
for (EOAttribute tagPrimaryKey : tagEntity.primaryKeyAttributes()) {
EOAttribute tagFKAttribute = new EOAttribute();
tagFKAttribute.setExternalType(tagPrimaryKey.externalType());
tagFKAttribute.setValueType(tagPrimaryKey.valueType());
tagFKAttribute.setName("tag_" + tagPrimaryKey.name());
tagFKAttribute.setColumnName("tag_" + tagPrimaryKey.columnName());
tagFKAttribute.setClassName(tagPrimaryKey.className());
tagFKAttribute.setWidth(tagPrimaryKey.width());
tagFKAttribute.setPrecision(tagPrimaryKey.precision());
tagFKAttribute.setScale(tagPrimaryKey.scale());
tagFKAttribute.setAllowsNull(false);
joinEntity.addAttribute(tagFKAttribute);
joinToTagRelationship.addJoin(new EOJoin(tagFKAttribute, tagPrimaryKey));
}
joinEntity.setPrimaryKeyAttributes(joinEntity.attributes());
joinEntity.setAttributesUsedForLocking(joinEntity.attributes());
entity.model().addEntity(joinEntity);
EORelationship itemToJoinRelationship = new EORelationship();
itemToJoinRelationship.setEntity(joinToItemRelationship.destinationEntity());
itemToJoinRelationship.setName("_eofInv_" + joinToItemRelationship.entity().name() + "_" + joinToItemRelationship.name());
NSArray<EOJoin> joinToItemRelationshipJoins = joinToItemRelationship.joins();
for (int joinNum = joinToItemRelationshipJoins.count() - 1; joinNum >= 0; joinNum--) {
EOJoin join = joinToItemRelationshipJoins.objectAtIndex(joinNum);
EOJoin inverseJoin = new EOJoin(join.destinationAttribute(), join.sourceAttribute());
itemToJoinRelationship.addJoin(inverseJoin);
}
itemToJoinRelationship.setDeleteRule(1); // cascade
itemToJoinRelationship.setJoinSemantic(EORelationship.InnerJoin);
itemToJoinRelationship.setToMany(true);
itemToJoinRelationship.setPropagatesPrimaryKey(true);
entity.addRelationship(itemToJoinRelationship);
NSMutableArray properties = entity.classProperties().mutableClone();
properties.remove(itemToJoinRelationship);
entity.setClassProperties(properties);
EORelationship itemToTagsRelationship = new EORelationship();
itemToTagsRelationship.setName(tagsRelationshipName);
entity.addRelationship(itemToTagsRelationship);
itemToTagsRelationship.setDefinition(itemToJoinRelationship.name() + "." + joinToTagRelationship.name());
tagsRelationship = itemToTagsRelationship;
}
else if (!tagsRelationship.isFlattened()) {
throw new IllegalArgumentException("The relationship '" + tagsRelationship.name() + "' on '" + entity.name() + "' must be flattened.");
}
else {
EORelationship itemToJoinRelationship = (EORelationship) tagsRelationship.componentRelationships().objectAtIndex(0);
joinEntity = itemToJoinRelationship.destinationEntity();
}
NSMutableDictionary userInfo = entity.userInfo().mutableClone();
userInfo.setObjectForKey(Boolean.TRUE, ERTaggableEntity.ERTAGGABLE_KEY);
userInfo.setObjectForKey(tagsRelationship.name(), ERTaggableEntity.ERTAGGABLE_TAG_RELATIONSHIP_KEY);
userInfo.setObjectForKey(tagEntity.name(), ERTaggableEntity.ERTAGGABLE_TAG_ENTITY_KEY);
entity.setUserInfo(userInfo);
if (taggableEntity != null) {
ERTaggableEntity.setTaggableEntityForEntityNamed(taggableEntity, entity.name());
}
return joinEntity;
}
/**
* Returns the tag normalizer for this entity.
*
* @return the tag normalizer for this entity
*/
public ERTagNormalizer normalizer() {
return _normalizer;
}
/**
* Sets the tag normalizer for this entity.
*
* @param normalizer the tag normalizer for this entity
*/
public void setNormalizer(ERTagNormalizer normalizer) {
_normalizer = normalizer;
}
/**
* Fetches the tag with the given name. If that tag doesn't exist and createIfMissing
* is true, a tag with that name will be created (otherwise null will be returned). Tags
* are created in a separate transaction to prevent race conditions with duplicate
* tag names from rolling back your primary editing context, which means that even if
* you rollback your editingContext, any tags created during its lifetime will remain.
*
* @param editingContext the editing context to fetch into
* @param tagName the name of the tag to lookup
* @param createIfMissing if true, missing tags will be created
* @return the corresponding ERTag (or null if not found)
*/
@SuppressWarnings( { "cast", "unchecked" })
public ERTag fetchTagNamed(EOEditingContext editingContext, String tagName, boolean createIfMissing) {
NSArray<ERTag> tags = (NSArray<ERTag>) ERXEOControlUtilities.objectsWithQualifier(editingContext, _tagEntity.name(), ERTag.NAME.is(tagName), null, true, true, true, true);
ERTag tag;
if (tags.count() == 0) {
if (createIfMissing) {
// Create it in another transaction so we can catch the dupe exception. Note that
// this means that tags will ALWAYS be created even if the parent transaction
// rolls back. It's mostly for your own good :)
EOEditingContext newEditingContext = ERXEC.newEditingContext();
try {
ERTag newTag = createTagNamed(newEditingContext, tagName);
newEditingContext.saveChanges();
tag = newTag.localInstanceIn(editingContext);
}
catch (EOGeneralAdaptorException e) {
// We'll assume this was because of a duplicate key exception and just retry the original
// fetch WITHOUT createIfMissing. If that returns a null, then we know it was some other
// crazy exception and just throw it.
tag = fetchTagNamed(editingContext, tagName, false);
if (tag == null) {
throw e;
}
}
}
else {
tag = null;
}
}
else if (tags.count() == 1) {
tag = tags.objectAtIndex(0);
}
else {
throw new IllegalArgumentException("There was more than one tag with the name '" + tagName + "'");
}
return tag;
}
/**
* Creates a tag with the given name.
*
* @param editingContext the editing context to create within
* @param tagName the new tag name
* @return the created tag
*/
public ERTag createTagNamed(EOEditingContext editingContext, String tagName) {
ERTag tag = (ERTag) EOUtilities.createAndInsertInstance(editingContext, _tagEntity.name());
tag.setName(tagName);
return tag;
}
/**
* Factory method for generating an ERTaggable wrapper for an EO.
*
* @param eo the EO to wrap
* @return an ERTaggable wrapper
*/
public ERTaggable<T> taggable(T eo) {
return new ERTaggable<>(this, eo);
}
/**
* Returns the name of the tags relationship for this entity.
*
* @return the name of the tags relationship for this entity
*/
public String tagsRelationshipName() {
return _tagsRelationship.name();
}
/**
* Returns the tags relationship for this entity.
*
* @return the tags relationship for this entity
*/
public EORelationship tagsRelationship() {
return _tagsRelationship;
}
/**
* Returns whether or not the given separator contains whitespace (and should be escaped).
*
* @return true if the given separator contains whitespace
*/
public static boolean isWhitespaceSeparator(String separator) {
return separator != null && (separator.contains("\\s") || separator.contains(" "));
}
/**
* Splits the given "tags" object (String, array of Strings, etc) into an array of normalized tag strings.
*
* @param tags the object that contains the tags to split
* @return the list of split tag names
*/
@SuppressWarnings("unchecked")
public NSArray<String> splitTagNames(Object tags) {
NSMutableSet<String> tagNames = new NSMutableSet<>();
if (tags != null) {
if (tags instanceof String) {
String[] strTags;
if (ERTaggableEntity.isWhitespaceSeparator(_separator)) {
List<String> strTagsList = new LinkedList<>();
ERXCommandLineTokenizer tagTokenizer = new ERXCommandLineTokenizer((String) tags);
while (tagTokenizer.hasMoreTokens()) {
String tag = tagTokenizer.nextElement();
strTagsList.add(tag);
}
strTags = strTagsList.toArray(new String[strTagsList.size()]);
}
else {
strTags = ((String) tags).split(_separator);
}
addNormalizedTags(tagNames, strTags);
}
else if (tags instanceof ERTag) {
tagNames.addObject(((ERTag) tags).name());
}
else if (tags instanceof NSArray) {
addNormalizedTags(tagNames, ((NSArray<Object>) tags).objects());
}
else if (tags instanceof Object[]) {
addNormalizedTags(tagNames, (Object[]) tags);
}
else {
throw new IllegalArgumentException("Unknown tag type '" + tags.getClass().getName() + "' (" + tags + " ).");
}
}
return tagNames.allObjects();
}
/**
* Normalizes tags from tags array and adds them to set
*
* @param set set that normalized tags should be added to
* @param tags array of unclean tags
*/
private void addNormalizedTags(NSMutableSet<String> set, Object[] tags) {
for (Object objTag : tags) {
if (objTag instanceof String) {
String strTag = (String) objTag;
String normalizedTag = _normalizer.normalize(strTag);
if (normalizedTag != null && normalizedTag.length() > 0) {
set.addObject(normalizedTag);
}
}
else if (objTag instanceof ERTag) {
set.addObject(((ERTag)objTag).name());
}
else {
throw new IllegalArgumentException("Unknown tag type '" + objTag.getClass().getName() + "' (" + objTag + " ).");
}
}
}
/**
* Fetches the list of objects of this entity type that are tagged
* with all of the given tags with unlimited results.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @return the array of matching eos
*/
public NSArray<T> fetchTaggedWith(EOEditingContext editingContext, Object tags) {
return fetchTaggedWith(editingContext, ERTag.Inclusion.ALL, tags);
}
/**
* Fetches the list of objects of this entity type that are tagged
* with the given tags with unlimited results.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @return the array of matching eos
*/
public NSArray<T> fetchTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, Object tags) {
return fetchTaggedWith(editingContext, inclusion, -1, tags);
}
/**
* Fetches the list of objects of this entity type that are tagged
* with the given tags.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @param limit limit the number of results to be returned (-1 for unlimited)
* @return the array of matching eos
*/
public NSArray<T> fetchTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, int limit, Object tags) {
return fetchTaggedWith(editingContext, inclusion, limit, tags, null);
}
/**
* Fetches the list of objects of this entity type that are tagged
* with the given tags.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @param limit limit the number of results to be returned (-1 for unlimited)
* @param additionalQualifier an additional qualifier to chain in
* @return the array of matching eos
*/
@SuppressWarnings("unchecked")
public NSArray<T> fetchTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, int limit, Object tags, EOQualifier additionalQualifier) {
return this.fetchTaggedWith(editingContext, inclusion, limit, tags, additionalQualifier, null);
}
/**
* Fetches the sorted list of objects of this entity type that are tagged
* with the given tags.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @param limit limit the number of results to be returned (-1 for unlimited)
* @param additionalQualifier an additional qualifier to chain in
* @param sortOrderings sort orderings for the fetch spec
* @return the array of matching eos
*/
@SuppressWarnings("unchecked")
public NSArray<T> fetchTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, int limit, Object tags, EOQualifier additionalQualifier, NSArray<EOSortOrdering> sortOrderings) {
NSArray<String> tagNames = splitTagNames(tags);
if (tagNames.count() == 0) {
throw new IllegalArgumentException("No tags were passed in.");
}
ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(_entity.model());
EOQualifier qualifier = new ERXKey<ERTag>(_tagsRelationship.name()).append(ERTag.NAME).in(tagNames);
if (additionalQualifier != null) {
qualifier = ERXQ.and(qualifier, additionalQualifier);
}
EOFetchSpecification fetchSpec = new EOFetchSpecification(_entity.name(), qualifier, sortOrderings);
EOSQLExpression sqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, fetchSpec, 0, limit);
sqlHelper.addGroupByClauseToExpression(editingContext, fetchSpec, sqlExpression);
if (inclusion == ERTag.Inclusion.ALL) {
sqlHelper.addHavingCountClauseToExpression(EOQualifier.QualifierOperatorEqual, tagNames.count(), sqlExpression);
}
NSArray<NSDictionary> rawRows = ERXEOAccessUtilities.rawRowsForSQLExpression(editingContext, _entity.model(), sqlExpression, sqlHelper.attributesToFetchForEntity(fetchSpec, _entity));
NSArray<T> objs;
objs = ERXEOControlUtilities.faultsForRawRowsFromEntity(editingContext, rawRows, _entity.name());
objs = ERXEOControlUtilities.objectsForFaultWithSortOrderings(editingContext, objs, fetchSpec.sortOrderings());
return objs;
}
/**
* Remove all of the tags from instances of this entity type.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to remove (String to tokenize, NSArray<String>, etc)
*/
public void removeTags(EOEditingContext editingContext, Object tags) {
replaceTags(editingContext, ERTag.Inclusion.ALL, tags, null);
}
/**
* Looks for items with oldTags and replaces them with all of newTags.
*
* @param editingContext the editing context to remove with
* @param oldTags the tags to find and remove (String to tokenize, NSArray<String>, etc)
* @param newTags the tags to add
* @param inclusion if ANY, finds any tags that match, removes them all, and adds newTags; if all, requires all tags to match before replacing
*/
public void replaceTags(EOEditingContext editingContext, ERTag.Inclusion inclusion, Object oldTags, Object newTags) {
for (T item : fetchTaggedWith(editingContext, inclusion, oldTags)) {
ERTaggable<T> taggable = taggable(item);
taggable.removeTags(oldTags);
taggable.addTags(newTags);
}
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. This
* does not include any restriction on the count required for results to be returned nor
* does it limit the number of results returned.
*
* @param editingContext the editing context to fetch into
* @return a dictionary of tags and their occurrence count
*/
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext) {
return tagCount(editingContext, null);
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. This
* does not include any restriction on the count required for results to be returned nor
* does it limit the number of results returned.
*
* @param editingContext the editing context to fetch into
* @param additionalQualifier an optional restrictingQualifier
* @return a dictionary of tags and their occurrence count
*/
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext, EOQualifier additionalQualifier) {
return tagCount(editingContext, -1, additionalQualifier);
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. This
* does not include any restriction on the count required for results to be returned.
*
* @param editingContext the editing context to fetch into
* @param limit the limit of the number of results to return (ordered by count DESC)
* @return a dictionary of tags and their occurrence count
*/
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext, int limit) {
return tagCount(editingContext, limit, null);
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. This
* does not include any restriction on the count required for results to be returned.
*
* @param editingContext the editing context to fetch into
* @param limit the limit of the number of results to return (ordered by count DESC)
* @param additionalQualifier an optional restrictingQualifier
* @return a dictionary of tags and their occurrence count
*/
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext, int limit, EOQualifier additionalQualifier) {
return tagCount(editingContext, null, -1, limit, additionalQualifier);
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. Providing
* a selector and count allows you to add a restriction on, for instance, the minimum number of
* occurrences required for a result to appear. As an example, you might have
* selector = EOQualifier.QualifierOperatorGreaterThan, count = 1 to only return tags with more
* than one occurrence.
*
* @param editingContext the editing context to fetch into
* @param selector a selector for the count restriction (see EOQualifier.QualifierOperators)
* @param count the count restriction required for the result to be returned
* @param limit the limit of the number of results to return (ordered by count DESC)
* @return a dictionary of tags and their occurrence count
*/
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext, NSSelector selector, int count, int limit) {
return tagCount(editingContext, selector, count, limit, null);
}
/**
* This method counts the number of times the tags have been applied to your objects
* and, by default, returns a dictionary in the form of { 'tag_name' => count, ... }. Providing
* a selector and count allows you to add a restriction on, for instance, the minimum number of
* occurrences required for a result to appear. As an example, you might have
* selector = EOQualifier.QualifierOperatorGreaterThan, count = 1 to only return tags with more
* than one occurrence.
*
* @param editingContext the editing context to fetch into
* @param selector a selector for the count restriction (see EOQualifier.QualifierOperators)
* @param count the count restriction required for the result to be returned
* @param limit the limit of the number of results to return (ordered by count DESC)
* @param additionalQualifier an optional restrictingQualifier. This is combined with the qualifier returned by additionalTagCountQualifier()
* @return a dictionary of tags and their occurrence count
*/
@SuppressWarnings("unchecked")
public NSDictionary<String, Integer> tagCount(EOEditingContext editingContext, NSSelector selector, int count, int limit, EOQualifier additionalQualifier) {
NSMutableArray<EOAttribute> fetchAttributes = new NSMutableArray<>();
ERXEOAttribute tagNameAttribute = new ERXEOAttribute(_entity, _tagsRelationship.name() + "." + ERTag.NAME_KEY);
tagNameAttribute.setName("tagName");
fetchAttributes.addObject(tagNameAttribute);
EOAttribute countAttribute = ERXEOAccessUtilities.createAggregateAttribute(editingContext, "COUNT", ERTag.NAME_KEY, _tagEntity.name(), Number.class, "i", "tagCount", "t2");
fetchAttributes.addObject(countAttribute);
ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(_entity.model());
EOQualifier combinedAdditionalQualifier = null;
EOQualifier additionalTagCountQualifier = additionalTagCountQualifier();
if (additionalTagCountQualifier != null || additionalQualifier != null) {
combinedAdditionalQualifier = ERXQ.and(additionalQualifier, additionalTagCountQualifier);
}
EOFetchSpecification fetchSpec = new EOFetchSpecification(_entity.name(), combinedAdditionalQualifier, null);
EOSQLExpression sqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, fetchSpec, 0, limit, fetchAttributes);
NSMutableArray<EOAttribute> groupByAttributes = new NSMutableArray<>(tagNameAttribute);
sqlHelper.addGroupByClauseToExpression(groupByAttributes, sqlExpression);
if (selector != null) {
sqlHelper.addHavingCountClauseToExpression(selector, count, sqlExpression);
}
if (limit > 0) {
// MS: This is lame, but the dynamic attribute is not properly resolved
// inside of EOSQLExpression because it's not actually part of the entity,
// so you can't order-by one of these attributes. So we just have to stick
// it on the end and hope for the best.
StringBuilder sqlBuffer = new StringBuilder(sqlExpression.statement());
int orderByIndex = sqlHelper._orderByIndex(sqlExpression);
sqlBuffer.insert(orderByIndex, " ORDER BY tagCount DESC");
sqlExpression.setStatement(sqlBuffer.toString());
}
NSMutableDictionary<String, Integer> tagCounts = new NSMutableDictionary<>();
NSArray<NSDictionary> rawRows = ERXEOAccessUtilities.rawRowsForSQLExpression(editingContext, _entity.model(), sqlExpression, fetchAttributes);
for (NSDictionary rawRow : rawRows) {
if (!NSKeyValueCoding.NullValue.equals(rawRow.objectForKey("tagName"))) {
String name = (String) rawRow.objectForKey("tagName");
Integer nameCount = (Integer) rawRow.objectForKey("tagCount");
tagCounts.setObjectForKey(nameCount, name);
}
}
return tagCounts;
}
/**
* This method returns a simple count of the number of distinct objects which match the tags provided.
*
* @param editingContext the editing context to fetch into
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
* @param inclusion find matches for ANY tags or ALL tags provided
* @return the count of distinct objects for the given tags
*/
public int countUniqueTaggedWith(EOEditingContext editingContext, ERTag.Inclusion inclusion, Object tags) {
NSArray<String> tagNames = splitTagNames(tags);
if (tagNames.count() == 0) {
throw new IllegalArgumentException("No tags were passed in.");
}
EOQualifier qualifier = new ERXKey<ERTag>(_tagsRelationship.name()).append(ERTag.NAME).in(tagNames);
EOFetchSpecification fetchSpec = new EOFetchSpecification(_entity.name(), qualifier, null);
fetchSpec.setUsesDistinct(true);
ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(_entity.model());
EOSQLExpression sqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, fetchSpec, 0, -1);
sqlHelper.addGroupByClauseToExpression(editingContext, fetchSpec, sqlExpression);
if (inclusion == ERTag.Inclusion.ALL) {
sqlHelper.addHavingCountClauseToExpression(EOQualifier.QualifierOperatorEqual, tagNames.count(), sqlExpression);
}
int count = sqlHelper.rowCountForFetchSpecification(editingContext, fetchSpec);
return count;
}
/**
* Finds other tags that are related to the tags passed through the tags
* parameter, by finding common records that share similar sets of tags.
* Useful for constructing 'Related tags' lists.
*
* @param tags the tags to search (String to tokenize, NSArray<String>, etc)
*/
@SuppressWarnings("unchecked")
public NSArray<String> fetchRelatedTags(EOEditingContext editingContext, Object tags) {
NSArray<String> tagNames = splitTagNames(tags);
if (tagNames.count() == 0) {
throw new IllegalArgumentException("No tags were passed in.");
}
NSArray<EOAttribute> pkAttrs = _entity.primaryKeyAttributes();
if (pkAttrs.count() > 1) {
throw new IllegalArgumentException("Composite primary keys are not supported for findRelatedTags.");
}
NSMutableArray<EOAttribute> fetchAttributes = new NSMutableArray<>();
fetchAttributes.addObjectsFromArray(_entity.primaryKeyAttributes());
ERXEOAttribute tagNameAttribute = new ERXEOAttribute(_entity, _tagsRelationship.name() + "." + ERTag.NAME_KEY);
tagNameAttribute.setName("tagName");
fetchAttributes.addObject(tagNameAttribute);
ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(_entity.model());
EOQualifier tagNameQualifier = new ERXKey<ERTag>(_tagsRelationship.name()).append(ERTag.NAME).in(tagNames);
EOFetchSpecification fetchSpec = new EOFetchSpecification(_entity.name(), tagNameQualifier, null);
EOSQLExpression sqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, fetchSpec, 0, -1, fetchAttributes);
NSMutableArray<EOAttribute> groupByAttributes = new NSMutableArray<>();
groupByAttributes.addObjectsFromArray(pkAttrs);
sqlHelper.addGroupByClauseToExpression(groupByAttributes, sqlExpression);
sqlHelper.addHavingCountClauseToExpression(EOQualifier.QualifierOperatorEqual, tagNames.count(), sqlExpression);
// MS: Sketchy, I know, but I don't know how to make it do the
// join for me without also having the tag name field selected. I'm sure it's
// possible if I drop down and use lower level API's than
// sqlExpr.selectStatementForAttributes.
sqlHelper.removeSelectFromExpression(tagNameAttribute, sqlExpression);
NSMutableArray<Object> itemPrimaryKeys = new NSMutableArray<>();
NSArray<NSDictionary> rawRows = ERXEOAccessUtilities.rawRowsForSQLExpression(editingContext, _entity.model(), sqlExpression, pkAttrs);
EOAttribute pkAttr = pkAttrs.objectAtIndex(0);
for (NSDictionary rawRow : rawRows) {
Object pk = rawRow.objectForKey(pkAttr.name());
itemPrimaryKeys.addObject(pk);
}
NSMutableArray<EOAttribute> tagsFetchAttributes = new NSMutableArray<>();
// MS: We put this in just because we want to force it to do the join ... We have to
// pull them out later.
tagsFetchAttributes.addObjectsFromArray(_entity.primaryKeyAttributes());
ERXEOAttribute tagIDAttribute = new ERXEOAttribute(_entity, _tagsRelationship.name() + ".id");
tagIDAttribute.setName("id");
tagsFetchAttributes.addObject(tagIDAttribute);
tagsFetchAttributes.addObject(tagNameAttribute);
EOAttribute countAttribute = ERXEOAccessUtilities.createAggregateAttribute(editingContext, "COUNT", ERTag.NAME_KEY, _tagEntity.name(), Number.class, "i", "tagCount", "t2");
tagsFetchAttributes.addObject(countAttribute);
EOQualifier idQualifier = new ERXKey<Object>("id").in(itemPrimaryKeys);
EOFetchSpecification tagsFetchSpec = new EOFetchSpecification(_entity.name(), idQualifier, null);
EOSQLExpression tagsSqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, tagsFetchSpec, 0, -1, tagsFetchAttributes);
NSMutableArray<EOAttribute> tagsGroupByAttributes = new NSMutableArray<>(new EOAttribute[] { tagNameAttribute, tagIDAttribute });
sqlHelper.addGroupByClauseToExpression(tagsGroupByAttributes, tagsSqlExpression);
// MS: This is lame, but the dynamic attribute is not properly resolved
// inside of EOSQLExpression because it's not actually part of the entity,
// so you can't order-by one of these attributes. So we just have to stick
// it on the end and hope for the best.
tagsSqlExpression.setStatement(tagsSqlExpression.statement() + " ORDER BY tagCount DESC");
for (EOAttribute attribute : _entity.primaryKeyAttributes()) {
sqlHelper.removeSelectFromExpression(attribute, tagsSqlExpression);
tagsFetchAttributes.removeObject(attribute);
}
NSMutableArray<String> relatedTagNames = new NSMutableArray<>();
NSArray<NSDictionary> tagsRawRows = ERXEOAccessUtilities.rawRowsForSQLExpression(editingContext, _entity.model(), tagsSqlExpression, tagsFetchAttributes);
for (NSDictionary rawRow : tagsRawRows) {
String name = (String) rawRow.objectForKey("tagName");
relatedTagNames.addObject(name);
}
return relatedTagNames;
}
/**
* Takes the result of a tagCount call and an array of categories and
* distributes the entries in the tagCount hash evenly across the
* categories based on the count value for each tag.
*
* Typically, this is used to display a 'tag cloud' in your UI.
*
* @param categoryList An array containing the categories to split the tags
* @return a dictionary mapping each tag name to its corresponding category
*/
public <U> NSDictionary<String, U> cloud(EOEditingContext editingContext, NSArray<U> categoryList) {
return cloud(tagCount(editingContext), categoryList);
}
/**
* Takes the result of a tagCount call and an array of categories and
* distributes the entries in the tagCount hash evenly across the
* categories based on the count value for each tag.
*
* Typically, this is used to display a 'tag cloud' in your UI.
*
* @param tagHash the tag dictionary returned from a tagCount call
* @param categoryList An array containing the categories to split the tags
* @return a dictionary mapping each tag name to its corresponding category
*/
public <U> NSDictionary<String, U> cloud(NSDictionary<String, Integer> tagHash, NSArray<U> categoryList) {
int min = 0;
int max = 0;
for (Integer count : tagHash.allValues()) {
if (count.intValue() > max) {
max = count.intValue();
}
if (count.intValue() < min) {
min = count.intValue();
}
}
NSMutableDictionary<String, U> cloud = new NSMutableDictionary<>();
int divisor = ((max - min) / categoryList.count()) + 1;
for (Map.Entry<String, Integer> entry : tagHash.entrySet()) {
U obj = categoryList.objectAtIndex((entry.getValue().intValue() - min) / divisor);
cloud.setObjectForKey(obj, entry.getKey());
}
return cloud;
}
/**
* Returns an array of all of the available tags in the system.
*
* @param editingContext the editing context to fetch into
* @return an array of matching tags
*/
@SuppressWarnings("unchecked")
public NSArray<String> fetchAllTags(EOEditingContext editingContext) {
NSArray<ERTag> erTags = ERTag.fetchAllERTags(editingContext);
NSArray<String> tags = (NSArray<String>) erTags.valueForKey(ERTag.NAME_KEY);
return tags;
}
/**
* Returns an array of all of the available tags in the system that start with
* the given string.
*
* @param startsWith the prefix to lookup
* @param editingContext the editing context to fetch into
* @return an array of matching tags
*/
@SuppressWarnings("unchecked")
public NSArray<String> fetchTagsLike(EOEditingContext editingContext, String startsWith) {
NSArray<ERTag> erTags = ERTag.fetchERTags(editingContext, ERTag.NAME.likeInsensitive(startsWith + "*"), null);
NSArray<String> tags = (NSArray<String>) erTags.valueForKey(ERTag.NAME_KEY);
return tags;
}
protected EOQualifier additionalTagCountQualifier () {
return null;
}
//I just can't muster the strength the port this one right now -- that query is ROUGH :)
///**
//* Finds other records that share the most tags with the record passed
//* as the +related+ parameter. Useful for constructing 'Related' or
//* 'See Also' boxes and lists.
//*
//* The options are:
//*
//* +:limit+: defaults to 5, which means the method will return the top 5 records
//* that share the greatest number of tags with the passed one.
//* +:conditions+: any additional conditions that should be appended to the
//* WHERE clause of the finder SQL. Just like regular +ActiveRecord::Base#find+ methods.
//*/
//public NSArray<T> findRelatedTagged(EOEditingContext editingContext, T related, int limit) {
// NSArray<EOSortOrdering> sortOrderings = null;
// EOFetchSpecification fetchSpec = new EOFetchSpecification(_entity.name(), null, sortOrderings);
//
// NSArray<EOAttribute> entityAttributes = _entity.attributesToFetch();
// NSMutableArray<EOAttribute> fetchAttributes = entityAttributes.mutableClone();
//
// EOAttribute countAttribute = ERXEOAccessUtilities.createAggregateAttribute(editingContext, "COUNT", ERTag.NAME_KEY, _tagEntity.name(), Number.class, "i", "tagCount");
// fetchAttributes.addObject(countAttribute);
//
// ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(_entity.model());
//
//
// EOSQLExpression sqlExpression = sqlHelper.sqlExpressionForFetchSpecification(editingContext, fetchSpec, 0, limit);
// sqlHelper.addGroupByClauseToExpression(editingContext, fetchSpec, sqlExpression);
// if (inclusion == ERTag.Inclusion.ALL) {
// sqlHelper.addHavingCountClauseToExpression(EOQualifier.QualifierOperatorEqual, tagNames.count(), sqlExpression);
// }
//
// NSArray<NSDictionary> rawRows = ERXEOAccessUtilities.rawRowsForSQLExpression(editingContext, _entity.model(), sqlExpression, sqlHelper.attributesToFetchForEntity(fetchSpec, _entity));
// NSArray<T> objs;
// objs = ERXEOControlUtilities.faultsForRawRowsFromEntity(editingContext, rawRows, _entity.name());
// objs = ERXEOControlUtilities.objectsForFaultWithSortOrderings(editingContext, objs, fetchSpec.sortOrderings());
// return objs;
//}
//def find_related_tagged(related, options = {})
// related_id = related.is_a?(self) ? related.id : related
// options = { :limit => 5 }.merge(options)
//
// o, o_pk, o_fk, t, tn, t_pk, t_fk, jt = set_locals_for_sql
// sql = "SELECT o.*, COUNT(jt2.#{o_fk}) AS count FROM #{o} o, #{jt} jt, #{t} t, #{jt} jt2
// WHERE jt.#{o_fk}=#{related_id} AND t.#{t_pk} = jt.#{t_fk}
// AND jt2.#{o_fk} != jt.#{o_fk}
// AND jt2.#{t_fk}=jt.#{t_fk} AND o.#{o_pk} = jt2.#{o_fk}"
// sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
// sql << " GROUP BY o.#{o_pk}"
// sql << " ORDER BY count DESC"
// add_limit!(sql, options)
//
// find_by_sql(sql)
//end
}