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

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 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 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 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 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 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) 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);
    }

}