All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.captaingoldfish.scim.sdk.client.builder.BulkBuilder Maven / Gradle / Ivy

There is a newer version: 1.26.0
Show newest version
// Generated by delombok at Sat Oct 07 17:30:34 CEST 2023
package de.captaingoldfish.scim.sdk.client.builder;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import com.fasterxml.jackson.databind.JsonNode;
import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
import de.captaingoldfish.scim.sdk.client.http.HttpResponse;
import de.captaingoldfish.scim.sdk.client.http.ScimHttpClient;
import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
import de.captaingoldfish.scim.sdk.common.constants.AttributeNames;
import de.captaingoldfish.scim.sdk.common.constants.EndpointPaths;
import de.captaingoldfish.scim.sdk.common.constants.HttpStatus;
import de.captaingoldfish.scim.sdk.common.constants.SchemaUris;
import de.captaingoldfish.scim.sdk.common.constants.enums.HttpMethod;
import de.captaingoldfish.scim.sdk.common.etag.ETag;
import de.captaingoldfish.scim.sdk.common.request.BulkRequest;
import de.captaingoldfish.scim.sdk.common.request.BulkRequestOperation;
import de.captaingoldfish.scim.sdk.common.resources.ServiceProvider;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimObjectNode;
import de.captaingoldfish.scim.sdk.common.resources.complex.BulkConfig;
import de.captaingoldfish.scim.sdk.common.response.BulkResponse;
import de.captaingoldfish.scim.sdk.common.response.BulkResponseOperation;
import de.captaingoldfish.scim.sdk.common.response.ErrorResponse;
import de.captaingoldfish.scim.sdk.common.tree.GenericTree;
import de.captaingoldfish.scim.sdk.common.tree.TreeNode;
import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;


