mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Nicolas Capponi
09.26.2014 b1ac698e6eb1838e20e62d360192ccde1debfb9c
OPENDJ-1592 CR-4782 Migrate time-based matching rules

Add time-based matching rules implementations in the SDK

Add unit tests for those implementations
4 files added
1 files modified
1188 ■■■■■ changed files
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java 654 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties 47 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java 202 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java 142 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java 143 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java
New file
@@ -0,0 +1,654 @@
/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2009 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2014 ForgeRock AS
 */
package org.forgerock.opendj.ldap.schema;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.GeneralizedTime;
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
import org.forgerock.opendj.ldap.spi.Indexer;
import org.forgerock.opendj.ldap.spi.IndexingOptions;
import com.forgerock.opendj.util.TimeSource;
import static com.forgerock.opendj.ldap.CoreMessages.*;
import static com.forgerock.opendj.util.StaticUtils.*;
/**
 * Implementations of time-based matching rules.
 */
public final class TimeBasedMatchingRulesImpl {
    private static final String RELATIVE_TIME_INDEX_ID = "rt";
    private static final String PARTIAL_DATE_TIME_INDEX_ID = "pdt";
    private static final String EXTENSIBLE_INDEX_ID = "ext";
    private static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("UTC");
    /** Constants for generating keys. */
    private static final char SECOND = 's';
    private static final char MINUTE = 'm';
    private static final char HOUR = 'h';
    private static final char MONTH = 'M';
    private static final char DATE = 'D';
    private static final char YEAR = 'Y';
    private TimeBasedMatchingRulesImpl() {
        // not instantiable
    }
    /**
     * Creates a relative time greater than matching rule.
     *
     * @return the matching rule implementation
     */
    public static MatchingRuleImpl relativeTimeGTOMatchingRule() {
        return new RelativeTimeGreaterThanOrderingMatchingRuleImpl();
    }
    /**
     * Creates a relative time less than matching rule.
     *
     * @return the matching rule implementation
     */
    public static MatchingRuleImpl relativeTimeLTOMatchingRule() {
        return new RelativeTimeLessThanOrderingMatchingRuleImpl();
    }
    /**
     * Creates a partial date and time matching rule.
     *
     * @return the matching rule implementation
     */
    public static MatchingRuleImpl partialDateAndTimeMatchingRule() {
        return new PartialDateAndTimeMatchingRuleImpl();
    }
    /**
     * This class defines a matching rule which is used for time-based searches.
     */
    private abstract static class TimeBasedMatchingRuleImpl extends AbstractMatchingRuleImpl {
        /** Unit tests can inject fake timestamps if necessary. */
        final TimeSource timeSource = TimeSource.DEFAULT;
        /** {@inheritDoc} */
        @Override
        public ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException {
            try {
                return ByteString.valueOf(GeneralizedTime.valueOf(value.toString()).getTimeInMillis());
            } catch (final LocalizedIllegalArgumentException e) {
                throw DecodeException.error(e.getMessageObject());
            }
        }
        int multiplyByTenAndAddUnits(int number, byte b) {
            switch (b) {
            case '0':
                return number * 10;
            case '1':
                return number * 10 + 1;
            case '2':
                return number * 10 + 2;
            case '3':
                return number * 10 + 3;
            case '4':
                return number * 10 + 4;
            case '5':
                return number * 10 + 5;
            case '6':
                return number * 10 + 6;
            case '7':
                return number * 10 + 7;
            case '8':
                return number * 10 + 8;
            case '9':
                return number * 10 + 9;
            }
            return number;
        }
    }
    /**
     * Defines the relative time ordering matching rule.
     */
    private abstract static class RelativeTimeOrderingMatchingRuleImpl extends TimeBasedMatchingRuleImpl {
        final Indexer indexer = new DefaultIndexer(RELATIVE_TIME_INDEX_ID + "." + EXTENSIBLE_INDEX_ID);
        /** {@inheritDoc} */
        @Override
        public Collection<? extends Indexer> getIndexers() {
            return Collections.singletonList(indexer);
        }
        /**
         * Normalize the provided assertion value.
         * <p>
         * An assertion value may contain one of the following:
         * <pre>
         * s = second
         * m = minute
         * h = hour
         * d = day
         * w = week
         * </pre>
         *
         * An example assertion is
         * <pre>
         *   OID:=(-)1d
         * </pre>
         *
         * where a '-' means that the user intends to search only the expired
         * events. In this example we are searching for an event expired 1 day
         * back.
         * <p>
         * This method takes the assertion value adds/substracts it to/from the
         * current time and calculates a time which will be used as a relative
         * time by inherited rules.
         */
        ByteString normalizeAssertionValue(ByteSequence assertionValue) throws DecodeException {
            int index = 0;
            boolean signed = false;
            byte firstByte = assertionValue.byteAt(0);
            if (firstByte == '-') {
                // Turn the sign on to go back in past.
                signed = true;
                index = 1;
            } else if (firstByte == '+') {
                // '+" is not required but we won't reject it either.
                index = 1;
            }
            long second = 0;
            long minute = 0;
            long hour = 0;
            long day = 0;
            long week = 0;
            boolean containsTimeUnit = false;
            int number = 0;
            for (; index < assertionValue.length(); index++) {
                byte b = assertionValue.byteAt(index);
                if (isDigit((char) b)) {
                    number = multiplyByTenAndAddUnits(number, b);
                } else {
                    if (containsTimeUnit) {
                        // We already have time unit found by now.
                        throw DecodeException.error(WARN_ATTR_CONFLICTING_ASSERTION_FORMAT.get(assertionValue));
                    } else {
                        switch (b) {
                        case 's':
                            second = number;
                            break;
                        case 'm':
                            minute = number;
                            break;
                        case 'h':
                            hour = number;
                            break;
                        case 'd':
                            day = number;
                            break;
                        case 'w':
                            week = number;
                            break;
                        default:
                            throw DecodeException.error(
                                WARN_ATTR_INVALID_RELATIVE_TIME_ASSERTION_FORMAT.get(assertionValue, (char) b));
                        }
                    }
                    containsTimeUnit = true;
                    number = 0;
                }
            }
            if (!containsTimeUnit) {
                // There was no time unit so assume it is seconds.
                second = number;
            }
            long delta = (second + minute * 60 + hour * 3600 + day * 24 * 3600 + week * 7 * 24 * 3600) * 1000;
            long now = timeSource.currentTimeMillis();
            return ByteString.valueOf(signed ? now - delta : now + delta);
        }
    }
    /**
     * Defines the "greater-than" relative time matching rule.
     */
    private static final class RelativeTimeGreaterThanOrderingMatchingRuleImpl extends
        RelativeTimeOrderingMatchingRuleImpl {
        /** {@inheritDoc} */
        @Override
        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
            final ByteString assertionValue = normalizeAssertionValue(value);
            return new Assertion() {
                @Override
                public ConditionResult matches(ByteSequence attributeValue) {
                    return ConditionResult.valueOf(attributeValue.compareTo(assertionValue) > 0);
                }
                @Override
                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
                    return factory.createRangeMatchQuery(indexer.getIndexID(), assertionValue, ByteString.empty(),
                        false, false);
                }
            };
        }
    }
    /**
     * Defines the "less-than" relative time matching rule.
     */
    private static final class RelativeTimeLessThanOrderingMatchingRuleImpl extends
        RelativeTimeOrderingMatchingRuleImpl {
        /** {@inheritDoc} */
        @Override
        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
            final ByteString assertionValue = normalizeAssertionValue(value);
            return new Assertion() {
                @Override
                public ConditionResult matches(ByteSequence attributeValue) {
                    return ConditionResult.valueOf(attributeValue.compareTo(assertionValue) < 0);
                }
                @Override
                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
                    return factory.createRangeMatchQuery(indexer.getIndexID(), ByteString.empty(), assertionValue,
                        false, false);
                }
            };
        }
    }
    /**
     * Defines the partial date and time matching rule.
     */
    private static final class PartialDateAndTimeMatchingRuleImpl extends TimeBasedMatchingRuleImpl {
        final Indexer indexer = new PartialDateAndTimeIndexer(this);
        /** {@inheritDoc} */
        @Override
        public Collection<? extends Indexer> getIndexers() {
            return Collections.singletonList(indexer);
        }
        /** {@inheritDoc} */
        @Override
        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
            final ByteString assertionValue = normalizeAssertionValue(value);
            return new Assertion() {
                @Override
                public ConditionResult matches(ByteSequence attributeValue) {
                    return valuesMatch(attributeValue, assertionValue);
                }
                @Override
                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
                    final ByteBuffer buffer = ByteBuffer.wrap(assertionValue.toByteArray());
                    int assertSecond = buffer.getInt(0);
                    int assertMinute = buffer.getInt(4);
                    int assertHour = buffer.getInt(8);
                    int assertDate = buffer.getInt(12);
                    int assertMonth = buffer.getInt(16);
                    int assertYear = buffer.getInt(20);
                    List<T> queries = new ArrayList<T>();
                    if (assertSecond >= 0) {
                        queries.add(createExactMatchQuery(factory, assertSecond, SECOND));
                    }
                    if (assertMinute >= 0) {
                        queries.add(createExactMatchQuery(factory, assertMinute, MINUTE));
                    }
                    if (assertHour >= 0) {
                        queries.add(createExactMatchQuery(factory, assertHour, HOUR));
                    }
                    if (assertDate > 0) {
                        queries.add(createExactMatchQuery(factory, assertDate, DATE));
                    }
                    if (assertMonth >= 0) {
                        queries.add(createExactMatchQuery(factory, assertMonth, MONTH));
                    }
                    if (assertYear > 0) {
                        queries.add(createExactMatchQuery(factory, assertYear, YEAR));
                    }
                    return factory.createIntersectionQuery(queries);
                }
                private <T> T createExactMatchQuery(IndexQueryFactory<T> factory, int assertionValue, char type) {
                    return factory.createExactMatchQuery(indexer.getIndexID(), getKey(assertionValue, type));
                }
            };
        }
        /**
         * Normalize the provided assertion value.
         * <p>
         * An assertion value may contain one or all of the following:
         * <pre>
         * D = day
         * M = month
         * Y = year
         * h = hour
         * m = month
         * s = second
         * </pre>
         *
         * An example assertion is
         * <pre>
         *  OID:=04M
         * </pre>
         * In this example we are searching for entries corresponding to month
         * of april.
         * <p>
         * The normalized value is actually the format of : smhDMY.
         */
        private ByteString normalizeAssertionValue(ByteSequence assertionValue) throws DecodeException {
            final int initDate = 0;
            final int initValue = -1;
            int second = initValue;
            int minute = initValue;
            int hour = initValue;
            int date = initDate;
            int month = initValue;
            int year = initDate;
            int number = 0;
            int length = assertionValue.length();
            for (int index = 0; index < length; index++) {
                byte b = assertionValue.byteAt(index);
                if (isDigit((char) b)) {
                    number = multiplyByTenAndAddUnits(number, b);
                } else {
                    LocalizableMessage message = null;
                    switch (b) {
                    case 's':
                        if (second != initValue) {
                            message = WARN_ATTR_DUPLICATE_SECOND_ASSERTION_FORMAT.get(assertionValue, date);
                        } else {
                            second = number;
                        }
                        break;
                    case 'm':
                        if (minute != initValue) {
                            message = WARN_ATTR_DUPLICATE_MINUTE_ASSERTION_FORMAT.get(assertionValue, date);
                        } else {
                            minute = number;
                        }
                        break;
                    case 'h':
                        if (hour != initValue) {
                            message = WARN_ATTR_DUPLICATE_HOUR_ASSERTION_FORMAT.get(assertionValue, date);
                        } else {
                            hour = number;
                        }
                        break;
                    case 'D':
                        if (number == 0) {
                            message = WARN_ATTR_INVALID_DATE_ASSERTION_FORMAT.get(assertionValue, number);
                        } else if (date != initDate) {
                            message = WARN_ATTR_DUPLICATE_DATE_ASSERTION_FORMAT.get(assertionValue, date);
                        } else {
                            date = number;
                        }
                        break;
                    case 'M':
                        if (number == 0) {
                            message = WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT.get(assertionValue, number);
                        } else if (month != initValue) {
                            message = WARN_ATTR_DUPLICATE_MONTH_ASSERTION_FORMAT.get(assertionValue, month);
                        } else {
                            month = number;
                        }
                        break;
                    case 'Y':
                        if (number == 0) {
                            message = WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT.get(assertionValue, number);
                        } else if (year != initDate) {
                            message = WARN_ATTR_DUPLICATE_YEAR_ASSERTION_FORMAT.get(assertionValue, year);
                        } else {
                            year = number;
                        }
                        break;
                    default:
                        message = WARN_ATTR_INVALID_PARTIAL_TIME_ASSERTION_FORMAT.get(assertionValue, (char) b);
                    }
                    if (message != null) {
                        throw DecodeException.error(message);
                    }
                    number = 0;
                }
            }
            month = toCalendarMonth(month, assertionValue);
            // Validate year, month , date , hour, minute and second in that order.
            // -1 values are allowed when these values have not been provided
            if (year < 0) {
                // A future date is allowed.
                throw DecodeException.error(WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT.get(assertionValue, year));
            }
            if (isDateInvalid(date, month, year)) {
                throw DecodeException.error(WARN_ATTR_INVALID_DATE_ASSERTION_FORMAT.get(assertionValue, date));
            }
            if (hour < initValue || hour > 23) {
                throw DecodeException.error(WARN_ATTR_INVALID_HOUR_ASSERTION_FORMAT.get(assertionValue, hour));
            }
            if (minute < initValue || minute > 59) {
                throw DecodeException.error(WARN_ATTR_INVALID_MINUTE_ASSERTION_FORMAT.get(assertionValue, minute));
            }
            // Consider leap seconds.
            if (second < initValue || second > 60) {
                throw DecodeException.error(WARN_ATTR_INVALID_SECOND_ASSERTION_FORMAT.get(assertionValue, second));
            }
            // Since we reached here we have a valid assertion value.
            // Construct a normalized value in the order: SECOND MINUTE HOUR DATE MONTH YEAR.
            return new ByteStringBuilder(6 * 4)
                .append(second).append(minute).append(hour)
                .append(date).append(month).append(year).toByteString();
        }
        private boolean isDateInvalid(int date, int month, int year) {
            switch (date) {
            case 29:
                return month == Calendar.FEBRUARY && !isLeapYear(year);
            case 30:
                return month == Calendar.FEBRUARY;
            case 31:
                return month != -1
                    && month != Calendar.JANUARY
                    && month != Calendar.MARCH
                    && month != Calendar.MAY
                    && month != Calendar.JULY
                    && month != Calendar.AUGUST
                    && month != Calendar.OCTOBER
                    && month != Calendar.DECEMBER;
            default:
                return date < 0 || date > 31;
            }
        }
        private boolean isLeapYear(int year) {
            return year % 400 == 0 || (year % 100 != 0 && year % 4 == 0);
        }
        private int toCalendarMonth(int month, ByteSequence value) throws DecodeException {
            switch (month) {
            case -1:
                // just allow this.
                return -1;
            case 1:
                return Calendar.JANUARY;
            case 2:
                return Calendar.FEBRUARY;
            case 3:
                return Calendar.MARCH;
            case 4:
                return Calendar.APRIL;
            case 5:
                return Calendar.MAY;
            case 6:
                return Calendar.JUNE;
            case 7:
                return Calendar.JULY;
            case 8:
                return Calendar.AUGUST;
            case 9:
                return Calendar.SEPTEMBER;
            case 10:
                return Calendar.OCTOBER;
            case 11:
                return Calendar.NOVEMBER;
            case 12:
                return Calendar.DECEMBER;
            default:
                throw DecodeException.error(WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT.get(value, month));
            }
        }
        private ConditionResult valuesMatch(ByteSequence attributeValue, ByteSequence assertionValue) {
            // Build the information from the attribute value.
            GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC);
            cal.setLenient(false);
            cal.setTimeInMillis(((ByteString) attributeValue).toLong());
            int second = cal.get(Calendar.SECOND);
            int minute = cal.get(Calendar.MINUTE);
            int hour = cal.get(Calendar.HOUR_OF_DAY);
            int date = cal.get(Calendar.DATE);
            int month = cal.get(Calendar.MONTH);
            int year = cal.get(Calendar.YEAR);
            // Build the information from the assertion value.
            ByteBuffer b = ByteBuffer.wrap(assertionValue.toByteArray());
            int assertSecond = b.getInt(0);
            int assertMinute = b.getInt(4);
            int assertHour = b.getInt(8);
            int assertDate = b.getInt(12);
            int assertMonth = b.getInt(16);
            int assertYear = b.getInt(20);
            // All the non-zero and non -1 values should match.
            if ((assertSecond != -1 && assertSecond != second)
                || (assertMinute != -1 && assertMinute != minute)
                || (assertHour != -1 && assertHour != hour)
                || (assertDate != 0 && assertDate != date)
                || (assertMonth != -1 && assertMonth != month)
                || (assertYear != 0 && assertYear != year)) {
                return ConditionResult.FALSE;
            }
            return ConditionResult.TRUE;
        }
        /**
         * Decomposes an attribute value into a set of partial date and time
         * index keys.
         *
         * @param attValue
         *            The normalized attribute value
         * @param set
         *            A set into which the keys will be inserted.
         */
        private void timeKeys(ByteSequence attributeValue, Collection<ByteString> keys) {
            long timeInMillis = 0L;
            try {
                timeInMillis = GeneralizedTime.valueOf(attributeValue.toString()).getTimeInMillis();
            } catch (IllegalArgumentException e) {
                return;
            }
            GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC);
            cal.setTimeInMillis(timeInMillis);
            addKeyIfNotZero(keys, cal, Calendar.SECOND, SECOND);
            addKeyIfNotZero(keys, cal, Calendar.MINUTE, MINUTE);
            addKeyIfNotZero(keys, cal, Calendar.HOUR_OF_DAY, HOUR);
            addKeyIfNotZero(keys, cal, Calendar.DATE, DATE);
            addKeyIfNotZero(keys, cal, Calendar.MONTH, MONTH);
            addKeyIfNotZero(keys, cal, Calendar.YEAR, YEAR);
        }
        private void addKeyIfNotZero(Collection<ByteString> keys, GregorianCalendar cal, int calField, char type) {
            int value = cal.get(calField);
            if (value >= 0) {
                keys.add(getKey(value, type));
            }
        }
        private ByteString getKey(int value, char type) {
            return new ByteStringBuilder().append(type).append(value).toByteString();
        }
    }
    /**
     * Indexer for Partial Date and Time Matching rules.
     */
    private static final class PartialDateAndTimeIndexer implements Indexer {
        private final PartialDateAndTimeMatchingRuleImpl matchingRule;
        private PartialDateAndTimeIndexer(PartialDateAndTimeMatchingRuleImpl matchingRule) {
            this.matchingRule = matchingRule;
        }
        /** {@inheritDoc} */
        @Override
        public void createKeys(Schema schema, ByteSequence value, IndexingOptions options,
                Collection<ByteString> keys) {
            matchingRule.timeKeys(value, keys);
        }
        /** {@inheritDoc} */
        @Override
        public String getIndexID() {
            return PARTIAL_DATE_TIME_INDEX_ID + "." + EXTENSIBLE_INDEX_ID;
        }
    }
}
opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
@@ -1595,3 +1595,50 @@
 character
