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