package er.rest; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.webobjects.appserver.WORequest; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSRange; import com.webobjects.foundation.NSSelector; import er.extensions.eof.ERXFetchSpecificationBatchIterator; import er.extensions.eof.ERXKey; import er.extensions.eof.ERXKeyFilter; import er.extensions.eof.ERXQ; import er.extensions.eof.ERXS; /** * ERXRestFetchSpecification provides a wrapper around fetching objects with batching, sort orderings, and (optionally) * qualifiers configured in the WORequest. * <p> * Example query string parameters: * <ul> * <li>sort=lastName|asc,firstName|desc</li> * <li>batchSize=25&batch=1 (Note that batch number is a zero-based index)</li> * <li>qualifier=firstName%3D'Mike'</li> * <li>Range=items%3D10-19 (Note that the index values for Range items are zero-based)</li> * </ul> * Because request EOQualifiers could possibly pose a security risk, you must explicitly enable request qualifiers by * calling enableRequestQualifiers(baseQualifier) or by using the longer constructor that takes an optional base * qualifier. A base qualifier is prepended (AND'd) to whatever qualifier is passed on the query string to restrict the * results of the user's query. * <p> * An example use: * <pre><code> * public WOActionResults indexAction() throws Throwable { * ERXRestFetchSpecification<Task> fetchSpec = new ERXRestFetchSpecification<Task>(Task.ENTITY_NAME, null, null, queryFilter(), Task.CREATION_DATE.descs(), 25); * NSArray<Task> tasks = fetchSpec.objects(editingContext(), options()); * return response(editingContext(), Task.ENTITY_NAME, tasks, showFilter()); * } * </code></pre> * In this example, we are fetching the "Task" entity, sorted by creation date, with a default batch size of 25, and * with request qualifiers enable (meaning, we allow users to pass in a qualifier in the query string), filtering the * qualifier with the ERXKeyFilter returned by the queryFilter() method. We then fetch the resulting tasks and return * the response to the user. * * @author mschrag * * @param <T> * the type of the objects being returned */ public class ERXRestFetchSpecification<T extends EOEnterpriseObject> { private static final Pattern _rangePattern = Pattern.compile("items=(.*)-(.*)"); private String _entityName; private EOQualifier _defaultQualifier; private EOQualifier _baseQualifier; private ERXKeyFilter _qualifierFilter; private NSArray<EOSortOrdering> _defaultSortOrderings; private int _maxBatchSize; private int _defaultBatchSize; private boolean _requestQualifiersEnabled; /** * Creates a new ERXRestFetchSpecification with a maximum batch size of 100, but with batching turned off by * default. * * @param entityName * the name of the entity being fetched * @param defaultQualifier * the default qualifiers (if none are specified in the request) * @param defaultSortOrderings * the default sort orderings (if none are specified in the request) */ public ERXRestFetchSpecification(String entityName, EOQualifier defaultQualifier, NSArray<EOSortOrdering> defaultSortOrderings) { this(entityName, defaultQualifier, defaultSortOrderings, -1); } /** * Creates a new ERXRestFetchSpecification with a maximum batch size of 100. default. * * @param entityName * the name of the entity being fetched * @param defaultQualifier * the default qualifiers (if none are specified in the request) * @param defaultSortOrderings * the default sort orderings (if none are specified in the request) * @param defaultBatchSize * the default batch size (-1 to disable) */ public ERXRestFetchSpecification(String entityName, EOQualifier defaultQualifier, NSArray<EOSortOrdering> defaultSortOrderings, int defaultBatchSize) { _entityName = entityName; _defaultQualifier = defaultQualifier; _defaultSortOrderings = defaultSortOrderings; _maxBatchSize = 100; _defaultBatchSize = defaultBatchSize; } /** * Creates a new ERXRestFetchSpecification with a maximum batch size of 100 and with request qualifiers enabled. * default. * * @param entityName * the name of the entity being fetched * @param defaultQualifier * the default qualifiers (if none are specified in the request) * @param baseQualifier * the base qualifier (see enableRequestQualifiers) * @param qualifierFilter * the key filter to apply against the query qualifier * @param defaultSortOrderings * the default sort orderings (if none are specified in the request) * @param defaultBatchSize * the default batch size (-1 to disable) */ public ERXRestFetchSpecification(String entityName, EOQualifier defaultQualifier, EOQualifier baseQualifier, ERXKeyFilter qualifierFilter, NSArray<EOSortOrdering> defaultSortOrderings, int defaultBatchSize) { _entityName = entityName; _defaultQualifier = defaultQualifier; _defaultSortOrderings = defaultSortOrderings; _maxBatchSize = 100; _defaultBatchSize = defaultBatchSize; enableRequestQualifiers(baseQualifier, qualifierFilter); } /** * Returns the name of the entity used in this fetch. * * @return the name of the entity used in this fetch */ public String entityName() { return _entityName; } /** * Returns the maximum batch size (defaults to 100). * * @return the maximum batch size */ public int maxBatchSize() { return _maxBatchSize; } /** * Sets the maximum batch size. * * @param maxBatchSize * the maximum batch size */ public void setMaxBatchSize(int maxBatchSize) { _maxBatchSize = maxBatchSize; } /** * Returns the default batch size (defaults to -1 = off). * * @return the default batch size */ public int defaultBatchSize() { return _defaultBatchSize; } /** * Sets the default batch size * * @param defaultBatchSize * the default batch size */ public void setDefaultBatchSize(int defaultBatchSize) { _defaultBatchSize = defaultBatchSize; } /** * Enables qualifiers in the request, but will be AND'd to the given base qualifier (in case you need to perform * security restrictions) * * @param baseQualifier * the base qualifier to and with * @param qualifierFilter * the key filter to apply against the query qualifier */ public void enableRequestQualifiers(EOQualifier baseQualifier, ERXKeyFilter qualifierFilter) { _baseQualifier = baseQualifier; _qualifierFilter = qualifierFilter; _requestQualifiersEnabled = true; } /** * Returns the effective sort orderings. * * @param editingContext * the editing context * @param options * the current options * @return the effective sort orderings */ public NSArray<EOSortOrdering> sortOrderings(EOEditingContext editingContext, NSKeyValueCoding options) { String sortKeysStr = (String) options.valueForKey("sort"); if (sortKeysStr == null || sortKeysStr.length() == 0) { return _defaultSortOrderings; } EOEntity entity = EOUtilities.entityNamed(editingContext, _entityName); NSMutableArray<EOSortOrdering> sortOrderings = new NSMutableArray<>(); for (String sortKeyStr : sortKeysStr.split(",")) { String[] sortAttributes = sortKeyStr.split("\\|"); String sortKey = sortAttributes[0]; NSSelector sortDirection; if (sortAttributes.length == 2) { if (sortAttributes[1].equalsIgnoreCase("asc")) { sortDirection = EOSortOrdering.CompareCaseInsensitiveAscending; } else if (sortAttributes[1].equalsIgnoreCase("desc")) { sortDirection = EOSortOrdering.CompareCaseInsensitiveDescending; } else { sortDirection = EOSortOrdering.CompareCaseInsensitiveAscending; } } else { sortDirection = EOSortOrdering.CompareCaseInsensitiveAscending; } if (_qualifierFilter != null && !_qualifierFilter.matches(new ERXKey<Object>(sortKey), ERXFilteredQualifierTraversal.typeForKeyInEntity(sortKey, entity))) { throw new SecurityException("You do not have access to the key path '" + sortKey + "'."); } sortOrderings.addObject(EOSortOrdering.sortOrderingWithKey(sortKey, sortDirection)); } return sortOrderings; } /** * Returns the effective qualifier. * * @param editingContext * the editing context * @param options * the current options * @return the effective qualifier */ public EOQualifier qualifier(EOEditingContext editingContext, NSKeyValueCoding options) { EOQualifier qualifier; if (!_requestQualifiersEnabled) { qualifier = _defaultQualifier; } else { String qualifierStr = (String) options.valueForKey("qualifier"); if (qualifierStr == null || qualifierStr.length() == 0) { if (_baseQualifier == null) { qualifier = _defaultQualifier; } else { qualifier = ERXQ.and(_baseQualifier, _defaultQualifier); } } else { qualifier = EOQualifier.qualifierWithQualifierFormat(qualifierStr, null); if (qualifier == null) { qualifier = _baseQualifier; } else { EOEntity entity = EOUtilities.entityNamed(editingContext, _entityName); ERXFilteredQualifierTraversal.checkQualifierForEntityWithFilter(qualifier, entity, _qualifierFilter); if (_baseQualifier != null) { qualifier = ERXQ.and(_baseQualifier, qualifier); } } } } return qualifier; } /** * Returns the effective batch number. * * @param options * the current options * @return the effective batch number */ public int batchNumber(NSKeyValueCoding options) { int batchNumber; String batchNumberStr = (String) options.valueForKey("batch"); if (batchNumberStr == null) { batchNumber = 0; } else { batchNumber = Integer.parseInt(batchNumberStr); } return batchNumber; } /** * Returns the range of this fetch. * * @param options * the current options * @return the effective batch number */ public NSRange range(NSKeyValueCoding options) { NSRange range = null; String rangeStr = (String)options.valueForKey("Range"); if (rangeStr != null) { Matcher rangeMatcher = _rangePattern.matcher(rangeStr); if (rangeMatcher.matches()) { int start = Integer.parseInt(rangeMatcher.group(1)); int length = Integer.parseInt(rangeMatcher.group(2)) - start + 1; range = new NSRange(start, length); } } else { int batchNumber = batchNumber(options); int batchSize = batchSize(options); if (batchSize > 0) { range = new NSRange(batchNumber * batchSize, batchSize); } } return range; } /** * Returns the effective batch size. * * @param options * the current options * @return the effective batch size */ public int batchSize(NSKeyValueCoding options) { int batchSize; String batchSizeStr = (String) options.valueForKey("batchSize"); if (batchSizeStr == null) { batchSize = _defaultBatchSize; } else { batchSize = Math.min(Integer.parseInt(batchSizeStr), _maxBatchSize); } return batchSize; } /** * Fetches the objects into the given editing context with the effective attributes of this fetch specification. * * @param editingContext * the editing context to fetch into * @param options * the current options * @return the fetch objects */ @SuppressWarnings("unchecked") public Results<T> results(EOEditingContext editingContext, NSKeyValueCoding options) { Results<T> results; NSArray<EOSortOrdering> sortOrderings = sortOrderings(editingContext, options); EOQualifier qualifier = qualifier(editingContext, options); EOFetchSpecification fetchSpec = new EOFetchSpecification(_entityName, qualifier, sortOrderings); fetchSpec.setIsDeep(true); NSArray<T> objects; NSRange range = range(options); if (range == null) { objects = editingContext.objectsWithFetchSpecification(fetchSpec); results = new Results<>(objects, 0, -1, objects.count()); } else { ERXFetchSpecificationBatchIterator<T> batchIterator = new ERXFetchSpecificationBatchIterator<>(fetchSpec, editingContext, range.length()); objects = batchIterator.batchWithRange(range); results = new Results<>(objects, range.location(), range.length(), batchIterator.count()); } return results; } /** * Fetches the objects into the given editing context with the effective attributes of this fetch specification. * * @param editingContext * the editing context to fetch into * @param options * the current options * @return the fetch objects */ public NSArray<T> objects(EOEditingContext editingContext, NSKeyValueCoding options) { Results<T> results = results(editingContext, options); return results == null ? null : results.objects(); } /** * Applies the effective attributes of this fetch specification to the given array, filtering, sorting, and cutting * into batches accordingly. * * @param objects * the objects to filter * @param editingContext * the editing context to evaluate the qualifer filter with * @param options * the current options * @return the filtered objects */ public NSArray<T> objects(NSArray<T> objects, EOEditingContext editingContext, NSKeyValueCoding options) { NSArray<EOSortOrdering> sortOrderings = sortOrderings(editingContext, options); EOQualifier qualifier = qualifier(editingContext, options); int batchSize = batchSize(options); NSArray<T> results = ERXS.sorted(ERXQ.filtered(objects, qualifier), sortOrderings); if (batchSize > 0) { int batchNumber = batchNumber(options); int offset = batchNumber * batchSize; int length = batchSize; if (offset >= results.count()) { results = NSArray.<T> emptyArray(); } else { NSRange range; if (offset + length > results.count()) { range = new NSRange(offset, results.count() - offset); } else { range = new NSRange(offset, length); } results = results.subarrayWithRange(range); } } return results; } /** * Fetches the objects into the given editing context with the effective attributes of this fetch specification. * * @param editingContext * the editing context to fetch into * @param request * the current request * @return the fetch objects */ public NSArray<T> objects(EOEditingContext editingContext, WORequest request) { return objects(editingContext, new ERXRequestFormValues(request)); } /** * Applies the effective attributes of this fetch specification to the given array, filtering, sorting, and cutting * into batches accordingly. * * @param objects * the objects to filter * @param editingContext * the editing context to evaluate the qualifer filter with * @param request * the current request * @return the filtered objects */ public NSArray<T> objects(NSArray<T> objects, EOEditingContext editingContext, WORequest request) { return objects(objects, editingContext, new ERXRequestFormValues(request)); } /** * Encapsulates the results of a fetch along with some fetch metadata. * * @author mschrag * * @param <T> the type of the result */ public static class Results<T> { private NSArray<T> _objects; private int _startIndex; private int _batchSize; private int _total; /** * Constructs a new Results object. * * @param objects the objects in the result * @param startIndex the start index of the fetch * @param batchSize the size of the batch * @param totalCount the total number of objects */ public Results(NSArray<T> objects, int startIndex, int batchSize, int totalCount) { _objects = objects; _startIndex = startIndex; _batchSize = batchSize; _total = totalCount; } /** * Returns the objects from this batch. * * @return the objects from this batch */ public NSArray<T> objects() { return _objects; } /** * Returns the start index of the fetch. * * @return the start index of the fetch */ public int startIndex() { return _startIndex; } /** * Returns the batch size. * * @return the batch size */ public int batchSize() { return _batchSize; } /** * Returns the total count of the results. * * @return the total count of the results */ public int totalCount() { return _total; } } }