/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2010 Sun Microsystems, Inc.
*/
package org.opends.sdk;
import static com.sun.opends.sdk.messages.Messages.*;
import java.util.*;
import org.opends.sdk.schema.MatchingRule;
import org.opends.sdk.schema.Schema;
import com.sun.opends.sdk.util.Validator;
/**
* A search result sort key as defined in RFC 2891 is used to specify how search
* result entries should be ordered. Sort keys are used with the server side
* sort request control
* {@link org.opends.sdk.controls.ServerSideSortRequestControl}, but could also
* be used for performing client side sorting as well.
*
* The following example illustrates how a single sort key may be used to sort
* entries as they are returned from a search operation using the {@code cn}
* attribute as the sort key:
*
*
* Connection connection = ...;
* SearchRequest request = ...;
*
* Comparator<Entry> comparator = SortKey.comparator("cn");
* Set<SearchResultEntry>; results = new TreeSet<SearchResultEntry>(comparator);
*
* connection.search(request, results);
*
*
* A sort key includes an attribute description and a boolean value that
* indicates whether the sort should be ascending or descending. It may also
* contain a specific ordering matching rule that should be used for the sorting
* process, although if none is provided it will use the default ordering
* matching rule for the attribute type.
*
* @see RFC 2891 - LDAP Control
* Extension for Server Side Sorting of Search Results
*/
public final class SortKey
{
private static final class CompositeEntryComparator implements
Comparator
{
private final List> comparators;
private CompositeEntryComparator(final List> comparators)
{
this.comparators = comparators;
}
public int compare(final Entry entry1, final Entry entry2)
{
for (final Comparator comparator : comparators)
{
final int result = comparator.compare(entry1, entry2);
if (result != 0)
{
return result;
}
}
return 0;
}
}
/**
* A comparator which can be used to compare entries using a sort key.
*/
private static final class EntryComparator implements Comparator
{
private final AttributeDescription attributeDescription;
private final MatchingRule matchingRule;
private final Comparator valueComparator;
private final boolean isReverseOrder;
private EntryComparator(final AttributeDescription attributeDescription,
final MatchingRule matchingRule, final boolean isReverseOrder)
{
this.attributeDescription = attributeDescription;
this.matchingRule = matchingRule;
this.valueComparator = matchingRule.comparator();
this.isReverseOrder = isReverseOrder;
}
/**
* We must use the lowest available value in both entries and missing
* attributes sort last.
*/
public int compare(final Entry entry1, final Entry entry2)
{
// Find an normalize the lowest value attribute in entry1
ByteString lowestNormalizedValue1 = null;
for (final Attribute attribute : entry1
.getAllAttributes(attributeDescription))
{
for (final ByteString value : attribute)
{
try
{
final ByteString tmp = matchingRule.normalizeAttributeValue(value);
if (lowestNormalizedValue1 == null)
{
lowestNormalizedValue1 = tmp;
}
else if (valueComparator.compare(tmp, lowestNormalizedValue1) < 0)
{
lowestNormalizedValue1 = tmp;
}
}
catch (final DecodeException ignored)
{
// Ignore the error - treat the value as missing.
}
}
}
// Find an normalize the lowest value attribute in entry2
ByteString lowestNormalizedValue2 = null;
for (final Attribute attribute : entry2
.getAllAttributes(attributeDescription))
{
for (final ByteString value : attribute)
{
try
{
final ByteString tmp = matchingRule.normalizeAttributeValue(value);
if (lowestNormalizedValue2 == null)
{
lowestNormalizedValue2 = tmp;
}
else if (valueComparator.compare(tmp, lowestNormalizedValue2) < 0)
{
lowestNormalizedValue2 = tmp;
}
}
catch (final DecodeException ignored)
{
// Ignore the error - treat the value as missing.
}
}
}
// Entries with missing attributes always sort after (regardless of
// order).
if (lowestNormalizedValue1 == null)
{
return lowestNormalizedValue2 != null ? 1 : 0;
}
if (lowestNormalizedValue2 == null)
{
return -1;
}
if (isReverseOrder)
{
return valueComparator.compare(lowestNormalizedValue2,
lowestNormalizedValue1);
}
else
{
return valueComparator.compare(lowestNormalizedValue1,
lowestNormalizedValue2);
}
}
}
/**
* Returns a {@code Comparator} which can be used to compare entries using the
* provided list of sort keys. The sort keys will be decoded using the default
* schema.
*
* @param keys
* The list of sort keys.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If one of the sort keys could not be converted to a comparator.
* @throws IllegalArgumentException
* If {@code keys} was empty.
* @throws NullPointerException
* If {@code keys} was {@code null}.
*/
public static Comparator comparator(final Collection keys)
throws LocalizedIllegalArgumentException, IllegalArgumentException,
NullPointerException
{
return comparator(Schema.getDefaultSchema(), keys);
}
/**
* Returns a {@code Comparator} which can be used to compare entries using the
* provided list of sort keys. The sort keys will be decoded using the
* provided schema.
*
* @param schema
* The schema which should be used for decoding the sort keys.
* @param keys
* The list of sort keys.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If one of the sort keys could not be converted to a comparator.
* @throws IllegalArgumentException
* If {@code keys} was empty.
* @throws NullPointerException
* If {@code schema} or {@code keys} was {@code null}.
*/
public static Comparator comparator(final Schema schema,
final Collection keys) throws LocalizedIllegalArgumentException,
IllegalArgumentException, NullPointerException
{
Validator.ensureNotNull(schema, keys);
Validator.ensureTrue(!keys.isEmpty(), "keys must not be empty");
final List> comparators = new ArrayList>(
keys.size());
for (final SortKey key : keys)
{
comparators.add(key.comparator(schema));
}
return new CompositeEntryComparator(comparators);
}
/**
* Returns a {@code Comparator} which can be used to compare entries using the
* provided list of sort keys. The sort keys will be decoded using the
* provided schema.
*
* @param schema
* The schema which should be used for decoding the sort keys.
* @param firstKey
* The first sort key.
* @param remainingKeys
* The remaining sort keys.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If one of the sort keys could not be converted to a comparator.
* @throws NullPointerException
* If {@code schema} or {@code firstKey} was {@code null}.
*/
public static Comparator comparator(final Schema schema,
final SortKey firstKey, final SortKey... remainingKeys)
throws LocalizedIllegalArgumentException, NullPointerException
{
Validator.ensureNotNull(schema, firstKey, remainingKeys);
final List> comparators = new ArrayList>(
1 + remainingKeys.length);
comparators.add(firstKey.comparator(schema));
for (final SortKey key : remainingKeys)
{
comparators.add(key.comparator(schema));
}
return new CompositeEntryComparator(comparators);
}
/**
* Returns a {@code Comparator} which can be used to compare entries using the
* provided list of sort keys. The sort keys will be decoded using the default
* schema.
*
* @param firstKey
* The first sort key.
* @param remainingKeys
* The remaining sort keys.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If one of the sort keys could not be converted to a comparator.
* @throws NullPointerException
* If {@code firstKey} was {@code null}.
*/
public static Comparator comparator(final SortKey firstKey,
final SortKey... remainingKeys) throws LocalizedIllegalArgumentException,
NullPointerException
{
return comparator(Schema.getDefaultSchema(), firstKey, remainingKeys);
}
/**
* Returns a {@code Comparator} which can be used to compare entries using the
* provided string representation of a list of sort keys. The sort keys will
* be decoded using the default schema. The string representation is comprised
* of a comma separate list of sort keys as defined in
* {@link #valueOf(String)}. There must be at least one sort key present in
* the string representation.
*
* @param sortKeys
* The list of sort keys.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If {@code sortKeys} is not a valid string representation of a
* list of sort keys, or if one of the sort keys could not be
* converted to a comparator.
* @throws NullPointerException
* If {@code sortKeys} was {@code null}.
*/
public static Comparator comparator(final String sortKeys)
throws LocalizedIllegalArgumentException, NullPointerException
{
Validator.ensureNotNull(sortKeys);
final List> comparators = new LinkedList>();
final StringTokenizer tokenizer = new StringTokenizer(sortKeys, ",");
while (tokenizer.hasMoreTokens())
{
final String token = tokenizer.nextToken().trim();
comparators.add(valueOf(token).comparator());
}
if (comparators.isEmpty())
{
final LocalizableMessage message = ERR_SORT_KEY_NO_SORT_KEYS
.get(sortKeys);
throw new LocalizedIllegalArgumentException(message);
}
return new CompositeEntryComparator(comparators);
}
/**
* Parses the provided string representation of a sort key as a {@code
* SortKey}. The string representation has the following ABNF (see RFC 4512
* for definitions of the elements below):
*
*
* SortKey = [ PLUS / HYPHEN ] ; order specifier
* attributedescription ; attribute description
* [ COLON oid ] ; ordering matching rule OID
*
*
* Examples:
*
*
* cn ; case ignore ascending sort on "cn"
* -cn ; case ignore descending sort on "cn"
* +cn;lang-fr ; case ignore ascending sort on "cn;lang-fr"
* -cn;lang-fr:caseExactMatch ; case exact ascending sort on "cn;lang-fr"
*
*
* @param sortKey
* The string representation of a sort key.
* @return The parsed {@code SortKey}.
* @throws LocalizedIllegalArgumentException
* If {@code sortKey} is not a valid string representation of a sort
* key.
* @throws NullPointerException
* If {@code sortKey} was {@code null}.
*/
public static final SortKey valueOf(String sortKey)
throws LocalizedIllegalArgumentException, NullPointerException
{
Validator.ensureNotNull(sortKey);
boolean reverseOrder = false;
if (sortKey.startsWith("-"))
{
reverseOrder = true;
sortKey = sortKey.substring(1);
}
else if (sortKey.startsWith("+"))
{
sortKey = sortKey.substring(1);
}
final int colonPos = sortKey.indexOf(':');
if (colonPos < 0)
{
if (sortKey.length() == 0)
{
final LocalizableMessage message = ERR_SORT_KEY_NO_ATTR_NAME
.get(sortKey);
throw new LocalizedIllegalArgumentException(message);
}
return new SortKey(sortKey, reverseOrder, null);
}
else if (colonPos == 0)
{
final LocalizableMessage message = ERR_SORT_KEY_NO_ATTR_NAME.get(sortKey);
throw new LocalizedIllegalArgumentException(message);
}
else if (colonPos == (sortKey.length() - 1))
{
final LocalizableMessage message = ERR_SORT_KEY_NO_MATCHING_RULE
.get(sortKey);
throw new LocalizedIllegalArgumentException(message);
}
else
{
final String attrName = sortKey.substring(0, colonPos);
final String ruleID = sortKey.substring(colonPos + 1);
return new SortKey(attrName, reverseOrder, ruleID);
}
}
private final String attributeDescription;
private final String orderingMatchingRule;
private final boolean isReverseOrder;
/**
* Creates a new sort key using the provided attribute description. The
* returned sort key will compare attributes in the order specified using the
* named ordering matching rule.
*
* @param attributeDescription
* The name of the attribute to be sorted using this sort key.
* @param isReverseOrder
* {@code true} if this sort key should be evaluated in reverse
* (descending) order.
* @param orderingMatchingRule
* The name or OID of the ordering matching rule, which should be
* used when comparing attributes using this sort key, or {@code
* null} if the default ordering matching rule associated with the
* attribute should be used.
* @throws NullPointerException
* If {@code AttributeDescription} was {@code null}.
*/
public SortKey(final AttributeDescription attributeDescription,
final boolean isReverseOrder, final MatchingRule orderingMatchingRule)
throws NullPointerException
{
Validator.ensureNotNull(attributeDescription);
this.attributeDescription = attributeDescription.toString();
this.orderingMatchingRule = orderingMatchingRule != null ? orderingMatchingRule
.getNameOrOID()
: null;
this.isReverseOrder = isReverseOrder;
}
/**
* Creates a new sort key using the provided attribute description. The
* returned sort key will compare attributes in ascending order using the
* default ordering matching rule associated with the attribute.
*
* @param attributeDescription
* The name of the attribute to be sorted using this sort key.
* @throws NullPointerException
* If {@code AttributeDescription} was {@code null}.
*/
public SortKey(final String attributeDescription) throws NullPointerException
{
this(attributeDescription, false, null);
}
/**
* Creates a new sort key using the provided attribute description. The
* returned sort key will compare attributes in the order specified using the
* default ordering matching rule associated with the attribute.
*
* @param attributeDescription
* The name of the attribute to be sorted using this sort key.
* @param isReverseOrder
* {@code true} if this sort key should be evaluated in reverse
* (descending) order.
* @throws NullPointerException
* If {@code AttributeDescription} was {@code null}.
*/
public SortKey(final String attributeDescription, final boolean isReverseOrder)
throws NullPointerException
{
this(attributeDescription, isReverseOrder, null);
}
/**
* Creates a new sort key using the provided attribute description. The
* returned sort key will compare attributes in the order specified using the
* named ordering matching rule.
*
* @param attributeDescription
* The name of the attribute to be sorted using this sort key.
* @param isReverseOrder
* {@code true} if this sort key should be evaluated in reverse
* (descending) order.
* @param orderingMatchingRule
* The name or OID of the ordering matching rule, which should be
* used when comparing attributes using this sort key, or {@code
* null} if the default ordering matching rule associated with the
* attribute should be used.
* @throws NullPointerException
* If {@code AttributeDescription} was {@code null}.
*/
public SortKey(final String attributeDescription,
final boolean isReverseOrder, final String orderingMatchingRule)
throws NullPointerException
{
Validator.ensureNotNull(attributeDescription);
this.attributeDescription = attributeDescription;
this.orderingMatchingRule = orderingMatchingRule;
this.isReverseOrder = isReverseOrder;
}
/**
* Returns a {@code Comparator} which can be used to compare entries using
* this sort key. The attribute description and matching rule, if present,
* will be decoded using the default schema.
*
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If attributeDescription is not a valid LDAP string representation
* of an attribute description, or if no ordering matching rule was
* found.
*/
public Comparator comparator()
throws LocalizedIllegalArgumentException
{
return comparator(Schema.getDefaultSchema());
}
/**
* Returns a {@code Comparator} which can be used to compare entries using
* this sort key. The attribute description and matching rule, if present,
* will be decoded using the provided schema.
*
* @param schema
* The schema which should be used for decoding the attribute
* description and matching rule.
* @return The {@code Comparator}.
* @throws LocalizedIllegalArgumentException
* If attributeDescription is not a valid LDAP string representation
* of an attribute description, or if no ordering matching rule was
* found.
* @throws NullPointerException
* If {@code schema} was {@code null}.
*/
public Comparator comparator(final Schema schema)
throws LocalizedIllegalArgumentException, NullPointerException
{
Validator.ensureNotNull(schema);
final AttributeDescription ad = AttributeDescription.valueOf(
attributeDescription, schema);
MatchingRule mrule;
if (orderingMatchingRule != null)
{
// FIXME: need to check that the matching rule is a matching rule and can
// be used with the attribute.
mrule = schema.getMatchingRule(orderingMatchingRule);
if (mrule == null)
{
// Specified ordering matching rule not found.
final LocalizableMessage message = ERR_SORT_KEY_MRULE_NOT_FOUND.get(
toString(), orderingMatchingRule);
throw new LocalizedIllegalArgumentException(message);
}
}
else
{
mrule = ad.getAttributeType().getOrderingMatchingRule();
if (mrule == null)
{
// No default ordering matching rule found.
final LocalizableMessage message = ERR_SORT_KEY_DEFAULT_MRULE_NOT_FOUND
.get(toString(), attributeDescription);
throw new LocalizedIllegalArgumentException(message);
}
}
return new EntryComparator(ad, mrule, isReverseOrder);
}
/**
* Returns the name of the attribute to be sorted using this sort key.
*
* @return The name of the attribute to be sorted using this sort key.
*/
public String getAttributeDescription()
{
return attributeDescription;
}
/**
* Returns the name or OID of the ordering matching rule, if specified, which
* should be used when comparing attributes using this sort key.
*
* @return The name or OID of the ordering matching rule, if specified, which
* should be used when comparing attributes using this sort key, or
* {@code null} if the default ordering matching rule associated with
* the attribute should be used.
*/
public String getOrderingMatchingRule()
{
return orderingMatchingRule;
}
/**
* Returns {@code true} if this sort key should be evaluated in reverse
* (descending) order. More specifically, comparisons performed using the
* ordering matching rule associated with this sort key will have their
* results inverted.
*
* @return {@code true} if this sort key should be evaluated in reverse
* (descending) order.
*/
public boolean isReverseOrder()
{
return isReverseOrder;
}
/**
* Returns a string representation of this sort key using the format defined
* in {@link #valueOf(String)}.
*
* @return A string representation of this sort key.
*/
@Override
public String toString()
{
final StringBuilder builder = new StringBuilder();
if (isReverseOrder)
{
builder.append('-');
}
builder.append(attributeDescription);
if (orderingMatchingRule != null)
{
builder.append(':');
builder.append(orderingMatchingRule);
}
return builder.toString();
}
}