ERR_ADDRESSMASK_FORMAT_DECODE_ERROR=Cannot decode the provided \
 address mask because the it has an invalid format
WARN_ATTR_CONFLICTING_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because more than \
 one time units are not allowed
WARN_ATTR_INVALID_RELATIVE_TIME_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because the \
 character '%c' is not allowed. The acceptable values are s (second), m (minute), \
 h (hour), d (day) and w (week)
WARN_ATTR_DUPLICATE_SECOND_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for s (second) specification
WARN_ATTR_DUPLICATE_MINUTE_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for m (minute) specification
WARN_ATTR_DUPLICATE_HOUR_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for h (hour) specification
WARN_ATTR_INVALID_DATE_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid date specification
WARN_ATTR_DUPLICATE_DATE_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for DD (date) specification
WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid month specification
WARN_ATTR_INVALID_MINUTE_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid minute specification
WARN_ATTR_INVALID_HOUR_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid hour specification
WARN_ATTR_INVALID_SECOND_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid second specification
WARN_ATTR_DUPLICATE_MONTH_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for MM (month) specification
WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because "%d" is not \
 a valid year specification
WARN_ATTR_DUPLICATE_YEAR_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because there is \
 conflicting value "%d" for YYYY (year) specification
WARN_ATTR_INVALID_PARTIAL_TIME_ASSERTION_FORMAT=The provided \
 value "%s" could not be parsed as a valid assertion value because the \
 character '%c' is not allowed. The acceptable values are s (second), \
 m (minute), h (hour), D (date), M (month) and Y (year)
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java
New file
@@ -0,0 +1,202 @@
/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2014 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.forgerock.opendj.util.TimeSource;
import static org.fest.assertions.Assertions.*;
import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
@SuppressWarnings("javadoc")
@Test
public class PartialDateAndTimeMatchingRuleTestCase extends MatchingRuleTest {
    /** {@inheritDoc} */
    @Override
    @DataProvider(name = "matchingRuleInvalidAttributeValues")
    public Object[][] createMatchingRuleInvalidAttributeValues() {
        return new Object[][] {
            // too short
            { "1Z" },
            // bad year
            { "201a0630Z" },
            // bad month
            { "20141330Z" },
            // bad day
            { "20140635Z" },
            // bad hour
            { "20140630351010Z" },
            // bad minute
            { "20140630108810Z" },
            // bad second
            { "20140630101080Z" },
        };
    }
    @DataProvider(name = "matchingRuleInvalidAssertionValues")
    public Object[][] createMatchingRuleInvalidAssertionValues() {
        return new Object[][] {
            { " " },
            { "bla" },
            // invalid time unit values
            { "-1Y03M11D12h48m32s" },
            { "0Y03M11D12h48m32s" },
            { "2014Y-1M11D12h48m32s" },
            { "2014Y0M11D12h48m32s" },
            { "2014Y13M11D12h48m32s" },
            { "2014Y03M-1D12h48m32s" },
            { "2014Y03M0D12h48m32s" },
            { "2014Y13M32D12h48m32s" },
            { "2014Y03M11D-1h48m32s" },
            { "2014Y03M11D24h48m32s" },
            { "2014Y03M11D12h-1m32s" },
            { "2014Y03M11D12h60m32s" },
            { "2014Y03M11D12h48m-1s" },
            { "2014Y03M11D12h48m61s" },
            // duplicate each time unit
            { "1Y2014Y03M11D12h" },
            { "2014Y1M03M11D12h" },
            { "2014Y03M1D11D12h" },
            { "2014Y03M11D1h12h" },
            { "2014Y03M11D12h1m48m" },
            { "2014Y03M11D12h48m1s32s" },
            // February and non leap years
            { "2014Y02M29D" },
            { "1800Y02M29D" },
            { "2000Y02M30D" },
            { "2000Y02M31D" },
            // 31st of months
            { "2012Y04M31D" },
            { "2012Y06M31D" },
            { "2012Y09M31D" },
            { "2012Y11M31D" },
        };
    }
    /** {@inheritDoc} */
    @Override
    @DataProvider(name = "matchingrules")
    public Object[][] createMatchingRuleTest() {
        final Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(TimeSource.DEFAULT.currentTimeMillis());
        final Date nowDate = calendar.getTime();
        calendar.add(Calendar.MONTH, 1);
        final Date oneMonthAheadDate = calendar.getTime();
        final SimpleDateFormat partialTimeUpToSeconds = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'mm'm'ss's'");
        final SimpleDateFormat generalizedTimeUpToSeconds = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
        final SimpleDateFormat partialTimeUpToMinutes = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'mm'm'");
        final SimpleDateFormat generalizedTimeUpToMinutes = new SimpleDateFormat("yyyyMMddHHmm'Z'");
        final SimpleDateFormat partialTimeUpToHours = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'");
        final SimpleDateFormat generalizedTimeUpToHours = new SimpleDateFormat("yyyyMMddHH'Z'");
        return new Object[][] {
            // expect 3 values : attribute value, assertion value, result
            // use now date and one month ahead dates
            { generalizedTimeUpToSeconds.format(nowDate), partialTimeUpToSeconds.format(nowDate),
                ConditionResult.TRUE },
            { generalizedTimeUpToSeconds.format(oneMonthAheadDate), partialTimeUpToSeconds.format(oneMonthAheadDate),
                ConditionResult.TRUE },
            { generalizedTimeUpToMinutes.format(nowDate), partialTimeUpToMinutes.format(nowDate),
                ConditionResult.TRUE },
            { generalizedTimeUpToMinutes.format(oneMonthAheadDate), partialTimeUpToMinutes.format(oneMonthAheadDate),
                ConditionResult.TRUE },
            { generalizedTimeUpToHours.format(nowDate), partialTimeUpToHours.format(nowDate),
                ConditionResult.TRUE },
            { generalizedTimeUpToHours.format(oneMonthAheadDate), partialTimeUpToHours.format(oneMonthAheadDate),
                ConditionResult.TRUE },
            // 29th of months and leap years
            { "20120329120000Z", "2012Y03M29D", ConditionResult.TRUE },
            { "20120229120000Z", "2012Y02M29D", ConditionResult.TRUE },
            { "20000229120000Z", "2000Y02M29D", ConditionResult.TRUE },
            // Generalized time implementation does not allow leap seconds
            // because Java does not support them. Apparently, it never will support them:
            // @see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4272347
            // leap seconds are allowed, even though no formula exists to validate them
            { "20120630235959Z", "2012Y06M30D23h59m60s", ConditionResult.FALSE },
            // no match
            { "20111231235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            { "20121031235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            { "20121230235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            { "20121231225930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            { "20121231235830Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            { "20121231235829Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
            // 30th of months
            { "19820930120000Z", "1982Y09M30D", ConditionResult.TRUE },
            // 31st of months
            { "20120131120000Z", "2012Y01M31D", ConditionResult.TRUE },
            { "20120331120000Z", "2012Y03M31D", ConditionResult.TRUE },
            { "20120531120000Z", "2012Y05M31D", ConditionResult.TRUE },
            { "20120731120000Z", "2012Y07M31D", ConditionResult.TRUE },
            { "20120831120000Z", "2012Y08M31D", ConditionResult.TRUE },
            { "20121031120000Z", "2012Y10M31D", ConditionResult.TRUE },
            { "20121231120000Z", "2012Y12M31D", ConditionResult.TRUE },
            // Only single time units
            { "20121231123000Z", "2012Y", ConditionResult.TRUE },
            { "20121231123000Z", "2012Y12M", ConditionResult.TRUE },
            { "20121231123000Z", "2012Y31D", ConditionResult.TRUE },
            { "20121231123000Z", "2012Y12h", ConditionResult.TRUE },
            { "20121231123000Z", "2012Y30m", ConditionResult.TRUE },
            { "20121231123000Z", "2012Y0s", ConditionResult.TRUE },
        };
    }
    /** {@inheritDoc} */
    @Override
    protected MatchingRule getRule() {
        // Note that oid and names are not used by the test (ie, they could be any value, test should pass anyway)
        // Only the implementation class and the provided locale are really tested here.
        String oid = "1.3.6.1.4.1.26027.1.4.7";
        Schema schema = new SchemaBuilder(Schema.getCoreSchema()).
            buildMatchingRule(oid).
                syntaxOID(SchemaConstants.SYNTAX_GENERALIZED_TIME_OID).
                names("partialDateAndTimeMatchingRule").
                implementation(TimeBasedMatchingRulesImpl.partialDateAndTimeMatchingRule()).
                addToSchema().
            toSchema();
        return schema.getMatchingRule(oid);
    }
    @Test
    public void testCreateIndexQuery() throws Exception {
        Assertion assertion = getRule().getAssertion(ByteString.valueOf("2012Y"));
        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
        assertThat(indexQuery).matches("intersect\\[exactMatch\\(pdt\\.ext, value=='[0-9A-Z ]*'\\)\\]");
    }
}
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java
New file
@@ -0,0 +1,142 @@
/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2014 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
import java.util.Calendar;
import java.util.Date;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.GeneralizedTime;
import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.forgerock.opendj.util.TimeSource;
import static org.fest.assertions.Assertions.*;
import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
@SuppressWarnings("javadoc")
@Test
public class RelativeTimeGreaterThanMatchingRuleTest extends MatchingRuleTest {
    /**
     * {@inheritDoc}
     */
    @Override
    @DataProvider(name = "matchingRuleInvalidAttributeValues")
    public Object[][] createMatchingRuleInvalidAttributeValues() {
        return new Object[][] {
            // too short
            { "1Z" },
            // bad year
            { "201a0630Z" },
            // bad month
            { "20141330Z" },
            // bad day
            { "20140635Z" },
            // bad hour
            { "20140630351010Z" },
            // bad minute
            { "20140630108810Z" },
            // bad second
            { "20140630101088Z" },
        };
    }
    @DataProvider(name = "matchingRuleInvalidAssertionValues")
    public Object[][] createMatchingRuleInvalidAssertionValues() {
        return new Object[][] {
            { " " },
            { "bla" },
            { "-30b" },
            { "-30ms" },
        };
    }
    /**
     * {@inheritDoc}
     */
    @Override
    @DataProvider(name = "matchingrules")
    public Object[][] createMatchingRuleTest() {
        final Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(TimeSource.DEFAULT.currentTimeMillis());
        final Date nowDate = calendar.getTime();
        calendar.add(Calendar.MONTH, 1);
        final Date oneMonthAheadDate = calendar.getTime();
        final String nowGT = GeneralizedTime.valueOf(nowDate).toString();
        final String oneMonthAheadGT = GeneralizedTime.valueOf(oneMonthAheadDate).toString();
        return new Object[][] {
            // attribute value, assertion value, result
            { oneMonthAheadGT, "1", ConditionResult.TRUE },
            { oneMonthAheadGT, "+1s", ConditionResult.TRUE },
            { oneMonthAheadGT, "+1h", ConditionResult.TRUE },
            { oneMonthAheadGT, "+1m", ConditionResult.TRUE },
            { nowGT, "-30d", ConditionResult.TRUE },
            { nowGT, "-30w", ConditionResult.TRUE },
            { nowGT, "-30m", ConditionResult.TRUE },
            { nowGT, "-30s", ConditionResult.TRUE },
            { oneMonthAheadGT, "6w", ConditionResult.FALSE },
            { nowGT, "1d", ConditionResult.FALSE },
            { nowGT, "10s", ConditionResult.FALSE },
            { nowGT, "+1h", ConditionResult.FALSE },
            { nowGT, "+1m", ConditionResult.FALSE },
            { nowGT, "+1w", ConditionResult.FALSE },
        };
    }
    /**
     * {@inheritDoc}
     */
    @Override
    protected MatchingRule getRule() {
        // Note that oid and names are not used by the test (ie, they could be any value, test should pass anyway)
        // Only the implementation class and the provided locale are really tested here.
        String oid = "1.3.6.1.4.1.26027.1.4.5";
        Schema schema = new SchemaBuilder(Schema.getCoreSchema()).
            buildMatchingRule(oid).
                syntaxOID(SchemaConstants.SYNTAX_GENERALIZED_TIME_OID).
                names("relativeTimeGTOrderingMatch.gt").
                implementation(TimeBasedMatchingRulesImpl.relativeTimeGTOMatchingRule()).
                addToSchema().
            toSchema();
        return schema.getMatchingRule(oid);
    }
    @Test
    public void testCreateIndexQuery() throws Exception {
        Assertion assertion = getRule().getAssertion(ByteString.valueOf("+5m"));
        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
        assertThat(indexQuery).startsWith("rangeMatch(rt.ext, '").endsWith("' < value < '')");
    }
}
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java
New file
@@ -0,0 +1,143 @@
/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2014 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
import java.util.Calendar;
import java.util.Date;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.GeneralizedTime;
import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.forgerock.opendj.util.TimeSource;
import static org.fest.assertions.Assertions.*;
import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
@SuppressWarnings("javadoc")
@Test
public class RelativeTimeLessThanMatchingRuleTest extends MatchingRuleTest {
    /**
     * {@inheritDoc}
     */
    @Override
    @DataProvider(name = "matchingRuleInvalidAttributeValues")
    public Object[][] createMatchingRuleInvalidAttributeValues() {
        return new Object[][] {
            // too short
            { "1Z" },
            // bad year
            { "201a0630Z" },
            // bad month
            { "20141330Z" },
            // bad day
            { "20140635Z" },
            // bad hour
            { "20140630351010Z" },
            // bad minute
            { "20140630108810Z" },
            // bad second
            { "20140630101088Z" },
        };
    }
    @DataProvider(name = "matchingRuleInvalidAssertionValues")
    public Object[][] createMatchingRuleInvalidAssertionValues() {
        return new Object[][] {
            { " " },
            { "bla" },
            { "-30b" },
            { "-30ms" },
        };
    }
    /**
     * {@inheritDoc}
     */
    @Override
    @DataProvider(name = "matchingrules")
    public Object[][] createMatchingRuleTest() {
        final Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(TimeSource.DEFAULT.currentTimeMillis());
        final Date nowDate = calendar.getTime();
        calendar.add(Calendar.MONTH, 1);
        final Date oneMonthAheadDate = calendar.getTime();
        final String nowGT = GeneralizedTime.valueOf(nowDate).toString();
        final String oneMonthAheadGT = GeneralizedTime.valueOf(oneMonthAheadDate).toString();
        return new Object[][] {
            // attribute value, assertion value, result
            { oneMonthAheadGT, "6w", ConditionResult.TRUE },
            { oneMonthAheadGT, "2400h", ConditionResult.TRUE },
            { nowGT, "1d", ConditionResult.TRUE },
            { nowGT, "10s", ConditionResult.TRUE },
            { nowGT, "+1h", ConditionResult.TRUE },
            { nowGT, "+1m", ConditionResult.TRUE },
            { nowGT, "+1w", ConditionResult.TRUE },
            { oneMonthAheadGT, "1", ConditionResult.FALSE },
            { oneMonthAheadGT, "+30s", ConditionResult.FALSE },
            { oneMonthAheadGT, "+2h", ConditionResult.FALSE },
            { oneMonthAheadGT, "+1m", ConditionResult.FALSE },
            { nowGT, "-1d", ConditionResult.FALSE },
            { nowGT, "-2w", ConditionResult.FALSE },
            { nowGT, "-10m", ConditionResult.FALSE },
            { nowGT, "-1s", ConditionResult.FALSE },
        };
    }
    /**
     * {@inheritDoc}
     */
    @Override
    protected MatchingRule getRule() {
        // Note that oid and names are not used by the test (ie, they could be any value, test should pass anyway)
        // Only the implementation class and the provided locale are really tested here.
        String oid = "1.3.6.1.4.1.26027.1.4.6";
        Schema schema = new SchemaBuilder(Schema.getCoreSchema()).
            buildMatchingRule(oid).
                syntaxOID(SchemaConstants.SYNTAX_GENERALIZED_TIME_OID).
                names("relativeTimeLTOrderingMatch.lt").
                implementation(TimeBasedMatchingRulesImpl.relativeTimeLTOMatchingRule()).
                addToSchema().
            toSchema();
        return schema.getMatchingRule(oid);
    }
    @Test
    public void testCreateIndexQuery() throws Exception {
        Assertion assertion = getRule().getAssertion(ByteString.valueOf("+5m"));
        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
        assertThat(indexQuery).startsWith("rangeMatch(rt.ext, '' < value < '");
    }
}