001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2017 ForgeRock AS.
015 */
016package org.opends.server.replication.plugin;
017
018import static org.forgerock.opendj.ldap.Assertion.UNDEFINED_ASSERTION;
019import static org.opends.messages.ReplicationMessages.*;
020import static org.opends.server.replication.plugin.HistoricalCsnOrderingMatchingRuleImpl.ORDERING_ID;
021import static org.opends.server.replication.plugin.HistoricalCsnOrderingMatchingRuleImpl.normalizeCsnAttributeValue;
022import static org.opends.server.replication.plugin.HistoricalCsnRangeMatchingRuleImpl.Operator.*;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import org.forgerock.opendj.ldap.Assertion;
030import org.forgerock.opendj.ldap.ByteSequence;
031import org.forgerock.opendj.ldap.ByteString;
032import org.forgerock.opendj.ldap.ConditionResult;
033import org.forgerock.opendj.ldap.DecodeException;
034import org.forgerock.opendj.ldap.schema.MatchingRuleImpl;
035import org.forgerock.opendj.ldap.schema.Schema;
036import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
037import org.forgerock.opendj.ldap.spi.Indexer;
038import org.forgerock.opendj.ldap.spi.IndexingOptions;
039import org.forgerock.util.Function;
040import org.forgerock.util.promise.NeverThrowsException;
041import org.opends.server.replication.common.CSN;
042
043/**
044 * Extensible matching rule to allow a range search on CSNs for a given replica.
045 * <p>
046 * It uses the same index as {@link HistoricalCsnOrderingMatchingRuleImpl}, but emulates a range search on
047 * a multivalued attribute. We can do that because the index is ordered by replica id and we're interested on
048 * a range search for a single replica only.
049 */
050public final class HistoricalCsnRangeMatchingRuleImpl implements MatchingRuleImpl {
051
052    private final Collection<? extends Indexer> indexers =
053            Collections.singleton(new HistoricalCsnOrderingMatchingRuleImpl.HistoricalIndexer());
054
055    /**
056     * Rule operator
057     */
058    enum Operator {
059        LESS_THAN("<", new Function<Integer, Boolean, NeverThrowsException>() {
060            public Boolean apply(final Integer v) throws NeverThrowsException {
061                return v < 0;
062            }
063        }),
064        GREATER_THAN(">", new Function<Integer, Boolean, NeverThrowsException>() {
065            public Boolean apply(final Integer v) throws NeverThrowsException {
066                return v > 0;
067            }
068        }),
069        LESS_OR_EQUAL_THAN("<=", new Function<Integer, Boolean, NeverThrowsException>() {
070            public Boolean apply(final Integer v) throws NeverThrowsException {
071                return v <= 0;
072            }
073        }),
074        GREATER_OR_EQUAL_THAN(">=", new Function<Integer, Boolean, NeverThrowsException>() {
075            public Boolean apply(final Integer v) throws NeverThrowsException {
076                return v >= 0;
077            }
078        });
079
080        private final String comparator;
081        private final Function<Integer, Boolean, NeverThrowsException> evaluationFunction;
082
083        Operator(String op, Function<Integer, Boolean, NeverThrowsException> evaluationFunction) {
084            this.comparator = op;
085            this.evaluationFunction = evaluationFunction;
086        }
087
088        public boolean evaluate(int value) {
089            return evaluationFunction.apply(value);
090        }
091
092        public static Operator parse(String s) throws DecodeException {
093            for (Operator op : values()) {
094                if (op.comparator.equals(s)) {
095                    return op;
096                }
097            }
098            throw DecodeException.error(ERR_INVALID_CSN_RANGE_COMPARISON_OPERATOR.get(s));
099        }
100    }
101
102    /**
103     * Represents a filter for a CSN range on a serverID.
104     * <p>
105     * Expected syntax is: "OP1 CSN1[, OP2 CSN2]"; where OP1 is one of ">", ">=", OP2 is one of "<", "<=" or the other
106     * way around.
107     * <p>
108     * Example of valid filters:
109     * <ul>
110     *  <li><= 0000018b6fc1f1861f3003d3e3c1, > 0000018b6fc1f1861f3003d3e3a0</li>
111     *  <li>> 0000018b6fc1f1861f3003d3e3a0, <= 0000018b6fc1f1861f3003d3e3c1 (same as before)</li>
112     *  <li>>= 0000018b6fc1f1861f3003d3e3c1</li>
113     *  <li>< 0000018b6fc1f1861f3003d3e3a0</li>
114     * </ul>
115     * When the specified range is an open interval, the other endpoint is automatically computed
116     * to be the maximum or minimum possible CSN for the serverID in the specified CSN.
117     */
118    private static final class CsnRangeFilter {
119        private static final String ENDPOINT = "(<|<=|>|>=)\\s*([a-fA-F0-9]{28})";
120        private static final Pattern PATTERN = Pattern.compile(ENDPOINT + "(\\s*,\\s*" + ENDPOINT + ")?");
121        final ByteString greaterThanCsn;
122        final ByteString lessThanCsn;
123        final Operator greaterThanOp;
124        final Operator lessThanOp;
125
126        static CsnRangeFilter buildFromAssertion(ByteSequence assertionValue) throws DecodeException {
127            final Matcher matcher = PATTERN.matcher(assertionValue.toString());
128            if (!matcher.matches()) {
129                throw DecodeException.error(ERR_INVALID_CSN_RANGE_ASSERTION_SYNTAX.get(assertionValue.toString()));
130            }
131            final Operator op1 = Operator.parse(matcher.group(1));
132            final CSN csn1 = CSN.valueOf(matcher.group(2));
133            if (matcher.group(3) == null) {
134                return buildFromSingleOperation(op1, csn1);
135            }
136            final Operator op2 = Operator.parse(matcher.group(4));
137            final CSN csn2 = CSN.valueOf(matcher.group(5));
138            return buildFromTwoOperations(op1, csn1, op2, csn2);
139        }
140
141        private static CsnRangeFilter buildFromSingleOperation(Operator op, CSN csn) throws DecodeException {
142            if (op == LESS_OR_EQUAL_THAN || op == LESS_THAN) {
143                final CSN minCsn = new CSN(0, 0, csn.getServerId());
144                return new CsnRangeFilter(GREATER_OR_EQUAL_THAN, minCsn, op, csn);
145            } else {
146                final CSN maxCsn = new CSN(Long.MAX_VALUE, Integer.MAX_VALUE, csn.getServerId());
147                return new CsnRangeFilter(op, csn, LESS_OR_EQUAL_THAN, maxCsn);
148            }
149        }
150
151        private static CsnRangeFilter buildFromTwoOperations(Operator op1, CSN csn1, Operator op2, CSN csn2)
152                throws DecodeException {
153            if (csn1.getServerId() != csn2.getServerId()) {
154                throw DecodeException.error(ERR_CSN_RANGE_INCLUDES_MORE_THAN_ONE_SERVER.get(csn1, csn2));
155            }
156            if (op1 == LESS_OR_EQUAL_THAN || op1 == LESS_THAN) {
157                if (op2 == LESS_THAN || op2 == LESS_OR_EQUAL_THAN) {
158                    throw DecodeException.error(ERR_CSN_RANGE_SAME_OPERATOR_TYPE.get(op1, op2));
159                }
160                return new CsnRangeFilter(op2, csn2, op1, csn1);
161            } else {
162                if (op2 == GREATER_THAN || op2 == GREATER_OR_EQUAL_THAN) {
163                    throw DecodeException.error(ERR_CSN_RANGE_SAME_OPERATOR_TYPE.get(op1, op2));
164                }
165                return new CsnRangeFilter(op1, csn1, op2, csn2);
166            }
167        }
168
169        private CsnRangeFilter(Operator greaterThanOp, CSN greaterThanCsn, Operator lessThanOp, CSN lessThanCsn)
170                throws DecodeException {
171            this.greaterThanOp = greaterThanOp;
172            this.greaterThanCsn = normalizeCsnAttributeValue(greaterThanCsn.toString());
173            this.lessThanOp = lessThanOp;
174            this.lessThanCsn = normalizeCsnAttributeValue(lessThanCsn.toString());
175        }
176
177        boolean evaluate(final ByteSequence csnValue) {
178            return lessThanOp.evaluate(csnValue.compareTo(lessThanCsn))
179                    && greaterThanOp.evaluate(csnValue.compareTo(greaterThanCsn));
180        }
181    }
182
183    @Override
184    public Assertion getAssertion(Schema schema, ByteSequence assertionValue) throws DecodeException {
185        final CsnRangeFilter filter = CsnRangeFilter.buildFromAssertion(assertionValue);
186
187        return new Assertion() {
188            @Override
189            public ConditionResult matches(final ByteSequence attributeValue) {
190                return ConditionResult.valueOf(filter.evaluate(attributeValue));
191            }
192
193            @Override
194            public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
195                return factory.createRangeMatchQuery(ORDERING_ID,
196                                                     filter.greaterThanCsn,
197                                                     filter.lessThanCsn,
198                                                     filter.greaterThanOp == GREATER_OR_EQUAL_THAN,
199                                                     filter.lessThanOp == LESS_OR_EQUAL_THAN);
200            }
201        };
202    }
203
204    @Override
205    public Assertion getSubstringAssertion(Schema schema, ByteSequence subInitial,
206            List<? extends ByteSequence> subAnyElements, ByteSequence subFinal) throws DecodeException {
207        return UNDEFINED_ASSERTION;
208    }
209
210    @Override
211    public Assertion getGreaterOrEqualAssertion(Schema schema, ByteSequence value) throws DecodeException {
212        return UNDEFINED_ASSERTION;
213    }
214
215    @Override
216    public Assertion getLessOrEqualAssertion(Schema schema, ByteSequence value) throws DecodeException {
217        return UNDEFINED_ASSERTION;
218    }
219
220    @Override
221    public ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException {
222        return HistoricalCsnOrderingMatchingRuleImpl.normalizeAttributeValue(value);
223    }
224
225    @Override
226    public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
227        return indexers;
228    }
229}