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