package org.yamcs.api.rest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.cookie.Cookie;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.yamcs.ConfigurationException;
import org.yamcs.api.MediaType;
import org.yamcs.api.YamcsApiException;
import org.yamcs.api.YamcsConnectionProperties;
import org.yamcs.protobuf.Rest.ListInstancesResponse;
import org.yamcs.protobuf.YamcsManagement.YamcsInstance;
import static org.yamcs.api.YamcsConnectionProperties.Protocol;
/**
* A simple Yamcs Rest client to help with basic requests.
*
* @author nm
*
*/
public class RestClient {
final YamcsConnectionProperties connectionProperties;
long timeout = 5000; //timeout in milliseconds
final HttpClient httpClient;
/** maximum size of the responses - this is not applicable to bulk requests */
final static int MAX_RESPONSE_LENGTH = 1024*1024;
/**max message length of an individual ProtoBuf message part of a bulk retrieval*/
final static int MAX_MESSAGE_LENGTH = 1024*1024;
private boolean autoclose = true;
/**
* Creates a rest client that communications using protobuf
* @param connectionProperties
*/
public RestClient(YamcsConnectionProperties connectionProperties) {
YamcsConnectionProperties.Protocol p = connectionProperties.getProtocol();
if(p!=null && p!=Protocol.http) {
throw new ConfigurationException("Unsupported protocol "+p+"; The only supported protocol is "+Protocol.http);
}
this.connectionProperties = connectionProperties;
httpClient = new HttpClient();
httpClient.setMaxResponseLength(MAX_RESPONSE_LENGTH);
httpClient.setAcceptMediaType(MediaType.PROTOBUF);
httpClient.setSendMediaType(MediaType.PROTOBUF);
}
/**
* Retrieve the list of yamcs instances from the server. The operation will block until the list is received.
*
* @return the list of yamcs instances configured on the server
* @throws Exception
*/
public List<YamcsInstance> blockingGetYamcsInstances() throws Exception {
try {
return getYamcsInstances().get(timeout, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if(t instanceof Exception) throw (Exception)t;
else throw new RuntimeException(t);//should never happen
}
}
public CompletableFuture<List<YamcsInstance>> getYamcsInstances() {
CompletableFuture<byte[]> future = doRequest("/instances",HttpMethod.GET);
return future.thenApply(b -> {
try {
return ListInstancesResponse.parseFrom(b).getInstanceList();
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
/**
* Performs a request with an empty body. Works using protobuf
* @param resource
* @param method
* @return a the response body
*/
public CompletableFuture<byte[]> 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 thrown in case the resource specification is invalid
*/
public CompletableFuture<String> doRequest(String resource, HttpMethod method, String body) {
CompletableFuture<byte[]> cf;
try {
cf = httpClient.doAsyncRequest(connectionProperties.getRestApiUrl()+resource, method, body.getBytes(), connectionProperties.getAuthenticationToken());
} catch (URISyntaxException e) { //throw a RuntimeException instead since if the code is not buggy it's unlikely to have this exception thrown
throw new IllegalArgumentException(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 body protobuf encoded data.
* @return future containing protobuf encoded data
*/
public CompletableFuture<byte[]> doRequest(String resource, HttpMethod method, byte[] body) {
CompletableFuture<byte[]> cf;
try {
cf = httpClient.doAsyncRequest(connectionProperties.getRestApiUrl()+resource, method, body, connectionProperties.getAuthenticationToken());
} catch (URISyntaxException 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;
}
/**
* 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 resource
* @param receiver
* @return future that is completed when the request is finished
* @throws RuntimeException - thrown if the uri + resource does not form a correct URL
*/
public CompletableFuture<Void> doBulkGetRequest(String resource, BulkRestDataReceiver receiver) {
return doBulkGetRequest(resource, new byte[0], receiver);
}
public CompletableFuture<Void> doBulkGetRequest(String resource, byte[] body, BulkRestDataReceiver receiver) {
CompletableFuture<Void> cf;
MessageSplitter splitter = new MessageSplitter(receiver);
try {
cf= httpClient.doBulkReceiveRequest(connectionProperties.getRestApiUrl()+resource, HttpMethod.GET, body, connectionProperties.getAuthenticationToken(), splitter);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
if(autoclose) {
cf.whenComplete((v, t)->{
close();
});
}
return cf;
}
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 YamcsApiException {
if(data.length>MAX_MESSAGE_LENGTH) {
throw new YamcsApiException("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 < writeOffset) {
bb.position(readOffset);
int msgLength = readVarInt32(bb);
if(msgLength>MAX_MESSAGE_LENGTH) throw new YamcsApiException("Message too long: decodedMessagLength: "+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 YamcsApiException {
byte b = bb.get();
int v = b &0x7F;
for (int shift = 7; (b & 0x80) != 0; shift += 7) {
if(shift>28) throw new YamcsApiException("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(MediaType sendMediaType) {
httpClient.setSendMediaType(sendMediaType);
}
public void setAcceptMediaType(MediaType acceptMediaType) {
httpClient.setAcceptMediaType(acceptMediaType);
}
public void setMaxResponseLength(int size) {
httpClient.setMaxResponseLength(size);
}
public void close() {
httpClient.close();
}
public boolean isAutoclose() {
return autoclose;
}
/**
* 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<Cookie> getCookies() {
return httpClient.getCookies();
}
public CompletableFuture<BulkRestDataSender> doBulkSendRequest(String resource, HttpMethod method) {
try {
return httpClient.doBulkSendRequest(connectionProperties.getRestApiUrl()+resource, method, connectionProperties.getAuthenticationToken());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}