kopia lustrzana https://github.com/bertrik/ttnhabbridge
Implement HabitatUploader.
rodzic
6c57c6eb46
commit
c08389576c
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package nl.sikken.bertrik.hab.habitat;
|
||||
|
||||
/**
|
||||
* Interface describing a HAB receiver.
|
||||
*/
|
||||
public interface IHabReceiver {
|
||||
|
||||
public String getCallsign();
|
||||
|
||||
public Location getLocation();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Ładowanie…
Reference in New Issue