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