From e5d8b1738b8eeb465975c60da295b67354e203a6 Mon Sep 17 00:00:00 2001
From: Jesper Zedlitz <jesper@zedlitz.de>
Date: Sat, 27 Nov 2021 08:55:48 +0100
Subject: [PATCH] Refactored to support HEAD requests

---
 .gitlab-ci.yml                                |   2 +-
 pom.xml                                       |  12 +-
 .../landsh/opendata/coronardeck/CoronaData.g4 |   2 +-
 .../de/landsh/opendata/dataproxy/App.java     |  57 ++++--
 .../dataproxy/CachingDataConverter.java       |  19 --
 .../dataproxy/CachingDataResponser.java       |  72 ++++++++
 .../opendata/dataproxy/CoronaRdEck.java       |  78 +++-----
 .../landsh/opendata/dataproxy/StrassenSH.java |  86 +++++++++
 .../dataproxy/StrassenSH2Geojson.java         | 111 ------------
 .../opendata/dataproxy/CoronaDataTest.java    |  79 +++++++++
 .../dataproxy/StrassenSH2GeojsonTest.java     |  28 +++
 src/test/resources/2021-11-12_100007.json     | 167 ++++++++++++++++++
 12 files changed, 512 insertions(+), 201 deletions(-)
 delete mode 100644 src/main/java/de/landsh/opendata/dataproxy/CachingDataConverter.java
 create mode 100644 src/main/java/de/landsh/opendata/dataproxy/CachingDataResponser.java
 create mode 100644 src/main/java/de/landsh/opendata/dataproxy/StrassenSH.java
 delete mode 100644 src/main/java/de/landsh/opendata/dataproxy/StrassenSH2Geojson.java
 create mode 100644 src/test/java/de/landsh/opendata/dataproxy/CoronaDataTest.java
 create mode 100644 src/test/java/de/landsh/opendata/dataproxy/StrassenSH2GeojsonTest.java
 create mode 100644 src/test/resources/2021-11-12_100007.json

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 808c596..6eb0171 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,5 +15,5 @@ build:
   script: "mvn clean package -B"
   artifacts:
     paths:
