From b1ac698e6eb1838e20e62d360192ccde1debfb9c Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Thu, 09 Oct 2014 13:26:55 +0000
Subject: [PATCH] OPENDJ-1592 CR-4782 Migrate time-based matching rules

---
 opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java    |  143 +++++++
 opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java |  142 +++++++
 opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java  |  202 ++++++++++
 opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties                                |   47 ++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java              |  654 ++++++++++++++++++++++++++++++++
 5 files changed, 1,188 insertions(+), 0 deletions(-)

diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java
new file mode 100644
index 0000000..1b85211
--- /dev/null
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java
@@ -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;
+        }
+    }
+}
diff --git a/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties b/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
index 4cba3a8..835d19b 100755
--- a/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
+++ b/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)
\ No newline at end of file
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java
new file mode 100644
index 0000000..d24f155
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java
@@ -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 ]*'\\)\\]");
+    }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java
new file mode 100644
index 0000000..e4be26c
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java
@@ -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 < '')");
+    }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java
new file mode 100644
index 0000000..2f39225
--- /dev/null
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java
@@ -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 < '");
+    }
+}

--
Gitblit v1.10.0