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.schema; 019 020import com.unboundid.scim.data.AttributeValueResolver; 021import com.unboundid.scim.data.BaseResource; 022import com.unboundid.scim.data.ResourceFactory; 023import com.unboundid.scim.sdk.InvalidResourceException; 024import com.unboundid.scim.sdk.SCIMConstants; 025import com.unboundid.scim.sdk.SCIMObject; 026 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035 036import static com.unboundid.scim.sdk.StaticUtils.toLowerCase; 037 038 039 040/** 041 * This class provides methods that describe the schema for a SCIM resource. It 042 * may be used to help read and write SCIM objects in their external XML and 043 * JSON representation, and to convert SCIM objects to and from LDAP entries. 044 */ 045public class ResourceDescriptor extends BaseResource 046{ 047 /** 048 * A <code>ResourceFactory</code> for creating <code>ResourceDescriptor</code> 049 * instances. 050 */ 051 public static final ResourceFactory<ResourceDescriptor> 052 RESOURCE_DESCRIPTOR_FACTORY = new ResourceFactory<ResourceDescriptor>() { 053 /** 054 * {@inheritDoc} 055 */ 056 public ResourceDescriptor createResource( 057 final ResourceDescriptor resourceDescriptor, 058 final SCIMObject scimObject) { 059 return new ResourceDescriptor(resourceDescriptor, scimObject); 060 } 061 }; 062 063 /** 064 * A schema -> name -> AttributeDescriptor map to quickly look up 065 * attributes. The attribute descriptors are keyed by the lower case 066 * attribute name because attribute names are case-insensitive. Likewise, 067 * the schema key is lower case because schema URNs are case-insensitive. 068 */ 069 private volatile Map<String,Map<String,AttributeDescriptor>> attributesCache; 070 071 /** 072 * Whether to use "strict mode" when looking up an attribute 073 * that doesn't exist in the attributesCache. Changed to false for better 074 * interoperability with SCIM implementations. 075 */ 076 private boolean strictMode = false; 077 078 /** 079 * Constructs a new ResourceDescriptor from a existing SCIMObject. 080 * 081 * @param resourceDescriptor The Resource Schema descriptor. 082 * @param scimObject The SCIMObject containing the schema. 083 */ 084 ResourceDescriptor(final ResourceDescriptor resourceDescriptor, 085 final SCIMObject scimObject) { 086 super(resourceDescriptor, scimObject); 087 } 088 089 /** 090 * Constructs a new empty ResourceDescriptor. 091 * 092 * @param resourceDescriptor The Resource Schema descriptor. 093 */ 094 private ResourceDescriptor(final ResourceDescriptor resourceDescriptor) { 095 super(resourceDescriptor); 096 } 097 098 /** 099 * Retrieves the attribute descriptor for a specified attribute. 100 * 101 * @param schema The attribute descriptor's associated schema URN. 102 * @param name The name of the attribute whose descriptor is to be retrieved. 103 * 104 * @return The attribute descriptor for the specified attribute. 105 * @throws InvalidResourceException if there is no such attribute. 106 */ 107 public AttributeDescriptor getAttribute(final String schema, 108 final String name) 109 throws InvalidResourceException 110 { 111 final Map<String, Map<String, AttributeDescriptor>> attrCache = 112 getAttributesCache(); 113 AttributeDescriptor attributeDescriptor = null; 114 Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema)); 115 if(map != null) 116 { 117 attributeDescriptor = map.get(toLowerCase(name)); 118 } 119 if(attributeDescriptor == null) 120 { 121 if (strictMode || SCIMConstants.SCHEMA_URI_CORE.equalsIgnoreCase(schema)) 122 { 123 throw new InvalidResourceException("Attribute " + schema + 124 SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE + name + 125 " is not defined for resource " + getName()); 126 } 127 else 128 { 129 attributeDescriptor = AttributeDescriptor.createMultiValuedAttribute( 130 name, "value", null, schema, false, false, false, 131 CoreSchema.createMultiValuedValueDescriptor( 132 schema, AttributeDescriptor.DataType.STRING)); 133 } 134 } 135 return attributeDescriptor; 136 } 137 138 /** 139 * Retrieves the attribute descriptor for a specified attribute, if the 140 * attribute name is defined. Similar to <code>getAttribute</code> but 141 * returns null rather than throwing an exception if the attribute is 142 * not defined. 143 * @param schema The attribute descriptor's associated schema URI. 144 * @param name The name of the attribute whose descriptor is to be retrieved. 145 * @return The attribute descriptor for the specified attribute, or null 146 * if the attribute is not defined on the resource. 147 */ 148 public AttributeDescriptor findAttribute(final String schema, 149 final String name) 150 { 151 final Map<String, Map<String, AttributeDescriptor>> attrCache = 152 getAttributesCache(); 153 Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema)); 154 return (map == null) ? null : map.get(toLowerCase(name)); 155 } 156 157 /** 158 * Retrieves all the attribute descriptors of the provided schema defined 159 * in the resource. 160 * 161 * @param schema The name of the schema. 162 * @return All the attribute descriptors of the provided schema defined 163 * for this resource. 164 */ 165 public Collection<AttributeDescriptor> getAttributes(final String schema) 166 { 167 final Map<String, Map<String, AttributeDescriptor>> attrCache = 168 getAttributesCache(); 169 Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema)); 170 if(map != null) 171 { 172 return map.values(); 173 } 174 return null; 175 } 176 177 /** 178 * Retrieves the set of unique schemas for the attribute descriptors defined 179 * in the resource. 180 * 181 * @return The set of unique schemas for the attribute descriptors defined 182 * in the resource. 183 */ 184 public Set<String> getAttributeSchemas() 185 { 186 return getAttributesCache().keySet(); 187 } 188 189 /** 190 * Retrieves the schema for a specified attribute name. 191 * 192 * @param name The name of the attribute whose schema is to be found. 193 * @param preferredSchemas an ordered list of schemas to prefer when matching 194 * 195 * @return The schema for the specified attribute or {@code null} if there is 196 * no such attribute name in any schema. 197 */ 198 public String findAttributeSchema(final String name, 199 final String... preferredSchemas) 200 { 201 final Map<String, Map<String, AttributeDescriptor>> attrCache = 202 getAttributesCache(); 203 Set<String> matchingSchemas = new HashSet<String>(); 204 for (String schema : getAttributeSchemas()) 205 { 206 Map<String, AttributeDescriptor> map = attrCache.get(toLowerCase(schema)); 207 if (map!= null && map.keySet().contains(name)) 208 { 209 matchingSchemas.add(schema); 210 } 211 } 212 if (matchingSchemas.isEmpty()) 213 { 214 // Element does not belong to any known schema 215 return null; 216 } 217 List<String> preferredSchemaList; 218 if (preferredSchemas == null || preferredSchemas.length < 1) 219 { 220 preferredSchemaList = new ArrayList<String>(2); 221 preferredSchemaList.add(getSchema()); 222 preferredSchemaList.add(SCIMConstants.SCHEMA_URI_CORE); 223 } 224 else 225 { 226 preferredSchemaList = Arrays.asList(preferredSchemas); 227 } 228 for (String preferredSchema : preferredSchemaList) 229 { 230 if (matchingSchemas.contains(preferredSchema)) 231 { 232 return preferredSchema; 233 } 234 } 235 // If no preferred schema was found then just return the first from the list 236 return matchingSchemas.iterator().next(); 237 } 238 239 240 /** 241 * Retrieve the name of the resource to be used in any external representation 242 * of the resource. 243 * 244 * @return Retrieve the name of the resource to be used in any external 245 * representation of the resource. It is never {@code null}. 246 */ 247 public String getName() 248 { 249 return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, "name", 250 AttributeValueResolver.STRING_RESOLVER); 251 } 252 253 /** 254 * Sets the "strict mode" for this ResourceDescriptor. If strict mode is off, 255 * then a call to {@link #getAttribute(String, String)} where the requested 256 * attribute does not exist in the attributesCache will result in the method 257 * generating an AttributeDescriptor on the fly. If strict mode were on in 258 * this case, it would throw an exception because that attribute was not 259 * defined. 260 * 261 * @param strictMode a boolean indicating whether to use strict mode or not. 262 */ 263 public void setStrictMode(final boolean strictMode) 264 { 265 this.strictMode = strictMode; 266 } 267 268 /** 269 * Gets the "strict mode" setting for this ResourceDescriptor. If strict mode 270 * is off, then a call to {@link #getAttribute(String, String)} where the 271 * requested attribute does not exist in the attributesCache will result in 272 * the method generating an AttributeDescriptor on the fly. If strict mode 273 * were on in this case, it would throw an exception because that attribute 274 * was not defined. 275 * 276 * @return boolean indicating whether strict mode is enabled. 277 */ 278 public boolean isStrictMode() 279 { 280 return this.strictMode; 281 } 282 283 /** 284 * Sets the name of the resource to be used in any external representation 285 * of the resource. 286 * 287 * @param name The name of the resource to be used in any external 288 * representation of the resource. 289 * @return this ResourceDescriptor. 290 */ 291 private ResourceDescriptor setName(final String name) 292 { 293 try { 294 setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 295 "name", AttributeValueResolver.STRING_RESOLVER, name); 296 } catch (InvalidResourceException e) { 297 // This should never happen as these are core attributes... 298 throw new RuntimeException(e); 299 } 300 return this; 301 } 302 303 /** 304 * Retrieves the list of all attribute descriptors defined in the resource. 305 * 306 * @return The list of attribute descriptors for the resource. It is never 307 * {@code null}. 308 */ 309 public Collection<AttributeDescriptor> getAttributes() 310 { 311 Collection<AttributeDescriptor> attributes = 312 getAttributeValues(SCIMConstants.SCHEMA_URI_CORE, 313 "attributes", AttributeDescriptor.ATTRIBUTE_DESCRIPTOR_RESOLVER); 314 return CoreSchema.addCommonAttributes(attributes); 315 } 316 317 /** 318 * Sets the list of attribute descriptors for the resource. 319 * 320 * @param attributes The list of attribute descriptors for the resource. 321 * @return this ResourceDescriptor. 322 */ 323 private ResourceDescriptor setAttributes( 324 final Collection<AttributeDescriptor> attributes) 325 { 326 try { 327 setAttributeValues(SCIMConstants.SCHEMA_URI_CORE, 328 "attributes", AttributeDescriptor.ATTRIBUTE_DESCRIPTOR_RESOLVER, 329 attributes); 330 attributesCache = null; 331 } catch (InvalidResourceException e) { 332 // This should never happen as these are core attributes... 333 throw new RuntimeException(e); 334 } 335 return this; 336 } 337 338 /** 339 * Returns the resource's XML schema (namespace) name. 340 * 341 * @return The XML namespace name. 342 */ 343 public String getSchema() 344 { 345 return toLowerCase( 346 getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, "schema", 347 AttributeValueResolver.STRING_RESOLVER)); 348 } 349 350 /** 351 * Sets the resource's XML schema (namespace) name. 352 * 353 * @param schema The XML namespace name. 354 * @return this ResourceDescriptor. 355 */ 356 private ResourceDescriptor setSchema(final String schema) 357 { 358 try { 359 setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 360 "schema", AttributeValueResolver.STRING_RESOLVER, schema); 361 } catch (InvalidResourceException e) { 362 // This should never happen as these are core attributes... 363 throw new RuntimeException(e); 364 } 365 return this; 366 } 367 368 /** 369 * Retrieves the resource's human readable description. 370 * 371 * @return The resource's human readable description. 372 */ 373 public String getDescription() 374 { 375 return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 376 "description", AttributeValueResolver.STRING_RESOLVER); 377 } 378 379 /** 380 * Sets the resource's human readable description. 381 * 382 * @param description The resource's human readable description. 383 * @return this ResourceDescriptor. 384 */ 385 private ResourceDescriptor setDescription(final String description) 386 { 387 try { 388 setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 389 "description", AttributeValueResolver.STRING_RESOLVER, description); 390 } catch (InvalidResourceException e) { 391 // This should never happen as these are core attributes... 392 throw new RuntimeException(e); 393 } 394 return this; 395 } 396 397 /** 398 * Retrieves the Resource's HTTP addressable endpoint relative to the 399 * Base URL. 400 * 401 * @return The Resource's HTTP addressable endpoint relative to the Base URL. 402 */ 403 public String getEndpoint() 404 { 405 return getSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 406 "endpoint", AttributeValueResolver.STRING_RESOLVER); 407 } 408 409 /** 410 * Sets the Resource's HTTP addressable endpoint relative to the 411 * Base URL. 412 * 413 * @param endpoint The Resource's HTTP addressable endpoint relative to 414 * the Base URL. 415 * @return this ResourceDescriptor. 416 */ 417 private ResourceDescriptor setEndpoint(final String endpoint) 418 { 419 try { 420 setSingularAttributeValue(SCIMConstants.SCHEMA_URI_CORE, 421 "endpoint", AttributeValueResolver.STRING_RESOLVER, endpoint); 422 } catch (InvalidResourceException e) { 423 // This should never happen as these are core attributes... 424 throw new RuntimeException(e); 425 } 426 return this; 427 } 428 429 /** 430 * Retrieves the attributesCache, using double-checked locking to avoid 431 * synchronization in the common case where the cache is already initialized. 432 * 433 * @return A snapshot of the attributesCache. The cache is regenerated when 434 * {@link #setAttributes(java.util.Collection)} is called. 435 */ 436 private Map<String, Map<String, AttributeDescriptor>> getAttributesCache() 437 { 438 Map<String, Map<String, AttributeDescriptor>> attrCache = attributesCache; 439 if (attrCache == null) 440 { 441 synchronized (this) 442 { 443 attrCache = attributesCache; 444 if (attrCache == null) 445 { 446 attrCache = new HashMap<String, 447 Map<String, AttributeDescriptor>>(); 448 for(AttributeDescriptor attributeDescriptor : getAttributes()) 449 { 450 final String lowerCaseSchema = 451 toLowerCase(attributeDescriptor.getSchema()); 452 Map<String, AttributeDescriptor> map = 453 attrCache.get(lowerCaseSchema); 454 if(map == null) 455 { 456 map = new HashMap<String, AttributeDescriptor>(); 457 attrCache.put(lowerCaseSchema, map); 458 } 459 map.put(toLowerCase(attributeDescriptor.getName()), 460 attributeDescriptor); 461 } 462 463 attributesCache = attrCache; 464 } 465 } 466 } 467 return attrCache; 468 } 469 470 /** 471 * {@inheritDoc} 472 */ 473 @Override 474 public int hashCode() 475 { 476 int hashCode = 31; 477 hashCode += hashCode * toLowerCase(getSchema()).hashCode(); 478 hashCode += hashCode * toLowerCase(getName()).hashCode(); 479 return hashCode; 480 } 481 482 /** 483 * {@inheritDoc} 484 */ 485 @Override 486 public boolean equals(final Object obj) 487 { 488 if (this == obj) 489 { 490 return true; 491 } 492 493 if (!(obj instanceof ResourceDescriptor)) 494 { 495 return false; 496 } 497 498 final ResourceDescriptor that = (ResourceDescriptor)obj; 499 final String thisSchema = getSchema(); 500 final String thisName = getName(); 501 final String thatSchema = that.getSchema(); 502 final String thatName = that.getName(); 503 if (thisSchema == null && thatSchema == null) 504 { 505 return thisName.equalsIgnoreCase(thatName); 506 } 507 else 508 { 509 return thisSchema != null && thatSchema != null && 510 thisSchema.equalsIgnoreCase(thatSchema) && 511 thisName.equalsIgnoreCase(thatName); 512 } 513 } 514 515 @Override 516 public String toString() 517 { 518 return "ResourceDescriptor{" + 519 "name='" + getName() + '\'' + 520 ", description='" + getDescription() + 521 ", schema='" + getSchema() + '\'' + 522 ", endpoint='" + getEndpoint() + '\'' + 523 ", attributes=" + getAttributes() + 524 '}'; 525 } 526 527 /** 528 * Construct a new resource descriptor with the provided information. 529 * The resource attributes specified here should not include common core 530 * attributes (ie. id, externalId, meta) as these will be added automatically. 531 * 532 * @param name The addressable Resource endpoint name. 533 * @param description The Resource's human readable description. 534 * @param schema The Resource's associated schema URN 535 * @param endpoint The Resource's HTTP addressable endpoint relative 536 * to the Base URL. 537 * @param attributes Specifies the set of associated Resource attributes. 538 * @return The newly constructed resource descriptor. 539 */ 540 public static ResourceDescriptor create( 541 final String name, final String description, final String schema, 542 final String endpoint, final AttributeDescriptor... attributes) 543 { 544 ResourceDescriptor resourceDescriptor = 545 new ResourceDescriptor(CoreSchema.RESOURCE_SCHEMA_DESCRIPTOR); 546 resourceDescriptor.setName(name); 547 resourceDescriptor.setDescription(description); 548 resourceDescriptor.setSchema(schema); 549 resourceDescriptor.setEndpoint(endpoint); 550 resourceDescriptor.setAttributes(Arrays.asList(attributes)); 551 552 return resourceDescriptor; 553 } 554}