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