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