001 /*
002 * Copyright 2011-2012 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018 package com.unboundid.scim.sdk;
019
020
021 import com.unboundid.scim.schema.AttributeDescriptor;
022
023 import java.util.Arrays;
024 import java.util.Collection;
025 import java.util.Date;
026 import java.util.List;
027
028 import javax.xml.bind.DatatypeConverter;
029
030
031 /**
032 * This class represents a Simple Cloud Identity Management (SCIM) attribute.
033 * Attributes are categorized as either single-valued or multi-valued. This
034 * class allows for the following kinds of attributes.
035 *
036 * <ol>
037 * <li>Simple type (String, Boolean, DateTime, Integer or Binary).
038 * An example is the 'userName' attribute in the core schema.</li>
039 *
040 * <li>Complex type. An example is the 'name' attribute in the core
041 * schema.</li>
042 *
043 * <li>Multi-valued simple type. Represented using multi-valued complex values,
044 * because the values may have 'type' and 'primary' sub-attributes to
045 * distinguish each primitive value. Examples of this are the 'emails' and
046 * 'photos' attributes in the core schema.</li>
047 *
048 * <li>Multi-valued complex type. An examples is the 'addresses' attribute in
049 * the core schema.</li>
050 * </ol>
051 *
052 */
053 public final class SCIMAttribute
054 {
055 /**
056 * The mapping descriptor of this attribute.
057 */
058 private final AttributeDescriptor attributeDescriptor;
059
060 /**
061 * The value(s) of this attribute.
062 */
063 private final SCIMAttributeValue[] values;
064
065
066 /**
067 * Create a new instance of an attribute.
068 *
069 * @param descriptor The mapping descriptor of this value.
070 * @param values The value(s) of this attribute.
071 */
072 private SCIMAttribute(final AttributeDescriptor descriptor,
073 final SCIMAttributeValue ... values)
074 {
075 this.attributeDescriptor = descriptor;
076 this.values = values;
077 }
078
079
080
081 /**
082 * Create an attribute.
083 *
084 * @param descriptor The mapping descriptor for this attribute.
085 * @param values The value(s) of this attribute.
086 *
087 * @return A new attribute.
088 */
089 public static SCIMAttribute create(
090 final AttributeDescriptor descriptor, final SCIMAttributeValue... values)
091 {
092 return new SCIMAttribute(descriptor, values);
093 }
094
095
096
097 /**
098 * Retrieve the name of the schema to which this attribute belongs.
099 *
100 * @return The name of the schema to which this attribute belongs.
101 */
102 public String getSchema()
103 {
104 return this.attributeDescriptor.getSchema();
105 }
106
107
108
109 /**
110 * Retrieve the name of this attribute. The name does not indicate which
111 * schema the attribute belongs to.
112 *
113 * @return The name of this attribute.
114 */
115 public String getName()
116 {
117 return this.attributeDescriptor.getName();
118 }
119
120
121
122 /**
123 * Retrieves the value of this attribute. This method should only be
124 * called if the attribute is single valued.
125 *
126 * @return The value of this attribute.
127 */
128 public SCIMAttributeValue getValue()
129 {
130 return values[0];
131 }
132
133
134
135 /**
136 * Retrieves the values of this attribute. This method should only be
137 * called if the attribute is multi-valued.
138 *
139 * @return The values of this attribute.
140 */
141 public SCIMAttributeValue[] getValues()
142 {
143 return values;
144 }
145
146 /**
147 * Retrieves the SCIM attribute mapping of this this attribute.
148 *
149 * @return The attribute descriptor
150 */
151 public AttributeDescriptor getAttributeDescriptor() {
152 return attributeDescriptor;
153 }
154
155
156
157 /**
158 * Determine whether this attribute matches the provided filter parameters.
159 *
160 * @param filter The filter parameters to be compared against this attribute.
161 *
162 * @return {@code true} if this attribute matches the provided filter, and
163 * {@code false} otherwise.
164 */
165 public boolean matchesFilter(final SCIMFilter filter)
166 {
167 final SCIMFilterType type = filter.getFilterType();
168 final List<SCIMFilter> components = filter.getFilterComponents();
169
170 switch(type)
171 {
172 case AND:
173 for(SCIMFilter component : components)
174 {
175 if(!matchesFilter(component))
176 {
177 return false;
178 }
179 }
180 return true;
181 case OR:
182 for(SCIMFilter component : components)
183 {
184 if(matchesFilter(component))
185 {
186 return true;
187 }
188 }
189 return false;
190 }
191
192 final String schema = filter.getFilterAttribute().getAttributeSchema();
193 if (!schema.equalsIgnoreCase(getSchema()))
194 {
195 return false;
196 }
197
198 final String attributeName = filter.getFilterAttribute().getAttributeName();
199 String subAttributeName =
200 filter.getFilterAttribute().getSubAttributeName();
201 if (subAttributeName == null)
202 {
203 subAttributeName = "value";
204 }
205
206 if (!attributeName.equalsIgnoreCase(getName()))
207 {
208 return false;
209 }
210
211 if (attributeDescriptor.isMultiValued())
212 {
213 for (final SCIMAttributeValue v : getValues())
214 {
215 if (v.isComplex())
216 {
217 final Collection<AttributeDescriptor> descriptors =
218 attributeDescriptor.getSubAttributes();
219 for (AttributeDescriptor descriptor : descriptors)
220 {
221 final SCIMAttribute a = v.getAttribute(descriptor.getName());
222
223 if (a != null)
224 {
225 // This is done because the client specifies 'emails' rather
226 // than 'emails.email'.
227 final AttributePath childPath =
228 new AttributePath(schema, a.getName(), subAttributeName);
229 if (a.matchesFilter(new SCIMFilter(type,
230 childPath,
231 filter.getFilterValue(),
232 filter.isQuoteFilterValue(),
233 filter.getFilterComponents())))
234 {
235 return true;
236 }
237 }
238 }
239 }
240 }
241 }
242 else
243 {
244 final SCIMAttributeValue v = getValue();
245 if (v.isComplex())
246 {
247 if (subAttributeName != null)
248 {
249 final SCIMAttribute a = v.getAttribute(subAttributeName);
250 if (a != null)
251 {
252 final AttributePath childPath =
253 new AttributePath(schema, subAttributeName, null);
254 return a.matchesFilter(
255 new SCIMFilter(type,
256 childPath,
257 filter.getFilterValue(),
258 filter.isQuoteFilterValue(),
259 filter.getFilterComponents()));
260 }
261 }
262 }
263 else
264 {
265 if (type == SCIMFilterType.PRESENCE)
266 {
267 return true;
268 }
269
270 final AttributeDescriptor.DataType dataType =
271 attributeDescriptor.getDataType();
272
273 String stringValue = null;
274 Double doubleValue = null;
275 Long longValue = null;
276 Date dateValue = null;
277 Boolean boolValue = null;
278 byte[] binValue = null;
279
280 switch(dataType)
281 {
282 case BINARY:
283 binValue = v.getBinaryValue();
284 if(binValue == null)
285 {
286 return false;
287 }
288 break;
289 case BOOLEAN:
290 boolValue = v.getBooleanValue();
291 if(boolValue == null)
292 {
293 return false;
294 }
295 break;
296 case DATETIME:
297 dateValue = v.getDateValue();
298 if(dateValue == null)
299 {
300 return false;
301 }
302 break;
303 case DECIMAL:
304 doubleValue = v.getDecimalValue();
305 if(doubleValue == null)
306 {
307 return false;
308 }
309 break;
310 case INTEGER:
311 longValue = v.getIntegerValue();
312 if(longValue == null)
313 {
314 return false;
315 }
316 break;
317 case STRING:
318 stringValue = v.getStringValue();
319 if(stringValue == null)
320 {
321 return false;
322 }
323 break;
324 default:
325 throw new RuntimeException(
326 "Invalid attribute data type: " + dataType);
327 }
328
329 final String filterValue = filter.getFilterValue();
330
331 // TODO support caseExact attributes
332
333 //Note: The code below explicitly unboxes the objects before comparing
334 // to avoid auto-unboxing and make it clear that it is just
335 // primitives being compared.
336 switch (type)
337 {
338 case EQUALITY:
339 if(stringValue != null)
340 {
341 return stringValue.equalsIgnoreCase(filterValue);
342 }
343 else if(doubleValue != null)
344 {
345 try
346 {
347 double filterValueDouble = Double.parseDouble(filterValue);
348 return doubleValue.doubleValue() == filterValueDouble;
349 }
350 catch(NumberFormatException e)
351 {
352 return false;
353 }
354 }
355 else if(longValue != null)
356 {
357 try
358 {
359 long filterValueLong = Long.parseLong(filterValue);
360 return longValue.longValue() == filterValueLong;
361 }
362 catch(NumberFormatException e)
363 {
364 return false;
365 }
366 }
367 else if(boolValue != null)
368 {
369 return boolValue.booleanValue() ==
370 Boolean.parseBoolean(filterValue);
371 }
372 else if(dateValue != null)
373 {
374 try
375 {
376 SimpleValue filterValueDate = new SimpleValue(filterValue);
377 return dateValue.equals(filterValueDate.getDateValue());
378 }
379 catch(IllegalArgumentException e)
380 {
381 return false;
382 }
383 }
384 else if(binValue != null)
385 {
386 //TODO: It's debatable whether this ought to just check whether
387 // the base-64 encoded string is equal, rather than checking
388 // if the bytes are equal. This seems more correct.
389 try
390 {
391 byte[] filterValueBytes =
392 DatatypeConverter.parseBase64Binary(filterValue);
393 return Arrays.equals(binValue, filterValueBytes);
394 }
395 catch(IllegalArgumentException e)
396 {
397 return false;
398 }
399 }
400 return false;
401 case CONTAINS:
402 if(stringValue != null)
403 {
404 return StaticUtils.toLowerCase(stringValue).contains(
405 StaticUtils.toLowerCase(filterValue));
406 }
407 else if(doubleValue != null)
408 {
409 try
410 {
411 double filterValueDouble = Double.parseDouble(filterValue);
412 return doubleValue.doubleValue() == filterValueDouble;
413 }
414 catch(NumberFormatException e)
415 {
416 return false;
417 }
418 }
419 else if(longValue != null)
420 {
421 try
422 {
423 long filterValueLong = Long.parseLong(filterValue);
424 return longValue.longValue() == filterValueLong;
425 }
426 catch(NumberFormatException e)
427 {
428 return false;
429 }
430 }
431 else if(boolValue != null)
432 {
433 return boolValue.booleanValue() ==
434 Boolean.parseBoolean(filterValue);
435 }
436 else if(dateValue != null)
437 {
438 try
439 {
440 SimpleValue filterValueDate = new SimpleValue(filterValue);
441 return dateValue.equals(filterValueDate.getDateValue());
442 }
443 catch(IllegalArgumentException e)
444 {
445 return false;
446 }
447 }
448 else if(binValue != null)
449 {
450 try
451 {
452 byte[] filterValueBytes =
453 DatatypeConverter.parseBase64Binary(filterValue);
454 return Arrays.equals(binValue, filterValueBytes);
455 }
456 catch(IllegalArgumentException e)
457 {
458 return false;
459 }
460 }
461 return false;
462 case STARTS_WITH:
463 if(stringValue != null)
464 {
465 return StaticUtils.toLowerCase(stringValue).startsWith(
466 StaticUtils.toLowerCase(filterValue));
467 }
468 else if(doubleValue != null)
469 {
470 return false;
471 }
472 else if(longValue != null)
473 {
474 return false;
475 }
476 else if(boolValue != null)
477 {
478 return false;
479 }
480 else if(dateValue != null)
481 {
482 return false;
483 }
484 else if(binValue != null)
485 {
486 return false;
487 }
488 return false;
489 case GREATER_THAN:
490 if(stringValue != null)
491 {
492 return stringValue.compareToIgnoreCase(filterValue) > 0;
493 }
494 else if(doubleValue != null)
495 {
496 try
497 {
498 double filterValueDouble = Double.parseDouble(filterValue);
499 return doubleValue.doubleValue() > filterValueDouble;
500 }
501 catch(NumberFormatException e)
502 {
503 return false;
504 }
505 }
506 else if(longValue != null)
507 {
508 try
509 {
510 long filterValueLong = Long.parseLong(filterValue);
511 return longValue.longValue() > filterValueLong;
512 }
513 catch(NumberFormatException e)
514 {
515 return false;
516 }
517 }
518 else if(boolValue != null)
519 {
520 return false;
521 }
522 else if(dateValue != null)
523 {
524 try
525 {
526 SimpleValue filterValueDate = new SimpleValue(filterValue);
527 return dateValue.after(filterValueDate.getDateValue());
528 }
529 catch(IllegalArgumentException e)
530 {
531 return false;
532 }
533 }
534 else if(binValue != null)
535 {
536 return false;
537 }
538 return false;
539 case GREATER_OR_EQUAL:
540 if(stringValue != null)
541 {
542 return stringValue.compareToIgnoreCase(filterValue) >= 0;
543 }
544 else if(doubleValue != null)
545 {
546 try
547 {
548 double filterValueDouble = Double.parseDouble(filterValue);
549 return doubleValue.doubleValue() >= filterValueDouble;
550 }
551 catch(NumberFormatException e)
552 {
553 return false;
554 }
555 }
556 else if(longValue != null)
557 {
558 try
559 {
560 long filterValueLong = Long.parseLong(filterValue);
561 return longValue.longValue() >= filterValueLong;
562 }
563 catch(NumberFormatException e)
564 {
565 return false;
566 }
567 }
568 else if(boolValue != null)
569 {
570 return false;
571 }
572 else if(dateValue != null)
573 {
574 try
575 {
576 SimpleValue filterValueDate = new SimpleValue(filterValue);
577 return dateValue.after(filterValueDate.getDateValue()) ||
578 dateValue.equals(filterValueDate.getDateValue());
579 }
580 catch(IllegalArgumentException e)
581 {
582 return false;
583 }
584 }
585 else if(binValue != null)
586 {
587 return false;
588 }
589 return false;
590 case LESS_THAN:
591 if(stringValue != null)
592 {
593 return stringValue.compareToIgnoreCase(filterValue) < 0;
594 }
595 else if(doubleValue != null)
596 {
597 try
598 {
599 double filterValueDouble = Double.parseDouble(filterValue);
600 return doubleValue.doubleValue() < filterValueDouble;
601 }
602 catch(NumberFormatException e)
603 {
604 return false;
605 }
606 }
607 else if(longValue != null)
608 {
609 try
610 {
611 long filterValueLong = Long.parseLong(filterValue);
612 return longValue.longValue() < filterValueLong;
613 }
614 catch(NumberFormatException e)
615 {
616 return false;
617 }
618 }
619 else if(boolValue != null)
620 {
621 return false;
622 }
623 else if(dateValue != null)
624 {
625 try
626 {
627 SimpleValue filterValueDate = new SimpleValue(filterValue);
628 return dateValue.before(filterValueDate.getDateValue());
629 }
630 catch(IllegalArgumentException e)
631 {
632 return false;
633 }
634 }
635 else if(binValue != null)
636 {
637 return false;
638 }
639 return false;
640 case LESS_OR_EQUAL:
641 if(stringValue != null)
642 {
643 return stringValue.compareToIgnoreCase(filterValue) <= 0;
644 }
645 else if(doubleValue != null)
646 {
647 try
648 {
649 double filterValueDouble = Double.parseDouble(filterValue);
650 return doubleValue.doubleValue() <= filterValueDouble;
651 }
652 catch(NumberFormatException e)
653 {
654 return false;
655 }
656 }
657 else if(longValue != null)
658 {
659 try
660 {
661 long filterValueLong = Long.parseLong(filterValue);
662 return longValue.longValue() <= filterValueLong;
663 }
664 catch(NumberFormatException e)
665 {
666 return false;
667 }
668 }
669 else if(boolValue != null)
670 {
671 return false;
672 }
673 else if(dateValue != null)
674 {
675 try
676 {
677 SimpleValue filterValueDate = new SimpleValue(filterValue);
678 return dateValue.before(filterValueDate.getDateValue()) ||
679 dateValue.equals(filterValueDate.getDateValue());
680 }
681 catch(IllegalArgumentException e)
682 {
683 return false;
684 }
685 }
686 else if(binValue != null)
687 {
688 return false;
689 }
690 return false;
691 }
692 }
693 }
694
695 return false;
696 }
697
698 @Override
699 public boolean equals(final Object o) {
700 if (this == o) {
701 return true;
702 }
703 if (o == null || getClass() != o.getClass()) {
704 return false;
705 }
706
707 SCIMAttribute that = (SCIMAttribute) o;
708
709 return attributeDescriptor.equals(that.attributeDescriptor) &&
710 Arrays.equals(values, that.values);
711 }
712
713 @Override
714 public int hashCode() {
715 int result = attributeDescriptor.hashCode();
716 result = 31 * result + (values != null ?
717 Arrays.hashCode(values) : 0);
718 return result;
719 }
720
721 @Override
722 public String toString()
723 {
724 final StringBuilder sb = new StringBuilder();
725 sb.append("SCIMAttribute");
726 sb.append("{attribute=").append(attributeDescriptor.getSchema());
727 sb.append(SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE);
728 sb.append(attributeDescriptor.getName());
729 sb.append(", values=").append(values == null ? "null" :
730 Arrays.asList(values).toString());
731 sb.append('}');
732 return sb.toString();
733 }
734 }