
com.github.davidmoten.odata.client.internal.RequestHelper Maven / Gradle / Ivy
package com.github.davidmoten.odata.client.internal;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.davidmoten.guavamini.Preconditions;
import com.github.davidmoten.odata.client.ClientException;
import com.github.davidmoten.odata.client.Context;
import com.github.davidmoten.odata.client.ContextPath;
import com.github.davidmoten.odata.client.HttpMethod;
import com.github.davidmoten.odata.client.HttpResponse;
import com.github.davidmoten.odata.client.HttpService;
import com.github.davidmoten.odata.client.ODataEntityType;
import com.github.davidmoten.odata.client.ODataType;
import com.github.davidmoten.odata.client.Path;
import com.github.davidmoten.odata.client.RequestHeader;
import com.github.davidmoten.odata.client.RequestOptions;
import com.github.davidmoten.odata.client.SchemaInfo;
import com.github.davidmoten.odata.client.Serializer;
import com.github.davidmoten.odata.client.StreamProvider;
import com.github.davidmoten.odata.client.StreamUploader;
public final class RequestHelper {
private static final int HTTP_OK_MIN = 200;
private static final int HTTP_OK_MAX = 299;
private static final String HTTPS = "https://";
private static final String CONTENT_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream";
private RequestHelper() {
// prevent instantiation
}
/**
* Returns the json from an HTTP GET of the url built from the contextPath and
* options. In the case where the returned object is actually a sub-class of T
* we lookup the sub-class from schemaInfo based on the namespaced type of the
* return object.
*
* @param return object type
* @param contextPath context and current path
* @param returnCls return class
* @param options request options
* @param returnSchemaInfo schema to be used for lookup generated class of of
* the returned object from the namespaced type
* @return object hydrated from json
*/
public static T get(ContextPath contextPath, Class returnCls, RequestOptions options,
SchemaInfo returnSchemaInfo) {
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
List h = cleanAndSupplementRequestHeaders(options, "minimal", false);
// get the response
HttpResponse response = cp.context().service().get(cp.toUrl(), h);
checkResponseCode(cp, response, HttpURLConnection.HTTP_OK);
// deserialize
// Though cls might be Class we might actually want to return a
// sub-class like FileAttachment (which extends Attachment). This method returns
// the actual sub-class by inspecting the json response.
Class extends T> c = getSubClass(cp, returnSchemaInfo, returnCls, response.getText());
// check if we need to deserialize into a subclass of T (e.g. return a
// FileAttachment which is a subclass of Attachment)
return cp.context().serializer().deserialize(response.getText(), c, contextPath, false);
}
public static void checkResponseCode(String url, HttpResponse response,
int expectedResponseCodeMin, int expectedResponseCodeMax) {
if (response.getResponseCode() < expectedResponseCodeMin
|| response.getResponseCode() > expectedResponseCodeMax) {
throw new ClientException(response.getResponseCode(), "responseCode=" + response.getResponseCode() + " from url="
+ url + ", expectedResponseCode in [" + expectedResponseCodeMin + ", "
+ expectedResponseCodeMax + "], message=\n" + response.getText());
}
}
public static void checkResponseCode(ContextPath cp, HttpResponse response,
int expectedResponseCodeMin, int expectedResponseCodeMax) {
checkResponseCode(cp.toUrl(), response, expectedResponseCodeMin, expectedResponseCodeMax);
}
public static void checkResponseCode(ContextPath cp, HttpResponse response,
int expectedResponseCode) {
checkResponseCode(cp, response, expectedResponseCode, expectedResponseCode);
}
public static T getWithParametricType(ContextPath contextPath, Class cls,
Class parametricTypeClass, RequestOptions options, SchemaInfo schemaInfo) {
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
List h = cleanAndSupplementRequestHeaders(options, "minimal", false);
// get the response
HttpResponse response = cp.context().service().get(cp.toUrl(), h);
checkResponseCode(cp, response, HttpURLConnection.HTTP_OK);
// deserialize
Class extends T> c = getSubClass(cp, schemaInfo, cls, response.getText());
// check if we need to deserialize into a subclass of T (e.g. return a
// FileAttachment which is a subclass of Attachment)
return cp.context().serializer().deserializeWithParametricType(response.getText(), c,
parametricTypeClass, contextPath, false);
}
// designed for saving a new entity and returning that entity
public static T post(T entity, ContextPath contextPath,
Class cls, RequestOptions options, SchemaInfo schemaInfo) {
return postAny(entity, contextPath, cls, options, schemaInfo);
}
public static void post(Map parameters, ContextPath contextPath,
RequestOptions options) {
String json = Serializer.INSTANCE.serialize(parameters);
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
final String url = cp.toUrl();
// get the response
HttpService service = cp.context().service();
final HttpResponse response = service.post(url, h, json);
checkResponseCode(cp, response, HTTP_OK_MIN, HTTP_OK_MAX);
}
public static T postAny(Object object, ContextPath contextPath, Class responseClass,
RequestOptions options, SchemaInfo responseSchemaInfo) {
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
String json = Serializer.INSTANCE.serialize(object);
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
// get the response
HttpResponse response = cp.context().service().post(cp.toUrl(), h, json);
// deserialize
checkResponseCode(cp, response, HttpURLConnection.HTTP_CREATED);
// deserialize
Class extends T> c = getSubClass(cp, responseSchemaInfo, responseClass,
response.getText());
// check if we need to deserialize into a subclass of T (e.g. return a
// FileAttachment which is a subclass of Attachment)
return cp.context().serializer().deserialize(response.getText(), c, contextPath, false);
}
public static T postAnyWithParametricType(Object object, ContextPath contextPath,
Class cls, Class parametricTypeClass, RequestOptions options,
SchemaInfo schemaInfo) {
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
String json = Serializer.INSTANCE.serialize(object);
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
// get the response
HttpResponse response = cp.context().service().post(cp.toUrl(), h, json);
checkResponseCode(cp, response, HttpURLConnection.HTTP_CREATED);
// deserialize
Class extends T> c = getSubClass(cp, schemaInfo, cls, response.getText());
// check if we need to deserialize into a subclass of T (e.g. return a
// FileAttachment which is a subclass of Attachment)
return cp.context().serializer().deserializeWithParametricType(response.getText(), c,
parametricTypeClass, contextPath, false);
}
public static T patch(T entity, ContextPath contextPath,
RequestOptions options) {
return patchOrPut(entity, contextPath, options, HttpMethod.PATCH);
}
public static void delete(ContextPath cp, RequestOptions options) {
String url = cp.toUrl();
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
HttpResponse response = cp.context().service().delete(url, h);
checkResponseCode(cp, response, HttpURLConnection.HTTP_NO_CONTENT);
}
public static T put(T entity, ContextPath contextPath,
RequestOptions options) {
return patchOrPut(entity, contextPath, options, HttpMethod.PUT);
}
@SuppressWarnings("unused")
private static T patchOrPut(T entity, ContextPath contextPath,
RequestOptions options, HttpMethod method) {
Preconditions.checkArgument(method == HttpMethod.PUT || method == HttpMethod.PATCH);
final String json;
if (method == HttpMethod.PATCH) {
json = Serializer.INSTANCE.serializeChangesOnly(entity);
} else {
json = Serializer.INSTANCE.serialize(entity);
}
// build the url
ContextPath cp = contextPath.addQueries(options.getQueries());
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
final String url;
String editLink = (String) entity.getUnmappedFields().get("@odata.editLink");
// TODO get patch working when editLink present (does not work with MsGraph)
if (editLink != null && false) {
if (editLink.startsWith(HTTPS) || editLink.startsWith("http://")) {
url = editLink;
} else {
// TOOD unit test relative url in editLink
// from
// http://docs.oasis-open.org/odata/odata-json-format/v4.01/cs01/odata-json-format-v4.01-cs01.html#_Toc499720582
String context = (String) entity.getUnmappedFields().get("@odata.context");
if (context != null) {
try {
URL u = new URL(context);
String p = u.getPath();
String basePath = p.substring(0, p.lastIndexOf('/'));
url = basePath + "/" + editLink;
} catch (MalformedURLException e) {
throw new ClientException(e);
}
} else {
url = cp.context().service().getBasePath().toUrl() + "/" + editLink;
}
}
} else {
url = cp.toUrl();
}
// get the response
HttpService service = cp.context().service();
final HttpResponse response = service.submitWithContent(method, url, h, json);
checkResponseCode(cp, response, HTTP_OK_MIN, HTTP_OK_MAX);
// TODO is service returning the entity that we should use rather than the
// original?
return entity;
}
public static void put(ContextPath contextPath, RequestOptions options, InputStream in) {
List h = cleanAndSupplementRequestHeaders(options, "minimal", true);
ContextPath cp = contextPath.addQueries(options.getQueries());
HttpService service = cp.context().service();
final HttpResponse response = service.put(cp.toUrl(), h, in);
checkResponseCode(cp, response, HTTP_OK_MIN, HTTP_OK_MAX);
}
@SuppressWarnings("unchecked")
public static Class extends T> getSubClass(ContextPath cp, SchemaInfo schemaInfo,
Class cls, String json) {
Optional namespacedType = cp.context().serializer().getODataType(json)
.map(x -> x.substring(1));
if (namespacedType.isPresent()) {
return (Class extends T>) schemaInfo
.getClassFromTypeWithNamespace(namespacedType.get());
} else {
return cls;
}
}
public static List cleanAndSupplementRequestHeaders(
List requestHeaders, String contentTypeOdataMetadataValue,
boolean isWrite) {
List list = new ArrayList<>();
list.add(RequestHeader.ODATA_VERSION);
if (isWrite) {
list.add(RequestHeader.contentTypeJsonWithMetadata(contentTypeOdataMetadataValue));
}
list.add(RequestHeader.ACCEPT_JSON);
list.addAll(requestHeaders);
// remove duplicates
List list2 = new ArrayList<>();
Set set = new HashSet<>();
for (RequestHeader r : list) {
if (!set.contains(r)) {
list2.add(r);
}
set.add(r);
}
// remove overriden accept header
if (list2.contains(RequestHeader.ACCEPT_JSON) && list2.stream()
.filter(x -> x.isAcceptJsonWithMetadata()).findFirst().isPresent()) {
list2.remove(RequestHeader.ACCEPT_JSON);
}
// only use the last accept with metadata request header
Optional m = list2 //
.stream() //
.filter(x -> x.isAcceptJsonWithMetadata()) //
.reduce((x, y) -> y);
List list3 = list2.stream()
.filter(x -> !x.isAcceptJsonWithMetadata() || !m.isPresent() || x.equals(m.get()))
.collect(Collectors.toList());
return list3;
}
public static List cleanAndSupplementRequestHeaders(RequestOptions options,
String contentTypeOdataMetadataValue, boolean isWrite) {
return cleanAndSupplementRequestHeaders(options.getRequestHeaders(),
contentTypeOdataMetadataValue, isWrite);
}
public static InputStream getStream(ContextPath contextPath, RequestOptions options,
String base64) {
if (base64 != null) {
return new ByteArrayInputStream(Base64.getDecoder().decode(base64));
} else {
ContextPath cp = contextPath.addQueries(options.getQueries());
return contextPath.context().service().getStream(cp.toUrl(),
options.getRequestHeaders());
}
}
// for HasStream case (only for entities, not for complexTypes)
public static Optional createStream(ContextPath contextPath,
ODataEntityType entity) {
String editLink = (String) entity.getUnmappedFields().get("@odata.mediaEditLink");
if (editLink == null) {
editLink = (String) entity.getUnmappedFields().get("@odata.editLink");
}
String contentType = (String) entity.getUnmappedFields().get("@odata.mediaContentType");
if (editLink == null && "false"
.equals(contextPath.context().getProperty("attempt.stream.when.no.metadata"))) {
return Optional.empty();
} else {
if (contentType == null) {
contentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
}
// TODO support relative editLink?
Context context = contextPath.context();
if (editLink == null) {
editLink = contextPath.toUrl();
}
if (!editLink.startsWith(HTTPS)) {
// TODO should use the base path from @odata.context field?
editLink = concatenate(contextPath.context().service().getBasePath().toUrl(),
editLink);
}
if ("true".equals(contextPath.context().getProperty("modify.stream.edit.link"))) {
// Bug fix for Microsoft Graph only?
// When a collection is returned the editLink is terminated with the subclass if
// the collection type has subclasses. For example when a collection of
// Attachment (with full metadata) is requested the editLink of an individual
// attachment may end in /itemAttachment to indicate the type of the attachment.
// To get the $value download working we need to remove that type cast.
int i = endsWith(editLink, "/" + entity.odataTypeName());
if (i == -1) {
i = endsWith(editLink, "/" + entity.odataTypeName() + "/$value");
}
if (i == -1) {
i = endsWith(editLink, "/" + entity.odataTypeName() + "/%24value");
}
if (i != -1) {
editLink = editLink.substring(0, i);
}
}
Path path = new Path(editLink, contextPath.path().style());
if (!path.toUrl().endsWith("/$value")) {
path = path.addSegment("$value");
}
return Optional.of(new StreamProvider( //
new ContextPath(context, path), //
RequestOptions.EMPTY, //
contentType, //
null));
}
}
private static int endsWith(String a, String b) {
if (a.endsWith(b)) {
return a.length() - b.length();
} else {
return -1;
}
}
// concatenate two url parts making sure there is a / delimiter
private static String concatenate(String a, String b) {
StringBuilder s = new StringBuilder();
s.append(a);
if (a.endsWith("/")) {
if (b.startsWith("/")) {
s.append(b, 1, b.length());
} else {
s.append(b);
}
} else {
if (!b.startsWith("/")) {
s.append('/');
}
s.append(b);
}
return s.toString();
}
public static Optional createStreamForEdmStream(ContextPath contextPath,
ODataType item, String fieldName, String base64) {
Preconditions.checkNotNull(fieldName);
String readLink = (String) item.getUnmappedFields().get(fieldName + "@odata.mediaReadLink");
String contentType = (String) item.getUnmappedFields()
.get(fieldName + "@odata.mediaContentType");
if (readLink == null && base64 != null) {
return Optional.empty();
} else {
if (contentType == null) {
contentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
}
// TODO support relative editLink?
Context context = contextPath.context();
Path path = new Path(readLink, contextPath.path().style()).addSegment("$value");
return Optional.of(new StreamProvider( //
new ContextPath(context, path), //
RequestOptions.EMPTY, //
contentType, //
base64));
}
}
public static Optional uploader(ContextPath contextPath, ODataType item,
String fieldName) {
Preconditions.checkNotNull(fieldName);
String editLink = (String) item.getUnmappedFields().get(fieldName + "@odata.mediaEditLink");
String contentType = (String) item.getUnmappedFields()
.get(fieldName + "@odata.mediaContentType");
if (editLink == null) {
return Optional.empty();
} else {
// TODO support relative editLink?
Context context = contextPath.context();
if (contentType == null) {
contentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
}
Path path = new Path(editLink, contextPath.path().style()).addSegment(fieldName);
return Optional.of(new StreamUploader(new ContextPath(context, path), contentType));
}
}
public static String createUploadSession(ContextPath contextPath,
List requestHeaders, String contentType) {
List h = cleanAndSupplementRequestHeaders(requestHeaders, contentType, true);
ContextPath cp = contextPath.addSegment("createUploadSession");
HttpResponse response = contextPath //
.context() //
.service() //
.post(cp.toUrl(), h, new ByteArrayInputStream(new byte[] {}));
checkResponseCode(cp, response, HTTP_OK_MIN, HTTP_OK_MAX);
ObjectMapper m = new ObjectMapper();
try {
JsonNode tree = m.readTree(response.getText());
JsonNode v = tree.get("uploadUrl");
if (v == null) {
throw new ClientException(
"could not create upload session because response does not contain 'uploadUrl' field:\n"
+ response.getText());
} else {
return v.asText();
}
} catch (JsonProcessingException e) {
throw new ClientException(e);
}
}
public static void putChunk(HttpService service, String url, InputStream in,
List requestHeaders, long startByte, long finishByte, long size) {
List h = new ArrayList(requestHeaders);
h.add(RequestHeader.create("Content-Length", "" + (finishByte - startByte)));
h.add(RequestHeader.create("Content-Range",
"bytes " + startByte + "-" + finishByte + "/" + size));
HttpResponse response = service.put(url, requestHeaders, in);
checkResponseCode(url, response, 200, 202);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy