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