001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldap.sdk.schema;
022
023
024
025 import java.util.ArrayList;
026 import java.util.Collections;
027 import java.util.Map;
028 import java.util.LinkedHashMap;
029
030 import com.unboundid.ldap.sdk.LDAPException;
031 import com.unboundid.ldap.sdk.ResultCode;
032 import com.unboundid.util.NotMutable;
033 import com.unboundid.util.ThreadSafety;
034 import com.unboundid.util.ThreadSafetyLevel;
035
036 import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
037 import static com.unboundid.util.StaticUtils.*;
038 import static com.unboundid.util.Validator.*;
039
040
041
042 /**
043 * This class provides a data structure that describes an LDAP matching rule
044 * schema element.
045 */
046 @NotMutable()
047 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
048 public final class MatchingRuleDefinition
049 extends SchemaElement
050 {
051 /**
052 * The serial version UID for this serializable class.
053 */
054 private static final long serialVersionUID = 8214648655449007967L;
055
056
057
058 // Indicates whether this matching rule is declared obsolete.
059 private final boolean isObsolete;
060
061 // The set of extensions for this matching rule.
062 private final Map<String,String[]> extensions;
063
064 // The description for this matching rule.
065 private final String description;
066
067 // The string representation of this matching rule.
068 private final String matchingRuleString;
069
070 // The OID for this matching rule.
071 private final String oid;
072
073 // The OID of the syntax for this matching rule.
074 private final String syntaxOID;
075
076 // The set of names for this matching rule.
077 private final String[] names;
078
079
080
081 /**
082 * Creates a new matching rule from the provided string representation.
083 *
084 * @param s The string representation of the matching rule to create, using
085 * the syntax described in RFC 4512 section 4.1.3. It must not be
086 * {@code null}.
087 *
088 * @throws LDAPException If the provided string cannot be decoded as a
089 * matching rule definition.
090 */
091 public MatchingRuleDefinition(final String s)
092 throws LDAPException
093 {
094 ensureNotNull(s);
095
096 matchingRuleString = s.trim();
097
098 // The first character must be an opening parenthesis.
099 final int length = matchingRuleString.length();
100 if (length == 0)
101 {
102 throw new LDAPException(ResultCode.DECODING_ERROR,
103 ERR_MR_DECODE_EMPTY.get());
104 }
105 else if (matchingRuleString.charAt(0) != '(')
106 {
107 throw new LDAPException(ResultCode.DECODING_ERROR,
108 ERR_MR_DECODE_NO_OPENING_PAREN.get(
109 matchingRuleString));
110 }
111
112
113 // Skip over any spaces until we reach the start of the OID, then read the
114 // OID until we find the next space.
115 int pos = skipSpaces(matchingRuleString, 1, length);
116
117 StringBuilder buffer = new StringBuilder();
118 pos = readOID(matchingRuleString, pos, length, buffer);
119 oid = buffer.toString();
120
121
122 // Technically, matching rule elements are supposed to appear in a specific
123 // order, but we'll be lenient and allow remaining elements to come in any
124 // order.
125 final ArrayList<String> nameList = new ArrayList<String>(1);
126 String descr = null;
127 Boolean obsolete = null;
128 String synOID = null;
129 final Map<String,String[]> exts = new LinkedHashMap<String,String[]>();
130
131 while (true)
132 {
133 // Skip over any spaces until we find the next element.
134 pos = skipSpaces(matchingRuleString, pos, length);
135
136 // Read until we find the next space or the end of the string. Use that
137 // token to figure out what to do next.
138 final int tokenStartPos = pos;
139 while ((pos < length) && (matchingRuleString.charAt(pos) != ' '))
140 {
141 pos++;
142 }
143
144 // It's possible that the token could be smashed right up against the
145 // closing parenthesis. If that's the case, then extract just the token
146 // and handle the closing parenthesis the next time through.
147 String token = matchingRuleString.substring(tokenStartPos, pos);
148 if ((token.length() > 1) && (token.endsWith(")")))
149 {
150 token = token.substring(0, token.length() - 1);
151 pos--;
152 }
153
154 final String lowerToken = toLowerCase(token);
155 if (lowerToken.equals(")"))
156 {
157 // This indicates that we're at the end of the value. There should not
158 // be any more closing characters.
159 if (pos < length)
160 {
161 throw new LDAPException(ResultCode.DECODING_ERROR,
162 ERR_MR_DECODE_CLOSE_NOT_AT_END.get(
163 matchingRuleString));
164 }
165 break;
166 }
167 else if (lowerToken.equals("name"))
168 {
169 if (nameList.isEmpty())
170 {
171 pos = skipSpaces(matchingRuleString, pos, length);
172 pos = readQDStrings(matchingRuleString, pos, length, nameList);
173 }
174 else
175 {
176 throw new LDAPException(ResultCode.DECODING_ERROR,
177 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
178 matchingRuleString, "NAME"));
179 }
180 }
181 else if (lowerToken.equals("desc"))
182 {
183 if (descr == null)
184 {
185 pos = skipSpaces(matchingRuleString, pos, length);
186
187 buffer = new StringBuilder();
188 pos = readQDString(matchingRuleString, pos, length, buffer);
189 descr = buffer.toString();
190 }
191 else
192 {
193 throw new LDAPException(ResultCode.DECODING_ERROR,
194 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
195 matchingRuleString, "DESC"));
196 }
197 }
198 else if (lowerToken.equals("obsolete"))
199 {
200 if (obsolete == null)
201 {
202 obsolete = true;
203 }
204 else
205 {
206 throw new LDAPException(ResultCode.DECODING_ERROR,
207 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
208 matchingRuleString, "OBSOLETE"));
209 }
210 }
211 else if (lowerToken.equals("syntax"))
212 {
213 if (synOID == null)
214 {
215 pos = skipSpaces(matchingRuleString, pos, length);
216
217 buffer = new StringBuilder();
218 pos = readOID(matchingRuleString, pos, length, buffer);
219 synOID = buffer.toString();
220 }
221 else
222 {
223 throw new LDAPException(ResultCode.DECODING_ERROR,
224 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
225 matchingRuleString, "SYNTAX"));
226 }
227 }
228 else if (lowerToken.startsWith("x-"))
229 {
230 pos = skipSpaces(matchingRuleString, pos, length);
231
232 final ArrayList<String> valueList = new ArrayList<String>();
233 pos = readQDStrings(matchingRuleString, pos, length, valueList);
234
235 final String[] values = new String[valueList.size()];
236 valueList.toArray(values);
237
238 if (exts.containsKey(token))
239 {
240 throw new LDAPException(ResultCode.DECODING_ERROR,
241 ERR_MR_DECODE_DUP_EXT.get(matchingRuleString,
242 token));
243 }
244
245 exts.put(token, values);
246 }
247 else
248 {
249 throw new LDAPException(ResultCode.DECODING_ERROR,
250 ERR_MR_DECODE_UNEXPECTED_TOKEN.get(
251 matchingRuleString, token));
252 }
253 }
254
255 description = descr;
256 syntaxOID = synOID;
257 if (syntaxOID == null)
258 {
259 throw new LDAPException(ResultCode.DECODING_ERROR,
260 ERR_MR_DECODE_NO_SYNTAX.get(matchingRuleString));
261 }
262
263 names = new String[nameList.size()];
264 nameList.toArray(names);
265
266 isObsolete = (obsolete != null);
267
268 extensions = Collections.unmodifiableMap(exts);
269 }
270
271
272
273 /**
274 * Creates a new matching rule with the provided information.
275 *
276 * @param oid The OID for this matching rule. It must not be
277 * {@code null}.
278 * @param name The names for this matching rule. It may be
279 * {@code null} if the matching rule should only be
280 * referenced by OID.
281 * @param description The description for this matching rule. It may be
282 * {@code null} if there is no description.
283 * @param syntaxOID The syntax OID for this matching rule. It must not be
284 * {@code null}.
285 * @param extensions The set of extensions for this matching rule.
286 * It may be {@code null} or empty if there should not be
287 * any extensions.
288 */
289 public MatchingRuleDefinition(final String oid, final String name,
290 final String description,
291 final String syntaxOID,
292 final Map<String,String[]> extensions)
293 {
294 this(oid, ((name == null) ? null : new String[] { name }), description,
295 false, syntaxOID, extensions);
296 }
297
298
299
300 /**
301 * Creates a new matching rule with the provided information.
302 *
303 * @param oid The OID for this matching rule. It must not be
304 * {@code null}.
305 * @param names The set of names for this matching rule. It may be
306 * {@code null} or empty if the matching rule should only
307 * be referenced by OID.
308 * @param description The description for this matching rule. It may be
309 * {@code null} if there is no description.
310 * @param isObsolete Indicates whether this matching rule is declared
311 * obsolete.
312 * @param syntaxOID The syntax OID for this matching rule. It must not be
313 * {@code null}.
314 * @param extensions The set of extensions for this matching rule.
315 * It may be {@code null} or empty if there should not be
316 * any extensions.
317 */
318 public MatchingRuleDefinition(final String oid, final String[] names,
319 final String description,
320 final boolean isObsolete,
321 final String syntaxOID,
322 final Map<String,String[]> extensions)
323 {
324 ensureNotNull(oid, syntaxOID);
325
326 this.oid = oid;
327 this.description = description;
328 this.isObsolete = isObsolete;
329 this.syntaxOID = syntaxOID;
330
331 if (names == null)
332 {
333 this.names = NO_STRINGS;
334 }
335 else
336 {
337 this.names = names;
338 }
339
340 if (extensions == null)
341 {
342 this.extensions = Collections.emptyMap();
343 }
344 else
345 {
346 this.extensions = Collections.unmodifiableMap(extensions);
347 }
348
349 final StringBuilder buffer = new StringBuilder();
350 createDefinitionString(buffer);
351 matchingRuleString = buffer.toString();
352 }
353
354
355
356 /**
357 * Constructs a string representation of this matching rule definition in the
358 * provided buffer.
359 *
360 * @param buffer The buffer in which to construct a string representation of
361 * this matching rule definition.
362 */
363 private void createDefinitionString(final StringBuilder buffer)
364 {
365 buffer.append("( ");
366 buffer.append(oid);
367
368 if (names.length == 1)
369 {
370 buffer.append(" NAME '");
371 buffer.append(names[0]);
372 buffer.append('\'');
373 }
374 else if (names.length > 1)
375 {
376 buffer.append(" NAME (");
377 for (final String name : names)
378 {
379 buffer.append(" '");
380 buffer.append(name);
381 buffer.append('\'');
382 }
383 buffer.append(" )");
384 }
385
386 if (description != null)
387 {
388 buffer.append(" DESC '");
389 encodeValue(description, buffer);
390 buffer.append('\'');
391 }
392
393 if (isObsolete)
394 {
395 buffer.append(" OBSOLETE");
396 }
397
398 buffer.append(" SYNTAX ");
399 buffer.append(syntaxOID);
400
401 for (final Map.Entry<String,String[]> e : extensions.entrySet())
402 {
403 final String name = e.getKey();
404 final String[] values = e.getValue();
405 if (values.length == 1)
406 {
407 buffer.append(' ');
408 buffer.append(name);
409 buffer.append(" '");
410 encodeValue(values[0], buffer);
411 buffer.append('\'');
412 }
413 else
414 {
415 buffer.append(' ');
416 buffer.append(name);
417 buffer.append(" (");
418 for (final String value : values)
419 {
420 buffer.append(" '");
421 encodeValue(value, buffer);
422 buffer.append('\'');
423 }
424 buffer.append(" )");
425 }
426 }
427
428 buffer.append(" )");
429 }
430
431
432
433 /**
434 * Retrieves the OID for this matching rule.
435 *
436 * @return The OID for this matching rule.
437 */
438 public String getOID()
439 {
440 return oid;
441 }
442
443
444
445 /**
446 * Retrieves the set of names for this matching rule.
447 *
448 * @return The set of names for this matching rule, or an empty array if it
449 * does not have any names.
450 */
451 public String[] getNames()
452 {
453 return names;
454 }
455
456
457
458 /**
459 * Retrieves the primary name that can be used to reference this matching
460 * rule. If one or more names are defined, then the first name will be used.
461 * Otherwise, the OID will be returned.
462 *
463 * @return The primary name that can be used to reference this matching rule.
464 */
465 public String getNameOrOID()
466 {
467 if (names.length == 0)
468 {
469 return oid;
470 }
471 else
472 {
473 return names[0];
474 }
475 }
476
477
478
479 /**
480 * Indicates whether the provided string matches the OID or any of the names
481 * for this matching rule.
482 *
483 * @param s The string for which to make the determination. It must not be
484 * {@code null}.
485 *
486 * @return {@code true} if the provided string matches the OID or any of the
487 * names for this matching rule, or {@code false} if not.
488 */
489 public boolean hasNameOrOID(final String s)
490 {
491 for (final String name : names)
492 {
493 if (s.equalsIgnoreCase(name))
494 {
495 return true;
496 }
497 }
498
499 return s.equalsIgnoreCase(oid);
500 }
501
502
503
504 /**
505 * Retrieves the description for this matching rule, if available.
506 *
507 * @return The description for this matching rule, or {@code null} if there
508 * is no description defined.
509 */
510 public String getDescription()
511 {
512 return description;
513 }
514
515
516
517 /**
518 * Indicates whether this matching rule is declared obsolete.
519 *
520 * @return {@code true} if this matching rule is declared obsolete, or
521 * {@code false} if it is not.
522 */
523 public boolean isObsolete()
524 {
525 return isObsolete;
526 }
527
528
529
530 /**
531 * Retrieves the OID of the syntax for this matching rule.
532 *
533 * @return The OID of the syntax for this matching rule.
534 */
535 public String getSyntaxOID()
536 {
537 return syntaxOID;
538 }
539
540
541
542 /**
543 * Retrieves the set of extensions for this matching rule. They will be
544 * mapped from the extension name (which should start with "X-") to the set
545 * of values for that extension.
546 *
547 * @return The set of extensions for this matching rule.
548 */
549 public Map<String,String[]> getExtensions()
550 {
551 return extensions;
552 }
553
554
555
556 /**
557 * {@inheritDoc}
558 */
559 @Override()
560 public int hashCode()
561 {
562 return oid.hashCode();
563 }
564
565
566
567 /**
568 * {@inheritDoc}
569 */
570 @Override()
571 public boolean equals(final Object o)
572 {
573 if (o == null)
574 {
575 return false;
576 }
577
578 if (o == this)
579 {
580 return true;
581 }
582
583 if (! (o instanceof MatchingRuleDefinition))
584 {
585 return false;
586 }
587
588 final MatchingRuleDefinition d = (MatchingRuleDefinition) o;
589 return (oid.equals(d.oid) &&
590 syntaxOID.equals(d.syntaxOID) &&
591 stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
592 bothNullOrEqualIgnoreCase(description, d.description) &&
593 (isObsolete == d.isObsolete) &&
594 extensionsEqual(extensions, d.extensions));
595 }
596
597
598
599 /**
600 * Retrieves a string representation of this matching rule definition, in the
601 * format described in RFC 4512 section 4.1.3.
602 *
603 * @return A string representation of this matching rule definition.
604 */
605 @Override()
606 public String toString()
607 {
608 return matchingRuleString;
609 }
610 }