Implement HabitatUploader.

koppelting
Bertrik Sikken 2017-08-15 08:34:46 +02:00
rodzic 6c57c6eb46
commit c08389576c
6 zmienionych plików z 319 dodań i 0 usunięć

Wyświetl plik

@ -4,6 +4,12 @@ mainClassName = 'nl.sikken.bertrik.TtnHabBridge'
dependencies {
compile libraries.slf4jlog4j
compile libraries.jersey_client
compile libraries.jackson
compile libraries.jetty
testCompile libraries.mockito
}
//Add configuration folder to classpath:

Wyświetl plik

@ -0,0 +1,180 @@
package nl.sikken.bertrik.hab.habitat;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.xml.bind.DatatypeConverter;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.proxy.WebResourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Habitat uploader.
*/
public final class HabitatUploader {
private static MessageDigest sha256;
private final Logger LOG = LoggerFactory.getLogger(HabitatUploader.class);
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Encoder base64Encoder = Base64.getEncoder();
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
private final IHabitatRestApi restClient;
static {
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No SHA-256 hash found");
}
}
/**
* Constructor.
*
* @param restClient the REST client used for uploading
*/
public HabitatUploader(IHabitatRestApi restClient) {
this.restClient = restClient;
}
/**
* Starts the uploader process.
*/
public void start() {
LOG.info("Starting habitat uploader");
LOG.info("Started habitat uploader");
}
/**
* Stops the uploader process.
* @throws InterruptedException in case of a termination problem
*/
public void stop() throws InterruptedException {
LOG.info("Stopping habitat uploader");
executor.shutdown();
LOG.info("Stopped habitat uploader");
}
/**
* Uploads a new sentence to the HAB network (non-blocking).
*
* @param sentence the ASCII sentence
* @param receivers list of receivers that got this sentence
* @param date the current date
*/
public void upload(String sentence, List<IHabReceiver> receivers, Date date) {
// encode sentence as raw bytes
final byte[] bytes = sentence.getBytes(StandardCharsets.US_ASCII);
// determine docId
final String docId = createDocId(bytes);
LOG.info("docid = {}", docId);
for (IHabReceiver receiver : receivers) {
LOG.info("Uploading for {}: {}", receiver.getCallsign(), sentence.trim());
// create Json
final String json = createJson(receiver, bytes, date, date);
// submit it to our processing thread
uploadTelemetry(docId, json);
}
}
/**
* Performs the actual upload as a REST-like call towards habitat.
*
* @param docId the document id
* @param json the JSON payload
*/
private void uploadTelemetry(String docId, String json) {
LOG.info("Sending for {}: {}", docId, json);
try {
final String response = restClient.updateListener(docId, json);
LOG.info("Response for {}: {}", docId, response);
} catch (WebApplicationException e) {
LOG.warn("Caught exception: {}", e.getMessage());
}
}
/**
* Creates the JSON payload.
*
* @param receiver the radio receiver properties
* @param bytes the raw sentence
* @param dateCreated the creation date
* @param dateUploaded the upload date
* @return a new JSON encoded string
*/
public String createJson(IHabReceiver receiver, byte[] bytes, Date dateCreated, Date dateUploaded) {
final JsonNodeFactory factory = new JsonNodeFactory(false);
final ObjectNode topNode = factory.objectNode();
// create data node
final ObjectNode dataNode = factory.objectNode();
dataNode.set("_raw", factory.binaryNode(bytes));
// create receivers node
final ObjectNode receiversNode = factory.objectNode();
final ObjectNode receiverNode = factory.objectNode();
receiverNode.set("time_created", factory.textNode(dateFormat.format(dateCreated)));
receiverNode.set("time_uploaded", factory.textNode(dateFormat.format(dateUploaded)));
receiversNode.set(receiver.getCallsign(), receiverNode);
// put it together in the top node
topNode.set("data", dataNode);
topNode.set("receivers", receiversNode);
return topNode.toString();
}
/**
* Creates the document id.
*
* @param bytes the raw sentence
* @return the document id
*/
public String createDocId(byte[] bytes) {
final byte[] base64 = base64Encoder.encode(bytes);
final byte[] hash = sha256.digest(base64);
return DatatypeConverter.printHexBinary(hash).toLowerCase();
}
/**
* Creates an actual REST client.
* Can be used in the constructor.
*
* @param url the URL to connect to
* @param timeout the connect and read timeout (ms)
* @return a new REST client
*/
public static IHabitatRestApi newRestClient(String url, int timeout) {
// create the REST client
final WebTarget target = ClientBuilder.newClient()
.property(ClientProperties.CONNECT_TIMEOUT, timeout)
.property(ClientProperties.READ_TIMEOUT, timeout)
.target(url);
return WebResourceFactory.newResource(IHabitatRestApi.class, target);
}
}

Wyświetl plik

@ -0,0 +1,12 @@
package nl.sikken.bertrik.hab.habitat;
/**
* Interface describing a HAB receiver.
*/
public interface IHabReceiver {
public String getCallsign();
public Location getLocation();
}

Wyświetl plik

@ -0,0 +1,25 @@
package nl.sikken.bertrik.hab.habitat;
import javax.ws.rs.Consumes;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
/**
* Interface definition for payload telemetry
*
* Publish this on "/habitat" for example.
*
*/
@Path("/_design/payload_telemetry")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface IHabitatRestApi {
@Path("/_update/add_listener/{doc_id}")
@PUT
public String updateListener(@PathParam("doc_id") String docId, String json);
}

Wyświetl plik

@ -0,0 +1,37 @@
package nl.sikken.bertrik.hab.habitat;
/**
* Representation of a HAB receiver location.
*/
public final class Location {
private final double lat;
private final double lon;
private final double alt;
/**
* Constructor.
*
* @param lat latitude (degrees)
* @param lon longitude (degrees)
* @param alt altitude (meter)
*/
public Location(double lat, double lon, double alt) {
this.lat = lat;
this.lon = lon;
this.alt = alt;
}
public double getLat() {
return lat;
}
public double getLon() {
return lon;
}
public double getAlt() {
return alt;
}
}

Wyświetl plik

@ -0,0 +1,59 @@
package nl.sikken.bertrik.hab.habitat;
import java.util.Arrays;
import java.util.Date;
import org.junit.Test;
import org.mockito.Mockito;
import nl.sikken.bertrik.hab.Sentence;
/**
* @author bertrik
*
*/
public final class HabitatUploaderTest {
/**
* Happy flow scenario.
*
* Verifies that a request to upload results in an actual upload.
*
* @throws InterruptedException
*/
@Test
public void testMockedHappyFlow() throws InterruptedException {
// create a mocked rest client
final IHabitatRestApi restClient = Mockito.mock(IHabitatRestApi.class);
Mockito.when(restClient.updateListener(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
final HabitatUploader uploader = new HabitatUploader(restClient);
// test it
uploader.start();
try {
final IHabReceiver receiver = createReceiver();
final Date date = new Date();
final Sentence sentence = new Sentence("NOTAFLIGHT", 1, date, 52.0182307, 4.695772, 1000);
uploader.upload(sentence.format(), Arrays.asList(receiver), date);
Mockito.verify(restClient, Mockito.timeout(3000)).updateListener(Mockito.anyString(), Mockito.anyString());
} finally {
uploader.stop();
}
}
private IHabReceiver createReceiver() {
final IHabReceiver receiver = new IHabReceiver() {
@Override
public Location getLocation() {
return new Location(52.0182307, 4.695772, 4);
}
@Override
public String getCallsign() {
return "BERTRIK";
}
};
return receiver;
}
}