001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.schema;
037
038
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.Map;
043import java.util.LinkedHashMap;
044
045import com.unboundid.ldap.sdk.LDAPException;
046import com.unboundid.ldap.sdk.ResultCode;
047import com.unboundid.util.NotMutable;
048import com.unboundid.util.NotNull;
049import com.unboundid.util.Nullable;
050import com.unboundid.util.StaticUtils;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.Validator;
054
055import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
056
057
058
059/**
060 * This class provides a data structure that describes an LDAP attribute syntax
061 * schema element.
062 */
063@NotMutable()
064@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
065public final class AttributeSyntaxDefinition
066       extends SchemaElement
067{
068  /**
069   * The serial version UID for this serializable class.
070   */
071  private static final long serialVersionUID = 8593718232711987488L;
072
073
074
075  // The set of extensions for this attribute syntax.
076  @NotNull private final Map<String,String[]> extensions;
077
078  // The description for this attribute syntax.
079  @Nullable private final String description;
080
081  // The string representation of this attribute syntax.
082  @NotNull private final String attributeSyntaxString;
083
084  // The OID for this attribute syntax.
085  @NotNull private final String oid;
086
087
088
089  /**
090   * Creates a new attribute syntax from the provided string representation.
091   *
092   * @param  s  The string representation of the attribute syntax to create,
093   *            using the syntax described in RFC 4512 section 4.1.5.  It must
094   *            not be {@code null}.
095   *
096   * @throws  LDAPException  If the provided string cannot be decoded as an
097   *                         attribute syntax definition.
098   */
099  public AttributeSyntaxDefinition(@NotNull final String s)
100         throws LDAPException
101  {
102    Validator.ensureNotNull(s);
103
104    attributeSyntaxString = s.trim();
105
106    // The first character must be an opening parenthesis.
107    final int length = attributeSyntaxString.length();
108    if (length == 0)
109    {
110      throw new LDAPException(ResultCode.DECODING_ERROR,
111                              ERR_ATTRSYNTAX_DECODE_EMPTY.get());
112    }
113    else if (attributeSyntaxString.charAt(0) != '(')
114    {
115      throw new LDAPException(ResultCode.DECODING_ERROR,
116                              ERR_ATTRSYNTAX_DECODE_NO_OPENING_PAREN.get(
117                                   attributeSyntaxString));
118    }
119
120
121    // Skip over any spaces until we reach the start of the OID, then read the
122    // OID until we find the next space.
123    int pos = skipSpaces(attributeSyntaxString, 1, length);
124
125    StringBuilder buffer = new StringBuilder();
126    pos = readOID(attributeSyntaxString, pos, length, buffer);
127    oid = buffer.toString();
128
129
130    // Technically, attribute syntax elements are supposed to appear in a
131    // specific order, but we'll be lenient and allow remaining elements to come
132    // in any order.
133    String descr = null;
134    final Map<String,String[]> exts  =
135         new LinkedHashMap<>(StaticUtils.computeMapCapacity(5));
136
137    while (true)
138    {
139      // Skip over any spaces until we find the next element.
140      pos = skipSpaces(attributeSyntaxString, pos, length);
141
142      // Read until we find the next space or the end of the string.  Use that
143      // token to figure out what to do next.
144      final int tokenStartPos = pos;
145      while ((pos < length) && (attributeSyntaxString.charAt(pos) != ' '))
146      {
147        pos++;
148      }
149
150      final String token = attributeSyntaxString.substring(tokenStartPos, pos);
151      final String lowerToken = StaticUtils.toLowerCase(token);
152      if (lowerToken.equals(")"))
153      {
154        // This indicates that we're at the end of the value.  There should not
155        // be any more closing characters.
156        if (pos < length)
157        {
158          throw new LDAPException(ResultCode.DECODING_ERROR,
159                                  ERR_ATTRSYNTAX_DECODE_CLOSE_NOT_AT_END.get(
160                                       attributeSyntaxString));
161        }
162        break;
163      }
164      else if (lowerToken.equals("desc"))
165      {
166        if (descr == null)
167        {
168          pos = skipSpaces(attributeSyntaxString, pos, length);
169
170          buffer = new StringBuilder();
171          pos = readQDString(attributeSyntaxString, pos, length, token, buffer);
172          descr = buffer.toString();
173        }
174        else
175        {
176          throw new LDAPException(ResultCode.DECODING_ERROR,
177                                  ERR_ATTRSYNTAX_DECODE_MULTIPLE_DESC.get(
178                                       attributeSyntaxString));
179        }
180      }
181      else if (lowerToken.startsWith("x-"))
182      {
183        pos = skipSpaces(attributeSyntaxString, pos, length);
184
185        final ArrayList<String> valueList = new ArrayList<>(5);
186        pos = readQDStrings(attributeSyntaxString, pos, length, token,
187             valueList);
188
189        final String[] values = new String[valueList.size()];
190        valueList.toArray(values);
191
192        if (exts.containsKey(token))
193        {
194          throw new LDAPException(ResultCode.DECODING_ERROR,
195                                  ERR_ATTRSYNTAX_DECODE_DUP_EXT.get(
196                                       attributeSyntaxString, token));
197        }
198
199        exts.put(token, values);
200      }
201      else
202      {
203        throw new LDAPException(ResultCode.DECODING_ERROR,
204                                  ERR_ATTRSYNTAX_DECODE_UNEXPECTED_TOKEN.get(
205                                       attributeSyntaxString, token));
206      }
207    }
208
209    description = descr;
210    extensions  = Collections.unmodifiableMap(exts);
211  }
212
213
214
215  /**
216   * Creates a new attribute syntax use with the provided information.
217   *
218   * @param  oid          The OID for this attribute syntax.  It must not be
219   *                      {@code null}.
220   * @param  description  The description for this attribute syntax.  It may be
221   *                      {@code null} if there is no description.
222   * @param  extensions   The set of extensions for this attribute syntax.  It
223   *                      may be {@code null} or empty if there should not be
224   *                      any extensions.
225   */
226  public AttributeSyntaxDefinition(@NotNull final String oid,
227              @Nullable final String description,
228              @Nullable final Map<String,String[]> extensions)
229  {
230    Validator.ensureNotNull(oid);
231
232    this.oid         = oid;
233    this.description = description;
234
235    if (extensions == null)
236    {
237      this.extensions = Collections.emptyMap();
238    }
239    else
240    {
241      this.extensions = Collections.unmodifiableMap(extensions);
242    }
243
244    final StringBuilder buffer = new StringBuilder();
245    createDefinitionString(buffer);
246    attributeSyntaxString = buffer.toString();
247  }
248
249
250
251  /**
252   * Constructs a string representation of this attribute syntax definition in
253   * the provided buffer.
254   *
255   * @param  buffer  The buffer in which to construct a string representation of
256   *                 this attribute syntax definition.
257   */
258  private void createDefinitionString(@NotNull final StringBuilder buffer)
259  {
260    buffer.append("( ");
261    buffer.append(oid);
262
263    if (description != null)
264    {
265      buffer.append(" DESC '");
266      encodeValue(description, buffer);
267      buffer.append('\'');
268    }
269
270    for (final Map.Entry<String,String[]> e : extensions.entrySet())
271    {
272      final String   name   = e.getKey();
273      final String[] values = e.getValue();
274      if (values.length == 1)
275      {
276        buffer.append(' ');
277        buffer.append(name);
278        buffer.append(" '");
279        encodeValue(values[0], buffer);
280        buffer.append('\'');
281      }
282      else
283      {
284        buffer.append(' ');
285        buffer.append(name);
286        buffer.append(" (");
287        for (final String value : values)
288        {
289          buffer.append(" '");
290          encodeValue(value, buffer);
291          buffer.append('\'');
292        }
293        buffer.append(" )");
294      }
295    }
296
297    buffer.append(" )");
298  }
299
300
301
302  /**
303   * Retrieves the OID for this attribute syntax.
304   *
305   * @return  The OID for this attribute syntax.
306   */
307  @NotNull()
308  public String getOID()
309  {
310    return oid;
311  }
312
313
314
315  /**
316   * Retrieves the description for this attribute syntax, if available.
317   *
318   * @return  The description for this attribute syntax, or {@code null} if
319   *          there is no description defined.
320   */
321  @Nullable()
322  public String getDescription()
323  {
324    return description;
325  }
326
327
328
329  /**
330   * Retrieves the set of extensions for this matching rule use.  They will be
331   * mapped from the extension name (which should start with "X-") to the set
332   * of values for that extension.
333   *
334   * @return  The set of extensions for this matching rule use.
335   */
336  @NotNull()
337  public Map<String,String[]> getExtensions()
338  {
339    return extensions;
340  }
341
342
343
344  /**
345   * {@inheritDoc}
346   */
347  @Override()
348  @NotNull()
349  public SchemaElementType getSchemaElementType()
350  {
351    return SchemaElementType.ATTRIBUTE_SYNTAX;
352  }
353
354
355
356  /**
357   * {@inheritDoc}
358   */
359  @Override()
360  public int hashCode()
361  {
362    return oid.hashCode();
363  }
364
365
366
367  /**
368   * {@inheritDoc}
369   */
370  @Override()
371  public boolean equals(@Nullable final Object o)
372  {
373    if (o == null)
374    {
375      return false;
376    }
377
378    if (o == this)
379    {
380      return true;
381    }
382
383    if (! (o instanceof AttributeSyntaxDefinition))
384    {
385      return false;
386    }
387
388    final AttributeSyntaxDefinition d = (AttributeSyntaxDefinition) o;
389    return (oid.equals(d.oid) &&
390         StaticUtils.bothNullOrEqualIgnoreCase(description, d.description) &&
391         extensionsEqual(extensions, d.extensions));
392  }
393
394
395
396  /**
397   * Retrieves a string representation of this attribute syntax, in the format
398   * described in RFC 4512 section 4.1.5.
399   *
400   * @return  A string representation of this attribute syntax definition.
401   */
402  @Override()
403  @NotNull()
404  public String toString()
405  {
406    return attributeSyntaxString;
407  }
408}