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 020import com.unboundid.scim.schema.AttributeDescriptor; 021import com.unboundid.scim.schema.CoreSchema; 022import com.unboundid.scim.schema.ResourceDescriptor; 023 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import static com.unboundid.scim.sdk.StaticUtils.toLowerCase; 034 035 036 037/** 038 * This class represents a System for Cross-Domain Identity Management (SCIM) 039 * object. A SCIM object may be composed of common schema attributes and a 040 * collection of attributes from one or more additional schema definitions. 041 * This class is not designed to be thread-safe. 042 */ 043public class SCIMObject 044{ 045 046 /** 047 * The set of attributes in this object grouped by the URI of the schema to 048 * which they belong. 049 */ 050 private final HashMap<String,LinkedHashMap<String,SCIMAttribute>> attributes; 051 052 053 054 /** 055 * Create an empty SCIM object that initially has no attributes. The type of 056 * resource is not specified. 057 */ 058 public SCIMObject() 059 { 060 this.attributes = 061 new HashMap<String, LinkedHashMap<String, SCIMAttribute>>(); 062 } 063 064 065 /** 066 * Create a new copy of the provided SCIM object. 067 * 068 * @param scimObject The SCIMObject to copy. 069 */ 070 public SCIMObject(final SCIMObject scimObject) 071 { 072 // Since SCIMAttribute is immutable, just copy the maps. 073 this.attributes = 074 new HashMap<String, LinkedHashMap<String, SCIMAttribute>>(); 075 for(Map.Entry<String, LinkedHashMap<String, SCIMAttribute>> entry : 076 scimObject.attributes.entrySet()) 077 { 078 this.attributes.put(entry.getKey(), 079 new LinkedHashMap<String, SCIMAttribute>(entry.getValue())); 080 } 081 } 082 083 084 085 /** 086 * Retrieves the set of schemas currently contributing attributes to this 087 * object. 088 * 089 * @return An immutable collection of the URIs of schemas currently 090 * contributing attributes to this object. 091 */ 092 public Set<String> getSchemas() 093 { 094 return Collections.unmodifiableSet(attributes.keySet()); 095 } 096 097 098 099 /** 100 * Determines whether this object contains any attributes in the specified 101 * schema. 102 * 103 * @param schema The URI of the schema for which to make the determination. 104 * It must not be {@code null}. 105 * 106 * @return {@code true} if this object contains any attributes in the 107 * specified schema, or {@code false} if not. 108 */ 109 public boolean hasSchema(final String schema) 110 { 111 return attributes.containsKey(toLowerCase(schema)); 112 } 113 114 115 116 /** 117 * Retrieves the attribute with the specified name. 118 * <p> 119 * Note that this method does not support retrieving sub-attributes directly; 120 * you can retrieve a SCIMAttribute and then iterate over its values to get 121 * the sub-attributes, if the values are complex. 122 * 123 * @param schema The URI of the schema containing the attribute to retrieve. 124 * 125 * @param name The name of the attribute to retrieve. It must not be 126 * {@code null}. 127 * 128 * @return The requested attribute from this object, or {@code null} if the 129 * specified attribute is not present in this object. 130 */ 131 public SCIMAttribute getAttribute(final String schema, final String name) 132 { 133 final LinkedHashMap<String,SCIMAttribute> attrs = 134 attributes.get(toLowerCase(schema)); 135 136 if (attrs == null) 137 { 138 return null; 139 } 140 else 141 { 142 return attrs.get(toLowerCase(name)); 143 } 144 } 145 146 147 148 /** 149 * Retrieves the set of attributes in this object from the specified schema. 150 * 151 * @param schema The URI of the schema whose attributes are to be retrieved. 152 * 153 * @return An immutable collection of the attributes in this object from the 154 * specified schema, or the empty collection if there are no such 155 * attributes. 156 */ 157 public Collection<SCIMAttribute> getAttributes(final String schema) 158 { 159 final LinkedHashMap<String, SCIMAttribute> attrs = 160 attributes.get(toLowerCase(schema)); 161 162 if (attrs == null) 163 { 164 return Collections.emptyList(); 165 } 166 else 167 { 168 return Collections.unmodifiableCollection(attrs.values()); 169 } 170 } 171 172 173 174 /** 175 * Determines whether this object contains the specified attribute or 176 * sub-attribute. 177 * 178 * @param schema The URI of the schema containing the attribute. 179 * @param name The name of the attribute or sub-attribute. 180 * Sub-attributes can be referenced with the standard 181 * attribute notation (i.e. "attribute.subattribute". 182 * This parameter must not be {@code null}. 183 * 184 * @return {@code true} if this object contains the specified attribute, or 185 * {@code false} if not. 186 */ 187 public boolean hasAttribute(final String schema, final String name) 188 { 189 final LinkedHashMap<String, SCIMAttribute> attrs = 190 attributes.get(toLowerCase(schema)); 191 192 AttributePath path = AttributePath.parse(name, schema); 193 String attrName = toLowerCase(path.getAttributeName()); 194 String subAttrName = path.getSubAttributeName(); 195 196 if (attrs != null && attrs.containsKey(attrName)) 197 { 198 if (subAttrName != null) 199 { 200 SCIMAttribute attr = attrs.get(attrName); 201 for (SCIMAttributeValue value : attr.getValues()) 202 { 203 if (value.isComplex() && value.hasAttribute(subAttrName)) 204 { 205 return true; 206 } 207 } 208 } 209 else 210 { 211 return true; 212 } 213 } 214 215 return false; 216 } 217 218 219 220 /** 221 * Adds the provided attribute to this object. If this object already contains 222 * an attribute with the same name from the same schema, then the provided 223 * attribute will not be added. 224 * 225 * @param attribute The attribute to be added. It must not be {@code null}. 226 * 227 * @return {@code true} if the object was updated, or {@code false} if the 228 * object already contained an attribute with the same name. 229 */ 230 public boolean addAttribute(final SCIMAttribute attribute) 231 { 232 final String lowerCaseSchema = toLowerCase(attribute.getSchema()); 233 final String lowerCaseName = toLowerCase(attribute.getName()); 234 235 LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema); 236 if (attrs == null) 237 { 238 attrs = new LinkedHashMap<String, SCIMAttribute>(); 239 attrs.put(lowerCaseName, attribute); 240 attributes.put(lowerCaseSchema, attrs); 241 return true; 242 } 243 else 244 { 245 if (attrs.containsKey(lowerCaseName)) 246 { 247 return false; 248 } 249 else 250 { 251 attrs.put(lowerCaseName, attribute); 252 return true; 253 } 254 } 255 } 256 257 258 259 /** 260 * Adds the provided attribute to this object, replacing any existing 261 * attribute with the same name. 262 * 263 * @param attribute The attribute to be added. It must not be {@code null}. 264 */ 265 public void setAttribute(final SCIMAttribute attribute) 266 { 267 final String lowerCaseSchema = toLowerCase(attribute.getSchema()); 268 final String lowerCaseName = toLowerCase(attribute.getName()); 269 270 LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema); 271 if (attrs == null) 272 { 273 attrs = new LinkedHashMap<String, SCIMAttribute>(); 274 attrs.put(lowerCaseName, attribute); 275 attributes.put(lowerCaseSchema, attrs); 276 } 277 else 278 { 279 attrs.put(lowerCaseName, attribute); 280 } 281 } 282 283 284 285 /** 286 * Removes the specified attribute or sub-attribute from this object. 287 * 288 * @param schema The URI of the schema to which the attribute belongs. 289 * @param name The name of the attribute or sub-attribute to remove. 290 * Sub-attributes can be referenced with the standard 291 * attribute notation (i.e. "attribute.subattribute". 292 * This parameter must not be {@code null}. 293 * 294 * @return {@code true} if the attribute was removed from the object, or 295 * {@code false} if it was not present. 296 */ 297 public boolean removeAttribute(final String schema, final String name) 298 { 299 final String lowerCaseSchema = toLowerCase(schema); 300 LinkedHashMap<String,SCIMAttribute> attrs = attributes.get(lowerCaseSchema); 301 if (attrs == null) 302 { 303 return false; 304 } 305 else 306 { 307 AttributePath path = AttributePath.parse(name, schema); 308 String attrName = toLowerCase(path.getAttributeName()); 309 String subAttrName = path.getSubAttributeName(); 310 311 boolean removed = false; 312 313 if (subAttrName != null) 314 { 315 //We are removing a sub-attribute 316 if (attrs.containsKey(attrName)) 317 { 318 SCIMAttribute attr = attrs.get(attrName); 319 List<SCIMAttributeValue> finalComplexValues = 320 new ArrayList<SCIMAttributeValue>(4); 321 322 for(SCIMAttributeValue value : attr.getValues()) 323 { 324 if(value.isComplex()) 325 { 326 Map<String, SCIMAttribute> subAttrMap = value.getAttributes(); 327 List<SCIMAttribute> attrList = new ArrayList<SCIMAttribute>(10); 328 329 //We need to keep track if only normative sub-attributes are left 330 //after the sub-attribute removal; if that is the case, then the 331 //entire attribute value should be removed since it no longer has 332 //a value. 333 boolean nonNormativeSubAttributeExists = false; 334 335 for(String n : subAttrMap.keySet()) 336 { 337 if(!n.equalsIgnoreCase(subAttrName)) 338 { 339 attrList.add(subAttrMap.get(n)); 340 341 if (!n.equals("type") && !n.equals("primary") && 342 !n.equals("operation") && !n.equals("display")) 343 { 344 nonNormativeSubAttributeExists = true; 345 } 346 } 347 else 348 { 349 removed = true; 350 } 351 } 352 353 if(!attrList.isEmpty() && nonNormativeSubAttributeExists) 354 { 355 SCIMAttributeValue newComplexValue = 356 SCIMAttributeValue.createComplexValue(attrList); 357 finalComplexValues.add(newComplexValue); 358 } 359 } 360 } 361 362 if (removed) 363 { 364 if(!finalComplexValues.isEmpty()) 365 { 366 SCIMAttribute finalAttr = SCIMAttribute.create( 367 attr.getAttributeDescriptor(), finalComplexValues.toArray( 368 new SCIMAttributeValue[finalComplexValues.size()])); 369 attrs.put(attrName, finalAttr); 370 } 371 else 372 { 373 //After removing the specified sub-attribute, there are no values 374 //left, so the entire attribute should be removed. 375 attrs.remove(attrName); 376 } 377 } 378 } 379 } 380 else 381 { 382 removed = (attrs.remove(attrName) != null); 383 } 384 385 if(removed && attrs.isEmpty()) 386 { 387 attributes.remove(lowerCaseSchema); 388 } 389 390 return removed; 391 } 392 } 393 394 395 396 /** 397 * Determine whether this object matches the provided filter parameters. 398 * 399 * @param filter The filter parameters to compare against the object. 400 * 401 * @return {@code true} if this object matches the provided filter, and 402 * {@code false} otherwise. 403 */ 404 public boolean matchesFilter(final SCIMFilter filter) 405 { 406 final SCIMFilterType type = filter.getFilterType(); 407 final List<SCIMFilter> components = filter.getFilterComponents(); 408 409 switch(type) 410 { 411 case AND: 412 for(SCIMFilter component : components) 413 { 414 if(!matchesFilter(component)) 415 { 416 return false; 417 } 418 } 419 return true; 420 case OR: 421 for(SCIMFilter component : components) 422 { 423 if(matchesFilter(component)) 424 { 425 return true; 426 } 427 } 428 return false; 429 } 430 431 final String schema = filter.getFilterAttribute().getAttributeSchema(); 432 final String attributeName = filter.getFilterAttribute().getAttributeName(); 433 434 final SCIMAttribute attribute = getAttribute(schema, attributeName); 435 return attribute != null && attribute.matchesFilter(filter); 436 437 } 438 439 440 441 /** 442 * Check this object for potential schema violations based on the provided 443 * resource descriptor. 444 * 445 * 446 * @param resourceDescriptor The ResourceDescriptor to check against. 447 * @param includeCommonAttributes Whether to enforce the schema for common 448 * attributes like id and meta. 449 * @throws InvalidResourceException If a schema violation is found. 450 */ 451 public void checkSchema(final ResourceDescriptor resourceDescriptor, 452 final boolean includeCommonAttributes) 453 throws InvalidResourceException 454 { 455 // Make sure all required attributes are present 456 for(String schema : resourceDescriptor.getAttributeSchemas()) 457 { 458 for(AttributeDescriptor attributeDescriptor : 459 resourceDescriptor.getAttributes(schema)) 460 { 461 if(!includeCommonAttributes && 462 (attributeDescriptor.equals(CoreSchema.ID_DESCRIPTOR) || 463 attributeDescriptor.equals(CoreSchema.META_DESCRIPTOR) || 464 attributeDescriptor.equals(CoreSchema.EXTERNAL_ID_DESCRIPTOR))) 465 { 466 continue; 467 } 468 469 if (attributeDescriptor.isReadOnly()) 470 { 471 continue; 472 } 473 474 SCIMAttribute attribute = 475 getAttribute(schema, attributeDescriptor.getName()); 476 if(attributeDescriptor.isRequired() && attribute == null) 477 { 478 throw new InvalidResourceException("Attribute '" + 479 schema + ":" + attributeDescriptor.getName() + 480 "' is required"); 481 } 482 483 Collection<AttributeDescriptor> subAttributes = 484 attributeDescriptor.getSubAttributes(); 485 if(subAttributes != null && attribute != null) 486 { 487 // Make sure all required sub-attributes are present as well 488 for(AttributeDescriptor subAttribute : subAttributes) 489 { 490 if(subAttribute.isRequired()) 491 { 492 if(attributeDescriptor.isMultiValued()) 493 { 494 for(SCIMAttributeValue value : attribute.getValues()) 495 { 496 if(!value.hasAttribute(subAttribute.getName())) 497 { 498 throw new InvalidResourceException("Sub-Attribute '" + 499 schema + ":" + attributeDescriptor.getName() + "." + 500 subAttribute.getName() + "' is required for all " + 501 "values of the multi-valued attribute"); 502 } 503 } 504 } 505 else 506 { 507 if(!attribute.getValue().hasAttribute(subAttribute.getName())) 508 { 509 throw new InvalidResourceException("Sub-Attribute '" + 510 schema + ":" + attributeDescriptor.getName() + "." + 511 subAttribute.getName() + "' is required"); 512 } 513 } 514 } 515 } 516 } 517 } 518 } 519 } 520 521 522 523 /** 524 * {@inheritDoc} 525 */ 526 @Override 527 public boolean equals(final Object o) { 528 if (this == o) { 529 return true; 530 } 531 if (o == null || getClass() != o.getClass()) { 532 return false; 533 } 534 535 SCIMObject that = (SCIMObject) o; 536 537 return attributes.equals(that.attributes); 538 539 } 540 541 542 /** 543 * {@inheritDoc} 544 */ 545 @Override 546 public int hashCode() { 547 return attributes.hashCode(); 548 } 549 550 551 /** 552 * {@inheritDoc} 553 */ 554 @Override 555 public String toString() { 556 return "SCIMObject{" + 557 "attributes=" + attributes + 558 '}'; 559 } 560}