001 /*
002 * Copyright 2007-2013 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2013 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 final String token = matchingRuleString.substring(tokenStartPos, pos);
145 final String lowerToken = toLowerCase(token);
146 if (lowerToken.equals(")"))
147 {
148 // This indicates that we're at the end of the value. There should not
149 // be any more closing characters.
150 if (pos < length)
151 {
152 throw new LDAPException(ResultCode.DECODING_ERROR,
153 ERR_MR_DECODE_CLOSE_NOT_AT_END.get(
154 matchingRuleString));
155 }
156 break;
157 }
158 else if (lowerToken.equals("name"))
159 {
160 if (nameList.isEmpty())
161 {
162 pos = skipSpaces(matchingRuleString, pos, length);
163 pos = readQDStrings(matchingRuleString, pos, length, nameList);
164 }
165 else
166 {
167 throw new LDAPException(ResultCode.DECODING_ERROR,
168 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
169 matchingRuleString, "NAME"));
170 }
171 }
172 else if (lowerToken.equals("desc"))
173 {
174 if (descr == null)
175 {
176 pos = skipSpaces(matchingRuleString, pos, length);
177
178 buffer = new StringBuilder();
179 pos = readQDString(matchingRuleString, pos, length, buffer);
180 descr = buffer.toString();
181 }
182 else
183 {
184 throw new LDAPException(ResultCode.DECODING_ERROR,
185 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
186 matchingRuleString, "DESC"));
187 }
188 }
189 else if (lowerToken.equals("obsolete"))
190 {
191 if (obsolete == null)
192 {
193 obsolete = true;
194 }
195 else
196 {
197 throw new LDAPException(ResultCode.DECODING_ERROR,
198 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
199 matchingRuleString, "OBSOLETE"));
200 }
201 }
202 else if (lowerToken.equals("syntax"))
203 {
204 if (synOID == null)
205 {
206 pos = skipSpaces(matchingRuleString, pos, length);
207
208 buffer = new StringBuilder();
209 pos = readOID(matchingRuleString, pos, length, buffer);
210 synOID = buffer.toString();
211 }
212 else
213 {
214 throw new LDAPException(ResultCode.DECODING_ERROR,
215 ERR_MR_DECODE_MULTIPLE_ELEMENTS.get(
216 matchingRuleString, "SYNTAX"));
217 }
218 }
219 else if (lowerToken.startsWith("x-"))
220 {
221 pos = skipSpaces(matchingRuleString, pos, length);
222
223 final ArrayList<String> valueList = new ArrayList<String>();
224 pos = readQDStrings(matchingRuleString, pos, length, valueList);
225
226 final String[] values = new String[valueList.size()];
227 valueList.toArray(values);
228
229 if (exts.containsKey(token))
230 {
231 throw new LDAPException(ResultCode.DECODING_ERROR,
232 ERR_MR_DECODE_DUP_EXT.get(matchingRuleString,
233 token));
234 }
235
236 exts.put(token, values);
237 }
238 else
239 {
240 throw new LDAPException(ResultCode.DECODING_ERROR,
241 ERR_MR_DECODE_UNEXPECTED_TOKEN.get(
242 matchingRuleString, token));
243 }
244 }
245
246 description = descr;
247 syntaxOID = synOID;
248 if (syntaxOID == null)
249 {
250 throw new LDAPException(ResultCode.DECODING_ERROR,
251 ERR_MR_DECODE_NO_SYNTAX.get(matchingRuleString));
252 }
253
254 names = new String[nameList.size()];
255 nameList.toArray(names);
256
257 isObsolete = (obsolete != null);
258
259 extensions = Collections.unmodifiableMap(exts);
260 }
261
262
263
264 /**
265 * Creates a new matching rule with the provided information.
266 *
267 * @param oid The OID for this matching rule. It must not be
268 * {@code null}.
269 * @param names The set of names for this matching rule. It may be
270 * {@code null} or empty if the matching rule should only
271 * be referenced by OID.
272 * @param description The description for this matching rule. It may be
273 * {@code null} if there is no description.
274 * @param isObsolete Indicates whether this matching rule is declared
275 * obsolete.
276 * @param syntaxOID The syntax OID for this matching rule. It must not be
277 * {@code null}.
278 * @param extensions The set of extensions for this matching rule.
279 * It may be {@code null} or empty if there should not be
280 * any extensions.
281 */
282 public MatchingRuleDefinition(final String oid, final String[] names,
283 final String description,
284 final boolean isObsolete,
285 final String syntaxOID,
286 final Map<String,String[]> extensions)
287 {
288 ensureNotNull(oid, syntaxOID);
289
290 this.oid = oid;
291 this.description = description;
292 this.isObsolete = isObsolete;
293 this.syntaxOID = syntaxOID;
294
295 if (names == null)
296 {
297 this.names = NO_STRINGS;
298 }
299 else
300 {
301 this.names = names;
302 }
303
304 if (extensions == null)
305 {
306 this.extensions = Collections.emptyMap();
307 }
308 else
309 {
310 this.extensions = Collections.unmodifiableMap(extensions);
311 }
312
313 final StringBuilder buffer = new StringBuilder();
314 createDefinitionString(buffer);
315 matchingRuleString = buffer.toString();
316 }
317
318
319
320 /**
321 * Constructs a string representation of this matching rule definition in the
322 * provided buffer.
323 *
324 * @param buffer The buffer in which to construct a string representation of
325 * this matching rule definition.
326 */
327 private void createDefinitionString(final StringBuilder buffer)
328 {
329 buffer.append("( ");
330 buffer.append(oid);
331
332 if (names.length == 1)
333 {
334 buffer.append(" NAME '");
335 buffer.append(names[0]);
336 buffer.append('\'');
337 }
338 else if (names.length > 1)
339 {
340 buffer.append(" NAME (");
341 for (final String name : names)
342 {
343 buffer.append(" '");
344 buffer.append(name);
345 buffer.append('\'');
346 }
347 buffer.append(" )");
348 }
349
350 if (description != null)
351 {
352 buffer.append(" DESC '");
353 encodeValue(description, buffer);
354 buffer.append('\'');
355 }
356
357 if (isObsolete)
358 {
359 buffer.append(" OBSOLETE");
360 }
361
362 buffer.append(" SYNTAX ");
363 buffer.append(syntaxOID);
364
365 for (final Map.Entry<String,String[]> e : extensions.entrySet())
366 {
367 final String name = e.getKey();
368 final String[] values = e.getValue();
369 if (values.length == 1)
370 {
371 buffer.append(' ');
372 buffer.append(name);
373 buffer.append(" '");
374 encodeValue(values[0], buffer);
375 buffer.append('\'');
376 }
377 else
378 {
379 buffer.append(' ');
380 buffer.append(name);
381 buffer.append(" (");
382 for (final String value : values)
383 {
384 buffer.append(" '");
385 encodeValue(value, buffer);
386 buffer.append('\'');
387 }
388 buffer.append(" )");
389 }
390 }
391
392 buffer.append(" )");
393 }
394
395
396
397 /**
398 * Retrieves the OID for this matching rule.
399 *
400 * @return The OID for this matching rule.
401 */
402 public String getOID()
403 {
404 return oid;
405 }
406
407
408
409 /**
410 * Retrieves the set of names for this matching rule.
411 *
412 * @return The set of names for this matching rule, or an empty array if it
413 * does not have any names.
414 */
415 public String[] getNames()
416 {
417 return names;
418 }
419
420
421
422 /**
423 * Retrieves the primary name that can be used to reference this matching
424 * rule. If one or more names are defined, then the first name will be used.
425 * Otherwise, the OID will be returned.
426 *
427 * @return The primary name that can be used to reference this matching rule.
428 */
429 public String getNameOrOID()
430 {
431 if (names.length == 0)
432 {
433 return oid;
434 }
435 else
436 {
437 return names[0];
438 }
439 }
440
441
442
443 /**
444 * Indicates whether the provided string matches the OID or any of the names
445 * for this matching rule.
446 *
447 * @param s The string for which to make the determination. It must not be
448 * {@code null}.
449 *
450 * @return {@code true} if the provided string matches the OID or any of the
451 * names for this matching rule, or {@code false} if not.
452 */
453 public boolean hasNameOrOID(final String s)
454 {
455 for (final String name : names)
456 {
457 if (s.equalsIgnoreCase(name))
458 {
459 return true;
460 }
461 }
462
463 return s.equalsIgnoreCase(oid);
464 }
465
466
467
468 /**
469 * Retrieves the description for this matching rule, if available.
470 *
471 * @return The description for this matching rule, or {@code null} if there
472 * is no description defined.
473 */
474 public String getDescription()
475 {
476 return description;
477 }
478
479
480
481 /**
482 * Indicates whether this matching rule is declared obsolete.
483 *
484 * @return {@code true} if this matching rule is declared obsolete, or
485 * {@code false} if it is not.
486 */
487 public boolean isObsolete()
488 {
489 return isObsolete;
490 }
491
492
493
494 /**
495 * Retrieves the OID of the syntax for this matching rule.
496 *
497 * @return The OID of the syntax for this matching rule.
498 */
499 public String getSyntaxOID()
500 {
501 return syntaxOID;
502 }
503
504
505
506 /**
507 * Retrieves the set of extensions for this matching rule. They will be
508 * mapped from the extension name (which should start with "X-") to the set
509 * of values for that extension.
510 *
511 * @return The set of extensions for this matching rule.
512 */
513 public Map<String,String[]> getExtensions()
514 {
515 return extensions;
516 }
517
518
519
520 /**
521 * {@inheritDoc}
522 */
523 @Override()
524 public int hashCode()
525 {
526 return oid.hashCode();
527 }
528
529
530
531 /**
532 * {@inheritDoc}
533 */
534 @Override()
535 public boolean equals(final Object o)
536 {
537 if (o == null)
538 {
539 return false;
540 }
541
542 if (o == this)
543 {
544 return true;
545 }
546
547 if (! (o instanceof MatchingRuleDefinition))
548 {
549 return false;
550 }
551
552 final MatchingRuleDefinition d = (MatchingRuleDefinition) o;
553 return (oid.equals(d.oid) &&
554 syntaxOID.equals(d.syntaxOID) &&
555 stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
556 bothNullOrEqualIgnoreCase(description, d.description) &&
557 (isObsolete == d.isObsolete) &&
558 extensionsEqual(extensions, d.extensions));
559 }
560
561
562
563 /**
564 * Retrieves a string representation of this matching rule definition, in the
565 * format described in RFC 4512 section 4.1.3.
566 *
567 * @return A string representation of this matching rule definition.
568 */
569 @Override()
570 public String toString()
571 {
572 return matchingRuleString;
573 }
574 }