001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2009-2010 Sun Microsystems, Inc.
025 *      Portions copyright 2013-2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.ldap.schema;
028
029import static com.forgerock.opendj.ldap.CoreMessages.*;
030
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.Iterator;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.opendj.ldap.Assertion;
040import org.forgerock.opendj.ldap.ByteSequence;
041import org.forgerock.opendj.ldap.ByteString;
042import org.forgerock.opendj.ldap.DecodeException;
043import org.forgerock.opendj.ldap.spi.Indexer;
044import org.forgerock.opendj.ldap.spi.IndexingOptions;
045
046/**
047 * This class defines a data structure for storing and interacting with matching
048 * rules, which are used by servers to compare attribute values against
049 * assertion values when performing Search and Compare operations. They are also
050 * used to identify the value to be added or deleted when modifying entries, and
051 * are used when comparing a purported distinguished name with the name of an
052 * entry.
053 * <p>
054 * Matching rule implementations must extend the
055 * <code>MatchingRuleImplementation</code> class so they can be used by OpenDJ.
056 * <p>
057 * Where ordered sets of names, or extra properties are provided, the ordering
058 * will be preserved when the associated fields are accessed via their getters
059 * or via the {@link #toString()} methods.
060 */
061public final class MatchingRule extends SchemaElement {
062
063    /** A fluent API for incrementally constructing matching rules. */
064    public static final class Builder extends SchemaElementBuilder<Builder> {
065        private String oid;
066        private final List<String> names = new LinkedList<>();
067        private boolean isObsolete;
068        private String syntaxOID;
069        private MatchingRuleImpl impl;
070
071        Builder(final MatchingRule mr, final SchemaBuilder builder) {
072            super(builder, mr);
073            this.oid = mr.oid;
074            this.names.addAll(mr.names);
075            this.isObsolete = mr.isObsolete;
076            this.syntaxOID = mr.syntaxOID;
077            this.impl = mr.impl;
078        }
079
080        Builder(final String oid, final SchemaBuilder builder) {
081            super(builder);
082            oid(oid);
083        }
084
085        /**
086         * Adds this matching rule to the schema, throwing a
087         * {@code ConflictingSchemaElementException} if there is an existing
088         * matching rule with the same numeric OID.
089         *
090         * @return The parent schema builder.
091         * @throws ConflictingSchemaElementException
092         *             If there is an existing matching rule with the same
093         *             numeric OID.
094         */
095        public SchemaBuilder addToSchema() {
096            return getSchemaBuilder().addMatchingRule(new MatchingRule(this), false);
097        }
098
099        /**
100         * Adds this matching rule to the schema overwriting any existing matching rule with the same numeric OID.
101         *
102         * @return The parent schema builder.
103         */
104        public SchemaBuilder addToSchemaOverwrite() {
105            return getSchemaBuilder().addMatchingRule(new MatchingRule(this), true);
106        }
107
108        @Override
109        public Builder description(final String description) {
110            return description0(description);
111        }
112
113        @Override
114        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
115            return extraProperties0(extraProperties);
116        }
117
118        @Override
119        public Builder extraProperties(final String extensionName, final String... extensionValues) {
120            return extraProperties0(extensionName, extensionValues);
121        }
122
123        /**
124         * Adds the provided user friendly names.
125         *
126         * @param names
127         *            The user friendly names.
128         * @return This builder.
129         */
130        public Builder names(final Collection<String> names) {
131            this.names.addAll(names);
132            return this;
133        }
134
135        /**
136         * Adds the provided user friendly names.
137         *
138         * @param names
139         *            The user friendly names.
140         * @return This builder.
141         */
142        public Builder names(final String... names) {
143            return names(Arrays.asList(names));
144        }
145
146        /**
147         * Specifies whether or not this schema element is obsolete.
148         *
149         * @param isObsolete
150         *            {@code true} if this schema element is obsolete (default is {@code false}).
151         * @return This builder.
152         */
153        public Builder obsolete(final boolean isObsolete) {
154            this.isObsolete = isObsolete;
155            return this;
156        }
157
158        /**
159         * Sets the numeric OID which uniquely identifies this matching rule.
160         *
161         * @param oid
162         *            The numeric OID.
163         * @return This builder.
164         */
165        public Builder oid(final String oid) {
166            this.oid = oid;
167            return this;
168        }
169
170        /**
171         * Sets the syntax OID of this matching rule.
172         *
173         * @param syntax
174         *            The syntax OID.
175         * @return This builder.
176         */
177        public Builder syntaxOID(final String syntax) {
178            this.syntaxOID = syntax;
179            return this;
180        }
181
182        /**
183         * Sets the matching rule implementation.
184         *
185         * @param implementation
186         *            The matching rule implementation.
187         * @return This builder.
188         */
189        public Builder implementation(final MatchingRuleImpl implementation) {
190            this.impl = implementation;
191            return this;
192        }
193
194        @Override
195        public Builder removeAllExtraProperties() {
196            return removeAllExtraProperties0();
197        }
198
199        /**
200         * Removes all user friendly names.
201         *
202         * @return This builder.
203         */
204        public Builder removeAllNames() {
205            this.names.clear();
206            return this;
207        }
208
209        @Override
210        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
211            return removeExtraProperty0(extensionName, extensionValues);
212        }
213
214        /**
215         * Removes the provided user friendly name.
216         *
217         * @param name
218         *            The user friendly name to be removed.
219         * @return This builder.
220         */
221        public Builder removeName(final String name) {
222            names.remove(name);
223            return this;
224        }
225
226        @Override
227        Builder getThis() {
228            return this;
229        }
230    }
231
232    private final String oid;
233    private final List<String> names;
234    private final boolean isObsolete;
235    private final String syntaxOID;
236    private MatchingRuleImpl impl;
237    private Syntax syntax;
238    private Schema schema;
239
240    private MatchingRule(final Builder builder) {
241        super(builder);
242
243        // Checks for required attributes.
244        if (builder.oid == null || builder.oid.isEmpty()) {
245            throw new IllegalArgumentException("An OID must be specified.");
246        }
247        if (builder.syntaxOID == null || builder.syntaxOID.isEmpty()) {
248            throw new IllegalArgumentException("Required syntax OID must be specified.");
249        }
250
251        oid = builder.oid;
252        names = SchemaUtils.unmodifiableCopyOfList(builder.names);
253        isObsolete = builder.isObsolete;
254        syntaxOID = builder.syntaxOID;
255        impl = builder.impl;
256    }
257
258    /**
259     * Returns {@code true} if the provided object is a matching rule having the
260     * same numeric OID as this matching rule.
261     *
262     * @param o
263     *            The object to be compared.
264     * @return {@code true} if the provided object is a matching rule having the
265     *         same numeric OID as this matching rule.
266     */
267    @Override
268    public boolean equals(final Object o) {
269        if (this == o) {
270            return true;
271        } else if (o instanceof MatchingRule) {
272            final MatchingRule other = (MatchingRule) o;
273            return oid.equals(other.oid);
274        } else {
275            return false;
276        }
277    }
278
279    /**
280     * Returns the normalized form of the provided assertion value, which is
281     * best suited for efficiently performing matching operations on that value.
282     * The assertion value is guaranteed to be valid against this matching
283     * rule's assertion syntax.
284     *
285     * @param value
286     *            The syntax checked assertion value to be normalized.
287     * @return The normalized version of the provided assertion value.
288     * @throws DecodeException
289     *             if the syntax of the value is not valid.
290     */
291    public Assertion getAssertion(final ByteSequence value) throws DecodeException {
292        return impl.getAssertion(schema, value);
293    }
294
295    /**
296     * Returns the normalized form of the provided assertion substring values,
297     * which is best suited for efficiently performing matching operations on
298     * that value.
299     *
300     * @param subInitial
301     *            The normalized substring value fragment that should appear at
302     *            the beginning of the target value.
303     * @param subAnyElements
304     *            The normalized substring value fragments that should appear in
305     *            the middle of the target value.
306     * @param subFinal
307     *            The normalized substring value fragment that should appear at
308     *            the end of the target value.
309     * @return The normalized version of the provided assertion value.
310     * @throws DecodeException
311     *             if the syntax of the value is not valid.
312     */
313    public Assertion getSubstringAssertion(final ByteSequence subInitial,
314            final List<? extends ByteSequence> subAnyElements, final ByteSequence subFinal)
315            throws DecodeException {
316        return impl.getSubstringAssertion(schema, subInitial, subAnyElements, subFinal);
317    }
318
319    /**
320     * Returns the normalized form of the provided assertion value, which is
321     * best suited for efficiently performing greater than or equal ordering
322     * matching operations on that value. The assertion value is guaranteed to
323     * be valid against this matching rule's assertion syntax.
324     *
325     * @param value
326     *            The syntax checked assertion value to be normalized.
327     * @return The normalized version of the provided assertion value.
328     * @throws DecodeException
329     *             if the syntax of the value is not valid.
330     */
331    public Assertion getGreaterOrEqualAssertion(final ByteSequence value) throws DecodeException {
332        return impl.getGreaterOrEqualAssertion(schema, value);
333    }
334
335    /**
336     * Returns the normalized form of the provided assertion value, which is
337     * best suited for efficiently performing greater than or equal ordering
338     * matching operations on that value. The assertion value is guaranteed to
339     * be valid against this matching rule's assertion syntax.
340     *
341     * @param value
342     *            The syntax checked assertion value to be normalized.
343     * @return The normalized version of the provided assertion value.
344     * @throws DecodeException
345     *             if the syntax of the value is not valid.
346     */
347    public Assertion getLessOrEqualAssertion(final ByteSequence value) throws DecodeException {
348        return impl.getLessOrEqualAssertion(schema, value);
349    }
350
351    /**
352     * Returns the indexers for this matching rule configured using the provided indexing options.
353     *
354     * @param options
355     *            The indexing options
356     * @return the collection of indexers for this matching rule.
357     */
358    public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
359        return impl.createIndexers(options);
360    }
361
362    /**
363     * Returns the name or OID for this schema definition. If it has one or more
364     * names, then the primary name will be returned. If it does not have any
365     * names, then the OID will be returned.
366     *
367     * @return The name or OID for this schema definition.
368     */
369    public String getNameOrOID() {
370        if (names.isEmpty()) {
371            return oid;
372        }
373        return names.get(0);
374    }
375
376    /**
377     * Returns an unmodifiable list containing the user-defined names that may
378     * be used to reference this schema definition.
379     *
380     * @return Returns an unmodifiable list containing the user-defined names
381     *         that may be used to reference this schema definition.
382     */
383    public List<String> getNames() {
384        return names;
385    }
386
387    /**
388     * Returns the OID for this schema definition.
389     *
390     * @return The OID for this schema definition.
391     */
392    public String getOID() {
393        return oid;
394    }
395
396    /**
397     * Returns the OID of the assertion value syntax with which this matching
398     * rule is associated.
399     *
400     * @return The OID of the assertion value syntax with which this matching
401     *         rule is associated.
402     */
403    public Syntax getSyntax() {
404        return syntax;
405    }
406
407    /**
408     * Returns the hash code for this matching rule. It will be calculated as
409     * the hash code of the numeric OID.
410     *
411     * @return The hash code for this matching rule.
412     */
413    @Override
414    public int hashCode() {
415        return oid.hashCode();
416    }
417
418    /**
419     * Indicates whether this schema definition has the specified name.
420     *
421     * @param name
422     *            The name for which to make the determination.
423     * @return <code>true</code> if the specified name is assigned to this
424     *         schema definition, or <code>false</code> if not.
425     */
426    public boolean hasName(final String name) {
427        for (final String n : names) {
428            if (n.equalsIgnoreCase(name)) {
429                return true;
430            }
431        }
432        return false;
433    }
434
435    /**
436     * Indicates whether this schema definition has the specified name or OID.
437     *
438     * @param value
439     *            The value for which to make the determination.
440     * @return <code>true</code> if the provided value matches the OID or one of
441     *         the names assigned to this schema definition, or
442     *         <code>false</code> if not.
443     */
444    public boolean hasNameOrOID(final String value) {
445        return hasName(value) || getOID().equals(value);
446    }
447
448    /**
449     * Indicates whether this schema definition is declared "obsolete".
450     *
451     * @return <code>true</code> if this schema definition is declared
452     *         "obsolete", or <code>false</code> if not.
453     */
454    public boolean isObsolete() {
455        return isObsolete;
456    }
457
458    /**
459     * Returns the normalized form of the provided attribute value, which is
460     * best suited for efficiently performing matching operations on that value.
461     * The returned normalized representation can be compared for equality with
462     * other values normalized with this matching rule using
463     * {@link ByteSequence#equals(Object)}. In addition, normalized values can
464     * be compared using {@link ByteSequence#compareTo(ByteSequence)}, although
465     * the sort order is only defined for ordering matching rules.
466     *
467     * @param value
468     *            The attribute value to be normalized.
469     * @return The normalized version of the provided attribute value.
470     * @throws DecodeException
471     *             If the syntax of the value is not valid.
472     */
473    public ByteString normalizeAttributeValue(final ByteSequence value) throws DecodeException {
474        return impl.normalizeAttributeValue(schema, value);
475    }
476
477    @Override
478    void toStringContent(final StringBuilder buffer) {
479        buffer.append(oid);
480
481        if (!names.isEmpty()) {
482            final Iterator<String> iterator = names.iterator();
483
484            final String firstName = iterator.next();
485            if (iterator.hasNext()) {
486                buffer.append(" NAME ( '");
487                buffer.append(firstName);
488
489                while (iterator.hasNext()) {
490                    buffer.append("' '");
491                    buffer.append(iterator.next());
492                }
493
494                buffer.append("' )");
495            } else {
496                buffer.append(" NAME '");
497                buffer.append(firstName);
498                buffer.append("'");
499            }
500        }
501
502        appendDescription(buffer);
503
504        if (isObsolete) {
505            buffer.append(" OBSOLETE");
506        }
507
508        buffer.append(" SYNTAX ");
509        buffer.append(syntaxOID);
510    }
511
512    void validate(final Schema schema, final List<LocalizableMessage> warnings)
513            throws SchemaException {
514        // Try finding an implementation in the core schema
515        if (impl == null && Schema.getDefaultSchema().hasMatchingRule(oid)) {
516            impl = Schema.getDefaultSchema().getMatchingRule(oid).impl;
517        }
518        if (impl == null && Schema.getCoreSchema().hasMatchingRule(oid)) {
519            impl = Schema.getCoreSchema().getMatchingRule(oid).impl;
520        }
521
522        if (impl == null) {
523            final MatchingRule defaultMatchingRule = schema.getDefaultMatchingRule();
524            if (defaultMatchingRule.impl == null) {
525                // The default matching rule was never validated.
526                defaultMatchingRule.validate(schema, warnings);
527            }
528            impl = defaultMatchingRule.impl;
529            final LocalizableMessage message =
530                    WARN_MATCHING_RULE_NOT_IMPLEMENTED1.get(getNameOrOID(), schema
531                            .getDefaultMatchingRule().getOID());
532            warnings.add(message);
533        }
534
535        try {
536            // Make sure the specific syntax is defined in this schema.
537            syntax = schema.getSyntax(syntaxOID);
538        } catch (final UnknownSchemaElementException e) {
539            final LocalizableMessage message =
540                    ERR_ATTR_SYNTAX_MR_UNKNOWN_SYNTAX1.get(getNameOrOID(), syntaxOID);
541            throw new SchemaException(message, e);
542        }
543
544        this.schema = schema;
545    }
546
547    /**
548     * Indicates if the matching rule has been validated, which means it has a
549     * non-null schema.
550     *
551     * @return {@code true} if and only if this matching rule has been validated
552     */
553    boolean isValidated() {
554        return schema != null;
555    }
556}