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 Sun Microsystems, Inc.
025 *      Portions copyright 2013-2014 ForgeRock AS.
026 */
027
028package org.forgerock.opendj.ldap.schema;
029
030import static com.forgerock.opendj.ldap.CoreMessages.*;
031
032import java.util.Collections;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.regex.Pattern;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.i18n.LocalizableMessageBuilder;
040import org.forgerock.opendj.ldap.ByteSequence;
041import org.forgerock.util.Reject;
042
043/**
044 * This class defines a data structure for storing and interacting with an LDAP
045 * syntaxes, which constrain the structure of attribute values stored in an LDAP
046 * directory, and determine the representation of attribute and assertion values
047 * transferred in the LDAP protocol.
048 * <p>
049 * Syntax implementations must extend the {@link SyntaxImpl} interface so they
050 * can be used by OpenDJ to validate attribute values.
051 * <p>
052 * Where ordered sets of names, or extra properties are provided, the ordering
053 * will be preserved when the associated fields are accessed via their getters
054 * or via the {@link #toString()} methods.
055 */
056public final class Syntax extends SchemaElement {
057
058    /** A fluent API for incrementally constructing syntaxes. */
059    public static final class Builder extends SchemaElementBuilder<Builder> {
060
061        private String oid;
062        private SyntaxImpl impl;
063
064        Builder(final Syntax syntax, final SchemaBuilder builder) {
065            super(builder, syntax);
066            this.oid = syntax.oid;
067            this.impl = syntax.impl;
068        }
069
070        Builder(final String oid, final SchemaBuilder builder) {
071            super(builder);
072            oid(oid);
073        }
074
075        /**
076         * Adds this syntax to the schema, throwing a
077         * {@code ConflictingSchemaElementException} if there is an existing
078         * syntax with the same numeric OID.
079         *
080         * @return The parent schema builder.
081         * @throws ConflictingSchemaElementException
082         *             If there is an existing syntax with the same numeric OID.
083         */
084        public SchemaBuilder addToSchema() {
085            return getSchemaBuilder().addSyntax(new Syntax(this), false);
086        }
087
088        /**
089         * Adds this syntax to the schema overwriting any existing syntax with the same numeric OID.
090         *
091         * @return The parent schema builder.
092         */
093        public SchemaBuilder addToSchemaOverwrite() {
094            return getSchemaBuilder().addSyntax(new Syntax(this), true);
095        }
096
097        /**
098         * Adds this syntax to the schema - overwriting any existing syntax with the same numeric OID
099         * if the overwrite parameter is set to {@code true}.
100         *
101         * @param overwrite
102         *            {@code true} if any syntax with the same OID should be overwritten.
103         * @return The parent schema builder.
104         */
105        SchemaBuilder addToSchema(final boolean overwrite) {
106            if (overwrite) {
107                return addToSchemaOverwrite();
108            }
109            return addToSchema();
110        }
111
112        @Override
113        public Builder description(final String description) {
114            return description0(description);
115        }
116
117        @Override
118        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
119            return extraProperties0(extraProperties);
120        }
121
122        @Override
123        public Builder extraProperties(final String extensionName, final String... extensionValues) {
124            return extraProperties0(extensionName, extensionValues);
125        }
126
127        /**
128         * Sets the numeric OID which uniquely identifies this syntax.
129         *
130         * @param oid
131         *            The numeric OID.
132         * @return This builder.
133         */
134        public Builder oid(final String oid) {
135            this.oid = oid;
136            return this;
137        }
138
139        @Override
140        public Builder removeAllExtraProperties() {
141            return removeAllExtraProperties0();
142        }
143
144        @Override
145        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
146            return removeExtraProperty0(extensionName, extensionValues);
147        }
148
149        /**
150         * Sets the syntax implementation.
151         *
152         * @param implementation
153         *            The syntax implementation.
154         * @return This builder.
155         */
156        public Builder implementation(final SyntaxImpl implementation) {
157            this.impl = implementation;
158            return this;
159        }
160
161        @Override
162        Builder getThis() {
163            return this;
164        }
165    }
166
167    private final String oid;
168    private MatchingRule equalityMatchingRule;
169    private MatchingRule orderingMatchingRule;
170    private MatchingRule substringMatchingRule;
171    private MatchingRule approximateMatchingRule;
172    private Schema schema;
173    private SyntaxImpl impl;
174
175    private Syntax(final Builder builder) {
176        super(builder);
177
178        // Checks for required attributes.
179        if (builder.oid == null || builder.oid.isEmpty()) {
180            throw new IllegalArgumentException("An OID must be specified.");
181        }
182
183        oid = builder.oid;
184        impl = builder.impl;
185    }
186
187    /**
188     * Creates a syntax representing an unrecognized syntax and whose
189     * implementation is substituted by the schema's default syntax.
190     *
191     * @param schema
192     *            The parent schema.
193     * @param oid
194     *            The numeric OID of the unrecognized syntax.
195     */
196    Syntax(final Schema schema, final String oid) {
197        super("", Collections.singletonMap("X-SUBST", Collections.singletonList(schema.getDefaultSyntax().getOID())),
198                null);
199
200        Reject.ifNull(oid);
201        this.oid = oid;
202        this.schema = schema;
203
204        final Syntax defaultSyntax = schema.getDefaultSyntax();
205        this.impl = defaultSyntax.impl;
206        this.approximateMatchingRule = defaultSyntax.getApproximateMatchingRule();
207        this.equalityMatchingRule = defaultSyntax.getEqualityMatchingRule();
208        this.orderingMatchingRule = defaultSyntax.getOrderingMatchingRule();
209        this.substringMatchingRule = defaultSyntax.getSubstringMatchingRule();
210    }
211
212    /**
213     * Returns {@code true} if the provided object is an attribute syntax having
214     * the same numeric OID as this attribute syntax.
215     *
216     * @param o
217     *            The object to be compared.
218     * @return {@code true} if the provided object is an attribute syntax having
219     *         the same numeric OID as this attribute syntax.
220     */
221    @Override
222    public boolean equals(final Object o) {
223        if (this == o) {
224            return true;
225        } else if (o instanceof Syntax) {
226            final Syntax other = (Syntax) o;
227            return oid.equals(other.oid);
228        } else {
229            return false;
230        }
231    }
232
233    /**
234     * Retrieves the default approximate matching rule that will be used for
235     * attributes with this syntax.
236     *
237     * @return The default approximate matching rule that will be used for
238     *         attributes with this syntax, or {@code null} if approximate
239     *         matches will not be allowed for this type by default.
240     */
241    public MatchingRule getApproximateMatchingRule() {
242        return approximateMatchingRule;
243    }
244
245    /**
246     * Retrieves the default equality matching rule that will be used for
247     * attributes with this syntax.
248     *
249     * @return The default equality matching rule that will be used for
250     *         attributes with this syntax, or {@code null} if equality matches
251     *         will not be allowed for this type by default.
252     */
253    public MatchingRule getEqualityMatchingRule() {
254        return equalityMatchingRule;
255    }
256
257    /**
258     * Retrieves the OID for this attribute syntax.
259     *
260     * @return The OID for this attribute syntax.
261     */
262    public String getOID() {
263        return oid;
264    }
265
266    /**
267     * Retrieves the name for this attribute syntax.
268     *
269     * @return The name for this attribute syntax.
270     */
271    public String getName() {
272        return impl.getName();
273    }
274
275    /**
276     * Retrieves the default ordering matching rule that will be used for
277     * attributes with this syntax.
278     *
279     * @return The default ordering matching rule that will be used for
280     *         attributes with this syntax, or {@code null} if ordering matches
281     *         will not be allowed for this type by default.
282     */
283    public MatchingRule getOrderingMatchingRule() {
284        return orderingMatchingRule;
285    }
286
287    /**
288     * Retrieves the default substring matching rule that will be used for
289     * attributes with this syntax.
290     *
291     * @return The default substring matching rule that will be used for
292     *         attributes with this syntax, or {@code null} if substring matches
293     *         will not be allowed for this type by default.
294     */
295    public MatchingRule getSubstringMatchingRule() {
296        return substringMatchingRule;
297    }
298
299    /**
300     * Returns the hash code for this attribute syntax. It will be calculated as
301     * the hash code of the numeric OID.
302     *
303     * @return The hash code for this attribute syntax.
304     */
305    @Override
306    public int hashCode() {
307        return oid.hashCode();
308    }
309
310    /**
311     * Indicates whether this attribute syntax requires that values must be
312     * encoded using the Basic Encoding Rules (BER) used by X.500 directories
313     * and always include the {@code binary} attribute description option.
314     *
315     * @return {@code true} this attribute syntax requires that values must be
316     *         BER encoded and always include the {@code binary} attribute
317     *         description option, or {@code false} if not.
318     * @see <a href="http://tools.ietf.org/html/rfc4522">RFC 4522 - Lightweight
319     *      Directory Access Protocol (LDAP): The Binary Encoding Option </a>
320     */
321    public boolean isBEREncodingRequired() {
322        return impl.isBEREncodingRequired();
323    }
324
325    /**
326     * Indicates whether this attribute syntax would likely be a human readable
327     * string.
328     *
329     * @return {@code true} if this attribute syntax would likely be a human
330     *         readable string or {@code false} if not.
331     */
332    public boolean isHumanReadable() {
333        return impl.isHumanReadable();
334    }
335
336    /**
337     * Indicates whether the provided value is acceptable for use in an
338     * attribute with this syntax. If it is not, then the reason may be appended
339     * to the provided buffer.
340     *
341     * @param value
342     *            The value for which to make the determination.
343     * @param invalidReason
344     *            The buffer to which the invalid reason should be appended.
345     * @return {@code true} if the provided value is acceptable for use with
346     *         this syntax, or {@code false} if not.
347     */
348    public boolean valueIsAcceptable(final ByteSequence value,
349            final LocalizableMessageBuilder invalidReason) {
350        return impl.valueIsAcceptable(schema, value, invalidReason);
351    }
352
353    @Override
354    void toStringContent(final StringBuilder buffer) {
355        buffer.append(oid);
356        appendDescription(buffer);
357    }
358
359    void validate(final Schema schema, final List<LocalizableMessage> warnings)
360            throws SchemaException {
361        this.schema = schema;
362        if (impl == null) {
363            // See if we need to override the implementation of the syntax
364            for (final Map.Entry<String, List<String>> property : getExtraProperties().entrySet()) {
365                // Enums are handled in the schema builder.
366                if ("x-subst".equalsIgnoreCase(property.getKey())) {
367                    /**
368                     * One unimplemented syntax can be substituted by another
369                     * defined syntax. A substitution syntax is an
370                     * LDAPSyntaxDescriptionSyntax with X-SUBST extension.
371                     */
372                    final Iterator<String> values = property.getValue().iterator();
373                    if (values.hasNext()) {
374                        final String value = values.next();
375                        if (value.equals(oid)) {
376                            throw new SchemaException(ERR_ATTR_SYNTAX_CYCLIC_SUB_SYNTAX.get(oid));
377                        }
378                        if (!schema.hasSyntax(value)) {
379                            throw new SchemaException(ERR_ATTR_SYNTAX_UNKNOWN_SUB_SYNTAX.get(oid, value));
380                        }
381                        final Syntax subSyntax = schema.getSyntax(value);
382                        if (subSyntax.impl == null) {
383                            // The substitution syntax was never validated.
384                            subSyntax.validate(schema, warnings);
385                        }
386                        impl = subSyntax.impl;
387                    }
388                } else if ("x-pattern".equalsIgnoreCase(property.getKey())) {
389                    final Iterator<String> values = property.getValue().iterator();
390                    if (values.hasNext()) {
391                        final String value = values.next();
392                        try {
393                            final Pattern pattern = Pattern.compile(value);
394                            impl = new RegexSyntaxImpl(pattern);
395                        } catch (final Exception e) {
396                            throw new SchemaException(
397                                    WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_PATTERN.get(oid, value));
398                        }
399                    }
400                }
401            }
402
403            // Try to find an implementation in the core schema
404            if (impl == null && Schema.getDefaultSchema().hasSyntax(oid)) {
405                impl = Schema.getDefaultSchema().getSyntax(oid).impl;
406            }
407            if (impl == null && Schema.getCoreSchema().hasSyntax(oid)) {
408                impl = Schema.getCoreSchema().getSyntax(oid).impl;
409            }
410
411            if (impl == null) {
412                final Syntax defaultSyntax = schema.getDefaultSyntax();
413                if (defaultSyntax.impl == null) {
414                    // The default syntax was never validated.
415                    defaultSyntax.validate(schema, warnings);
416                }
417                impl = defaultSyntax.impl;
418                final LocalizableMessage message = WARN_ATTR_SYNTAX_NOT_IMPLEMENTED1.get(getDescription(), oid, schema
419                        .getDefaultSyntax().getOID());
420                warnings.add(message);
421            }
422        }
423
424        // Get references to the default matching rules. It will be ok
425        // if we can't find some. Just warn.
426        if (impl.getEqualityMatchingRule() != null) {
427            if (schema.hasMatchingRule(impl.getEqualityMatchingRule())) {
428                equalityMatchingRule = schema.getMatchingRule(impl.getEqualityMatchingRule());
429            } else {
430                final LocalizableMessage message =
431                        ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE.get(impl
432                                .getEqualityMatchingRule(), impl.getName());
433                warnings.add(message);
434            }
435        }
436
437        if (impl.getOrderingMatchingRule() != null) {
438            if (schema.hasMatchingRule(impl.getOrderingMatchingRule())) {
439                orderingMatchingRule = schema.getMatchingRule(impl.getOrderingMatchingRule());
440            } else {
441                final LocalizableMessage message =
442                        ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE.get(impl
443                                .getOrderingMatchingRule(), impl.getName());
444                warnings.add(message);
445            }
446        }
447
448        if (impl.getSubstringMatchingRule() != null) {
449            if (schema.hasMatchingRule(impl.getSubstringMatchingRule())) {
450                substringMatchingRule = schema.getMatchingRule(impl.getSubstringMatchingRule());
451            } else {
452                final LocalizableMessage message =
453                        ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE.get(impl
454                                .getSubstringMatchingRule(), impl.getName());
455                warnings.add(message);
456            }
457        }
458
459        if (impl.getApproximateMatchingRule() != null) {
460            if (schema.hasMatchingRule(impl.getApproximateMatchingRule())) {
461                approximateMatchingRule = schema.getMatchingRule(impl.getApproximateMatchingRule());
462            } else {
463                final LocalizableMessage message =
464                        ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE.get(impl
465                                .getApproximateMatchingRule(), impl.getName());
466                warnings.add(message);
467            }
468        }
469    }
470
471    /**
472     * Indicates if the syntax has been validated, which means it has a non-null
473     * schema.
474     *
475     * @return {@code true} if and only if this syntax has been validated
476     */
477    boolean isValidated() {
478        return schema != null;
479    }
480}