001/* 002 * Copyright 2015-2019 Ping Identity Corporation 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.scim2.server.utils; 019 020import com.unboundid.scim2.common.Path; 021import com.unboundid.scim2.common.types.AttributeDefinition; 022import com.unboundid.scim2.common.types.ResourceTypeResource; 023import com.unboundid.scim2.common.types.SchemaResource; 024import com.unboundid.scim2.common.utils.SchemaUtils; 025import com.unboundid.scim2.server.annotations.ResourceType; 026 027import java.net.URI; 028import java.net.URISyntaxException; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037 038/** 039 * Declaration of a resource type including all schemas. 040 */ 041public final class ResourceTypeDefinition 042{ 043 private final String id; 044 private final String name; 045 private final String description; 046 private final String endpoint; 047 private final SchemaResource coreSchema; 048 private final Map<SchemaResource, Boolean> schemaExtensions; 049 private final Map<Path, AttributeDefinition> attributeNotationMap; 050 private final boolean discoverable; 051 052 /** 053 * Builder for creating a ResourceTypeDefinition. 054 */ 055 public static class Builder 056 { 057 private final String name; 058 private final String endpoint; 059 private String id; 060 private String description; 061 private SchemaResource coreSchema; 062 private Set<SchemaResource> requiredSchemaExtensions = 063 new HashSet<SchemaResource>(); 064 private Set<SchemaResource> optionalSchemaExtensions = 065 new HashSet<SchemaResource>(); 066 private boolean discoverable = true; 067 068 /** 069 * Create a new builder. 070 * 071 * @param name The name of the resource type. 072 * @param endpoint The endpoint of the resource type. 073 */ 074 public Builder(final String name, final String endpoint) 075 { 076 if(name == null) 077 { 078 throw new IllegalArgumentException("name must not be null"); 079 } 080 if(endpoint == null) 081 { 082 throw new IllegalArgumentException("endpoint must not be null"); 083 } 084 this.name = name; 085 this.endpoint = endpoint; 086 } 087 088 /** 089 * Sets the ID of the resource type. 090 * 091 * @param id the ID of the resource type. 092 * @return this builder. 093 */ 094 public Builder setId(final String id) 095 { 096 this.id = id; 097 return this; 098 } 099 100 /** 101 * Sets the description of the resource type. 102 * 103 * @param description the description of the resource type. 104 * @return this builder. 105 */ 106 public Builder setDescription(final String description) 107 { 108 this.description = description; 109 return this; 110 } 111 112 /** 113 * Sets the core schema of the resource type. 114 * 115 * @param coreSchema the core schema of the resource type. 116 * @return this builder. 117 */ 118 public Builder setCoreSchema(final SchemaResource coreSchema) 119 { 120 this.coreSchema = coreSchema; 121 return this; 122 } 123 124 /** 125 * Adds a required schema extension for a resource type. 126 * 127 * @param schemaExtension the required schema extension for the resource 128 * type. 129 * @return this builder. 130 */ 131 public Builder addRequiredSchemaExtension( 132 final SchemaResource schemaExtension) 133 { 134 this.requiredSchemaExtensions.add(schemaExtension); 135 return this; 136 } 137 138 /** 139 * Adds a operation schema extension for a resource type. 140 * 141 * @param schemaExtension the operation schema extension for the resource 142 * type. 143 * @return this builder. 144 */ 145 public Builder addOptionalSchemaExtension( 146 final SchemaResource schemaExtension) 147 { 148 this.optionalSchemaExtensions.add(schemaExtension); 149 return this; 150 } 151 152 /** 153 * Sets whether this resource type is discoverable over the /ResourceTypes 154 * endpoint. 155 * 156 * @param discoverable {@code true} this resource type is discoverable over 157 * the /ResourceTypes endpoint or {@code false} 158 * otherwise. 159 * @return this builder. 160 */ 161 public Builder setDiscoverable( 162 final boolean discoverable) 163 { 164 this.discoverable = discoverable; 165 return this; 166 } 167 168 /** 169 * Build the ResourceTypeDefinition. 170 * 171 * @return The newly created ResourceTypeDefinition. 172 */ 173 public ResourceTypeDefinition build() 174 { 175 Map<SchemaResource, Boolean> schemaExtensions = 176 new HashMap<SchemaResource, Boolean>(requiredSchemaExtensions.size() + 177 optionalSchemaExtensions.size()); 178 for(SchemaResource schema : requiredSchemaExtensions) 179 { 180 schemaExtensions.put(schema, true); 181 } 182 for(SchemaResource schema : optionalSchemaExtensions) 183 { 184 schemaExtensions.put(schema, false); 185 } 186 return new ResourceTypeDefinition(id, name, description, endpoint, 187 coreSchema, schemaExtensions, discoverable); 188 } 189 } 190 191 /** 192 * Create a new ResourceType. 193 * 194 * @param coreSchema The core schema for the resource type. 195 * @param schemaExtensions A map of schema extensions to whether it is 196 * required for the resource type. 197 */ 198 private ResourceTypeDefinition( 199 final String id, final String name, final String description, 200 final String endpoint, 201 final SchemaResource coreSchema, 202 final Map<SchemaResource, Boolean> schemaExtensions, 203 final boolean discoverable) 204 { 205 this.id = id; 206 this.name = name; 207 this.description = description; 208 this.endpoint = endpoint; 209 this.coreSchema = coreSchema; 210 this.schemaExtensions = Collections.unmodifiableMap(schemaExtensions); 211 this.discoverable = discoverable; 212 this.attributeNotationMap = new HashMap<Path, AttributeDefinition>(); 213 214 // Add the common attributes 215 buildAttributeNotationMap(Path.root(), 216 SchemaUtils.COMMON_ATTRIBUTE_DEFINITIONS); 217 218 // Add the core attributes 219 if(coreSchema != null) 220 { 221 buildAttributeNotationMap(Path.root(), coreSchema.getAttributes()); 222 } 223 224 // Add the extension attributes 225 for(SchemaResource schemaExtension : schemaExtensions.keySet()) 226 { 227 buildAttributeNotationMap(Path.root(schemaExtension.getId()), 228 schemaExtension.getAttributes()); 229 } 230 } 231 232 private void buildAttributeNotationMap( 233 final Path parentPath, 234 final Collection<AttributeDefinition> attributes) 235 { 236 for(AttributeDefinition attribute : attributes) 237 { 238 Path path = parentPath.attribute(attribute.getName()); 239 attributeNotationMap.put(path, attribute); 240 if(attribute.getSubAttributes() != null) 241 { 242 buildAttributeNotationMap(path, attribute.getSubAttributes()); 243 } 244 } 245 } 246 247 /** 248 * Gets the resource type name. 249 * 250 * @return the name of the resource type. 251 */ 252 public String getName() 253 { 254 return name; 255 } 256 257 /** 258 * Gets the description of the resource type. 259 * 260 * @return the description of the resource type. 261 */ 262 public String getDescription() 263 { 264 return description; 265 } 266 267 /** 268 * Gets the resource type's endpoint. 269 * 270 * @return the endpoint for the resource type. 271 */ 272 public String getEndpoint() 273 { 274 return endpoint; 275 } 276 277 /** 278 * Gets the resource type's schema. 279 * 280 * @return the schema for the resource type. 281 */ 282 public SchemaResource getCoreSchema() 283 { 284 return coreSchema; 285 } 286 287 /** 288 * Gets the resource type's schema extensions. 289 * 290 * @return the schema extensions for the resource type. 291 */ 292 public Map<SchemaResource, Boolean> getSchemaExtensions() 293 { 294 return schemaExtensions; 295 } 296 297 /** 298 * Whether this resource type and its associated schemas should be 299 * discoverable using the SCIM 2 standard /resourceTypes and /schemas 300 * endpoints. 301 * 302 * @return {@code true} if discoverable or {@code false} otherwise. 303 */ 304 public boolean isDiscoverable() 305 { 306 return discoverable; 307 } 308 309 /** 310 * Retrieve the attribute definition for the attribute in the path. 311 * 312 * @param path The attribute path. 313 * @return The attribute definition or {@code null} if there is no attribute 314 * defined for the path. 315 */ 316 public AttributeDefinition getAttributeDefinition(final Path path) 317 { 318 return attributeNotationMap.get(normalizePath(path).withoutFilters()); 319 } 320 321 /** 322 * Normalize a path by removing the schema URN for core attributes. 323 * 324 * @param path The path to normalize. 325 * @return The normalized path. 326 */ 327 public Path normalizePath(final Path path) 328 { 329 if(path.getSchemaUrn() != null && coreSchema != null && 330 path.getSchemaUrn().equalsIgnoreCase(coreSchema.getId())) 331 { 332 return Path.root().attribute(path); 333 } 334 return path; 335 } 336 337 /** 338 * Retrieve the ResourceType SCIM resource that represents this definition. 339 * 340 * @return The ResourceType SCIM resource that represents this definition. 341 */ 342 public ResourceTypeResource toScimResource() 343 { 344 try 345 { 346 URI coreSchemaUri = null; 347 if(coreSchema != null) 348 { 349 coreSchemaUri = new URI(coreSchema.getId()); 350 } 351 List<ResourceTypeResource.SchemaExtension> schemaExtensionList = null; 352 if (schemaExtensions.size() > 0) 353 { 354 schemaExtensionList = 355 new ArrayList<ResourceTypeResource.SchemaExtension>( 356 schemaExtensions.size()); 357 358 for(Map.Entry<SchemaResource, Boolean> schemaExtension : 359 schemaExtensions.entrySet()) 360 { 361 schemaExtensionList.add(new ResourceTypeResource.SchemaExtension( 362 URI.create(schemaExtension.getKey().getId()), 363 schemaExtension.getValue())); 364 } 365 } 366 367 return new ResourceTypeResource(id == null ? name : id, name, description, 368 URI.create(endpoint), coreSchemaUri, schemaExtensionList); 369 } 370 catch(URISyntaxException e) 371 { 372 throw new RuntimeException(e); 373 } 374 } 375 376 /** 377 * Create a new instance representing the resource type implemented by a 378 * root JAX-RS resource class. 379 * 380 * @param resource a root resource whose 381 * {@link com.unboundid.scim2.server.annotations.ResourceType} 382 * and {@link javax.ws.rs.Path} values will be used to 383 * initialize the ResourceTypeDefinition. 384 * @return a new ResourceTypeDefinition or {@code null} if resource is not 385 * annotated with {@link com.unboundid.scim2.server.annotations.ResourceType} 386 * and {@link javax.ws.rs.Path}. 387 */ 388 public static ResourceTypeDefinition fromJaxRsResource( 389 final Class<?> resource) 390 { 391 Class<?> c = resource; 392 ResourceType resourceType; 393 do 394 { 395 resourceType = c.getAnnotation(ResourceType.class); 396 c = c.getSuperclass(); 397 } 398 while(c != null && resourceType == null); 399 400 c = resource; 401 javax.ws.rs.Path path; 402 do 403 { 404 path = c.getAnnotation(javax.ws.rs.Path.class); 405 c = c.getSuperclass(); 406 } 407 while(c != null && path == null); 408 409 if(resourceType == null || path == null) 410 { 411 return null; 412 } 413 414 try 415 { 416 ResourceTypeDefinition.Builder builder = 417 new Builder(resourceType.name(), path.value()); 418 builder.setDescription(resourceType.description()); 419 builder.setCoreSchema(SchemaUtils.getSchema(resourceType.schema())); 420 builder.setDiscoverable( 421 resourceType.discoverable()); 422 423 for (Class<?> optionalSchemaExtension : 424 resourceType.optionalSchemaExtensions()) 425 { 426 builder.addOptionalSchemaExtension( 427 SchemaUtils.getSchema(optionalSchemaExtension)); 428 } 429 430 for (Class<?> requiredSchemaExtension : 431 resourceType.requiredSchemaExtensions()) 432 { 433 builder.addRequiredSchemaExtension( 434 SchemaUtils.getSchema(requiredSchemaExtension)); 435 } 436 437 return builder.build(); 438 } 439 catch(Exception e) 440 { 441 throw new IllegalArgumentException(e); 442 } 443 } 444}