blob: 482c288fb5b92951a9d3d676d81a362ebdd0d91c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.directory.scim.server.rest;
import java.util.*;
import java.util.regex.Pattern;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.directory.scim.core.repository.ResourceException;
import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
import org.apache.directory.scim.server.exception.UnableToDeleteResourceException;
import org.apache.directory.scim.server.exception.UnableToRetrieveResourceException;
import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
import org.apache.directory.scim.core.repository.Repository;
import org.apache.directory.scim.core.repository.RepositoryRegistry;
import org.apache.directory.scim.core.repository.UpdateRequest;
import org.apache.directory.scim.protocol.BulkResource;
import org.apache.directory.scim.protocol.data.BulkOperation;
import org.apache.directory.scim.protocol.data.BulkOperation.Method;
import org.apache.directory.scim.protocol.data.BulkOperation.StatusWrapper;
import org.apache.directory.scim.protocol.data.BulkRequest;
import org.apache.directory.scim.protocol.data.BulkResponse;
import org.apache.directory.scim.protocol.data.ErrorResponse;
import org.apache.directory.scim.spec.resources.BaseResource;
import org.apache.directory.scim.spec.resources.ScimResource;
import org.apache.directory.scim.spec.schema.Schema;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.directory.scim.core.schema.SchemaRegistry;
@Slf4j
@ApplicationScoped
public class BulkResourceImpl implements BulkResource {
// private static final StatusWrapper OKAY_STATUS = new StatusWrapper();
// private static final StatusWrapper CREATED_STATUS = new StatusWrapper();
// private static final StatusWrapper NO_CONTENT_STATUS = new StatusWrapper();
// private static final StatusWrapper METHOD_NOT_ALLOWED_STATUS = new StatusWrapper();
// private static final StatusWrapper CONFLICT_STATUS = new StatusWrapper();
// private static final StatusWrapper CLIENT_ERROR_STATUS = new StatusWrapper();
// private static final StatusWrapper NOT_FOUND_STATUS = new StatusWrapper();
// private static final StatusWrapper INTERNAL_SERVER_ERROR_STATUS = new StatusWrapper();
// private static final StatusWrapper METHOD_NOT_IMPLEMENTED_STATUS = new StatusWrapper();
// private static final String OKAY = "200";
// private static final String CREATED = "201";
// private static final String NO_CONTENT = "204";
// private static final String CLIENT_ERROR = "400";
// private static final String NOT_FOUND = "404";
// private static final String METHOD_NOT_ALLOWED = "405";
// private static final String CONFLICT = "409";
// private static final String INTERNAL_SERVER_ERROR = "500";
// private static final String METHOD_NOT_IMPLEMENTED = "501";
private static final String BULK_ID_DOES_NOT_EXIST = "Bulk ID cannot be resolved because it refers to no bulkId in any Bulk Operation: %s";
private static final String BULK_ID_REFERS_TO_FAILED_RESOURCE = "Bulk ID cannot be resolved because the resource it refers to had failed to be created: %s";
private static final String OPERATION_DEPENDS_ON_FAILED_OPERATION = "Operation depends on failed bulk operation: %s";
private static final Pattern PATH_PATTERN = Pattern.compile("^/[^/]+/[^/]+$");
// static {
// METHOD_NOT_ALLOWED_STATUS.setCode(METHOD_NOT_ALLOWED);
// OKAY_STATUS.setCode(OKAY);
// CREATED_STATUS.setCode(CREATED);
// NO_CONTENT_STATUS.setCode(NO_CONTENT);
// CONFLICT_STATUS.setCode(CONFLICT);
// CLIENT_ERROR_STATUS.setCode(CLIENT_ERROR);
// NOT_FOUND_STATUS.setCode(NOT_FOUND);
// INTERNAL_SERVER_ERROR_STATUS.setCode(INTERNAL_SERVER_ERROR);
// METHOD_NOT_IMPLEMENTED_STATUS.setCode(METHOD_NOT_IMPLEMENTED);
// }
private final SchemaRegistry schemaRegistry;
private final RepositoryRegistry repositoryRegistry;
@Inject
public BulkResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry) {
this.schemaRegistry = schemaRegistry;
this.repositoryRegistry = repositoryRegistry;
}
BulkResourceImpl() {
this(null, null);
}
@Override
public Response doBulk(BulkRequest request, UriInfo uriInfo) {
BulkResponse response;
int errorCount = 0;
Integer requestFailOnErrors = request.getFailOnErrors();
int maxErrorCount = requestFailOnErrors != null && requestFailOnErrors > 0 ? requestFailOnErrors : Integer.MAX_VALUE;
int errorCountIncrement = requestFailOnErrors == null || requestFailOnErrors > 0 ? 1 : 0;
List<BulkOperation> bulkOperations = request.getOperations();
Map<String, BulkOperation> bulkIdKeyToOperationResult = new HashMap<>();
List<IWishJavaHadTuples> allUnresolveds = new ArrayList<>();
Map<String, Set<String>> reverseDependenciesGraph = this.generateReverseDependenciesGraph(bulkOperations);
Map<String, Set<String>> transitiveReverseDependencies = generateTransitiveDependenciesGraph(reverseDependenciesGraph);
log.debug("Reverse dependencies: {}", reverseDependenciesGraph);
log.debug("Transitive reverse dependencies: {}", transitiveReverseDependencies);
// clean out unwanted data
for (BulkOperation operationRequest : bulkOperations) {
operationRequest.setResponse(null);
operationRequest.setStatus(null);
}
// get all known bulkIds, handle bad input
for (BulkOperation operationRequest : bulkOperations) {
String bulkId = operationRequest.getBulkId();
Method method = operationRequest.getMethod();
String bulkIdKey = bulkId != null ? "bulkId:" + bulkId : null;
boolean errorOccurred = false;
// duplicate bulkId
if (bulkIdKey != null) {
if (!bulkIdKeyToOperationResult.containsKey(bulkIdKey)) {
bulkIdKeyToOperationResult.put(bulkIdKey, operationRequest);
} else {
errorOccurred = true;
BulkOperation duplicateOperation = bulkIdKeyToOperationResult.get(bulkIdKey);
createAndSetErrorResponse(operationRequest, Status.CONFLICT, "Duplicate bulkId");
if (!(duplicateOperation.getResponse() instanceof ErrorResponse)) {
duplicateOperation.setData(null);
createAndSetErrorResponse(duplicateOperation, Status.CONFLICT, "Duplicate bulkId");
}
}
}
// bad/missing input for method
if (method != null && !(operationRequest.getResponse() instanceof ErrorResponse)) {
switch (method) {
case POST:
case PUT: {
if (operationRequest.getData() == null) {
errorOccurred = true;
createAndSetErrorResponse(operationRequest, Status.BAD_REQUEST, "data not provided");
}
}
break;
case DELETE: {
String path = operationRequest.getPath();
if (path == null) {
errorOccurred = true;
createAndSetErrorResponse(operationRequest, Status.BAD_REQUEST, "path not provided");
} else if (!PATH_PATTERN.matcher(path)
.matches()) {
errorOccurred = true;
createAndSetErrorResponse(operationRequest, Status.BAD_REQUEST, "path is not a valid path (e.g. \"/Groups/123abc\", \"/Users/123xyz\", ...)");
} else {
String endPoint = path.substring(0, path.lastIndexOf('/'));
Class<ScimResource> clazz = (Class<ScimResource>) schemaRegistry.findScimResourceClassFromEndpoint(endPoint);
if (clazz == null) {
errorOccurred = true;
createAndSetErrorResponse(operationRequest, Status.BAD_REQUEST, "path does not contain a recognized endpoint (e.g. \"/Groups/...\", \"/Users/...\", ...)");
}
}
}
break;
case PATCH: {
errorOccurred = true;
createAndSetErrorResponse(operationRequest, Status.NOT_IMPLEMENTED, "Method not implemented: PATCH");
}
break;
default: {
}
break;
}
} else if (method == null) {
errorOccurred = true;
operationRequest.setData(null);
createAndSetErrorResponse(operationRequest, Status.BAD_REQUEST, "no method provided (e.g. PUT, POST, ...");
}
if (errorOccurred) {
operationRequest.setData(null);
if (bulkIdKey != null) {
Set<String> reverseDependencies = transitiveReverseDependencies.getOrDefault(bulkIdKey, Collections.emptySet());
String detail = String.format(OPERATION_DEPENDS_ON_FAILED_OPERATION, bulkIdKey);
for (String dependentBulkIdKey : reverseDependencies) {
BulkOperation dependentOperation = bulkIdKeyToOperationResult.get(dependentBulkIdKey);
if (!(dependentOperation.getResponse() instanceof ErrorResponse)) {
dependentOperation.setData(null);
createAndSetErrorResponse(dependentOperation, Status.CONFLICT, detail);
}
}
}
}
}
boolean errorCountExceeded = false;
// do the operations
for (BulkOperation operationResult : bulkOperations) {
if (!errorCountExceeded && !(operationResult.getResponse() instanceof ErrorResponse)) {
try {
this.handleBulkOperationMethod(allUnresolveds, operationResult, bulkIdKeyToOperationResult, uriInfo);
} catch (ResourceException resourceException) {
log.error("Failed to do bulk operation", resourceException);
errorCount += errorCountIncrement;
errorCountExceeded = errorCount >= maxErrorCount;
String detail = resourceException.getLocalizedMessage();
createAndSetErrorResponse(operationResult, resourceException.getStatus(), detail);
if (operationResult.getBulkId() != null) {
String bulkIdKey = "bulkId:" + operationResult.getBulkId();
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
operationResult.setData(null);
}
} catch (UnresolvableOperationException unresolvableOperationException) {
log.error("Could not resolve bulkId during Bulk Operation method handling", unresolvableOperationException);
errorCount += errorCountIncrement;
String detail = unresolvableOperationException.getLocalizedMessage();
createAndSetErrorResponse(operationResult, Status.CONFLICT, detail);
if (operationResult.getBulkId() != null) {
String bulkIdKey = "bulkId:" + operationResult.getBulkId();
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
operationResult.setData(null);
}
}
} else if (errorCountExceeded) {
// continue processing bulk operations to cleanup any dependencies
createAndSetErrorResponse(operationResult, Status.CONFLICT, "failOnErrors count reached");
if (operationResult.getBulkId() != null) {
String bulkIdKey = "bulkId:" + operationResult.getBulkId();
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
}
}
}
// Resolve unresolved bulkIds
for (IWishJavaHadTuples iwjht : allUnresolveds) {
BulkOperation bulkOperationResult = iwjht.bulkOperationResult;
String bulkIdKey = iwjht.bulkIdKey;
ScimResource scimResource = bulkOperationResult.getData();
try {
for (UnresolvedTopLevel unresolved : iwjht.unresolveds) {
log.debug("Final resolution pass for {}", unresolved);
unresolved.resolve(scimResource, bulkIdKeyToOperationResult);
}
String scimResourceId = scimResource.getId();
@SuppressWarnings("unchecked")
Class<ScimResource> scimResourceClass = (Class<ScimResource>) scimResource.getClass();
Repository<ScimResource> repository = repositoryRegistry.getRepository(scimResourceClass);
ScimResource original = repository.get(scimResourceId);
UpdateRequest<ScimResource> updateRequest = new UpdateRequest<>(scimResourceId, original, scimResource, schemaRegistry);
repository.update(updateRequest);
} catch (UnresolvableOperationException unresolvableOperationException) {
log.error("Could not complete final resolution pass, unresolvable bulkId", unresolvableOperationException);
String detail = unresolvableOperationException.getLocalizedMessage();
bulkOperationResult.setData(null);
bulkOperationResult.setLocation(null);
createAndSetErrorResponse(bulkOperationResult, Status.CONFLICT, detail);
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
} catch (UnableToUpdateResourceException unableToUpdateResourceException) {
log.error("Failed to update Scim Resource with resolved bulkIds", unableToUpdateResourceException);
String detail = unableToUpdateResourceException.getLocalizedMessage();
bulkOperationResult.setData(null);
bulkOperationResult.setLocation(null);
createAndSetErrorResponse(bulkOperationResult, unableToUpdateResourceException.getStatus(), detail);
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
} catch (ResourceException e) {
log.error("Could not complete final resolution pass, unresolvable bulkId", e);
String detail = e.getLocalizedMessage();
bulkOperationResult.setData(null);
bulkOperationResult.setLocation(null);
createAndSetErrorResponse(bulkOperationResult, Status.NOT_FOUND, detail);
this.cleanup(bulkIdKey, transitiveReverseDependencies, bulkIdKeyToOperationResult);
}
}
Status status = errorCountExceeded ? Status.BAD_REQUEST : Status.OK;
response = new BulkResponse()
.setOperations(bulkOperations)
.setStatus(status);
return Response.status(status)
.entity(response)
.build();
}
/**
* Delete resources that depend on {@code bulkIdKeyToCleanup}, remove
* {@link BulkOperation}s data, and set their code and response
*
* @param bulkIdKeyToCleanup
* @param transitiveReverseDependencies
* @param bulkIdKeyToOperationResult
*/
private void cleanup(String bulkIdKeyToCleanup, Map<String, Set<String>> transitiveReverseDependencies, Map<String, BulkOperation> bulkIdKeyToOperationResult) {
Set<String> reverseDependencies = transitiveReverseDependencies.getOrDefault(bulkIdKeyToCleanup, Collections.emptySet());
BulkOperation operationResult = bulkIdKeyToOperationResult.get(bulkIdKeyToCleanup);
String bulkId = operationResult.getBulkId();
ScimResource scimResource = operationResult.getData();
@SuppressWarnings("unchecked")
Class<ScimResource> scimResourceClass = (Class<ScimResource>) scimResource.getClass();
Repository<ScimResource> repository = this.repositoryRegistry.getRepository(scimResourceClass);
try {
if (StringUtils.isNotBlank(scimResource.getId())) {
repository.delete(scimResource.getId());
}
} catch (ResourceException unableToDeleteResourceException) {
log.error("Could not delete ScimResource after failure: {}", scimResource);
}
for (String dependentBulkIdKey : reverseDependencies) {
BulkOperation dependentOperationResult = bulkIdKeyToOperationResult.get(dependentBulkIdKey);
if (!(dependentOperationResult.getResponse() instanceof ErrorResponse))
try {
ScimResource dependentResource = dependentOperationResult.getData();
String dependentResourceId = dependentResource.getId();
@SuppressWarnings("unchecked")
Class<ScimResource> dependentResourceClass = (Class<ScimResource>) dependentResource.getClass();
Repository<ScimResource> dependentResourceRepository = this.repositoryRegistry.getRepository(dependentResourceClass);
dependentOperationResult.setData(null);
dependentOperationResult.setLocation(null);
createAndSetErrorResponse(dependentOperationResult, Status.CONFLICT, String.format(OPERATION_DEPENDS_ON_FAILED_OPERATION, bulkId, dependentBulkIdKey));
dependentResourceRepository.delete(dependentResourceId);
} catch (ResourceException unableToDeleteResourceException) {
log.error("Could not delete depenedent ScimResource after failing to update dependee", unableToDeleteResourceException);
}
}
}
/**
* Based on the method requested by {@code operationResult}, invoke that
* method. Fill {@code unresolveds} with unresolved bulkIds and complexes that
* contain unresolved bulkIds.
*
* @param unresolveds
* @param operationResult
* @param bulkIdKeyToOperationResult
* @param uriInfo
* @throws UnableToCreateResourceException
* @throws UnableToDeleteResourceException
* @throws UnableToUpdateResourceException
* @throws UnresolvableOperationException
*/
private void handleBulkOperationMethod(List<IWishJavaHadTuples> unresolveds, BulkOperation operationResult, Map<String, BulkOperation> bulkIdKeyToOperationResult, UriInfo uriInfo) throws ResourceException, UnresolvableOperationException {
ScimResource scimResource = operationResult.getData();
Method bulkOperationMethod = operationResult.getMethod();
String bulkId = operationResult.getBulkId();
Class<ScimResource> scimResourceClass;
if (scimResource == null) {
String path = operationResult.getPath();
String endPoint = path.substring(0, path.lastIndexOf('/'));
Class<ScimResource> clazz = (Class<ScimResource>) schemaRegistry.findScimResourceClassFromEndpoint(endPoint);
scimResourceClass = clazz;
} else {
@SuppressWarnings("unchecked")
Class<ScimResource> clazz = (Class<ScimResource>) scimResource.getClass();
scimResourceClass = clazz;
}
Repository<ScimResource> repository = repositoryRegistry.getRepository(scimResourceClass);
switch (bulkOperationMethod) {
case POST: {
log.debug("POST: {}", scimResource);
this.resolveTopLevel(unresolveds, operationResult, bulkIdKeyToOperationResult);
log.debug("Creating {}", scimResource);
ScimResource newScimResource = repository.create(scimResource);
String bulkOperationPath = operationResult.getPath();
String newResourceId = newScimResource.getId();
String newResourceUri = uriInfo.getBaseUriBuilder()
.path(bulkOperationPath)
.path(newResourceId)
.build()
.toString();
if (bulkId != null) {
String bulkIdKey = "bulkId:" + bulkId;
log.debug("adding {} = {}", bulkIdKey, newResourceId);
bulkIdKeyToOperationResult.get(bulkIdKey)
.setData(newScimResource);
}
operationResult.setData(newScimResource);
operationResult.setLocation(newResourceUri);
operationResult.setPath(null);
operationResult.setStatus(StatusWrapper.wrap(Status.CREATED));
}
break;
case DELETE: {
log.debug("DELETE: {}", operationResult.getPath());
String scimResourceId = operationResult.getPath()
.substring(operationResult.getPath()
.lastIndexOf("/")
+ 1);
repository.delete(scimResourceId);
operationResult.setStatus(StatusWrapper.wrap(Status.NO_CONTENT));
}
break;
case PUT: {
log.debug("PUT: {}", scimResource);
this.resolveTopLevel(unresolveds, operationResult, bulkIdKeyToOperationResult);
String id = operationResult.getPath()
.substring(operationResult.getPath()
.lastIndexOf("/")
+ 1);
try {
ScimResource original = repository.get(id);
UpdateRequest<ScimResource> updateRequest = new UpdateRequest<>(id, original, scimResource, schemaRegistry);
repository.update(updateRequest);
operationResult.setStatus(StatusWrapper.wrap(Status.OK));
} catch (UnableToRetrieveResourceException e) {
operationResult.setStatus(StatusWrapper.wrap(Status.NOT_FOUND));
}
}
break;
default: {
BulkOperation.Method method = operationResult.getMethod();
String detail = "Method not allowed: " + method;
log.error("Received unallowed method: {}", method);
createAndSetErrorResponse(operationResult, Status.METHOD_NOT_ALLOWED, detail);
}
break;
}
}
private static void createAndSetErrorResponse(BulkOperation operationResult, int statusCode, String detail) {
createAndSetErrorResponse(operationResult, Status.fromStatusCode(statusCode), detail);
}
private static void createAndSetErrorResponse(BulkOperation operationResult, Status status, String detail) {
ErrorResponse error = new ErrorResponse(status, detail);
operationResult.setResponse(error);
operationResult.setStatus(new StatusWrapper(status));
operationResult.setPath(null);
}
@AllArgsConstructor
private static class IWishJavaHadTuples {
public final String bulkIdKey;
public final List<UnresolvedTopLevel> unresolveds;
public final BulkOperation bulkOperationResult;
}
private static class UnresolvableOperationException extends Exception {
private static final long serialVersionUID = -6081994707016671935L;
public UnresolvableOperationException(String message) {
super(message);
}
}
@AllArgsConstructor
private static class UnresolvedComplex {
private final Object object;
private final Schema.AttributeAccessor accessor;
private final String bulkIdKey;
public void resolve(Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException {
BulkOperation resolvedOperation = bulkIdKeyToOperationResult.get(this.bulkIdKey);
BaseResource response = resolvedOperation.getResponse();
ScimResource resolvedResource = resolvedOperation.getData();
if ((response == null || !(response instanceof ErrorResponse)) && resolvedResource != null) {
String resolvedId = resolvedResource.getId();
this.accessor.set(this.object, resolvedId);
} else {
throw new UnresolvableOperationException(String.format(BULK_ID_REFERS_TO_FAILED_RESOURCE, this.bulkIdKey));
}
}
}
@AllArgsConstructor
private static abstract class UnresolvedTopLevel {
protected final Schema.AttributeAccessor accessor;
public abstract void resolve(ScimResource scimResource, Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException;
}
private static class UnresolvedTopLevelBulkId extends UnresolvedTopLevel {
private final String unresolvedBulkIdKey;
public UnresolvedTopLevelBulkId(Schema.AttributeAccessor accessor, String bulkIdKey) {
super(accessor);
this.unresolvedBulkIdKey = bulkIdKey;
}
@Override
public void resolve(ScimResource scimResource, Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException {
BulkOperation resolvedOperationResult = bulkIdKeyToOperationResult.get(this.unresolvedBulkIdKey);
BaseResource response = resolvedOperationResult.getResponse();
ScimResource resolvedResource = resolvedOperationResult.getData();
if ((response == null || !(response instanceof ErrorResponse)) && resolvedResource != null) {
String resolvedId = resolvedResource.getId();
super.accessor.set(scimResource, resolvedId);
} else {
throw new UnresolvableOperationException("Bulk ID cannot be resolved because the resource it refers to had failed to be created: " + this.unresolvedBulkIdKey);
}
}
}
private static class UnresolvedTopLevelComplex extends UnresolvedTopLevel {
public final Object complex;
public final List<UnresolvedComplex> unresolveds;
public UnresolvedTopLevelComplex(Schema.AttributeAccessor accessor, Object complex, List<UnresolvedComplex> unresolveds) {
super(accessor);
this.complex = complex;
this.unresolveds = unresolveds;
}
@Override
public void resolve(ScimResource scimResource, Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException {
for (UnresolvedComplex unresolved : this.unresolveds) {
unresolved.resolve(bulkIdKeyToOperationResult);
}
this.accessor.set(scimResource, this.complex);
}
}
/**
* Search through the subattribute {@code subAttributeValue} and fill
* {@code unresolveds} with unresolved bulkIds.
*
* @param unresolveds
* @param attributeValue
* @param attribute
* @param bulkIdKeyToOperationResult
* @return
* @throws UnresolvableOperationException
*/
private static List<UnresolvedComplex> resolveAttribute(List<UnresolvedComplex> unresolveds, Object attributeValue, Schema.Attribute attribute, Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException {
if (attributeValue == null) {
return unresolveds;
}
List<Schema.Attribute> attributes = attribute.getAttributes();
for (Schema.Attribute subAttribute : attributes) {
Schema.AttributeAccessor accessor = subAttribute.getAccessor();
if (subAttribute.isScimResourceIdReference()) {
// TODO - This will fail if field is a char or Character array
String bulkIdKey = accessor.get(attributeValue);
if (bulkIdKey != null && bulkIdKey.startsWith("bulkId:")) {
log.debug("Found bulkId: {}", bulkIdKey);
if (bulkIdKeyToOperationResult.containsKey(bulkIdKey)) {
BulkOperation resolvedOperationResult = bulkIdKeyToOperationResult.get(bulkIdKey);
BaseResource response = resolvedOperationResult.getResponse();
ScimResource resolvedResource = resolvedOperationResult.getData();
if ((response == null || !(response instanceof ErrorResponse)) && resolvedResource != null && resolvedResource.getId() != null) {
String resolvedId = resolvedResource.getId();
accessor.set(attributeValue, resolvedId);
} else {
UnresolvedComplex unresolved = new UnresolvedComplex(attributeValue, accessor, bulkIdKey);
unresolveds.add(unresolved);
}
} else {
throw new UnresolvableOperationException(String.format(BULK_ID_DOES_NOT_EXIST, bulkIdKey));
}
}
} else if (subAttribute.getType() == Schema.Attribute.Type.COMPLEX) {
Object subFieldValue = accessor.get(attributeValue);
if (subFieldValue != null) {
Class<?> subFieldClass = subFieldValue.getClass();
boolean isCollection = Collection.class.isAssignableFrom(subFieldClass);
if (isCollection || subFieldClass.isArray()) {
@SuppressWarnings("unchecked")
Collection<Object> subFieldValues = isCollection ? (Collection<Object>) subFieldValue : Arrays.asList((Object[]) subFieldValue);
for (Object subArrayFieldValue : subFieldValues) {
resolveAttribute(unresolveds, subArrayFieldValue, subAttribute, bulkIdKeyToOperationResult);
}
} else {
resolveAttribute(unresolveds, subFieldValue, subAttribute, bulkIdKeyToOperationResult);
}
}
}
}
log.debug("Resolved attribute had {} unresolved fields", unresolveds.size());
return unresolveds;
}
/**
* Attempt to resolve the bulkIds referenced inside of the
* {@link ScimResource} contained inside of {@code bulkOperationResult}. Fill
* {@code unresolveds} with bulkIds that could not be yet resolved.
*
* @param unresolveds
* @param bulkOperationResult
* @param bulkIdKeyToOperationResult
* @throws UnresolvableOperationException
*/
private void resolveTopLevel(List<IWishJavaHadTuples> unresolveds, BulkOperation bulkOperationResult, Map<String, BulkOperation> bulkIdKeyToOperationResult) throws UnresolvableOperationException {
ScimResource scimResource = bulkOperationResult.getData();
String schemaUrn = scimResource.getBaseUrn();
Schema schema = this.schemaRegistry.getSchema(schemaUrn);
List<UnresolvedTopLevel> unresolvedTopLevels = new ArrayList<>();
for (Schema.Attribute attribute : schema.getAttributes()) {
Schema.AttributeAccessor accessor = attribute.getAccessor();
if (attribute.isScimResourceIdReference()) {
String bulkIdKey = accessor.get(scimResource);
if (bulkIdKey != null && bulkIdKey.startsWith("bulkId:")) {
if (bulkIdKeyToOperationResult.containsKey(bulkIdKey)) {
BulkOperation resolvedOperationResult = bulkIdKeyToOperationResult.get(bulkIdKey);
BaseResource response = resolvedOperationResult.getResponse();
ScimResource resolvedResource = resolvedOperationResult.getData();
if ((response == null || !(response instanceof ErrorResponse)) && resolvedResource != null) {
String resolvedId = resolvedResource.getId();
accessor.set(scimResource, resolvedId);
} else {
UnresolvedTopLevel unresolved = new UnresolvedTopLevelBulkId(accessor, bulkIdKey);
accessor.set(scimResource, null);
unresolvedTopLevels.add(unresolved);
}
} else {
throw new UnresolvableOperationException(String.format(BULK_ID_DOES_NOT_EXIST, bulkIdKey));
}
}
} else if (attribute.getType() == Schema.Attribute.Type.COMPLEX) {
Object attributeFieldValue = accessor.get(scimResource);
if (attributeFieldValue != null) {
List<UnresolvedComplex> subUnresolveds = new ArrayList<>();
Class<?> subFieldClass = attributeFieldValue.getClass();
boolean isCollection = Collection.class.isAssignableFrom(subFieldClass);
if (isCollection || subFieldClass.isArray()) {
@SuppressWarnings("unchecked")
Collection<Object> subFieldValues = isCollection ? (Collection<Object>) attributeFieldValue : Arrays.asList((Object[]) attributeFieldValue);
for (Object subArrayFieldValue : subFieldValues) {
resolveAttribute(subUnresolveds, subArrayFieldValue, attribute, bulkIdKeyToOperationResult);
}
} else {
resolveAttribute(subUnresolveds, attributeFieldValue, attribute, bulkIdKeyToOperationResult);
}
if (subUnresolveds.size() > 0) {
UnresolvedTopLevel unresolved = new UnresolvedTopLevelComplex(accessor, attributeFieldValue, subUnresolveds);
accessor.set(scimResource, null);
unresolvedTopLevels.add(unresolved);
}
}
}
}
if (unresolvedTopLevels.size() > 0) {
String bulkIdKey = "bulkId:" + bulkOperationResult.getBulkId();
unresolveds.add(new IWishJavaHadTuples(bulkIdKey, unresolvedTopLevels, bulkOperationResult));
}
}
/**
* Traverse the provided dependency graph and fill {@code visited} with
* visited bulkIds.
*
* @param visited
* @param dependencyGraph
* @param root
* @param current
*/
private static void generateVisited(Set<String> visited, Map<String, Set<String>> dependencyGraph, String root, String current) {
if (!root.equals(current) && !visited.contains(current)) {
visited.add(current);
Set<String> dependencies = dependencyGraph.getOrDefault(current, Collections.emptySet());
for (String dependency : dependencies) {
generateVisited(visited, dependencyGraph, root, dependency);
}
}
}
/**
* If A -> {B} and B -> {C} then A -> {B, C}.
*
* @param dependenciesGraph
* @return
*/
private static Map<String, Set<String>> generateTransitiveDependenciesGraph(Map<String, Set<String>> dependenciesGraph) {
Map<String, Set<String>> transitiveDependenciesGraph = new HashMap<>();
for (Map.Entry<String, Set<String>> entry : dependenciesGraph.entrySet()) {
String root = entry.getKey();
Set<String> dependencies = entry.getValue();
Set<String> visited = new HashSet<>();
transitiveDependenciesGraph.put(root, visited);
for (String dependency : dependencies) {
generateVisited(visited, dependenciesGraph, root, dependency);
}
}
return transitiveDependenciesGraph;
}
private static void generateReverseDependenciesGraph(Map<String, Set<String>> reverseDependenciesGraph, String dependentBulkId, Object scimObject, List<Schema.Attribute> scimObjectAttributes) {
for (Schema.Attribute scimObjectAttribute : scimObjectAttributes)
if (scimObjectAttribute.isScimResourceIdReference()) {
String reference = scimObjectAttribute.getAccessor().get(scimObject);
if (reference != null && reference.startsWith("bulkId:")) {
Set<String> dependents = reverseDependenciesGraph.computeIfAbsent(reference, (unused) -> new HashSet<>());
dependents.add("bulkId:" + dependentBulkId);
}
} else if (scimObjectAttribute.isMultiValued()) { // all multiValueds
// are COMPLEX, not
// all COMPLEXES are
// multiValued
Object attributeObject = scimObjectAttribute.getAccessor().get(scimObject);
if (attributeObject != null) {
Class<?> attributeObjectClass = attributeObject.getClass();
boolean isCollection = Collection.class.isAssignableFrom(attributeObjectClass);
Collection<?> attributeValues = isCollection ? (Collection<?>) attributeObject : List.of(attributeObject);
List<Schema.Attribute> subAttributes = scimObjectAttribute.getAttributes();
for (Object attributeValue : attributeValues) {
generateReverseDependenciesGraph(reverseDependenciesGraph, dependentBulkId, attributeValue, subAttributes);
}
}
} else if (scimObjectAttribute.getType() == Schema.Attribute.Type.COMPLEX) {
Object attributeValue = scimObjectAttribute.getAccessor().get(scimObject);
List<Schema.Attribute> subAttributes = scimObjectAttribute.getAttributes();
generateReverseDependenciesGraph(reverseDependenciesGraph, dependentBulkId, attributeValue, subAttributes);
}
}
/**
* Finds the reverse dependencies of each {@link BulkOperation}.
*
* @param bulkOperations
* @return
*/
private Map<String, Set<String>> generateReverseDependenciesGraph(List<BulkOperation> bulkOperations) {
Map<String, Set<String>> reverseDependenciesGraph = new HashMap<>();
for (BulkOperation bulkOperation : bulkOperations) {
String bulkId = bulkOperation.getBulkId();
if (bulkId != null) {
ScimResource scimResource = bulkOperation.getData();
String scimResourceBaseUrn = scimResource.getBaseUrn();
Schema schema = this.schemaRegistry.getSchema(scimResourceBaseUrn);
List<Schema.Attribute> attributes = schema.getAttributes();
generateReverseDependenciesGraph(reverseDependenciesGraph, bulkId, scimResource, attributes);
}
}
return reverseDependenciesGraph;
}
}