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.fasterxml.jackson.databind.JsonNode; 021import com.fasterxml.jackson.databind.node.ArrayNode; 022import com.fasterxml.jackson.databind.node.ObjectNode; 023import com.unboundid.scim2.common.GenericScimResource; 024import com.unboundid.scim2.common.Path; 025import com.unboundid.scim2.common.ScimResource; 026import com.unboundid.scim2.common.annotations.NotNull; 027import com.unboundid.scim2.common.annotations.Nullable; 028import com.unboundid.scim2.common.exceptions.BadRequestException; 029import com.unboundid.scim2.common.messages.PatchOperation; 030import com.unboundid.scim2.common.types.Meta; 031import com.unboundid.scim2.common.utils.StaticUtils; 032 033import jakarta.ws.rs.core.MultivaluedMap; 034import jakarta.ws.rs.core.UriBuilder; 035import jakarta.ws.rs.core.UriInfo; 036import java.net.URI; 037import java.util.Collections; 038import java.util.Iterator; 039import java.util.LinkedHashMap; 040import java.util.LinkedHashSet; 041import java.util.Map; 042import java.util.Set; 043 044import static com.unboundid.scim2.common.utils.ApiConstants.*; 045 046/** 047 * Utility to prepare a resource to return to the client. This includes: 048 * 049 * <ul> 050 * <li> 051 * Returning the attributes based on the returned constraint of the 052 * attribute definition in the schema. 053 * </li> 054 * <li> 055 * Returning the attributes requested by the client using the request 056 * resource as well as the attributes or excludedAttributes query parameter. 057 * </li> 058 * <li> 059 * Setting the meta.resourceType and meta.location attributes if not 060 * already set. 061 * </li> 062 * </ul> 063 */ 064public class ResourcePreparer<T extends ScimResource> 065{ 066 @NotNull 067 private final ResourceTypeDefinition resourceType; 068 069 @NotNull 070 private final URI baseUri; 071 072 @NotNull 073 private final Set<Path> queryAttributes; 074 075 private final boolean excluded; 076 077 /** 078 * Create a new ResourcePreparer for preparing returned resources for a 079 * SCIM operation. 080 * 081 * @param resourceType The resource type definition for resources to prepare. 082 * @param requestUriInfo The UriInfo for the request. 083 * @throws BadRequestException If an attribute path specified by attributes 084 * and excludedAttributes is invalid. 085 */ 086 public ResourcePreparer(@NotNull final ResourceTypeDefinition resourceType, 087 @NotNull final UriInfo requestUriInfo) 088 throws BadRequestException 089 { 090 this(resourceType, 091 requestUriInfo.getQueryParameters().getFirst( 092 QUERY_PARAMETER_ATTRIBUTES), 093 requestUriInfo.getQueryParameters().getFirst( 094 QUERY_PARAMETER_EXCLUDED_ATTRIBUTES), 095 requestUriInfo.getBaseUriBuilder(). 096 path(resourceType.getEndpoint()). 097 buildFromMap(singleValuedMapFromMultivaluedMap( 098 requestUriInfo.getPathParameters()))); 099 } 100 101 @NotNull 102 private static Map<String, String> singleValuedMapFromMultivaluedMap( 103 @NotNull final MultivaluedMap<String, String> multivaluedMap) 104 { 105 final Map<String, String> returnMap = new LinkedHashMap<String, String>(); 106 for (String k : multivaluedMap.keySet()) 107 { 108 returnMap.put(k, multivaluedMap.getFirst(k)); 109 } 110 111 return returnMap; 112 } 113 114 /** 115 * Private constructor used by unit-test. 116 * 117 * @param resourceType The resource type definition for resources to prepare. 118 * @param attributesString The attributes query param. 119 * @param excludedAttributesString The excludedAttributes query param. 120 * @param baseUri The resource type base URI. 121 */ 122 ResourcePreparer(@NotNull final ResourceTypeDefinition resourceType, 123 @NotNull final String attributesString, 124 @Nullable final String excludedAttributesString, 125 @NotNull final URI baseUri) 126 throws BadRequestException 127 { 128 if(attributesString != null && !attributesString.isEmpty()) 129 { 130 Set<String> attributeSet = StaticUtils.arrayToSet( 131 StaticUtils.splitCommaSeparatedString(attributesString)); 132 this.queryAttributes = new LinkedHashSet<Path>(attributeSet.size()); 133 for(String attribute : attributeSet) 134 { 135 Path normalizedPath; 136 try 137 { 138 normalizedPath = resourceType.normalizePath( 139 Path.fromString(attribute)).withoutFilters(); 140 } 141 catch (BadRequestException e) 142 { 143 throw BadRequestException.invalidValue("'" + attribute + 144 "' is not a valid value for the attributes parameter: " + 145 e.getMessage()); 146 } 147 this.queryAttributes.add(normalizedPath); 148 149 } 150 this.excluded = false; 151 } 152 else if(excludedAttributesString != null && 153 !excludedAttributesString.isEmpty()) 154 { 155 Set<String> attributeSet = StaticUtils.arrayToSet( 156 StaticUtils.splitCommaSeparatedString(excludedAttributesString)); 157 this.queryAttributes = new LinkedHashSet<Path>(attributeSet.size()); 158 for(String attribute : attributeSet) 159 { 160 Path normalizedPath; 161 try 162 { 163 normalizedPath = resourceType.normalizePath( 164 Path.fromString(attribute)).withoutFilters(); 165 } 166 catch (BadRequestException e) 167 { 168 throw BadRequestException.invalidValue("'" + attribute + 169 "' is not a valid value for the excludedAttributes parameter: " + 170 e.getMessage()); 171 } 172 this.queryAttributes.add(normalizedPath); 173 } 174 this.excluded = true; 175 } 176 else 177 { 178 this.queryAttributes = Collections.emptySet(); 179 this.excluded = true; 180 } 181 this.resourceType = resourceType; 182 this.baseUri = baseUri; 183 } 184 185 /** 186 * Trim attributes of the resources returned from a search or retrieve 187 * operation based on schema and the request parameters. 188 * 189 * @param returnedResource The resource to return. 190 * @return The trimmed resource ready to return to the client. 191 */ 192 @NotNull 193 public GenericScimResource trimRetrievedResource( 194 @NotNull final T returnedResource) 195 { 196 return trimReturned(returnedResource, null, null); 197 } 198 199 /** 200 * Trim attributes of the resources returned from a create operation based on 201 * schema as well as the request resource and request parameters. 202 * 203 * @param returnedResource The resource to return. 204 * @param requestResource The resource in the create request or 205 * {@code null} if not available. 206 * @return The trimmed resource ready to return to the client. 207 */ 208 @NotNull 209 public GenericScimResource trimCreatedResource( 210 @NotNull final T returnedResource, 211 @Nullable final T requestResource) 212 { 213 return trimReturned(returnedResource, requestResource, null); 214 } 215 216 /** 217 * Trim attributes of the resources returned from a replace operation based on 218 * schema as well as the request resource and request parameters. 219 * 220 * @param returnedResource The resource to return. 221 * @param requestResource The resource in the replace request or 222 * {@code null} if not available. 223 * @return The trimmed resource ready to return to the client. 224 */ 225 @NotNull 226 public GenericScimResource trimReplacedResource( 227 @NotNull final T returnedResource, 228 @Nullable final T requestResource) 229 { 230 return trimReturned(returnedResource, requestResource, null); 231 } 232 233 /** 234 * Trim attributes of the resources returned from a modify operation based on 235 * schema as well as the patch request and request parameters. 236 * 237 * @param returnedResource The resource to return. 238 * @param patchOperations The operations in the patch request or 239 * {@code null} if not available. 240 * @return The trimmed resource ready to return to the client. 241 */ 242 @NotNull 243 public GenericScimResource trimModifiedResource( 244 @NotNull final T returnedResource, 245 @Nullable final Iterable<PatchOperation> patchOperations) 246 { 247 return trimReturned(returnedResource, null, patchOperations); 248 } 249 250 /** 251 * Sets the meta.resourceType and meta.location metadata attribute values. 252 * 253 * @param returnedResource The resource to set the attributes. 254 */ 255 public void setResourceTypeAndLocation(@NotNull final T returnedResource) 256 { 257 Meta meta = returnedResource.getMeta(); 258 259 boolean metaUpdated = false; 260 if(meta == null) 261 { 262 meta = new Meta(); 263 } 264 265 if(meta.getResourceType() == null) 266 { 267 meta.setResourceType(resourceType.getName()); 268 metaUpdated = true; 269 } 270 271 if(meta.getLocation() == null) 272 { 273 String id = returnedResource.getId(); 274 if (id != null) 275 { 276 UriBuilder locationBuilder = UriBuilder.fromUri(baseUri); 277 locationBuilder.segment(ServerUtils.encodeTemplateNames(id)); 278 meta.setLocation(locationBuilder.build()); 279 } 280 else 281 { 282 meta.setLocation(baseUri); 283 } 284 metaUpdated = true; 285 } 286 287 if(metaUpdated) 288 { 289 returnedResource.setMeta(meta); 290 } 291 } 292 293 /** 294 * Trim attributes of the resources to return based on schema and the client 295 * request. 296 * 297 * @param returnedResource The resource to return. 298 * @param requestResource The resource in the PUT or POST request or 299 * {@code null} for other requests. 300 * @param patchOperations The patch operations in the PATCH request or 301 * {@code null} for other requests. 302 * @return The trimmed resource ready to return to the client. 303 */ 304 @NotNull 305 private GenericScimResource trimReturned( 306 @NotNull final T returnedResource, 307 @Nullable final T requestResource, 308 @Nullable final Iterable<PatchOperation> patchOperations) 309 { 310 Set<Path> requestAttributes = Collections.emptySet(); 311 if(requestResource != null) 312 { 313 ObjectNode requestObject = 314 requestResource.asGenericScimResource().getObjectNode(); 315 requestAttributes = new LinkedHashSet<Path>(); 316 collectAttributes(Path.root(), requestAttributes, requestObject); 317 } 318 319 if(patchOperations != null) 320 { 321 requestAttributes = new LinkedHashSet<Path>(); 322 collectAttributes(requestAttributes, patchOperations); 323 } 324 325 setResourceTypeAndLocation(returnedResource); 326 GenericScimResource genericReturnedResource = 327 returnedResource.asGenericScimResource(); 328 ScimResourceTrimmer trimmer = 329 new ScimResourceTrimmer(resourceType, requestAttributes, 330 queryAttributes, excluded); 331 GenericScimResource preparedResource = 332 new GenericScimResource( 333 trimmer.trimObjectNode(genericReturnedResource.getObjectNode())); 334 return preparedResource; 335 } 336 337 /** 338 * Collect a list of attributes in the object node. 339 * 340 * @param parentPath The parent path of attributes in the object. 341 * @param paths The set of paths to add to. 342 * @param objectNode The object node to collect from. 343 */ 344 private void collectAttributes(@NotNull final Path parentPath, 345 @NotNull final Set<Path> paths, 346 @NotNull final ObjectNode objectNode) 347 { 348 Iterator<Map.Entry<String, JsonNode>> i = objectNode.fields(); 349 while(i.hasNext()) 350 { 351 Map.Entry<String, JsonNode> field = i.next(); 352 Path path = parentPath.attribute(field.getKey()); 353 if(path.size() > 1 || path.getSchemaUrn() == null) 354 { 355 // Don't add a path for the extension schema object itself. 356 paths.add(path); 357 } 358 if (field.getValue().isArray()) 359 { 360 collectAttributes(path, paths, (ArrayNode) field.getValue()); 361 } 362 else if (field.getValue().isObject()) 363 { 364 collectAttributes(path, paths, (ObjectNode) field.getValue()); 365 } 366 } 367 } 368 369 /** 370 * Collect a list of attributes in the array node. 371 * 372 * @param parentPath The parent path of attributes in the array. 373 * @param paths The set of paths to add to. 374 * @param arrayNode The array node to collect from. 375 */ 376 private void collectAttributes(@NotNull final Path parentPath, 377 @NotNull final Set<Path> paths, 378 @NotNull final ArrayNode arrayNode) 379 { 380 for(JsonNode value : arrayNode) 381 { 382 if(value.isArray()) 383 { 384 collectAttributes(parentPath, paths, (ArrayNode) value); 385 } 386 else if(value.isObject()) 387 { 388 collectAttributes(parentPath, paths, (ObjectNode) value); 389 } 390 } 391 } 392 393 /** 394 * Collect a list of attributes in the patch operation. 395 * 396 * @param paths The set of paths to add to. 397 * @param patchOperations The patch operation to collect attributes from. 398 */ 399 private void collectAttributes( 400 @NotNull final Set<Path> paths, 401 @NotNull final Iterable<PatchOperation> patchOperations) 402 403 { 404 for(PatchOperation patchOperation : patchOperations) 405 { 406 Path path = Path.root(); 407 if(patchOperation.getPath() != null) 408 { 409 path = resourceType.normalizePath(patchOperation.getPath()). 410 withoutFilters(); 411 paths.add(path); 412 } 413 if(patchOperation.getJsonNode() != null) 414 { 415 if(patchOperation.getJsonNode().isArray()) 416 { 417 collectAttributes( 418 path, paths, (ArrayNode) patchOperation.getJsonNode()); 419 } 420 else if(patchOperation.getJsonNode().isObject()) 421 { 422 collectAttributes( 423 path, paths, (ObjectNode) patchOperation.getJsonNode()); 424 } 425 } 426 } 427 } 428}