001 /*
002 * Copyright 2009-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2009-2014 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.matchingrules;
022
023
024
025 import java.util.ArrayList;
026 import java.util.Collections;
027 import java.util.Iterator;
028 import java.util.List;
029
030 import com.unboundid.asn1.ASN1OctetString;
031 import com.unboundid.ldap.sdk.LDAPException;
032 import com.unboundid.ldap.sdk.ResultCode;
033 import com.unboundid.util.ThreadSafety;
034 import com.unboundid.util.ThreadSafetyLevel;
035
036 import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*;
037 import static com.unboundid.util.Debug.*;
038 import static com.unboundid.util.StaticUtils.*;
039
040
041
042 /**
043 * This class provides an implementation of a matching rule that may be used to
044 * process values containing lists of items, in which each item is separated by
045 * a dollar sign ($) character. Substring matching is also supported, but
046 * ordering matching is not.
047 */
048 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
049 public final class CaseIgnoreListMatchingRule
050 extends MatchingRule
051 {
052 /**
053 * The singleton instance that will be returned from the {@code getInstance}
054 * method.
055 */
056 private static final CaseIgnoreListMatchingRule INSTANCE =
057 new CaseIgnoreListMatchingRule();
058
059
060
061 /**
062 * The name for the caseIgnoreListMatch equality matching rule.
063 */
064 public static final String EQUALITY_RULE_NAME = "caseIgnoreListMatch";
065
066
067
068 /**
069 * The name for the caseIgnoreListMatch equality matching rule, formatted in
070 * all lowercase characters.
071 */
072 static final String LOWER_EQUALITY_RULE_NAME =
073 toLowerCase(EQUALITY_RULE_NAME);
074
075
076
077 /**
078 * The OID for the caseIgnoreListMatch equality matching rule.
079 */
080 public static final String EQUALITY_RULE_OID = "2.5.13.11";
081
082
083
084 /**
085 * The name for the caseIgnoreListSubstringsMatch substring matching rule.
086 */
087 public static final String SUBSTRING_RULE_NAME =
088 "caseIgnoreListSubstringsMatch";
089
090
091
092 /**
093 * The name for the caseIgnoreListSubstringsMatch substring matching rule,
094 * formatted in all lowercase characters.
095 */
096 static final String LOWER_SUBSTRING_RULE_NAME =
097 toLowerCase(SUBSTRING_RULE_NAME);
098
099
100
101 /**
102 * The OID for the caseIgnoreListSubstringsMatch substring matching rule.
103 */
104 public static final String SUBSTRING_RULE_OID = "2.5.13.12";
105
106
107
108 /**
109 * The serial version UID for this serializable class.
110 */
111 private static final long serialVersionUID = 7795143670808983466L;
112
113
114
115 /**
116 * Creates a new instance of this case-ignore list matching rule.
117 */
118 public CaseIgnoreListMatchingRule()
119 {
120 // No implementation is required.
121 }
122
123
124
125 /**
126 * Retrieves a singleton instance of this matching rule.
127 *
128 * @return A singleton instance of this matching rule.
129 */
130 public static CaseIgnoreListMatchingRule getInstance()
131 {
132 return INSTANCE;
133 }
134
135
136
137 /**
138 * {@inheritDoc}
139 */
140 @Override()
141 public String getEqualityMatchingRuleName()
142 {
143 return EQUALITY_RULE_NAME;
144 }
145
146
147
148 /**
149 * {@inheritDoc}
150 */
151 @Override()
152 public String getEqualityMatchingRuleOID()
153 {
154 return EQUALITY_RULE_OID;
155 }
156
157
158
159 /**
160 * {@inheritDoc}
161 */
162 @Override()
163 public String getOrderingMatchingRuleName()
164 {
165 return null;
166 }
167
168
169
170 /**
171 * {@inheritDoc}
172 */
173 @Override()
174 public String getOrderingMatchingRuleOID()
175 {
176 return null;
177 }
178
179
180
181 /**
182 * {@inheritDoc}
183 */
184 @Override()
185 public String getSubstringMatchingRuleName()
186 {
187 return SUBSTRING_RULE_NAME;
188 }
189
190
191
192 /**
193 * {@inheritDoc}
194 */
195 @Override()
196 public String getSubstringMatchingRuleOID()
197 {
198 return SUBSTRING_RULE_OID;
199 }
200
201
202
203 /**
204 * {@inheritDoc}
205 */
206 @Override()
207 public boolean valuesMatch(final ASN1OctetString value1,
208 final ASN1OctetString value2)
209 throws LDAPException
210 {
211 return normalize(value1).equals(normalize(value2));
212 }
213
214
215
216 /**
217 * {@inheritDoc}
218 */
219 @Override()
220 public boolean matchesSubstring(final ASN1OctetString value,
221 final ASN1OctetString subInitial,
222 final ASN1OctetString[] subAny,
223 final ASN1OctetString subFinal)
224 throws LDAPException
225 {
226 String normStr = normalize(value).stringValue();
227
228 if (subInitial != null)
229 {
230 final String normSubInitial = normalizeSubstring(subInitial,
231 SUBSTRING_TYPE_SUBINITIAL).stringValue();
232 if (normSubInitial.indexOf('$') >= 0)
233 {
234 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
235 ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
236 normSubInitial));
237 }
238
239 if (! normStr.startsWith(normSubInitial))
240 {
241 return false;
242 }
243
244 normStr = normStr.substring(normSubInitial.length());
245 }
246
247 if (subFinal != null)
248 {
249 final String normSubFinal = normalizeSubstring(subFinal,
250 SUBSTRING_TYPE_SUBFINAL).stringValue();
251 if (normSubFinal.indexOf('$') >= 0)
252 {
253 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
254 ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
255 normSubFinal));
256 }
257
258 if (! normStr.endsWith(normSubFinal))
259 {
260
261 return false;
262 }
263
264 normStr = normStr.substring(0, normStr.length() - normSubFinal.length());
265 }
266
267 if (subAny != null)
268 {
269 for (final ASN1OctetString s : subAny)
270 {
271 final String normSubAny =
272 normalizeSubstring(s, SUBSTRING_TYPE_SUBANY).stringValue();
273 if (normSubAny.indexOf('$') >= 0)
274 {
275 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
276 ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
277 normSubAny));
278 }
279
280 final int pos = normStr.indexOf(normSubAny);
281 if (pos < 0)
282 {
283 return false;
284 }
285
286 normStr = normStr.substring(pos + normSubAny.length());
287 }
288 }
289
290 return true;
291 }
292
293
294
295 /**
296 * {@inheritDoc}
297 */
298 @Override()
299 public int compareValues(final ASN1OctetString value1,
300 final ASN1OctetString value2)
301 throws LDAPException
302 {
303 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
304 ERR_CASE_IGNORE_LIST_ORDERING_MATCHING_NOT_SUPPORTED.get());
305 }
306
307
308
309 /**
310 * {@inheritDoc}
311 */
312 @Override()
313 public ASN1OctetString normalize(final ASN1OctetString value)
314 throws LDAPException
315 {
316 final List<String> items = getLowercaseItems(value);
317 final Iterator<String> iterator = items.iterator();
318
319 final StringBuilder buffer = new StringBuilder();
320 while (iterator.hasNext())
321 {
322 normalizeItem(buffer, iterator.next());
323 if (iterator.hasNext())
324 {
325 buffer.append('$');
326 }
327 }
328
329 return new ASN1OctetString(buffer.toString());
330 }
331
332
333
334 /**
335 * {@inheritDoc}
336 */
337 @Override()
338 public ASN1OctetString normalizeSubstring(final ASN1OctetString value,
339 final byte substringType)
340 throws LDAPException
341 {
342 return CaseIgnoreStringMatchingRule.getInstance().normalizeSubstring(value,
343 substringType);
344 }
345
346
347
348 /**
349 * Retrieves a list of the items contained in the provided value. The items
350 * will use the case of the provided value.
351 *
352 * @param value The value for which to obtain the list of items. It must
353 * not be {@code null}.
354 *
355 * @return An unmodifiable list of the items contained in the provided value.
356 *
357 * @throws LDAPException If the provided value does not represent a valid
358 * list in accordance with this matching rule.
359 */
360 public static List<String> getItems(final ASN1OctetString value)
361 throws LDAPException
362 {
363 return getItems(value.stringValue());
364 }
365
366
367
368 /**
369 * Retrieves a list of the items contained in the provided value. The items
370 * will use the case of the provided value.
371 *
372 * @param value The value for which to obtain the list of items. It must
373 * not be {@code null}.
374 *
375 * @return An unmodifiable list of the items contained in the provided value.
376 *
377 * @throws LDAPException If the provided value does not represent a valid
378 * list in accordance with this matching rule.
379 */
380 public static List<String> getItems(final String value)
381 throws LDAPException
382 {
383 final ArrayList<String> items = new ArrayList<String>(10);
384
385 final int length = value.length();
386 final StringBuilder buffer = new StringBuilder();
387 for (int i=0; i < length; i++)
388 {
389 final char c = value.charAt(i);
390 if (c == '\\')
391 {
392 try
393 {
394 buffer.append(decodeHexChar(value, i+1));
395 i += 2;
396 }
397 catch (Exception e)
398 {
399 debugException(e);
400 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
401 ERR_CASE_IGNORE_LIST_MALFORMED_HEX_CHAR.get(value), e);
402 }
403 }
404 else if (c == '$')
405 {
406 final String s = buffer.toString().trim();
407 if (s.length() == 0)
408 {
409 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
410 ERR_CASE_IGNORE_LIST_EMPTY_ITEM.get(value));
411 }
412
413 items.add(s);
414 buffer.delete(0, buffer.length());
415 }
416 else
417 {
418 buffer.append(c);
419 }
420 }
421
422 final String s = buffer.toString().trim();
423 if (s.length() == 0)
424 {
425 if (items.isEmpty())
426 {
427 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
428 ERR_CASE_IGNORE_LIST_EMPTY_LIST.get(value));
429 }
430 else
431 {
432 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
433 ERR_CASE_IGNORE_LIST_EMPTY_ITEM.get(value));
434 }
435 }
436 items.add(s);
437
438 return Collections.unmodifiableList(items);
439 }
440
441
442
443 /**
444 * Retrieves a list of the lowercase representations of the items contained in
445 * the provided value.
446 *
447 * @param value The value for which to obtain the list of items. It must
448 * not be {@code null}.
449 *
450 * @return An unmodifiable list of the items contained in the provided value.
451 *
452 * @throws LDAPException If the provided value does not represent a valid
453 * list in accordance with this matching rule.
454 */
455 public static List<String> getLowercaseItems(final ASN1OctetString value)
456 throws LDAPException
457 {
458 return getLowercaseItems(value.stringValue());
459 }
460
461
462
463 /**
464 * Retrieves a list of the lowercase representations of the items contained in
465 * the provided value.
466 *
467 * @param value The value for which to obtain the list of items. It must
468 * not be {@code null}.
469 *
470 * @return An unmodifiable list of the items contained in the provided value.
471 *
472 * @throws LDAPException If the provided value does not represent a valid
473 * list in accordance with this matching rule.
474 */
475 public static List<String> getLowercaseItems(final String value)
476 throws LDAPException
477 {
478 return getItems(toLowerCase(value));
479 }
480
481
482
483 /**
484 * Normalizes the provided list item.
485 *
486 * @param buffer The buffer to which to append the normalized representation
487 * of the given item.
488 * @param item The item to be normalized. It must already be trimmed and
489 * all characters converted to lowercase.
490 */
491 static void normalizeItem(final StringBuilder buffer, final String item)
492 {
493 final int length = item.length();
494
495 boolean lastWasSpace = false;
496 for (int i=0; i < length; i++)
497 {
498 final char c = item.charAt(i);
499 if (c == '\\')
500 {
501 buffer.append("\\5c");
502 lastWasSpace = false;
503 }
504 else if (c == '$')
505 {
506 buffer.append("\\24");
507 lastWasSpace = false;
508 }
509 else if (c == ' ')
510 {
511 if (! lastWasSpace)
512 {
513 buffer.append(' ');
514 lastWasSpace = true;
515 }
516 }
517 else
518 {
519 buffer.append(c);
520 lastWasSpace = false;
521 }
522 }
523 }
524
525
526
527 /**
528 * Reads two characters from the specified position in the provided string and
529 * returns the character that they represent.
530 *
531 * @param s The string from which to take the hex characters.
532 * @param p The position at which the hex characters begin.
533 *
534 * @return The character that was read and decoded.
535 *
536 * @throws LDAPException If either of the characters are not hexadecimal
537 * digits.
538 */
539 static char decodeHexChar(final String s, final int p)
540 throws LDAPException
541 {
542 char c = 0;
543
544 for (int i=0, j=p; (i < 2); i++,j++)
545 {
546 c <<= 4;
547
548 switch (s.charAt(j))
549 {
550 case '0':
551 break;
552 case '1':
553 c |= 0x01;
554 break;
555 case '2':
556 c |= 0x02;
557 break;
558 case '3':
559 c |= 0x03;
560 break;
561 case '4':
562 c |= 0x04;
563 break;
564 case '5':
565 c |= 0x05;
566 break;
567 case '6':
568 c |= 0x06;
569 break;
570 case '7':
571 c |= 0x07;
572 break;
573 case '8':
574 c |= 0x08;
575 break;
576 case '9':
577 c |= 0x09;
578 break;
579 case 'a':
580 case 'A':
581 c |= 0x0A;
582 break;
583 case 'b':
584 case 'B':
585 c |= 0x0B;
586 break;
587 case 'c':
588 case 'C':
589 c |= 0x0C;
590 break;
591 case 'd':
592 case 'D':
593 c |= 0x0D;
594 break;
595 case 'e':
596 case 'E':
597 c |= 0x0E;
598 break;
599 case 'f':
600 case 'F':
601 c |= 0x0F;
602 break;
603 default:
604 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
605 ERR_CASE_IGNORE_LIST_NOT_HEX_DIGIT.get(s.charAt(j)));
606 }
607 }
608
609 return c;
610 }
611 }