/**
 * author Pascal Knueppel 
* created at: 08.03.2020
*
*/ public class BulkBuilder extends RequestBuilder { @java.lang.SuppressWarnings("all") private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BulkBuilder.class); /** * the builder object to build the bulk request */ private final BulkRequest.BulkRequestBuilder builder; /** * the bulk request operations that should be executed */ private final List bulkRequestOperationList; /** * a thread safe-map that holds the request operations references. This can be used by client implementations * to compare the request with the returned response operations. This might be useful to write log-messages * based on the requests content. */ private final Map bulkRequestOperationMap; /** * the fully qualified url to the required resource */ private final String fullUrl; /** * contains the configuration of the service provider that is used to determine the max-operations of a bulk * request and to help to split the operations into several requests if necessary.
* This object might be null */ private Supplier serviceProviderSupplier; /** * if the resource should be retrieved by using the fully qualified url * * @param baseUrl the fully qualified url to the required resource * @param scimHttpClient the http client instance * @param isFullUrl if the given base url is the fully qualified url or not * @param serviceProviderSupplier contains the configuration of the service provider that is used to determine * the max-operations of a bulk request and to help to split the operations into several requests if * necessary.
* This object might be null */ public BulkBuilder(String baseUrl, ScimHttpClient scimHttpClient, boolean isFullUrl, Supplier serviceProviderSupplier) { super(isFullUrl ? null : baseUrl, EndpointPaths.BULK, BulkResponse.class, scimHttpClient); builder = BulkRequest.builder(); bulkRequestOperationList = Collections.synchronizedList(new ArrayList<>()); bulkRequestOperationMap = new ConcurrentHashMap<>(); builder.bulkRequestOperation(bulkRequestOperationList); this.fullUrl = isFullUrl ? baseUrl : null; this.serviceProviderSupplier = Optional.ofNullable(serviceProviderSupplier).orElse(() -> null); } /** * {@inheritDoc} */ @Override public BulkBuilder setExpectedResponseHeaders(Map requiredResponseHeaders) { return (BulkBuilder)super.setExpectedResponseHeaders(requiredResponseHeaders); } /** * retrieves a request operation from the builder by its bulkId. Modifying the returned operation will also * modify the request * * @param bulkId the bulkId of the operation that should be returned * @return the request operation with the matching bulkId */ public BulkRequestOperation getOperationByBulkId(String bulkId) { return bulkRequestOperationMap.get(bulkId); } /** * {@inheritDoc} */ @Override protected boolean isExpectedResponseCode(int httpStatus) { return HttpStatus.OK == httpStatus; } /** * {@inheritDoc} */ @Override protected HttpUriRequest getHttpUriRequest() { HttpPost httpPost; if (StringUtils.isBlank(fullUrl)) { httpPost = new HttpPost(getBaseUrl() + getEndpoint()); } else { httpPost = new HttpPost(fullUrl); } StringEntity stringEntity = new StringEntity(getResource(), StandardCharsets.UTF_8); httpPost.setEntity(stringEntity); return httpPost; } /** * {@inheritDoc} */ protected HttpUriRequest getHttpUriRequest(JsonNode requestData) { HttpPost httpPost; if (StringUtils.isBlank(fullUrl)) { httpPost = new HttpPost(getBaseUrl() + getEndpoint()); } else { httpPost = new HttpPost(fullUrl); } StringEntity stringEntity = new StringEntity(requestData.toString(), StandardCharsets.UTF_8); httpPost.setEntity(stringEntity); return httpPost; } /** * overrides the default method from the superclass to have easier control of the resource that will be put * into the request body */ @Override public String getResource() { return builder.build().toString(); } /** * checks if the response contains a schema-uri that matches the value of * {@link de.captaingoldfish.scim.sdk.common.constants.SchemaUris#BULK_RESPONSE_URI} */ @Override protected Function isResponseParseable() { return httpResponse -> { String responseBody = httpResponse.getResponseBody(); if (StringUtils.isNotBlank(responseBody) && responseBody.contains(SchemaUris.BULK_RESPONSE_URI)) { return true; } return false; }; } /** * sets how many errors are allowed on the server side before the request should be rolled back * * @param failOnErrors the number of errors that are accepted on the server side */ public BulkBuilder failOnErrors(Integer failOnErrors) { builder.failOnErrors(failOnErrors); return this; } /** * sets the path to the resource endpoint e.g. "/Users" or "/Groups" */ public BulkRequestOperationCreator bulkRequestOperation(String path) { return bulkRequestOperation(path, null); } /** * sets the path to the resource endpoint e.g. "/Users" or "/Groups" * * @param path "/Users", "/Groups" or any other registered resource path * @param id the id of an existing resource in case of patch, update or delete */ public BulkRequestOperationCreator bulkRequestOperation(String path, String id) { String idPath = StringUtils.isBlank(id) ? "" : "/" + id; return new BulkRequestOperationCreator(this, path + idPath); } /** * adds the given list of operations */ public BulkBuilder addOperations(List requestOperations) { for ( BulkRequestOperation requestOperation : requestOperations ) { if (!requestOperation.getBulkId().isPresent()) { requestOperation.setBulkId(UUID.randomUUID().toString()); } bulkRequestOperationMap.put(requestOperation.getBulkId().get(), requestOperation); } bulkRequestOperationList.addAll(requestOperations); return this; } /** * send the request to the server * * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break * @return the response from the server */ public ServerResponse sendRequest(boolean runSplittedRequestsParallel) { return sendRequestWithMultiHeaders(Collections.emptyMap(), null, runSplittedRequestsParallel); } /** * send the request to the server * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled * @return the response from the server */ public ServerResponse sendRequest(Consumer> responseHandler) { return sendRequestWithMultiHeaders(Collections.emptyMap(), responseHandler, false); } /** * send the request to the server * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break * @return the response from the server */ public ServerResponse sendRequest(Consumer> responseHandler, boolean runSplittedRequestsParallel) { return sendRequestWithMultiHeaders(Collections.emptyMap(), responseHandler, runSplittedRequestsParallel); } /** * send the request to the server * * @param headers the http headers to send additionally to the default headery within the request * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled * @return the response from the server */ public ServerResponse sendRequest(Map headers, Consumer> responseHandler) { return sendRequest(headers, responseHandler, false); } /** * send the request to the server * * @param headers the http headers to send additionally to the default headery within the request * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break * @return the response from the server */ public ServerResponse sendRequest(Map headers, Consumer> responseHandler, boolean runSplittedRequestsParallel) { Map multiHeader = new HashMap<>(); headers.forEach((key, value) -> multiHeader.put(key, new String[]{value})); return sendRequestWithMultiHeaders(multiHeader, responseHandler, runSplittedRequestsParallel); } /** * {@inheritDoc} */ @Override public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders) { return sendRequestWithMultiHeaders(httpHeaders, null, false); } /** * {@inheritDoc} * * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break */ public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders, boolean runSplittedRequestsParallel) { return sendRequestWithMultiHeaders(httpHeaders, null, runSplittedRequestsParallel); } /** * {@inheritDoc} * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled */ public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders, Consumer> responseHandler) { return sendRequestWithMultiHeaders(httpHeaders, responseHandler, false); } /** * {@inheritDoc} * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break */ public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders, Consumer> responseHandler, boolean runSplittedRequestsParallel) { final int maxNumberOfOperationns = getMaxNumberOfOperations(); final boolean isSplittingFeatureDisabled = !getScimHttpClient().getScimClientConfig() .isEnableAutomaticBulkRequestSplitting(); final boolean fitsIntoASingleRequest = bulkRequestOperationList.size() <= maxNumberOfOperationns; if (isSplittingFeatureDisabled || fitsIntoASingleRequest) { return super.sendRequestWithMultiHeaders(httpHeaders); } return sendMultipleBulkRequests(httpHeaders, responseHandler, runSplittedRequestsParallel); } /** * retrieves the current maximum number of operations allowed at the bulk endpoint */ private int getMaxNumberOfOperations() { return Optional.ofNullable(serviceProviderSupplier.get()) .map(ServiceProvider::getBulkConfig) .map(BulkConfig::getMaxOperations) .orElse(Integer.MAX_VALUE); } /** * splits the currently created bulk-request into several requests and tries to sort them based on their * dependencies of other resources. Afterwards all responses will be put together into a single response * object. * * @param httpHeaders allows the user to add additional http headers to the request * @param responseHandler a helper method to will allow the client to react to each individual response * @param runSplittedRequestsParallel if the requests should be run parallel. This is only recommended if no * relations between the different bulk-request-operations are set. So if no bulkId-references are * set. Otherwise, the relation between these requests might break * @return a composed response object that results from several responses of the different requests */ private ServerResponse sendMultipleBulkRequests(Map httpHeaders, Consumer> responseHandler, boolean runSplittedRequestsParallel) { try { boolean containsBulkIdReferences = getResource().contains(String.format("\"%s:", AttributeNames.RFC7643.BULK_ID)); BulkRequestIdResolverWrapper bulkRequestIdResolverWrapper; if (containsBulkIdReferences) { // sort operations and split with relations preserved bulkRequestIdResolverWrapper = splitRequestsWithRelationOrderPreserved(); } else { // simply split the requests List> bulkRequestOperationRequestList = splitRequestsSimple(bulkRequestOperationList); bulkRequestIdResolverWrapper = new BulkRequestIdResolverWrapper(bulkRequestOperationRequestList, new HashMap<>()); } BulkResponse compositeBulkResponse = new BulkResponse(); List> bulkRequestOperationsList = bulkRequestIdResolverWrapper.getRequestsList(); // @formatter:off ArrayBlockingQueue> serverResponseList = new ArrayBlockingQueue<>(bulkRequestOperationsList.size()); // @formatter:on ServiceProvider serviceProvider = serviceProviderSupplier.get(); ScimClientConfig scimClientConfig = getScimHttpClient().getScimClientConfig(); IntStream bulkOperationIndexStream = IntStream.range(0, bulkRequestOperationsList.size()); Function runInPool = runnable -> { serviceProvider.getThreadPool().awaitQuiescence(scimClientConfig.getSocketTimeout(), TimeUnit.SECONDS); return serviceProvider.getThreadPool().submit(runnable); }; Runnable runnable = () -> { IntStream indexStream = runSplittedRequestsParallel ? bulkOperationIndexStream.parallel() : bulkOperationIndexStream; indexStream.forEach(index -> { List bulkRequestOperations = bulkRequestOperationsList.get(index); log.debug("Handling bulk request \'{}\' of \'{}\' with \'{}\' operations.", index + 1, bulkRequestIdResolverWrapper.getRequestsList().size(), bulkRequestOperations.size()); boolean isFullUrl = getBaseUrl() == null; replaceBulkRequestOperations(bulkRequestOperations, bulkRequestIdResolverWrapper); BulkBuilder splitBulkBuilder = new BulkBuilder(getBaseUrl(), getScimHttpClient(), isFullUrl, serviceProviderSupplier); Integer failOnErrors = builder.getFailOnErrors(); splitBulkBuilder.failOnErrors(failOnErrors).addOperations(bulkRequestOperations); // the request in the super-class is created from the builder, so we need to replace the original here. and // afterwards we are changing it back to restore the original state ServerResponse response = splitBulkBuilder.sendRequestWithMultiHeaders(httpHeaders); log.debug("Received response for bulk request \'{}\' of \'{}\'.", index + 1, bulkRequestIdResolverWrapper.getRequestsList().size()); validateResponseAndResolveResults(bulkRequestOperations, bulkRequestIdResolverWrapper, response, compositeBulkResponse); Optional.ofNullable(responseHandler).ifPresent(handler -> handler.accept(response)); serverResponseList.add(response); }); }; runInPool.apply(runnable).get(); log.debug("Finished handling all bulk requests. The requests will be merged and returned in a single " + "response-object"); // validate responses and also content of responses // if no error occurred until now everything is fine and all operations completed successfully HttpResponse httpResponse = // take the response headers from any request they should be the same HttpResponse.builder() .httpStatusCode(HttpStatus.OK) .responseBody(compositeBulkResponse.toString()) .responseHeaders(Optional.ofNullable(serverResponseList.peek()) .map(ServerResponse::getHttpHeaders) .orElse(Collections.emptyMap())) .build(); return new ServerResponse<>(httpResponse, true, BulkResponse.class, isResponseParseable(), getRequiredResponseHeaders()); } catch (final java.lang.Throwable $ex) { throw lombok.Lombok.sneakyThrow($ex); } } /** * replaces the bulkId references with the children ids from previous requests * * @param bulkRequestOperations the current operations that should be executed next * @param bulkRequestIdResolverWrapper this object contains the results from previous requests that need to be * added into the new structure */ private void replaceBulkRequestOperations(List bulkRequestOperations, BulkRequestIdResolverWrapper bulkRequestIdResolverWrapper) { for ( int i = 0 ; i < bulkRequestOperations.size() ; i++ ) { BulkRequestOperation bulkRequestOperation = bulkRequestOperations.get(i); String operationString = bulkRequestOperation.toString(); final String bulkId = bulkRequestOperation.getBulkId().orElse(null); if (bulkId == null) { throw new IllegalStateException("Cannot use auto-splitting feature for bulk requests if the bulkId " + "elements are missing. Please assign a bulkId to each single operation!"); } List childOperationList = bulkRequestIdResolverWrapper.getParentChildRelationMap() .get(bulkId); if (childOperationList == null) { break; } for ( BulkRequestOperation childOperation : childOperationList ) { final String childBulkId = childOperation.getBulkId().get(); final String childResourceId = bulkRequestIdResolverWrapper.getResolvedBulkIds().get(childBulkId); final String oldReference = String.format("\"%s:%s\"", AttributeNames.RFC7643.BULK_ID, childBulkId); final String newReference = String.format("\"%s\"", childResourceId); operationString = operationString.replaceAll(oldReference, newReference); } bulkRequestOperations.remove(i); BulkRequestOperation newOperation = JsonHelper.readJsonDocument(operationString, BulkRequestOperation.class); bulkRequestOperations.add(i, newOperation); } } /** * validates the response from the server and resolves the necessary for upcoming secondary requests * * @param bulkRequestOperations the list of operations that were just executed * @param bulkRequestIdResolverWrapper the wrapper object that shall be extended by the created ids of * previous requests * @param response the response from the server * @param compositeBulkResponse a composition of response-operations. Since we are sending several requests we * will gather all response-operations in a single BulkResponse that will be returned */ private void validateResponseAndResolveResults(List bulkRequestOperations, BulkRequestIdResolverWrapper bulkRequestIdResolverWrapper, ServerResponse response, BulkResponse compositeBulkResponse) { if (!response.isSuccess()) { log.error("Bulk error on automatically splitted requests. Please note that this might cause unwanted results " + "on the server that need to be fixed manually. The following log messages shall help identifying " + "the problem:"); log.error("The following request operations were not successful: \n{}", bulkRequestOperations.stream().map(ScimObjectNode::toPrettyString).collect(Collectors.joining("\n"))); log.error("Response from the server: {}", Optional.ofNullable(response.getErrorResponse()) .map(ErrorResponse::toPrettyString) .orElseGet(response::getResponseBody)); final int indexOfFailedRequest = bulkRequestIdResolverWrapper.getRequestsList().indexOf(bulkRequestOperations); if (indexOfFailedRequest > 0) { String successOperations = bulkRequestIdResolverWrapper.getRequestsList() .subList(0, indexOfFailedRequest) .stream() .flatMap(Collection::stream) .map(ScimObjectNode::toPrettyString) .collect(Collectors.joining("\n")); log.error("The following operations were executed successfully on the server and were persisted: \n{}", successOperations); } throw new IllegalStateException(String.format("The bulk request failed with status: %s and message: %s", response.getHttpStatus(), response.getResponseBody())); } BulkResponse bulkResponse = response.getResource(); for ( BulkResponseOperation bulkResponseOperation : bulkResponse.getBulkResponseOperations() ) { final String bulkId = bulkResponseOperation.getBulkId().orElseThrow(() -> { return new IllegalStateException("Missing bulkId in response cannot resolve relations of split operations."); }); final String resourceId = bulkResponseOperation.getResourceId().orElseGet(() -> { return getIdFromLocationAttribute(bulkResponseOperation); }); // if the resourceId is null the operation did fail if (resourceId != null) { bulkRequestIdResolverWrapper.getResolvedBulkIds().put(bulkId, resourceId); } List compositeOperations = compositeBulkResponse.getBulkResponseOperations(); compositeOperations.add(bulkResponseOperation); compositeBulkResponse.setBulkResponseOperations(compositeOperations); } } /** * extracted into its own method for unit tests. */ protected String getIdFromLocationAttribute(BulkResponseOperation bulkResponseOperation) { String[] locationParts = bulkResponseOperation.getLocation().map(s -> s.split("/")).orElse(null); if (locationParts == null) { return null; } return locationParts[locationParts.length - 1]; } /** * splits the list of operations into several lists with the relation order intact. This means that requests * containing bulkId relations will be put together. If this is not possible because the resulting list is * still too large we will try to send a first request in order to resolve the parent-child relationship step * by step.
*
* the method code is build with the following tree in mind and the service provider having a maxOperations * value. The relationships are based on the bulkId references meaning that node 488 has a value-field with a * string representation of "bulkId:111" and another one with "bulkId:523" * *
   *     ServiceProviderConfig.BulkConfig.maxOperations = 5
   *
   *     (936)              (619)              (219)              (333)              (987)              (222)
   *       |                                     |                  |
   *       |                                     |                  |
   *     (123)                                  / \                 |
   *       |  \                                /   \               /|
   *       |   \                              /     \             / |
   *       |    ---------- (443)--------------       - (488) -----  |
   *       |                                             |          |
   *       |                                             |          |
   *       |                                            / \         |
   *     (582)                                         /   \        |
   *                                                  /     \       |
   *                                                 /       \      |
   *                                               (523)      |     |
   *                                                 |        |     |
   *                                                  \       |     |
   *                                                   \      |     |
   *                                                     -- (111) --
   * 
* * unparented leaf-nodes are (619), (987) and (222). These nodes can be isolated and do not need any specific * attention, so we store the operations in a separate list and remove them from the tree.
* Then we will isolate the leaf-nodes which are (582), (443) and (111). We will create a {@link Map} element * that uses the bulkIds of the roots as keys and stores their children under the key. Afterwards the leafs * will be deleted from the tree and so parent-child relationship will no longer be represented by this tree * but by the created {@link Map}
* Now we are doing this again until the number of maximum operations is reached. We will create a first list * of operations. In the end we will return a wrapper object with two objects. First a two-dimensional list * that contains the operations in the order they should be executed and the number of lists represents the * number of requests that will be sent to the server. The second object is the {@link Map} that represents * the parent-child relations, so that we can replace bulkId-references after each request within the map. */ private BulkRequestIdResolverWrapper splitRequestsWithRelationOrderPreserved() { final int maxNumberOfOperationns = getMaxNumberOfOperations(); // determine parent-child relations. We need this to build an operation-relation-chain GenericTree childParentRelationsTree = getParentChildRelationsOfRequest(); List unparentedOperations = extractUnparentedOperationsFromTree(childParentRelationsTree); // now we removed the unparented operations. In the tree-example above this would be (619), (987) and (222) // this nested list will represent the requests that will eventually be sent to the server. Each list // represents a single request List> requestLists = new ArrayList<>(); // the children of any node will be put with its bulkId into this map Map> parentChildRelationMap = new HashMap<>(); for ( TreeNode treeNode : childParentRelationsTree.getAllNodes() ) { if (treeNode.isLeaf()) { continue; } parentChildRelationMap.put(treeNode.getValue().getBulkId().get(), treeNode.getChildren().stream().map(TreeNode::getValue).collect(Collectors.toList())); } // now iterate for as long as the tree has still nodes left while (childParentRelationsTree.hasNodes()) { List operationList = new ArrayList<>(); // iterate for as long as the tree has nodes left and the operation-list is not full while (operationList.size() != maxNumberOfOperationns && childParentRelationsTree.hasNodes()) { // iterate over the tree-leafs and enter them into the operations list until the list matches its maximum // number of operations for ( TreeNode leaf : childParentRelationsTree.getLeafs() ) { childParentRelationsTree.removeNodeFromTree(leaf); operationList.add(leaf.getValue()); if (operationList.size() == maxNumberOfOperationns) { break; } } // if no nodes are left anymore we are finished. This will end both loops inner and outer if (!childParentRelationsTree.hasNodes()) { requestLists.add(operationList); break; } // if the operations-list is not full yet, we go into the next iteration and add more operations to the list boolean areMoreOperationsAvailable = operationList.size() < maxNumberOfOperationns; if (areMoreOperationsAvailable) { continue; } // this case represents the case that the operation list is full so we will go into the next iteration phase // of the outer-loop, so add the operations-list to the requests-list. requestLists.add(operationList); } } /* * taking the example tree above when iterating from left to right we will get the following result: * * // @formatter:off * unparentedOperations: * [(619), (987), (222)] * * requestLists: * [0] (582), (443), (111), (123), (523) * [1] (936), (488), (219), (333) * // @formatter:on * * as last operation we need to merge the unparented operations at the end of the list to get the following result: * * // @formatter:off * requestLists: * [0] (582), (443), (111), (123), (523) * [1] (936), (488), (219), (333), (619) * [2] (987), (222) * // @formatter:on */ List lastParentedList = requestLists.get(requestLists.size() - 1); // fill the last list with the unparented operations list if (lastParentedList.size() < maxNumberOfOperationns) { while (!unparentedOperations.isEmpty() && lastParentedList.size() < maxNumberOfOperationns) { BulkRequestOperation bulkRequestOperation = unparentedOperations.get(0); unparentedOperations.remove(0); lastParentedList.add(bulkRequestOperation); } } // if the unparented operationslist is still not empty we need to add additional lists to the requests-list if (!unparentedOperations.isEmpty()) { List> splittedLists = splitRequestsSimple(unparentedOperations); requestLists.addAll(splittedLists); } return new BulkRequestIdResolverWrapper(requestLists, parentChildRelationMap); } /** * gets all nodes from the tree that are leafs and roots and the same time and removes them from the tree */ private List extractUnparentedOperationsFromTree(GenericTree childParentRelationsTree) { List unparentedOperations = new ArrayList<>(); // first isolate all operations that do not have parents and thus do not have other operations referenced for ( TreeNode leaf : childParentRelationsTree.getLeafs() ) { boolean isNodeWithoutRelationsships = leaf.isLeaf() && leaf.isRoot(); if (isNodeWithoutRelationsships) { unparentedOperations.add(leaf.getValue()); childParentRelationsTree.removeNodeFromTree(leaf); } } return unparentedOperations; } /** * this method tries its best to identify parent-child relations in the request and will place them in a map * based on the child-entry so that we can identify the parents of the operation. For example if one operation * has several bulkId references set within the code each reference is expected to be a parent of the current * operation because this operation relies on the existence of the other resources. * * @return a multi-parent-tree representation of the child-parent-relationships within the request */ private GenericTree getParentChildRelationsOfRequest() { final String regex = String.format("\"%s:(.*?)\"", AttributeNames.RFC7643.BULK_ID); Pattern bulkIdPattern = Pattern.compile(regex); GenericTree childParentRelations = new GenericTree<>(); for ( BulkRequestOperation bulkRequestOperation : getBulkRequestOperationList() ) { final String currentResource = bulkRequestOperation.toString(); Matcher bulkIdMatcher = bulkIdPattern.matcher(currentResource); TreeNode parentNode = childParentRelations.addDistinctNode(bulkRequestOperation); while (bulkIdMatcher.find()) { final String bulkId = bulkIdMatcher.group(1); BulkRequestOperation operation = bulkRequestOperationList.stream() .filter(op -> bulkId.equals(op.getBulkId() .orElse(null))) .findAny() .orElseThrow(() -> { String error = "found illegal bulkId in request \'" + bulkId + "\': has no parent."; return new IllegalStateException(error); }); TreeNode childNode = childParentRelations.addDistinctNode(operation); parentNode.addChild(childNode); } } return childParentRelations; } /** * splits the list of operations into several lists so that all lists contain equal or fewer operations than * the maximum number of allowed requests at the service provider */ private List> splitRequestsSimple(List operationsToSplit) { final int maxNumberOfOperationns = getMaxNumberOfOperations(); List> splittedListParts = new ArrayList<>(); if (operationsToSplit.size() <= maxNumberOfOperationns) { splittedListParts.add(operationsToSplit); return splittedListParts; } int currentIndex = 0; while (currentIndex < operationsToSplit.size()) { final int nextIndex = currentIndex + maxNumberOfOperationns; final int effectiveListIndex = Math.min(nextIndex, operationsToSplit.size()); List subList = operationsToSplit.subList(currentIndex, effectiveListIndex); splittedListParts.add(new ArrayList<>(subList)); currentIndex += subList.size(); } log.debug("Splitted bulk operations into \'{}\' individual bulk-requests", splittedListParts.size()); return splittedListParts; } /** * an additional build step class that allows to set the values of a bulk operation */ public static class BulkRequestOperationCreator { /** * the owning top level class reference */ private final BulkBuilder bulkBuilder; /** * the builder object that is used to build the operation */ private final BulkRequestOperation.BulkRequestOperationBuilder builder = BulkRequestOperation.builder(); public BulkRequestOperationCreator(BulkBuilder bulkBuilder, String path) { this.bulkBuilder = bulkBuilder; builder.path(path); } /** * sets the http method for this bulk operation */ public BulkRequestOperationCreator method(HttpMethod method) { builder.method(method); return this; } /** * sets the bulkId for this operation. Required if http method is post and optional in any other cases */ public BulkRequestOperationCreator bulkId(String bulkId) { builder.bulkId(bulkId); return this; } /** * sets the request body for this operation if any is required */ public BulkRequestOperationCreator data(String data) { builder.data(data); return this; } /** * sets the request body for this operation if any is required */ public BulkRequestOperationCreator data(JsonNode data) { builder.data(data.toString()); return this; } /** * sets the etag version for this operation which may be used on update, path and delete requests */ public BulkRequestOperationCreator version(ETag version) { builder.version(version); return this; } /** * asks the server to return the resource within the bulk response. This feature is supported only by the * SCIM-SDK implementation. * * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/wiki/Return-resources-on-Bulk-Responses */ public BulkRequestOperationCreator returnResource(boolean returnResource) { builder.returnResource(returnResource); return this; } /** * only usable for the SCIM-SDKs Bulk-Get custom feature. It limits the amount of resources to be returned * from the server if the bulk-get feature is utilized */ public BulkRequestOperationCreator maxResourceLevel(int maxResourceLevel) { builder.maxResourceLevel(maxResourceLevel); return this; } /** * @return builds the operation object and returns to the owning top level instance */ public BulkBuilder next() { BulkRequestOperation operation = builder.build(); bulkBuilder.getBulkRequestOperationList().add(operation); if (!operation.getBulkId().isPresent()) { operation.setBulkId(UUID.randomUUID().toString()); } bulkBuilder.bulkRequestOperationMap.put(operation.getBulkId().get(), operation); return bulkBuilder; } /** * builds the operation and directly sends the request to the server */ public ServerResponse sendRequest() { return sendRequest(Collections.emptyMap()); } /** * builds the operation and directly sends the request to the server * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled */ public ServerResponse sendRequest(Consumer> responseHandler) { return sendRequest(Collections.emptyMap(), responseHandler); } /** * builds the operation and directly sends the request to the server */ public ServerResponse sendRequest(Map httpHeaders) { return next().sendRequest(httpHeaders); } /** * builds the operation and directly sends the request to the server * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled */ public ServerResponse sendRequest(Map httpHeaders, Consumer> responseHandler) { return next().sendRequest(httpHeaders, responseHandler); } /** * builds the operation and directly sends the request to the server */ public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders) { return next().sendRequestWithMultiHeaders(httpHeaders); } /** * builds the operation and directly sends the request to the server * * @param responseHandler a helper method to will allow the client to react to each individual response. This * makes only sense if the feature {@link ScimClientConfig#isEnableAutomaticBulkRequestSplitting()} * is enabled */ public ServerResponse sendRequestWithMultiHeaders(Map httpHeaders, Consumer> responseHandler) { return next().sendRequestWithMultiHeaders(httpHeaders, responseHandler); } } /** * this wrapper object is used when resolving bulkId-requests into several requests */ private static class BulkRequestIdResolverWrapper { /** * an ordered list that must be executed in the order presented */ private final List> requestsList; /** * a parent child relationship map that uses the bulkId of the parents as keys and has its children as values */ private final Map> parentChildRelationMap; /** * after each request the results will be stored within this map in order to resolve the references of the * next request */ private final Map resolvedBulkIds; public BulkRequestIdResolverWrapper(List> requestsList, Map> parentChildRelationMap) { this.requestsList = Objects.requireNonNull(requestsList); this.parentChildRelationMap = Objects.requireNonNull(parentChildRelationMap); this.resolvedBulkIds = new HashMap<>(); } @java.lang.SuppressWarnings("all") public List> getRequestsList() { return this.requestsList; } @java.lang.SuppressWarnings("all") public Map> getParentChildRelationMap() { return this.parentChildRelationMap; } @java.lang.SuppressWarnings("all") public Map getResolvedBulkIds() { return this.resolvedBulkIds; } } /** * the bulk request operations that should be executed */ @java.lang.SuppressWarnings("all") public List getBulkRequestOperationList() { return this.bulkRequestOperationList; } /** * contains the configuration of the service provider that is used to determine the max-operations of a bulk * request and to help to split the operations into several requests if necessary.
* This object might be null */ @java.lang.SuppressWarnings("all") public void setServiceProviderSupplier(final Supplier serviceProviderSupplier) { this.serviceProviderSupplier = serviceProviderSupplier; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy