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

org.yamcs.client.base.RestClient Maven / Gradle / Ivy

There is a newer version: 5.10.8
Show newest version
package org.yamcs.client.base;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.yamcs.client.ClientException;
import org.yamcs.client.Credentials;

import com.google.protobuf.Message;

import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.cookie.Cookie;

/**
 * A simple Yamcs Rest client to help with basic requests.
 * 
 * @author nm
 *
 */
public class RestClient {

    private ServerURL serverURL;
    private final HttpClient httpClient;

    /** maximum size of the responses - this is not applicable to bulk requests */
    private final static int MAX_RESPONSE_LENGTH = 10 * 1024 * 1024;

    /** max message length of an individual ProtoBuf message part of a bulk retrieval */
    private final static int MAX_MESSAGE_LENGTH = 10 * 1024 * 1024;

    private boolean autoclose = true;

    /**
     * Creates a rest client that communications using protobuf
     */
    public RestClient(ServerURL serverURL) {
        this.serverURL = serverURL;
        httpClient = new HttpClient();
        httpClient.setMaxResponseLength(MAX_RESPONSE_LENGTH);
        httpClient.setAcceptMediaType(HttpClient.MT_PROTOBUF);
        httpClient.setSendMediaType(HttpClient.MT_PROTOBUF);
    }

    public synchronized void login(String username, char[] password) throws ClientException {
        String tokenUrl = serverURL + "/auth/token";
        httpClient.login(tokenUrl, username, password);
    }

    public synchronized void loginWithAuthorizationCode(String authorizationCode) throws ClientException {
        String tokenUrl = serverURL + "/auth/token";
        httpClient.loginWithAuthorizationCode(tokenUrl, authorizationCode);
    }

    public synchronized String authorizeKerberos(SpnegoInfo info) throws ClientException {
        return httpClient.authorizeKerberos(info);
    }

    /**
     * Performs a request with an empty body. Works using protobuf
     * 
     * @param resource
     * @param method
     * @return a the response body
     */
    public CompletableFuture doRequest(String resource, HttpMethod method) {
        return doRequest(resource, method, new byte[0]);
    }

    /**
     * Perform asynchronously the request indicated by the HTTP method and return the result as a future providing byte
     * array.
     * 
     * Note that the response body will be limited to {@value #MAX_RESPONSE_LENGTH} - in case the server sends more than
     * that, the CompletableFuture will completed with an error (the get() method will throw an Exception); the partial
     * response will not be available.
     * 
     * @param resource
     *            the url and query parameters after the "/api" part.
     * @param method
     *            http method to use
     * @param body
     *            the body of the request. Can be used even for the GET requests although strictly not allowed by the
     *            HTTP standard.
     * @return the response body
     * @throws IllegalArgumentException
     *             when the resource specification is invalid
     */
    public CompletableFuture doRequest(String resource, HttpMethod method, String body) {
        CompletableFuture cf;
        try {
            cf = httpClient.doAsyncRequest(serverURL + "/api" + resource, method, body.getBytes());
        } catch (ClientException | IOException | GeneralSecurityException e) {
            // throw a RuntimeException instead since if the code is not buggy it's
            // unlikely to have this exception thrown
            throw new RuntimeException(e);
        }

        if (autoclose) {
            cf.whenComplete((v, t) -> {
                close();
            });
        }
        return cf.thenApply(b -> {
            return new String(b);
        });
    }

    /**
     * Perform asynchronously the request indicated by the HTTP method and return the result as a future providing byte
     * array.
     * 
     * To be used when performing protobuf requests.
     * 
     * @param resource
     * @param method
     * @param message
     * @return future containing protobuf encoded data
     */
    public CompletableFuture doRequest(String resource, HttpMethod method, Message message) {
        return doBaseRequest("/api" + resource, method, message.toByteArray());
    }

    /**
     * Perform asynchronously the request indicated by the HTTP method and return the result as a future providing byte
     * array.
     * 
     * To be used when performing protobuf requests.
     * 
     * @param resource
     * @param method
     * @param body
     *            protobuf encoded data.
     * @return future containing protobuf encoded data
     */
    public CompletableFuture doRequest(String resource, HttpMethod method, byte[] body) {
        return doBaseRequest("/api" + resource, method, body);
    }

    /**
     * Perform asynchronously the request indicated by the HTTP method and return the result as a future providing byte
     * array.
     * 
     * To be used when performing protobuf requests.
     * 
     * @param resource
     * @param method
     * @param body
     *            protobuf encoded data.
     * @return future containing protobuf encoded data
     */
    public CompletableFuture doBaseRequest(String resource, HttpMethod method, byte[] body) {
        CompletableFuture cf;
        try {
            cf = httpClient.doAsyncRequest(serverURL + resource, method, body);
        } catch (ClientException | IOException | GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
        if (autoclose) {
            cf.whenComplete((v, t) -> {
                close();
            });
        }

        return cf;
    }

    public CompletableFuture doBulkRequest(HttpMethod method, String resource, BulkRestDataReceiver receiver) {
        return doBulkRequest(method, resource, new byte[0], receiver);
    }

    /**
     * Performs a bulk request and provides the result piece by piece to the receiver.
     * 
     * The potentially large result is split into messages based on the VarInt size preceding each message. The maximum
     * size of each individual message is limited to {@value #MAX_MESSAGE_LENGTH}
     * 
     * @param method
     * @param resource
     * @param receiver
     * @return future that is completed when the request is finished
     * @throws RuntimeException
     *             if the uri + resource does not form a correct URL
     */
    public CompletableFuture doBulkRequest(HttpMethod method, String resource, byte[] body,
            BulkRestDataReceiver receiver) {
        CompletableFuture cf;
        MessageSplitter splitter = new MessageSplitter(receiver);
        try {
            cf = httpClient.doBulkReceiveRequest(serverURL + "/api" + resource, method,
                    body, splitter);
        } catch (ClientException | IOException | GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
        if (autoclose) {
            cf.whenComplete((v, t) -> {
                close();
            });
        }
        return cf;
    }

    private static class MessageSplitter implements BulkRestDataReceiver {
        BulkRestDataReceiver finalReceiver;
        byte[] buffer = new byte[2 * MAX_MESSAGE_LENGTH];
        int readOffset = 0;
        int writeOffset = 0;

        MessageSplitter(BulkRestDataReceiver finalReceiver) {
            this.finalReceiver = finalReceiver;
        }

        @Override
        public void receiveData(byte[] data) throws ClientException {
            if (data.length > MAX_MESSAGE_LENGTH) {
                throw new ClientException(
                        "Message too long: received " + data.length + " max length: " + MAX_MESSAGE_LENGTH);
            }

            int length = (data.length < buffer.length - writeOffset) ? data.length : buffer.length - writeOffset;
            System.arraycopy(data, 0, buffer, writeOffset, length);
            writeOffset += length;
            ByteBuffer bb = ByteBuffer.wrap(buffer);

            while (readOffset + 5 < writeOffset) {
                bb.position(readOffset);
                int msgLength = readVarInt32(bb);
                if (msgLength > MAX_MESSAGE_LENGTH) {
                    throw new ClientException("Message too long: decodedMessageLength: " + msgLength + " max length: "
                            + MAX_MESSAGE_LENGTH);
                }
                if (msgLength > writeOffset - bb.position()) {
                    break;
                }

                readOffset = bb.position();
                byte[] b = new byte[msgLength];
                System.arraycopy(buffer, readOffset, b, 0, msgLength);
                readOffset += msgLength;
                finalReceiver.receiveData(b);
            }

            System.arraycopy(buffer, readOffset, buffer, 0, writeOffset - readOffset);
            writeOffset -= readOffset;
            readOffset = 0;
            if (length < data.length) {
                System.arraycopy(buffer, writeOffset, data, length, data.length - length);
                writeOffset += (data.length - length);
            }
        }

        @Override
        public void receiveException(Throwable t) {
            finalReceiver.receiveException(t);
        }
    }

    public static int readVarInt32(ByteBuffer bb) throws ClientException {
        byte b = bb.get();
        int v = b & 0x7F;
        for (int shift = 7; (b & 0x80) != 0; shift += 7) {
            if (shift > 28) {
                throw new ClientException("Invalid VarInt32: more than 5 bytes!");
            }

            if (!bb.hasRemaining()) {
                return Integer.MAX_VALUE;// we miss some bytes from the size itself
            }
            b = bb.get();
            v |= (b & 0x7F) << shift;

        }
        return v;
    }

    public void setSendMediaType(String sendMediaType) {
        httpClient.setSendMediaType(sendMediaType);
    }

    public void setAcceptMediaType(String acceptMediaType) {
        httpClient.setAcceptMediaType(acceptMediaType);
    }

    public void setMaxResponseLength(int size) {
        httpClient.setMaxResponseLength(size);
    }

    public void setUserAgent(String userAgent) {
        httpClient.setUserAgent(userAgent);
    }

    public void close() {
        httpClient.close();
    }

    public boolean isAutoclose() {
        return autoclose;
    }

    public Credentials getCredentials() {
        return httpClient.getCredentials();
    }

    public void setCredentials(Credentials credentials) {
        httpClient.setCredentials(credentials);
    }

    /**
     * if autoclose is set, the httpClient will be automatically closed at the end of the request, so the netty
     * eventgroup is shutdown. Otherwise it has to be done manually - but then the same object can be used to perform
     * multiple requests.
     * 
     * @param autoclose
     */
    public void setAutoclose(boolean autoclose) {
        this.autoclose = autoclose;
    }

    public void addCookie(Cookie c) {
        httpClient.addCookie(c);
    }

    public List getCookies() {
        return httpClient.getCookies();
    }

    public CompletableFuture doBulkSendRequest(String resource, HttpMethod method) {
        try {
            return httpClient.doBulkSendRequest(serverURL + "/api" + resource, method);
        } catch (ClientException | IOException | GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean isInsecureTls() {
        return httpClient.isInsecureTls();
    }

    /**
     * if true and https connections are used, do not verify server certificate
     * 
     * @param insecureTls
     */
    public void setInsecureTls(boolean insecureTls) {
        httpClient.setInsecureTls(insecureTls);
    }

    /**
     * In case of https connections, this file contains the CA certificates that are used to verify server certificate.
     * 
     * If this is not set, java will use the default mechanism with the trustStore that can be configured via the
     * javax.net.ssl.trustStore system property.
     * 
     * @param caCertFile
     */
    public void setCaCertFile(String caCertFile) throws IOException, GeneralSecurityException {
        httpClient.setCaCertFile(caCertFile);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy