/* * 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.opends.server.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.HashSet; import java.util.List; import java.util.Set; import java.util.TimeZone; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigException; 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.schema.Schema; import org.forgerock.opendj.ldap.spi.IndexQueryFactory; import org.forgerock.opendj.ldap.spi.IndexingOptions; import org.opends.server.admin.std.server.MatchingRuleCfg; import org.opends.server.api.AbstractMatchingRule; import org.opends.server.api.ExtensibleIndexer; import org.opends.server.api.ExtensibleMatchingRule; import org.opends.server.api.MatchingRule; import org.opends.server.api.MatchingRuleFactory; import org.opends.server.api.OrderingMatchingRule; import org.opends.server.core.DirectoryServer; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import org.opends.server.util.StaticUtils; import static org.opends.messages.SchemaMessages.*; import static org.opends.server.schema.GeneralizedTimeSyntax.*; import static org.opends.server.schema.SchemaConstants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import static org.opends.server.util.TimeThread.*; /** * This class acts as a factory for time-based matching rules. */ public final class TimeBasedMatchingRuleFactory extends MatchingRuleFactory { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** Greater-than RelativeTimeMatchingRule. */ private MatchingRule greaterThanRTMRule; /** Less-than RelativeTimeMatchingRule. */ private MatchingRule lessThanRTMRule; /** PartialDayAndTimeMatchingRule. */ private MatchingRule partialDTMatchingRule; /** A Collection of matching rules managed by this factory. */ private Set matchingRules; private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone(TIME_ZONE_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'; /** {@inheritDoc} */ @Override public void initializeMatchingRule(MatchingRuleCfg configuration) throws ConfigException, InitializationException { matchingRules = new HashSet(); greaterThanRTMRule = new RelativeTimeGTOrderingMatchingRule(); matchingRules.add(greaterThanRTMRule); lessThanRTMRule = new RelativeTimeLTOrderingMatchingRule(); matchingRules.add(lessThanRTMRule); partialDTMatchingRule = new PartialDateAndTimeMatchingRule(); matchingRules.add(partialDTMatchingRule); } /** {@inheritDoc} */ @Override public Collection getMatchingRules() { return Collections.unmodifiableCollection(matchingRules); } /** * This class defines a matching rule which is used for time-based searches. */ private abstract class TimeBasedMatchingRule extends AbstractMatchingRule implements ExtensibleMatchingRule { /** {@inheritDoc} */ @Override public String getDescription() { //There is no standard definition. return null; } /** {@inheritDoc} */ @Override public String getSyntaxOID() { return SYNTAX_GENERALIZED_TIME_OID; } /** {@inheritDoc} */ @Override public ByteString normalizeAttributeValue(ByteSequence value) throws DecodeException { try { long timestamp = decodeGeneralizedTimeValue(value); return ByteString.valueOf(timestamp); } catch (DirectoryException de) { switch (DirectoryServer.getSyntaxEnforcementPolicy()) { case REJECT: throw DecodeException.error(de.getMessageObject(), de); case WARN: logger.error(de.getMessageObject()); break; } return value.toByteString(); } } } /** * This class defines a matching rule which matches the relative time for * time-based searches. */ private abstract class RelativeTimeOrderingMatchingRule extends TimeBasedMatchingRule implements OrderingMatchingRule { /** * The serial version identifier required to satisfy the compiler because * this class implements the java.io.Serializable interface. * This value was generated using the serialver command-line * utility included with the Java SDK. */ private static final long serialVersionUID = -3501812894473163490L; /** * Indexer associated with this instance. */ protected ExtensibleIndexer indexer; /** {@inheritDoc} */ @Override public ByteString normalizeAssertionValue(ByteSequence value) throws DecodeException { /** An assertion value may contain one of the following: s = second m = minute h = hour d = day w = week An example assertion is OID:=(-)1d, 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. Use this method to parse, validate and normalize the assertion value into a format to be recognized by the valuesMatch routine. 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. */ int index = 0; boolean signed = false; byte firstByte = value.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 getIndexers() { if(indexer == null) { indexer = new RelativeTimeExtensibleIndexer(this); } return Collections.singletonList(indexer); } } /** * This class defines a matching rule which calculates the "greater-than" * relative time for time-based searches. */ private final class RelativeTimeGTOrderingMatchingRule extends RelativeTimeOrderingMatchingRule { /** All the names for this matching rule. */ private final List names; /** * The serial version identifier required to satisfy the compiler because * this class implements the java.io.Serializable interface. * This value was generated using the serialver command-line * utility included with the Java SDK. */ private static final long serialVersionUID = 7247241496402474136L; RelativeTimeGTOrderingMatchingRule() { names = new ArrayList(); names.add(EXT_OMR_RELATIVE_TIME_GT_NAME); names.add(EXT_OMR_RELATIVE_TIME_GT_ALT_NAME); } /** {@inheritDoc} */ @Override public Collection getNames() { return Collections.unmodifiableList(names); } /** {@inheritDoc} */ @Override public String getOID() { return EXT_OMR_RELATIVE_TIME_GT_OID; } /** {@inheritDoc} */ @Override public ConditionResult valuesMatch(ByteSequence attributeValue, ByteSequence assertionValue) { int ret = compareValues(attributeValue, assertionValue); return ConditionResult.valueOf(ret > 0); } /** {@inheritDoc} */ @Override public Assertion getAssertion(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 createIndexQuery(IndexQueryFactory factory) throws DecodeException { return factory.createRangeMatchQuery(indexer.getExtensibleIndexID(), assertionValue, ByteString.empty(), false, false); } }; } /** {@inheritDoc} */ @Override public T createIndexQuery(ByteSequence assertionValue, IndexQueryFactory factory) throws DecodeException { return getAssertion(assertionValue).createIndexQuery(factory); } } /** * This class defines a matching rule which calculates the "less-than" * relative time for time-based searches. */ private final class RelativeTimeLTOrderingMatchingRule extends RelativeTimeOrderingMatchingRule { /** All the names for this matching rule. */ private final List names; /** * The serial version identifier required to satisfy the compiler because * this class implements the java.io.Serializable interface. * This value was generated using the serialver command-line * utility included with the Java SDK. */ private static final long serialVersionUID = -5122459830973558441L; RelativeTimeLTOrderingMatchingRule() { names = new ArrayList(); names.add(EXT_OMR_RELATIVE_TIME_LT_NAME); names.add(EXT_OMR_RELATIVE_TIME_LT_ALT_NAME); } /** {@inheritDoc} */ @Override public Collection getNames() { return Collections.unmodifiableList(names); } /** {@inheritDoc} */ @Override public String getOID() { return EXT_OMR_RELATIVE_TIME_LT_OID; } /** {@inheritDoc} */ @Override public ConditionResult valuesMatch(ByteSequence attributeValue, ByteSequence assertionValue) { int ret = compareValues(attributeValue, assertionValue); return ConditionResult.valueOf(ret < 0); } /** {@inheritDoc} */ @Override public Assertion getAssertion(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 createIndexQuery(IndexQueryFactory factory) throws DecodeException { return factory.createRangeMatchQuery(indexer.getExtensibleIndexID(), ByteString.empty(), assertionValue, false, false); } }; } /** {@inheritDoc} */ @Override public T createIndexQuery(ByteSequence assertionValue, IndexQueryFactory factory) throws DecodeException { return getAssertion(assertionValue).createIndexQuery(factory); } } /** * Extensible Indexer class for Relative Time Matching rules which share * the same index. This Indexer is shared by both greater than and less than * Relative Time Matching Rules. */ private final class RelativeTimeExtensibleIndexer extends ExtensibleIndexer { /** * The Extensible Matching Rule. */ private final RelativeTimeOrderingMatchingRule matchingRule; /** * Creates a new instance of RelativeTimeExtensibleIndexer. * * @param matchingRule The relative time Matching Rule. */ private RelativeTimeExtensibleIndexer( RelativeTimeOrderingMatchingRule matchingRule) { this.matchingRule = matchingRule; } /** {@inheritDoc} */ @Override public String getExtensibleIndexID() { return EXTENSIBLE_INDEXER_ID_DEFAULT; } /** {@inheritDoc} */ @Override public final void createKeys(Schema schema, ByteSequence value2, IndexingOptions options, Collection keys) throws DecodeException { keys.add(matchingRule.normalizeAttributeValue(value2)); } /** {@inheritDoc} */ @Override public String getIndexID() { return RELATIVE_TIME_INDEX_NAME + "." + getExtensibleIndexID(); } } /** * This class performs the partial date and time matching capabilities. */ private final class PartialDateAndTimeMatchingRule extends TimeBasedMatchingRule { /** * Indexer associated with this instance. */ private ExtensibleIndexer indexer; /** {@inheritDoc} */ @Override public String getOID() { return EXT_PARTIAL_DATE_TIME_OID; } /** {@inheritDoc} */ @Override public Collection getNames() { return Collections.singleton(EXT_PARTIAL_DATE_TIME_NAME); } /** {@inheritDoc} */ @Override public ByteString normalizeAssertionValue(ByteSequence value) throws DecodeException { /** An assertion value may contain one or all of the following: D = day M = month Y = year h = hour m = month s = second An example assertion is OID:=04M. In this example we are searching for entries corresponding to month of april. Use this method to parse, validate and normalize the assertion value into a format to be recognized by the compare routine. The normalized value is actually the format of : smhDMY. */ final int initDate = 0; final int initVal = -1; int second = initVal; int minute = initVal; int hour = initVal; int date = initDate; int month = initVal; int year = initDate; int number = 0; int length = value.length(); for(int index=0; index 23) { logAndThrow(WARN_ATTR_INVALID_HOUR_ASSERTION_FORMAT.get(value, hour)); } if (minute < initVal || minute > 59) { logAndThrow(WARN_ATTR_INVALID_MINUTE_ASSERTION_FORMAT.get(value, minute)); } if (second < initVal || second > 60) // Consider leap seconds. { logAndThrow(WARN_ATTR_INVALID_SECOND_ASSERTION_FORMAT.get(value, second)); } // Since we reached here we have a valid assertion value. // Construct a normalized value in the order: SECOND MINUTE HOUR DATE MONTH YEAR. ByteBuffer bb = ByteBuffer.allocate(6*4); bb.putInt(second); bb.putInt(minute); bb.putInt(hour); bb.putInt(date); bb.putInt(month); bb.putInt(year); return ByteString.wrap(bb.array()); } private void logAndThrow(LocalizableMessage message) throws DecodeException { logger.warn(message); throw DecodeException.error(message); } 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) { if (year % 400 == 0) { return true; } if (year % 100 == 0) { return false; } return 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: LocalizableMessage message = WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT.get(value, month); logger.warn(message); throw DecodeException.error(message); } } /** {@inheritDoc} */ @Override public ConditionResult valuesMatch(ByteSequence attributeValue, ByteSequence assertionValue) { // Build the information from the attribute value. GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC_OBJ); 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 bb = ByteBuffer.wrap(assertionValue.toByteArray()); int assertSecond = bb.getInt(0); int assertMinute = bb.getInt(4); int assertHour = bb.getInt(8); int assertDate = bb.getInt(12); int assertMonth = bb.getInt(16); int assertYear = bb.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; } /** {@inheritDoc} */ @Override public Collection getIndexers() { if(indexer == null) { indexer = new PartialDateAndTimeExtensibleIndexer(this); } return Collections.singletonList(indexer); } /** {@inheritDoc} */ @Override public T createIndexQuery(ByteSequence assertionValue, IndexQueryFactory factory) throws DecodeException { //Build the information from the assertion value. byte[] arr = normalizeAssertionValue(assertionValue).toByteArray(); ByteBuffer bb = ByteBuffer.wrap(arr); int assertSecond = bb.getInt(0); int assertMinute = bb.getInt(4); int assertHour = bb.getInt(8); int assertDate = bb.getInt(12); int assertMonth = bb.getInt(16); int assertYear = bb.getInt(20); List queries = new ArrayList(); 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 createExactMatchQuery(IndexQueryFactory factory, int assertionValue, char type) { return factory.createExactMatchQuery( indexer.getExtensibleIndexID(), getKey(assertionValue, type)); } /** * 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 keys) { long timeInMS = 0L; try { timeInMS = decodeGeneralizedTimeValue(attributeValue); } catch(DirectoryException de) { //If the schema check is on this should never reach here. If not then we //would return from here. return; } //Build the information from the attribute value. GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC_OBJ); cal.setTimeInMillis(timeInMS); 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 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) { ByteStringBuilder builder = new ByteStringBuilder(); builder.append(type); builder.append(value); return builder.toByteString(); } } private int multiplyByTenThenAddUnits(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; } /** * Extensible Indexer class for Partial Date and Time Matching rules. */ private final class PartialDateAndTimeExtensibleIndexer extends ExtensibleIndexer { /** The partial date and Time matching Rule. */ private final PartialDateAndTimeMatchingRule matchingRule; /** * Creates a new instance of PartialDateAndTimeExtensibleIndexer. * * @param matchingRule * The PartialDateAndTime Rule. */ private PartialDateAndTimeExtensibleIndexer( PartialDateAndTimeMatchingRule matchingRule) { this.matchingRule = matchingRule; } /** {@inheritDoc} */ @Override public void createKeys(Schema schema, ByteSequence value, IndexingOptions options, Collection keys) { matchingRule.timeKeys(value, keys); } /** {@inheritDoc} */ @Override public String getIndexID() { return PARTIAL_DATE_TIME_INDEX_NAME + "." + getExtensibleIndexID(); } /** {@inheritDoc} */ @Override public String getExtensibleIndexID() { return EXTENSIBLE_INDEXER_ID_DEFAULT; } } }