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