com.atlassian.bamboo.specs.util.RestHelper Maven / Gradle / Ivy
package com.atlassian.bamboo.specs.util;
import com.atlassian.bamboo.specs.api.exceptions.PropertiesValidationException;
import com.atlassian.bamboo.specs.exceptions.BambooSpecsRestRequestException;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestSchemeFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.StreamSupport;
public class RestHelper {
private static final Logger log = Logger.getLogger(RestHelper.class);
private static final String MIME_TYPE_APPLICATION_X_YAML = "application/x-yaml";
private static String sendRequest(final HttpRequestBase request,
final AuthenticationProvider authenticationProvider) throws IOException {
final Registry registry = RegistryBuilder.create().register(AuthSchemes.DIGEST, new DigestSchemeFactory()).build();
log.trace("%s - sending", request);
try {
authenticationProvider.authenticate(request);
} catch (final AuthenticationException exception) {
throw new RuntimeException(exception);
}
try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultAuthSchemeRegistry(registry).build()) {
final ResponseHandler responseHandler = response -> {
final int status = response.getStatusLine().getStatusCode();
final Optional responseEntityAsString = getResponseEntityAsString(response);
if (status >= 200 && status < 300) {
log.trace("%s - successful, status: %d", request, status);
responseEntityAsString.ifPresent(log::trace);
return "Result OK: " + responseEntityAsString.orElse("");
} else {
final Optional errorMessage = responseEntityAsString.flatMap(
RestHelper::tryGetErrorMessageFromResponse
);
log.trace("%s - failed, status: %d, error: %s", request, status, errorMessage.orElse(""));
responseEntityAsString.ifPresent(log::trace);
throw new BambooSpecsRestRequestException(status, errorMessage.orElse(null), responseEntityAsString.orElse(null));
}
};
return httpClient.execute(request, responseHandler);
}
}
/**
* Try to extract an error message as a flat String from a JSON response.
*
* The JSON may come in different formats, and this method will attempt to extract message for all scenarios, e.g.:
*
{@code
* { "message": "plan with key FOO-BAR not found" }
* }
* or
* {@code
* {
* "errors": [ "invalid configuration of plan FOO-BAR" ],
* "fieldErrors": {
* "name": [ "value is required" ]
* }
* }
* }
* The returned flattened error messages for the above examples would be respectively:
*
* - plan with key FOO-BAR not found
* - invalid configuration of plan FOO-BAR; name: value is required
*
*
* @param jsonAsString response in JSON format (if it's not a valid JSON object, this method will not fail, but will
* simply return no error message)
* @return error message extracted from the HTTP response or empty optional if extracting failed
*/
@NotNull
static Optional tryGetErrorMessageFromResponse(final String jsonAsString) {
try {
final JsonObject jsonObject = new JsonParser().parse(jsonAsString).getAsJsonObject();
String message = null;
if (jsonObject.has("message")) {
message = jsonObject.get("message").getAsString();
// trim Bamboo server exception class name if it's present
final String prefixToRemove = PropertiesValidationException.class.getName() + ":";
if (message.startsWith(prefixToRemove)) {
message = StringUtils.substringAfter(message, prefixToRemove).trim();
}
} else if (jsonObject.has("errors")
&& jsonObject.has("fieldErrors")) {
message = String.join("; ", extractErrorsFromRestErrorCollection(jsonObject));
}
return Optional.ofNullable(message);
} catch (final Exception e) {
return Optional.empty();
}
}
/**
* Extract list of errors from a JSON object representing Bamboo REST error collection. The JSON should be in
* format:
* {@code
* {
* "errors": [ ... ],
* "fieldErrors": {
* "field1: [ ... ],
* "field2: [ ... ],
* ...
* }
* }
* }
*
* @param jsonObject JSON object representing a serialised REST error collection from Bamboo
* @return list of errors extracted from the JSON response; field errors will be flattened to
* "fieldName: errorMessage".
*/
private static List extractErrorsFromRestErrorCollection(JsonObject jsonObject) {
final List messages = new ArrayList<>();
final JsonArray errors = jsonObject.get("errors").getAsJsonArray();
StreamSupport.stream(errors.spliterator(), false)
.map(JsonElement::getAsString)
.forEach(messages::add);
final Set> fieldErrors = jsonObject.get("fieldErrors")
.getAsJsonObject()
.entrySet();
fieldErrors.forEach((fieldErrorEntry) -> {
final String fieldName = fieldErrorEntry.getKey();
final JsonArray fieldErrorsArray = fieldErrorEntry.getValue().getAsJsonArray();
StreamSupport.stream(fieldErrorsArray.spliterator(), false)
.map(JsonElement::getAsString)
.forEach(fieldError -> messages.add(fieldName + ": " + fieldError));
});
return messages;
}
private static Optional getResponseEntityAsString(HttpResponse response) throws IOException {
final HttpEntity entity = response.getEntity();
return entity != null
? Optional.of(EntityUtils.toString(entity))
: Optional.empty();
}
public String post(final URI uri, final AuthenticationProvider authenticationProvider,
final String yamlContent) throws IOException {
log.trace("Sending the following content to %s via POST:\n%s", uri, yamlContent);
final HttpPost httpPost = new HttpPost(uri);
setYamlEntity(httpPost, yamlContent);
return sendRequest(httpPost, authenticationProvider);
}
public String put(final URI uri, final AuthenticationProvider authenticationProvider,
final String yamlContent) throws IOException {
log.trace("Sending the following content to %s via PUT:\n%s", uri, yamlContent);
final HttpPut httpPut = new HttpPut(uri);
setYamlEntity(httpPut, yamlContent);
return sendRequest(httpPut, authenticationProvider);
}
private void setYamlEntity(final HttpEntityEnclosingRequest request, final String yamlContent) {
final NameValuePair version = new BasicNameValuePair("version", BambooSpecVersion.getModelVersion());
final ContentType contentType = ContentType.create(MIME_TYPE_APPLICATION_X_YAML, StandardCharsets.UTF_8)
.withParameters(version);
final StringEntity entity = new StringEntity(yamlContent, contentType);
request.setEntity(entity);
}
}