-      - target/*-jar-with-dependencies.jar
+      - target/data-proxy-jar-with-dependencies.jar
 
diff --git a/pom.xml b/pom.xml
index ea819ec..1c40ddb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,7 @@
         <jena.version>4.2.0</jena.version>
     </properties>
     <build>
+        <finalName>data-proxy</finalName>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
@@ -26,7 +27,7 @@
             <plugin>
                 <groupId>org.antlr</groupId>
                 <artifactId>antlr4-maven-plugin</artifactId>
-                <version>4.5</version>
+                <version>4.9.3</version>
                 <executions>
                     <execution>
                         <goals>
@@ -85,7 +86,14 @@
         <dependency>
             <groupId>org.antlr</groupId>
             <artifactId>antlr4-runtime</artifactId>
-            <version>4.5</version>
+            <version>4.9.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.2</version>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>
diff --git a/src/main/antlr4/de/landsh/opendata/coronardeck/CoronaData.g4 b/src/main/antlr4/de/landsh/opendata/coronardeck/CoronaData.g4
index aec2ba3..d4f212c 100644
--- a/src/main/antlr4/de/landsh/opendata/coronardeck/CoronaData.g4
+++ b/src/main/antlr4/de/landsh/opendata/coronardeck/CoronaData.g4
@@ -12,7 +12,7 @@ pair:  NAME ':' VALUE ;
 
 NAME:  [a-z_]+ ;
 WS  :   [ \t\r\n]+ -> skip ; // Define whitespace rule, toss it out
-QUOTE:   [\'\"] ;
+QUOTE:   ['"] ;
 ARS:   '010' [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] ;
 VALUE:  [0-9]+ '.'? [0-9]* ;
 
diff --git a/src/main/java/de/landsh/opendata/dataproxy/App.java b/src/main/java/de/landsh/opendata/dataproxy/App.java
index 77ed16f..b78c91a 100644
--- a/src/main/java/de/landsh/opendata/dataproxy/App.java
+++ b/src/main/java/de/landsh/opendata/dataproxy/App.java
@@ -1,26 +1,57 @@
 package de.landsh.opendata.dataproxy;
 
-import fi.iki.elonen.router.RouterNanoHTTPD;
+import fi.iki.elonen.NanoHTTPD;
 import fi.iki.elonen.util.ServerRunner;
 
-public class App extends RouterNanoHTTPD {
-    public static final int PORT = 8081;
+public class App extends NanoHTTPD {
+    public static  int port = 8080;
 
-    @Override
-    public void addMappings() {
-        super.addMappings();
-        addRoute("/strassen-sh.geojson", StrassenSH2Geojson.class);
-        addRoute("/corona-rd-eck.json", CoronaRdEck.class);
-
-    }
+    private final StrassenSH strassenSH = new StrassenSH();
+    private final CoronaRdEck coronaRdEck = new CoronaRdEck();
 
     public App() {
-        super(PORT);
-        addMappings();
-        System.out.println("\nRunning! Point your browser to http://localhost:" + PORT + "/ \n");
+        super(port);
+        System.out.println("\nRunning! Point your browser to http://localhost:" + port + "/ \n");
     }
 
     public static void main(String[] args) {
+
+        if( args.length > 0) {
+            try {
+                App.port = Integer.parseInt(args[0]);
+            } catch (NumberFormatException ignore) {
+                System.err.println("Usage: java -jar data-proxy.jar [PORT]");
+                System.exit(1);
+            }
+        }
+
         ServerRunner.run(App.class);
     }
+
+    @Override
+    public Response serve(IHTTPSession session) {
+
+        final CachingDataResponser responder;
+        if ("/strassen-sh.geojson".equals(session.getUri())) {
+            responder = strassenSH;
+        } else if ("/corona-rd-eck.json".equals(session.getUri())) {
+            responder = coronaRdEck;
+        } else {
+            responder = null;
+        }
+
+        if (responder == null) {
+            return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
+        } else {
+            if (session.getMethod() == Method.GET) {
+                return responder.get();
+            } else if (session.getMethod() == Method.HEAD) {
+                return responder.head();
+            } else {
+                return newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, "text/plain", "Method not allowed");
+
+            }
+        }
+
+    }
 }
diff --git a/src/main/java/de/landsh/opendata/dataproxy/CachingDataConverter.java b/src/main/java/de/landsh/opendata/dataproxy/CachingDataConverter.java
deleted file mode 100644
index bb37308..0000000
--- a/src/main/java/de/landsh/opendata/dataproxy/CachingDataConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.landsh.opendata.dataproxy;
-
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.router.RouterNanoHTTPD;
-
-import java.io.IOException;
-
-public abstract class CachingDataConverter extends RouterNanoHTTPD.DefaultHandler {
-
-
-    /**
-     * Return the update interval in milliseconds;
-     */
-    abstract int getUpdateInterval();
-
-    abstract String loadData() throws IOException;
-
-
-}
diff --git a/src/main/java/de/landsh/opendata/dataproxy/CachingDataResponser.java b/src/main/java/de/landsh/opendata/dataproxy/CachingDataResponser.java
new file mode 100644
index 0000000..9665b47
--- /dev/null
+++ b/src/main/java/de/landsh/opendata/dataproxy/CachingDataResponser.java
@@ -0,0 +1,72 @@
+package de.landsh.opendata.dataproxy;
+
+import fi.iki.elonen.NanoHTTPD;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+public abstract class CachingDataResponser {
+
+    private String data = "{}";
+    private long lastUpdate = -1;
+    private NanoHTTPD.Response.Status status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
+
+    /**
+     * Returns the update interval in milliseconds.
+     */
+    abstract int getUpdateInterval();
+
+    /**
+     * Returns the MIME type for a successful response.
+     */
+    abstract String getMimeType();
+
+    /**
+     * Load data from the original source.
+     */
+    abstract String loadData() throws IOException;
+
+    private void loadDataIfNecessary() {
+        if (System.currentTimeMillis() - getUpdateInterval() > lastUpdate) {
+            System.out.println("Loading data for " + getClass().getName());
+
+            try {
+                data = loadData();
+                status = NanoHTTPD.Response.Status.OK;
+            } catch (IOException e) {
+                data = "{}";
+                status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
+            }
+            lastUpdate = System.currentTimeMillis();
+        }
+    }
+
+    /**
+     * Handle GET requests.
+     */
+    public NanoHTTPD.Response get() {
+        loadDataIfNecessary();
+
+        NanoHTTPD.Response response = NanoHTTPD.newFixedLengthResponse(status, getMimeType(), data);
+        response.addHeader("Access-Control-Allow-Origin", "*");
+        return response;
+    }
+
+    /**
+     * Handle HEAD requests.
+     */
+    public NanoHTTPD.Response head() {
+        loadDataIfNecessary();
+        NanoHTTPD.Response response = new HeadResponse(status, getMimeType(), data.length());
+        response.addHeader("Access-Control-Allow-Origin", "*");
+        return response;
+    }
+
+    static class HeadResponse extends NanoHTTPD.Response {
+
+        protected HeadResponse(IStatus status, String mimeType, long totalBytes) {
+            super(status, mimeType, new ByteArrayInputStream(new byte[0]), totalBytes);
+        }
+    }
+
+}
diff --git a/src/main/java/de/landsh/opendata/dataproxy/CoronaRdEck.java b/src/main/java/de/landsh/opendata/dataproxy/CoronaRdEck.java
index 686274a..7c25109 100644
--- a/src/main/java/de/landsh/opendata/dataproxy/CoronaRdEck.java
+++ b/src/main/java/de/landsh/opendata/dataproxy/CoronaRdEck.java
@@ -2,8 +2,6 @@ package de.landsh.opendata.dataproxy;
 
 import de.landsh.opendata.coronardeck.CoronaDataLexer;
 import de.landsh.opendata.coronardeck.CoronaDataParser;
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.router.RouterNanoHTTPD;
 import org.antlr.v4.runtime.ANTLRInputStream;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.antlr.v4.runtime.tree.ParseTree;
@@ -14,30 +12,40 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.net.URL;
-import java.util.Map;
 
-public class CoronaRdEck implements RouterNanoHTTPD.UriResponder {
-    public static final int UPDATE_INTERVAL = 600000; // 10 minutes
-    private static String data = "{}";
-    private static long lastUpdate = -1;
-    private static NanoHTTPD.Response.Status status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
+public class CoronaRdEck extends CachingDataResponser {
+
+    private final String originalURL;
+
+    public CoronaRdEck() {
+        originalURL = "https://covid19dashboardrdeck.aco/daten/daten.js";
+    }
+
+    CoronaRdEck(String url) {
+        originalURL = url;
+    }
+
+    @Override
+    int getUpdateInterval() {
+        return 600000; // 10 minutes
+    }
+
+    @Override
+    String getMimeType() {
+        return "application/json";
+    }
 
     String loadData() throws IOException {
-        InputStream inputStream = new URL("https://covid19dashboardrdeck.aco/daten/daten.js").openStream();
+        final InputStream inputStream = new URL(originalURL).openStream();
         final ANTLRInputStream input = new ANTLRInputStream(inputStream);
-
         final CoronaDataLexer lexer = new CoronaDataLexer(input);
-
         final CommonTokenStream tokens = new CommonTokenStream(lexer);
-
         final CoronaDataParser parser = new CoronaDataParser(tokens);
-
         final ParseTree tree = parser.data();
-
         final ParseTreeWalker walker = new ParseTreeWalker();
 
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        PrintStream out = new PrintStream(baos);
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final PrintStream out = new PrintStream(baos);
 
         walker.walk(new CoronaRdEckWalker(out), tree);
         out.close();
@@ -47,42 +55,4 @@ public class CoronaRdEck implements RouterNanoHTTPD.UriResponder {
     }
 
 
-    @Override
-    public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
-        if (System.currentTimeMillis() - UPDATE_INTERVAL > lastUpdate) {
-            System.out.println("Loading data for " + getClass().getName());
-
-            try {
-                data = loadData();
-                status = NanoHTTPD.Response.Status.OK;
-            } catch (IOException e) {
-                data = "{}";
-                status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
-            }
-            lastUpdate = System.currentTimeMillis();
-        }
-        return NanoHTTPD.newFixedLengthResponse(status, "application/json", data);
-    }
-
-    @Override
-    public NanoHTTPD.Response put(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response post(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response delete(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response other(String s, RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-
 }
diff --git a/src/main/java/de/landsh/opendata/dataproxy/StrassenSH.java b/src/main/java/de/landsh/opendata/dataproxy/StrassenSH.java
new file mode 100644
index 0000000..bfa6bbf
--- /dev/null
+++ b/src/main/java/de/landsh/opendata/dataproxy/StrassenSH.java
@@ -0,0 +1,86 @@
+package de.landsh.opendata.dataproxy;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+public class StrassenSH extends CachingDataResponser {
+
+    private final String baseURL;
+
+    public StrassenSH() {
+        baseURL = "https://strassen-sh.de/proxy/geojson/los/";
+    }
+
+    StrassenSH(String baseURL) {
+        this.baseURL = baseURL;
+    }
+
+    @Override
+    int getUpdateInterval() {
+        return 180000; // 3 minutes
+    }
+
+    @Override
+    String getMimeType() {
+        return "application/geo+json";
+    }
+
+    String loadData() throws IOException {
+        System.out.println("Loading data from strassen-sh.de...");
+
+        final JSONObject featureCollection = new JSONObject();
+        featureCollection.put("type", "FeatureCollection");
+        processQuery(featureCollection, new URL(baseURL + "2/100").openStream(), "frei");
+        processQuery(featureCollection, new URL(baseURL + "6/100").openStream(), "gestaut");
+        processQuery(featureCollection, new URL(baseURL + "4/100").openStream(), "dicht");
+        processQuery(featureCollection, new URL(baseURL + "5/100").openStream(), "zähfließend");
+        processQuery(featureCollection, new URL(baseURL + "9/100").openStream(), "Datenausfall");
+
+        return featureCollection.toString();
+    }
+
+    private void processQuery(JSONObject featureCollection, InputStream in, String status) {
+        if (!featureCollection.has("features")) {
+            featureCollection.put("features", new JSONArray());
+        }
+
+        final JSONObject json = new JSONObject(new JSONTokener(in));
+        final JSONArray featuresOut = featureCollection.getJSONArray("features");
+        final JSONArray featuresIn = json.getJSONArray("features");
+        for (int i = 0; i < featuresIn.length(); i++) {
+            final JSONObject featureIn = featuresIn.getJSONObject(i);
+            final JSONObject featureOut = convertFeature(featureIn);
+            featureOut.getJSONObject("properties").put("status", status);
+            featuresOut.put(featureOut);
+        }
+
+        if (json.has("properties")) {
+            featureCollection.put("properties", json.getJSONObject("properties"));
+        }
+    }
+
+    JSONObject convertFeature(JSONObject feature) {
+        final JSONObject result = new JSONObject();
+        result.put("type", "Feature");
+
+        final JSONObject geometry = new JSONObject();
+        final JSONObject properties = feature.getJSONObject("properties");
+        final String type = feature.getString("type");
+        final JSONArray coordinates = feature.getJSONArray("coordinates");
+
+        result.put("properties", properties);
+        // result.put("id", coordinates.hashCode());
+        geometry.put("type", type);
+        geometry.put("coordinates", coordinates);
+        result.put("geometry", geometry);
+
+        return result;
+    }
+
+
+}
diff --git a/src/main/java/de/landsh/opendata/dataproxy/StrassenSH2Geojson.java b/src/main/java/de/landsh/opendata/dataproxy/StrassenSH2Geojson.java
deleted file mode 100644
index 920e093..0000000
--- a/src/main/java/de/landsh/opendata/dataproxy/StrassenSH2Geojson.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package de.landsh.opendata.dataproxy;
-
-import fi.iki.elonen.NanoHTTPD;
-import fi.iki.elonen.router.RouterNanoHTTPD;
-import org.json.JSONArray;
-import org.json.JSONObject;
-import org.json.JSONTokener;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.Map;
-
-public class StrassenSH2Geojson implements RouterNanoHTTPD.UriResponder {
-    public static final int UPDATE_INTERVAL = 180000; // 3 minutes
-    private static String data = "{}";
-    private static long lastUpdate = -1;
-    private static NanoHTTPD.Response.Status status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
-
-    String loadData() throws IOException {
-        System.out.println("Loading data from strassen-sh.de...");
-
-        final JSONObject featureCollection = new JSONObject();
-        featureCollection.put("type", "FeatureCollection");
-        processQuery(featureCollection, new URL("https://strassen-sh.de/proxy/geojson/los/2/100").openStream(), "frei");
-        processQuery(featureCollection, new URL("https://strassen-sh.de/proxy/geojson/los/6/100").openStream(), "gestaut");
-        processQuery(featureCollection, new URL("https://strassen-sh.de/proxy/geojson/los/4/100").openStream(), "dicht");
-        processQuery(featureCollection, new URL("https://strassen-sh.de/proxy/geojson/los/5/100").openStream(), "zähfließend");
-        processQuery(featureCollection, new URL("https://strassen-sh.de/proxy/geojson/los/9/100").openStream(), "Datenausfall");
-
-        return featureCollection.toString();
-    }
-
-    private void processQuery(JSONObject featureCollection, InputStream in, String status) {
-        if (!featureCollection.has("features")) {
-            featureCollection.put("features", new JSONArray());
-        }
-
-        final JSONObject json = new JSONObject(new JSONTokener(in));
-        final JSONArray featuresOut = featureCollection.getJSONArray("features");
-        final JSONArray featuresIn = json.getJSONArray("features");
-        for (int i = 0; i < featuresIn.length(); i++) {
-            final JSONObject featureIn = featuresIn.getJSONObject(i);
-            final JSONObject featureOut = convertFeature(featureIn);
-            featureOut.getJSONObject("properties").put("status", status);
-            featuresOut.put(featureOut);
-        }
-
-        if (json.has("properties")) {
-            featureCollection.put("properties", json.getJSONObject("properties"));
-        }
-    }
-
-    JSONObject convertFeature(JSONObject feature) {
-        final JSONObject result = new JSONObject();
-        result.put("type", "Feature");
-
-        final JSONObject geometry = new JSONObject();
-        final JSONObject properties = feature.getJSONObject("properties");
-        final String type = feature.getString("type");
-        final JSONArray coordinates = feature.getJSONArray("coordinates");
-
-        result.put("properties", properties);
-        // result.put("id", coordinates.hashCode());
-        geometry.put("type", type);
-        geometry.put("coordinates", coordinates);
-        result.put("geometry", geometry);
-
-        return result;
-    }
-
-    @Override
-    public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
-        if (System.currentTimeMillis() - UPDATE_INTERVAL > lastUpdate) {
-            System.out.println("Loading data for " + getClass().getName());
-
-            try {
-                data = loadData();
-                status = NanoHTTPD.Response.Status.OK;
-            } catch (IOException e) {
-                data = "{}";
-                status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
-            }
-            lastUpdate = System.currentTimeMillis();
-        }
-        return NanoHTTPD.newFixedLengthResponse(status, "application/geo+json", data);
-    }
-
-
-    @Override
-    public NanoHTTPD.Response put(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response post(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response delete(RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-    @Override
-    public NanoHTTPD.Response other(String s, RouterNanoHTTPD.UriResource uriResource, Map<String, String> map, NanoHTTPD.IHTTPSession ihttpSession) {
-        return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, "text/plain", "method not allowed");
-    }
-
-
-}
diff --git a/src/test/java/de/landsh/opendata/dataproxy/CoronaDataTest.java b/src/test/java/de/landsh/opendata/dataproxy/CoronaDataTest.java
new file mode 100644
index 0000000..596b3df
--- /dev/null
+++ b/src/test/java/de/landsh/opendata/dataproxy/CoronaDataTest.java
@@ -0,0 +1,79 @@
+package de.landsh.opendata.dataproxy;
+
+import de.landsh.opendata.coronardeck.CoronaDataLexer;
+import de.landsh.opendata.coronardeck.CoronaDataParser;
+import org.antlr.v4.runtime.*;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit test for simple CoronaData.
+ */
+public class CoronaDataTest {
+
+    @Test
+    public void invalidData() {
+        final CharStream input = CharStreams.fromString("var data = {\n" +
+                "'010580005005: { amount_pt: 2.1037868162693, amount_t: 206, amount_i: 21, amount_d: 3, amount_h: 182 },\n}");
+
+        CoronaDataLexer lexer = new CoronaDataLexer(input);
+        CommonTokenStream tokens = new CommonTokenStream(lexer);
+        CoronaDataParser parser = new CoronaDataParser(tokens);
+
+        parser.addErrorListener(new BaseErrorListener() {
+            @Override
+            public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
+                throw new RuntimeException(msg);
+            }
+        });
+
+        try {
+            parser.data();
+            fail();
+        } catch (RuntimeException ignore) {
+            // ok
+        }
+    }
+
+    @Test
+    public void oneEntry() {
+        final CharStream input = CharStreams.fromString("var data = {\n" +
+                "'010580005005': { amount_pt: 2.1037868162693, amount_t: 206, amount_i: 21, amount_d: 3, amount_h: 182 },\n}");
+
+        CoronaDataLexer lexer = new CoronaDataLexer(input);
+        CommonTokenStream tokens = new CommonTokenStream(lexer);
+        CoronaDataParser parser = new CoronaDataParser(tokens);
+
+        parser.addErrorListener(new BaseErrorListener() {
+            @Override
+            public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
+                throw new RuntimeException(msg);
+            }
+        });
+
+        parser.data();
+
+
+    }
+
+    @Test
+    public void test() throws IOException {
+        final InputStream inputStream = getClass().getResourceAsStream("/2021-11-12_100007.json");
+        assertNotNull(inputStream);
+        final CharStream input = CharStreams.fromStream(inputStream);
+
+        CoronaDataLexer lexer = new CoronaDataLexer(input);
+
+        CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+        CoronaDataParser parser = new CoronaDataParser(tokens);
+
+        parser.data();
+
+    }
+}
diff --git a/src/test/java/de/landsh/opendata/dataproxy/StrassenSH2GeojsonTest.java b/src/test/java/de/landsh/opendata/dataproxy/StrassenSH2GeojsonTest.java
new file mode 100644
index 0000000..044853c
--- /dev/null
+++ b/src/test/java/de/landsh/opendata/dataproxy/StrassenSH2GeojsonTest.java
@@ -0,0 +1,28 @@
+package de.landsh.opendata.dataproxy;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class StrassenSH2GeojsonTest {
+
+    StrassenSH strassenSH2Geojson = new StrassenSH();
+
+    @Test
+    public void convertFeature() {
+        String json = "{\"properties\":{},\"type\":\"LineString\",\"coordinates\":[[11.1348821,54.42609306],[11.12938639,54.42167313],[11.12450702,54.41677863],[11.12069621,54.4118541],[11.11344491,54.40078343]]}";
+
+        JSONObject in = new JSONObject(json);
+        JSONObject result = strassenSH2Geojson.convertFeature(in);
+
+        assertTrue(result.has("properties"));
+        JSONObject geometry = result.getJSONObject("geometry");
+                assertNotNull(geometry);
+        assertEquals("LineString", geometry.getString("type"));
+        JSONArray coordinates = geometry.getJSONArray("coordinates");
+        assertNotNull(coordinates);
+        assertEquals(5, coordinates.length());
+    }
+}
diff --git a/src/test/resources/2021-11-12_100007.json b/src/test/resources/2021-11-12_100007.json
new file mode 100644
index 0000000..4c4de49
--- /dev/null
+++ b/src/test/resources/2021-11-12_100007.json
@@ -0,0 +1,167 @@
+var data = {
+'010580005005': { amount_pt: 2.1037868162693, amount_t: 207, amount_i: 21, amount_d: 3, amount_h: 183 },
+'010580034034': { amount_pt: 0.67107659860033, amount_t: 219, amount_i: 7, amount_d: 2, amount_h: 210 },
+'010580043043': { amount_pt: 0.96480749793256, amount_t: 457, amount_i: 21, amount_d: 8, amount_h: 428 },
+'010580092092': { amount_pt: 1.0036801605888, amount_t: 224, amount_i: 12, amount_d: 1, amount_h: 211 },
+'010580135135': { amount_pt: 1.7367744624683, amount_t: 1256, amount_i: 50, amount_d: 16, amount_h: 1190 },
+'010580169169': { amount_pt: 3.0395136778116, amount_t: 35, amount_i: 7, amount_d: 0, amount_h: 28 },
+'010585803001': { amount_pt: 0, amount_t: 14, amount_i: 0, amount_d: 0, amount_h: 14 },
+'010585803028': { amount_pt: 3.2851511169514, amount_t: 19, amount_i: 5, amount_d: 0, amount_h: 14 },
+'010585803050': { amount_pt: 1.4058106841612, amount_t: 39, amount_i: 3, amount_d: 0, amount_h: 36 },
+'010585803093': { amount_pt: 0, amount_t: 9, amount_i: 0, amount_d: 0, amount_h: 9 },
+'010585803104': { amount_pt: 0.53995680345572, amount_t: 34, amount_i: 1, amount_d: 0, amount_h: 33 },
+'010585803126': { amount_pt: 1.0526315789474, amount_t: 27, amount_i: 1, amount_d: 0, amount_h: 26 },
+'010585803130': { amount_pt: 1.7064846416382, amount_t: 25, amount_i: 3, amount_d: 0, amount_h: 22 },
+'010585803171': { amount_pt: 1.2730744748568, amount_t: 23, amount_i: 2, amount_d: 0, amount_h: 21 },
+'010585822037': { amount_pt: 2.326182476092, amount_t: 80, amount_i: 9, amount_d: 0, amount_h: 71 },
+'010585822116': { amount_pt: 1.1547344110855, amount_t: 11, amount_i: 1, amount_d: 0, amount_h: 10 },
+'010585822150': { amount_pt: 0, amount_t: 34, amount_i: 0, amount_d: 0, amount_h: 34 },
+'010585822157': { amount_pt: 1.3440860215054, amount_t: 14, amount_i: 2, amount_d: 0, amount_h: 12 },
+'010585824051': { amount_pt: 1.6820857863751, amount_t: 22, amount_i: 2, amount_d: 0, amount_h: 20 },
+'010585824058': { amount_pt: 2.119486024639, amount_t: 89, amount_i: 16, amount_d: 1, amount_h: 72 },
+'010585824096': { amount_pt: 5.1094890510949, amount_t: 18, amount_i: 7, amount_d: 0, amount_h: 11 },
+'010585824110': { amount_pt: 0, amount_t: 11, amount_i: 0, amount_d: 0, amount_h: 11 },
+'010585824112': { amount_pt: 0, amount_t: 17, amount_i: 0, amount_d: 0, amount_h: 17 },
+'010585824121': { amount_pt: 0.79396585946804, amount_t: 26, amount_i: 2, amount_d: 0, amount_h: 24 },
+'010585824142': { amount_pt: 2.9644268774704, amount_t: 10, amount_i: 3, amount_d: 0, amount_h: 7 },
+'010585824165': { amount_pt: 0, amount_t: 14, amount_i: 0, amount_d: 1, amount_h: 13 },
+'010585830019': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585830053': { amount_pt: 0.83114004709794, amount_t: 107, amount_i: 6, amount_d: 0, amount_h: 101 },
+'010585830145': { amount_pt: 0, amount_t: 3, amount_i: 0, amount_d: 0, amount_h: 3 },
+'010585830160': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585833003': { amount_pt: 2.1108179419525, amount_t: 35, amount_i: 4, amount_d: 0, amount_h: 31 },
+'010585833054': { amount_pt: 3.9701445132603, amount_t: 157, amount_i: 25, amount_d: 1, amount_h: 131 },
+'010585833118': { amount_pt: 0, amount_t: 36, amount_i: 0, amount_d: 1, amount_h: 35 },
+'010585833136': { amount_pt: 2.9296875, amount_t: 11, amount_i: 3, amount_d: 0, amount_h: 8 },
+'010585847010': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585847029': { amount_pt: 0, amount_t: 22, amount_i: 0, amount_d: 0, amount_h: 22 },
+'010585847036': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585847047': { amount_pt: 0, amount_t: 24, amount_i: 0, amount_d: 0, amount_h: 24 },
+'010585847055': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585847056': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585847070': { amount_pt: 0, amount_t: 50, amount_i: 0, amount_d: 0, amount_h: 50 },
+'010585847078': { amount_pt: 1.6535758577925, amount_t: 38, amount_i: 4, amount_d: 0, amount_h: 34 },
+'010585847089': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585847097': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585847129': { amount_pt: 0, amount_t: 7, amount_i: 0, amount_d: 0, amount_h: 7 },
+'010585847154': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585853031': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585853048': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585853068': { amount_pt: 0, amount_t: 15, amount_i: 0, amount_d: 0, amount_h: 15 },
+'010585853071': { amount_pt: 0, amount_t: 9, amount_i: 0, amount_d: 1, amount_h: 8 },
+'010585853075': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585853086': { amount_pt: 0.30111412225233, amount_t: 26, amount_i: 1, amount_d: 0, amount_h: 25 },
+'010585853101': { amount_pt: 0, amount_t: 9, amount_i: 0, amount_d: 0, amount_h: 9 },
+'010585853148': { amount_pt: 0, amount_t: 8, amount_i: 0, amount_d: 0, amount_h: 8 },
+'010585853155': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585853172': { amount_pt: 0.9954210631097, amount_t: 90, amount_i: 5, amount_d: 0, amount_h: 85 },
+'010585859018': { amount_pt: 2.8490028490028, amount_t: 20, amount_i: 2, amount_d: 0, amount_h: 18 },
+'010585859105': { amount_pt: 4.3859649122807, amount_t: 39, amount_i: 6, amount_d: 0, amount_h: 33 },
+'010585859107': { amount_pt: 0.59654006760787, amount_t: 57, amount_i: 3, amount_d: 1, amount_h: 53 },
+'010585859138': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585859139': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585859141': { amount_pt: 0, amount_t: 4, amount_i: 0, amount_d: 1, amount_h: 3 },
+'010585864011': { amount_pt: 5.5788005578801, amount_t: 21, amount_i: 4, amount_d: 0, amount_h: 17 },
+'010585864021': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585864023': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585864027': { amount_pt: 5.7803468208092, amount_t: 8, amount_i: 2, amount_d: 0, amount_h: 6 },
+'010585864038': { amount_pt: 0, amount_t: 16, amount_i: 0, amount_d: 0, amount_h: 16 },
+'010585864045': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585864046': { amount_pt: 0, amount_t: 10, amount_i: 0, amount_d: 0, amount_h: 10 },
+'010585864049': { amount_pt: 0, amount_t: 27, amount_i: 0, amount_d: 0, amount_h: 27 },
+'010585864059': { amount_pt: 0.85178875638842, amount_t: 4, amount_i: 1, amount_d: 0, amount_h: 3 },
+'010585864065': { amount_pt: 0, amount_t: 16, amount_i: 0, amount_d: 1, amount_h: 15 },
+'010585864091': { amount_pt: 0, amount_t: 4, amount_i: 0, amount_d: 0, amount_h: 4 },
+'010585864094': { amount_pt: 0, amount_t: 27, amount_i: 0, amount_d: 0, amount_h: 27 },
+'010585864117': { amount_pt: 1.1682242990654, amount_t: 142, amount_i: 8, amount_d: 3, amount_h: 131 },
+'010585864120': { amount_pt: 0, amount_t: 3, amount_i: 0, amount_d: 0, amount_h: 3 },
+'010585864147': { amount_pt: 1.3071895424837, amount_t: 7, amount_i: 1, amount_d: 0, amount_h: 6 },
+'010585864163': { amount_pt: 0, amount_t: 30, amount_i: 0, amount_d: 0, amount_h: 30 },
+'010585864168': { amount_pt: 1.4577259475219, amount_t: 5, amount_i: 1, amount_d: 0, amount_h: 4 },
+'010585888026': { amount_pt: 3.6563071297989, amount_t: 17, amount_i: 4, amount_d: 0, amount_h: 13 },
+'010585888073': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585888122': { amount_pt: 5.1457975986278, amount_t: 15, amount_i: 3, amount_d: 0, amount_h: 12 },
+'010585888124': { amount_pt: 0.9788566953798, amount_t: 101, amount_i: 5, amount_d: 0, amount_h: 96 },
+'010585888132': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585888140': { amount_pt: 1.2484394506866, amount_t: 126, amount_i: 6, amount_d: 1, amount_h: 119 },
+'010585888146': { amount_pt: 0, amount_t: 3, amount_i: 0, amount_d: 0, amount_h: 3 },
+'010585889016': { amount_pt: 6.2111801242236, amount_t: 1, amount_i: 1, amount_d: 0, amount_h: 0 },
+'010585889022': { amount_pt: 0.9085009733939, amount_t: 157, amount_i: 7, amount_d: 5, amount_h: 145 },
+'010585889033': { amount_pt: 0, amount_t: 22, amount_i: 0, amount_d: 1, amount_h: 21 },
+'010585889063': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585889064': { amount_pt: 0, amount_t: 3, amount_i: 0, amount_d: 0, amount_h: 3 },
+'010585889076': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585889098': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585889108': { amount_pt: 0, amount_t: 11, amount_i: 0, amount_d: 0, amount_h: 11 },
+'010585889109': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585889133': { amount_pt: 0, amount_t: 4, amount_i: 0, amount_d: 0, amount_h: 4 },
+'010585889143': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585889144': { amount_pt: 0, amount_t: 7, amount_i: 0, amount_d: 0, amount_h: 7 },
+'010585889153': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585889170': { amount_pt: 0, amount_t: 27, amount_i: 0, amount_d: 1, amount_h: 26 },
+'010585890008': { amount_pt: 1.0277492291881, amount_t: 36, amount_i: 1, amount_d: 4, amount_h: 31 },
+'010585890024': { amount_pt: 0, amount_t: 24, amount_i: 0, amount_d: 1, amount_h: 23 },
+'010585890030': { amount_pt: 0, amount_t: 9, amount_i: 0, amount_d: 0, amount_h: 9 },
+'010585890035': { amount_pt: 0, amount_t: 12, amount_i: 0, amount_d: 0, amount_h: 12 },
+'010585890039': { amount_pt: 4.8192771084337, amount_t: 7, amount_i: 2, amount_d: 0, amount_h: 5 },
+'010585890066': { amount_pt: 5.4858934169279, amount_t: 16, amount_i: 7, amount_d: 0, amount_h: 9 },
+'010585890069': { amount_pt: 1.779359430605, amount_t: 9, amount_i: 1, amount_d: 0, amount_h: 8 },
+'010585890080': { amount_pt: 3.8699690402477, amount_t: 16, amount_i: 5, amount_d: 0, amount_h: 11 },
+'010585890081': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585890083': { amount_pt: 0, amount_t: 8, amount_i: 0, amount_d: 0, amount_h: 8 },
+'010585890088': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585890111': { amount_pt: 0, amount_t: 9, amount_i: 0, amount_d: 0, amount_h: 9 },
+'010585890123': { amount_pt: 3.9370078740157, amount_t: 19, amount_i: 4, amount_d: 1, amount_h: 14 },
+'010585890127': { amount_pt: 0.27277686852155, amount_t: 87, amount_i: 1, amount_d: 1, amount_h: 85 },
+'010585890152': { amount_pt: 0, amount_t: 14, amount_i: 0, amount_d: 0, amount_h: 14 },
+'010585890175': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585893004': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585893012': { amount_pt: 3.2509752925878, amount_t: 30, amount_i: 5, amount_d: 0, amount_h: 25 },
+'010585893032': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585893040': { amount_pt: 0, amount_t: 20, amount_i: 0, amount_d: 0, amount_h: 20 },
+'010585893042': { amount_pt: 0, amount_t: 10, amount_i: 0, amount_d: 0, amount_h: 10 },
+'010585893052': { amount_pt: 2.3062730627306, amount_t: 28, amount_i: 5, amount_d: 0, amount_h: 23 },
+'010585893057': { amount_pt: 1.8832391713748, amount_t: 13, amount_i: 1, amount_d: 0, amount_h: 12 },
+'010585893067': { amount_pt: 1.3774104683196, amount_t: 15, amount_i: 1, amount_d: 0, amount_h: 14 },
+'010585893082': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585893084': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585893087': { amount_pt: 0, amount_t: 4, amount_i: 0, amount_d: 0, amount_h: 4 },
+'010585893090': { amount_pt: 0, amount_t: 11, amount_i: 0, amount_d: 0, amount_h: 11 },
+'010585893099': { amount_pt: 2.4301336573512, amount_t: 17, amount_i: 2, amount_d: 0, amount_h: 15 },
+'010585893102': { amount_pt: 0, amount_t: 21, amount_i: 0, amount_d: 0, amount_h: 21 },
+'010585893137': { amount_pt: 0.36845983787767, amount_t: 56, amount_i: 1, amount_d: 1, amount_h: 54 },
+'010585893162': { amount_pt: 2.5906735751295, amount_t: 3, amount_i: 1, amount_d: 0, amount_h: 2 },
+'010585893166': { amount_pt: 0, amount_t: 33, amount_i: 0, amount_d: 1, amount_h: 32 },
+'010585893173': { amount_pt: 0, amount_t: 12, amount_i: 0, amount_d: 0, amount_h: 12 },
+'010585893174': { amount_pt: 0, amount_t: 14, amount_i: 0, amount_d: 0, amount_h: 14 },
+'010585895007': { amount_pt: 0, amount_t: 10, amount_i: 0, amount_d: 1, amount_h: 9 },
+'010585895009': { amount_pt: 0, amount_t: 63, amount_i: 0, amount_d: 0, amount_h: 63 },
+'010585895013': { amount_pt: 10.791366906475, amount_t: 13, amount_i: 3, amount_d: 0, amount_h: 10 },
+'010585895014': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585895015': { amount_pt: 1.3422818791946, amount_t: 48, amount_i: 1, amount_d: 1, amount_h: 46 },
+'010585895025': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585895044': { amount_pt: 1.6447368421053, amount_t: 9, amount_i: 1, amount_d: 0, amount_h: 8 },
+'010585895061': { amount_pt: 0, amount_t: 19, amount_i: 0, amount_d: 0, amount_h: 19 },
+'010585895062': { amount_pt: 0, amount_t: 5, amount_i: 0, amount_d: 0, amount_h: 5 },
+'010585895072': { amount_pt: 2.7036160865157, amount_t: 65, amount_i: 8, amount_d: 2, amount_h: 55 },
+'010585895074': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585895077': { amount_pt: 1.3062138458668, amount_t: 167, amount_i: 7, amount_d: 2, amount_h: 158 },
+'010585895085': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585895100': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 1, amount_h: 5 },
+'010585895103': { amount_pt: 0, amount_t: 1, amount_i: 0, amount_d: 0, amount_h: 1 },
+'010585895106': { amount_pt: 4.2735042735043, amount_t: 6, amount_i: 1, amount_d: 0, amount_h: 5 },
+'010585895113': { amount_pt: 0, amount_t: 16, amount_i: 0, amount_d: 0, amount_h: 16 },
+'010585895115': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585895119': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585895125': { amount_pt: 0, amount_t: 12, amount_i: 0, amount_d: 0, amount_h: 12 },
+'010585895128': { amount_pt: 1.1771630370806, amount_t: 22, amount_i: 2, amount_d: 0, amount_h: 20 },
+'010585895131': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585895134': { amount_pt: 2.3201856148492, amount_t: 10, amount_i: 1, amount_d: 0, amount_h: 9 },
+'010585895151': { amount_pt: 0, amount_t: 2, amount_i: 0, amount_d: 0, amount_h: 2 },
+'010585895156': { amount_pt: 0, amount_t: 6, amount_i: 0, amount_d: 0, amount_h: 6 },
+'010585895158': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585895159': { amount_pt: 0, amount_t: 0, amount_i: 0, amount_d: 0, amount_h: 0 },
+'010585895161': { amount_pt: 0, amount_t: 8, amount_i: 0, amount_d: 0, amount_h: 8 },
+'010585895164': { amount_pt: 1.9607843137255, amount_t: 21, amount_i: 2, amount_d: 0, amount_h: 19 },
+'010585895167': { amount_pt: 0, amount_t: 8, amount_i: 0, amount_d: 1, amount_h: 7 },
+}
-- 
GitLab