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