]> gerrit.simantics Code Review - simantics/platform.git/commitdiff
Rest API for Historian data 96/1896/7
authorMarko Luukkainen <marko.luukkainen@semantum.fi>
Wed, 4 Jul 2018 10:28:35 +0000 (13:28 +0300)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Tue, 14 Aug 2018 11:46:16 +0000 (11:46 +0000)
gitlab #40

Change-Id: I4bc9cc2dec3d3c2dcf902d372fc895b073d5e767

15 files changed:
bundles/org.simantics.history.rest/.classpath [new file with mode: 0644]
bundles/org.simantics.history.rest/.project [new file with mode: 0644]
bundles/org.simantics.history.rest/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
bundles/org.simantics.history.rest/META-INF/MANIFEST.MF [new file with mode: 0644]
bundles/org.simantics.history.rest/build.properties [new file with mode: 0644]
bundles/org.simantics.history.rest/scl/Simantics/History/RESTServer.scl [new file with mode: 0644]
bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryCache.java [new file with mode: 0644]
bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestApi.java [new file with mode: 0644]
bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestServer.java [new file with mode: 0644]
bundles/org.simantics.history/src/org/simantics/history/HistorySamplerItem2.java
bundles/pom.xml
features/org.simantics.history.rest.feature/.project [new file with mode: 0644]
features/org.simantics.history.rest.feature/build.properties [new file with mode: 0644]
features/org.simantics.history.rest.feature/feature.xml [new file with mode: 0644]
features/pom.xml

diff --git a/bundles/org.simantics.history.rest/.classpath b/bundles/org.simantics.history.rest/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/bundles/org.simantics.history.rest/.project b/bundles/org.simantics.history.rest/.project
new file mode 100644 (file)
index 0000000..1422d66
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.history.rest</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/bundles/org.simantics.history.rest/.settings/org.eclipse.jdt.core.prefs b/bundles/org.simantics.history.rest/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..0c68a61
--- /dev/null
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.simantics.history.rest/META-INF/MANIFEST.MF b/bundles/org.simantics.history.rest/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..a1f311a
--- /dev/null
@@ -0,0 +1,37 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Simantics History REST API
+Bundle-SymbolicName: org.simantics.history.rest
+Bundle-Version: 1.0.0.qualifier
+Bundle-Vendor: Semantum Oy
+Automatic-Module-Name: org.simantics.history.rest
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Require-Bundle: org.simantics.history;bundle-version="1.0.0",
+ org.eclipse.core.runtime;bundle-version="3.13.0",
+ org.glassfish.jersey.core.jersey-server,
+ javax.ws.rs-api,
+ org.simantics.scl.compiler,
+ org.simantics.scl.osgi,
+ org.eclipse.jetty.servlet,
+ org.glassfish.jersey.containers.jersey-container-servlet-core,
+ javax.servlet-api,
+ org.eclipse.jetty.server,
+ org.eclipse.jetty.util,
+ org.eclipse.jetty.io,
+ org.eclipse.jetty.servlets,
+ com.fasterxml.jackson.core.jackson-core;bundle-version="2.8.8",
+ com.fasterxml.jackson.core.jackson-annotations;bundle-version="2.8.0",
+ com.fasterxml.jackson.core.jackson-databind;bundle-version="2.8.8",
+ org.glassfish.jersey.media.jersey-media-json-jackson;bundle-version="2.25.1",
+ org.slf4j.api,
+ org.glassfish.jersey.core.jersey-client,
+ org.glassfish.jersey.core.jersey-common;bundle-version="2.25.1",
+ org.simantics.scl.runtime,
+ gnu.trove3;bundle-version="3.0.3",
+ org.simantics.charts;bundle-version="0.0.1",
+ org.simantics.db;bundle-version="1.1.0",
+ org.simantics.db.common;bundle-version="1.1.0",
+ org.simantics.db.layer0;bundle-version="1.1.0",
+ org.simantics;bundle-version="1.0.0",
+ org.eclipse.jetty.servlets
+Bundle-ClassPath: .
diff --git a/bundles/org.simantics.history.rest/build.properties b/bundles/org.simantics.history.rest/build.properties
new file mode 100644 (file)
index 0000000..a4fd10d
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               scl/
diff --git a/bundles/org.simantics.history.rest/scl/Simantics/History/RESTServer.scl b/bundles/org.simantics.history.rest/scl/Simantics/History/RESTServer.scl
new file mode 100644 (file)
index 0000000..b8dce51
--- /dev/null
@@ -0,0 +1,6 @@
+importJava "org.simantics.history.rest.HistoryRestServer" where
+    start :: Integer -> <Proc> ()
+    stop :: <Proc> ()
+    running :: <Proc> Boolean
+    setAllowOrigin :: String -> <Proc>()
+    address :: <Proc> String
\ No newline at end of file
diff --git a/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryCache.java b/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryCache.java
new file mode 100644 (file)
index 0000000..35d9e8d
--- /dev/null
@@ -0,0 +1,115 @@
+package org.simantics.history.rest;
+
+import org.simantics.Simantics;
+import org.simantics.charts.Charts;
+import org.simantics.db.AsyncReadGraph;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.common.request.ResourceRead;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.variable.Variable;
+import org.simantics.db.procedure.AsyncListener;
+import org.simantics.db.request.Read;
+import org.simantics.db.service.SerialisationSupport;
+import org.simantics.history.HistorySamplerItem;
+import org.simantics.history.HistorySamplerItem2;
+import org.simantics.layer0.Layer0;
+import org.simantics.utils.datastructures.Pair;
+
+public class HistoryCache {
+       
+       long runId;
+       Variable run;
+       
+       gnu.trove.map.TLongObjectMap<HistorySamplerItem> historySamplers = new gnu.trove.map.hash.TLongObjectHashMap<>();
+       gnu.trove.map.TLongObjectMap<HistorySamplerItem2> historySamplers2 = new gnu.trove.map.hash.TLongObjectHashMap<>();
+       
+       boolean disposed = false;
+       
+       public HistoryCache(long runId) throws DatabaseException{
+               this.runId = runId;
+               Pair<Resource, Variable> pair = Simantics.getSession().syncRequest(new Read<Pair<Resource, Variable>>() {
+                       @Override
+                       public Pair<Resource, Variable> perform(ReadGraph graph) throws DatabaseException {
+                               Resource resource = getResource(graph, runId);
+                               return new Pair<>(resource, graph.adapt(resource, Variable.class));
+                       }
+               });
+               run = pair.second;
+               // Attach listener for automatic disposal
+               Simantics.getSession().async(new ResourceRead<Resource>(pair.first) {
+                       @Override
+                       public Resource perform(ReadGraph graph) throws DatabaseException {
+                               Layer0 L0 = Layer0.getInstance(graph);
+                               return graph.getPossibleObject(resource, L0.PartOf);
+                       }
+               }, new AsyncListener<Resource>() {
+                       @Override
+                       public void execute(AsyncReadGraph graph, Resource result) {
+                               if (result == null)
+                                       dispose();
+                               
+                       }
+                       
+                       @Override
+                       public void exception(AsyncReadGraph graph, Throwable throwable) {
+                               dispose();
+                       }
+                       @Override
+                       public boolean isDisposed() {
+                               return disposed;
+                       }
+               });
+       }
+       
+       public HistorySamplerItem getSamplerItem(final long itemId) throws DatabaseException {
+               HistorySamplerItem item = historySamplers.get(itemId);
+               if (item == null) {
+                       item = Simantics.getSession().syncRequest(new Read<HistorySamplerItem>() {
+                               @Override
+                               public HistorySamplerItem perform(ReadGraph graph) throws DatabaseException {
+                                       Resource item = getResource(graph, itemId);
+                                       return Charts.createHistorySamplerItem(graph,run, item);
+                               }
+                       });
+                       historySamplers.put(itemId, item);
+               }
+               return item;
+       }
+       
+       public HistorySamplerItem2 getSamplerItem2(final long itemId) throws DatabaseException {
+               HistorySamplerItem2 item = historySamplers2.get(itemId);
+               if (item == null) {
+                       item = Simantics.getSession().syncRequest(new Read<HistorySamplerItem2>() {
+                               @Override
+                               public HistorySamplerItem2 perform(ReadGraph graph) throws DatabaseException {
+                                       Resource item = getResource(graph, itemId);
+                                       return Charts.createHistorySamplerItem2(graph,run, item);
+                               }
+                       });
+                       historySamplers2.put(itemId, item);
+               }
+               return item;
+       }
+       
+       public void dispose() {
+               disposed = true;
+               for (HistorySamplerItem i : historySamplers.valueCollection())
+                       i.close();
+               historySamplers.clear();
+               for (HistorySamplerItem2 i : historySamplers2.valueCollection())
+                       i.close();
+               historySamplers2.clear();
+       }
+       
+       public boolean isDisposed() {
+               return disposed;
+       }
+       
+       
+       private Resource getResource(ReadGraph graph, long id) throws DatabaseException { 
+               SerialisationSupport ss = graph.getService(SerialisationSupport.class);
+           return ss.getResource(id);
+       }
+       
+}
diff --git a/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestApi.java b/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestApi.java
new file mode 100644 (file)
index 0000000..ec3ee25
--- /dev/null
@@ -0,0 +1,360 @@
+package org.simantics.history.rest;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.history.HistoryException;
+import org.simantics.history.HistorySampler;
+import org.simantics.history.HistorySamplerItem;
+import org.simantics.history.HistorySamplerItem2;
+import org.simantics.history.csv.ExportInterpolation;
+import org.simantics.history.util.HistoryExportUtil;
+import org.simantics.history.util.StreamIterator;
+import org.simantics.history.util.ValueBand;
+
+import gnu.trove.list.array.TDoubleArrayList;
+
+@Path("history")
+@Produces(MediaType.APPLICATION_JSON)
+public class HistoryRestApi {
+
+       private static gnu.trove.map.TLongObjectMap<HistoryCache> historyCaches = new gnu.trove.map.hash.TLongObjectHashMap<>();
+       
+       private static CacheControl cacheControl;
+       
+       static {
+               cacheControl = new CacheControl();
+               cacheControl.setNoCache(true);
+               cacheControl.setNoStore(true);
+       }
+
+       @Path("/register")
+       @POST
+       public static synchronized Response register(@QueryParam("runId") long run) {
+               if (historyCaches.containsKey(run))
+                       return Response.ok().build();
+               try {
+                       HistoryCache cache = new HistoryCache(run);
+                       historyCaches.put(run, cache);
+                       return Response.ok().cacheControl(cacheControl).build();
+               } catch (DatabaseException e) {
+                       return Response.status(Status.NOT_FOUND).build();
+               }
+       }
+
+       @Path("/unregister")
+       @POST
+       public static synchronized Response unregister(@QueryParam("runId") long run) {
+               HistoryCache cache = historyCaches.remove(run);
+               if (cache != null) {
+                       cache.dispose();
+               }
+               return Response.ok().cacheControl(cacheControl).build();
+       }
+
+       @Path("/values")
+       @GET
+       public static Response activeHistoryValuesAndTimesWithTimeStep(@QueryParam("runId") long run,@QueryParam("itemId") long item,@QueryParam("startTime") double startTime,@QueryParam("endTime") double endTime,@QueryParam("timeWindow") double timeWindow, @QueryParam("timeStep")double timeStep) {
+               try {
+                       HistoryCache cache = getOrCreateCache(run);
+                       TDoubleArrayList doubles;
+                       synchronized (cache) {
+                               HistorySamplerItem hi = cache.getSamplerItem(item);
+                               doubles= HistorySampler.sample(hi, startTime, endTime, timeWindow, timeStep, true);
+                       }        
+                       return Response.ok(buildJSONResponse("values",doubles.toArray())).cacheControl(cacheControl).build();
+               } catch (HistoryException | IOException | DatabaseException e) {
+                       return Response.serverError().build();
+               }
+
+       }
+       @Path("/lvalues")
+       @GET
+       public static Response sampleHistory(@QueryParam("runId")long run,@QueryParam("itemId") long item,@QueryParam("endTime") double endTime, @QueryParam("timeWindow") double timeWindow,@QueryParam("maxSamples") int maxSamples,@QueryParam("resample") boolean resample) {
+               try {
+                       HistoryCache cache = getOrCreateCache(run);
+                       TDoubleArrayList doubles = null;
+                       synchronized (cache) {
+                               HistorySamplerItem2 hi = cache.getSamplerItem2(item);
+                               doubles = HistorySampler.sample(hi, endTime, timeWindow, maxSamples, resample);
+                       }
+                       return Response.ok(buildJSONResponse("values",doubles.toArray())).cacheControl(cacheControl).build();
+               } catch (HistoryException | IOException | DatabaseException e) {
+                       return Response.serverError().build();
+               }
+       }
+       
+       @Path("/samples")
+       @GET
+       public static Response sampleHistory(@QueryParam("runId")long run,@QueryParam("itemId") long item,@QueryParam("startTime") double startTime, @QueryParam("endTime") double endTime, @QueryParam("maxSamples") int maxSamples) {
+               
+               if (maxSamples <= 0)
+                       return Response.status(Status.BAD_REQUEST).build();
+               try {
+                       HistoryCache cache = getOrCreateCache(run);
+                       Map<String, Object> result = null;
+                       synchronized (cache) {
+                               result = _sampleHistory(cache, item, startTime, endTime, maxSamples);
+                       }
+                       return Response.ok(result).cacheControl(cacheControl).build();
+               } catch (HistoryException | DatabaseException e) {
+                       return Response.serverError().build();
+               }
+       }
+       
+       @Path("/multisamples")
+       @GET
+       public static Response sampleHistories(@QueryParam("runId")long run,@QueryParam("itemId") final List<Long> items,@QueryParam("startTime") double startTime, @QueryParam("endTime") double endTime, @QueryParam("maxSamples") int maxSamples) {
+               
+               if (maxSamples <= 0)
+                       return Response.status(Status.BAD_REQUEST).build();
+               try {
+                       HistoryCache cache = getOrCreateCache(run);
+                       Map<String, Object> combined = new HashMap<>();
+                       synchronized (cache) {
+                               for (long item : items) {
+                                       Map<String, Object> result = _sampleHistory(cache, item, startTime, endTime, maxSamples);
+                                       combined.put(Long.toString(item), result);
+                               }
+                       }
+                       return Response.ok(combined).cacheControl(cacheControl).build();
+               } catch (HistoryException | DatabaseException e) {
+                       return Response.serverError().build();
+               }
+       }
+       
+       @Path("/ranges")
+       @GET
+       public static Response ranges(@QueryParam("runId")long run,@QueryParam("itemId") final List<Long> items) {
+               
+               try {
+                       HistoryCache cache = getOrCreateCache(run);
+                       Map<String, Object> combined = new HashMap<>();
+                       synchronized (cache) {
+                               for (long item : items) {
+                                       HistorySamplerItem2 hi = null;
+                                       try {
+                                               hi= cache.getSamplerItem2(item);
+                                               hi.open();
+                                               double first = hi.iter.getFirstTime();
+                                               double last = hi.iter.getLastTime();
+                                               combined.put(Long.toString(item), buildJSONResponse("start",first,"end",last));
+                                               
+                                       } finally {
+                                               hi.close();
+                                       }
+                                       
+                               }
+                       }
+                       return Response.ok(combined).cacheControl(cacheControl).build();
+                       
+               } catch (HistoryException | DatabaseException e) {
+                       return Response.serverError().build();
+               }
+       }
+       private static synchronized HistoryCache getOrCreateCache(long run) throws DatabaseException {
+               HistoryCache cache = historyCaches.get(run);
+               if (cache == null || cache.isDisposed()) {
+                       cache = new HistoryCache(run);
+                       historyCaches.put(run, cache);
+               } 
+               return cache;
+       }
+       
+       
+       
+       private static Map<String,Object> _sampleHistory(HistoryCache cache, long item, double startTime, double endTime, int maxSamples) throws DatabaseException, HistoryException {
+               HistorySamplerItem2 hi = null;
+               try {
+                       hi = cache.getSamplerItem2(item);
+                       
+                       double timeWindow = endTime - startTime;
+                       //double timeStep = 0.0; 
+                       double timeStep = timeWindow / maxSamples;
+                       double secondsPerPixel = timeWindow / (double) maxSamples;
+                       hi.flush();
+                       if (hi.iter == null)
+                               hi.open(secondsPerPixel);
+                       StreamIterator iter = hi.iter;
+                       if (iter.isEmpty()) {
+                               buildJSONResponse("time",new double[0],"value",new double[0]);
+                       }
+                       double dataFrom = iter.getFirstTime();
+                       double dataEnd = iter.getLastTime();
+                       boolean hasAnyValues = dataFrom != Double.MAX_VALUE && dataEnd != -Double.MAX_VALUE;
+                       if (!hasAnyValues) {
+                               return buildJSONResponse("time",new double[0],"value",new double[0]);   
+                       }
+                       double from = Math.max(startTime, dataFrom);
+                       double end = Math.min(endTime, dataEnd);
+                       iter.gotoTime(startTime);
+                       
+                       double time = from;
+                       
+                       BigDecimal bigTime = new BigDecimal(String.valueOf(time));
+                       BigDecimal bigTimeStep = new BigDecimal(String.valueOf(timeStep));
+       
+                       TDoubleArrayList times = new TDoubleArrayList();
+                       TDoubleArrayList values = new TDoubleArrayList();
+                       TDoubleArrayList min = new TDoubleArrayList();
+                       TDoubleArrayList max = new TDoubleArrayList();
+                       TDoubleArrayList avg = new TDoubleArrayList();
+                       if (!iter.gotoTime(time)) {
+                               return buildJSONResponse("time",new double[0],"value",new double[0]);   
+                       }
+       
+                       
+                       ExportInterpolation interpolation = ExportInterpolation.LINEAR_INTERPOLATION;
+                       do {
+                               //System.out.println("process " + time + " " + iter.getValueBand() + " (ignore = " + ignore + ")");
+       
+                               // Check for valid value
+                               if ( iter.hasValidValue() ) {
+       
+                                       // Write time
+                                       times.add(time);
+                                       // Write value
+                                       Object value = iter.getValueBand().getValue();
+                                       //System.out.print("Add value : " + value);
+                                       if (value instanceof Number) {
+                                               if (value instanceof Float || value instanceof Double) {
+                                                       switch (interpolation) {
+                                                       case PREVIOUS_SAMPLE:
+                                                               values.add(((Number) value).doubleValue());
+                                                               break;
+       
+                                                       case LINEAR_INTERPOLATION:
+                                                               if (time != iter.getValueBand().getTimeDouble() && iter.hasNext()) {
+                                                                       // Interpolate
+                                                                       int currentIndex = iter.getIndex();
+                                                                       ValueBand band = iter.getValueBand();
+                                                                       //double t1 = band.getTimeDouble();
+                                                                       Number v1 = (Number) value;
+                                                                       double t12 = band.getEndTimeDouble();
+                                                                       iter.next();
+                                                                       double t2 = iter.getValueBand().getTimeDouble();
+                                                                       Number v2 = (Number) iter.getValueBand().getValue();
+                                                                       iter.gotoIndex(currentIndex);
+       
+                                                                       double vs = v1.doubleValue();
+                                                                       if (time > t12)
+                                                                               vs = HistoryExportUtil.biglerp(t12, v1.doubleValue(), t2, v2.doubleValue(), time);
+       
+                                                                       values.add(vs);
+       
+                                                               } else {
+                                                                       // Exact timestamp match, or last sample.
+                                                                       // Don't interpolate nor extrapolate.
+                                                                       values.add(((Number) value).doubleValue());
+                                                               }
+                                                               break;
+       
+                                                       default:
+                                                               throw new UnsupportedOperationException("Unsupported interpolation: " + interpolation);
+                                                       }
+                                                       if (iter.getValueBand().hasMax())
+                                                               max.add(((Number) iter.getValueBand().getMax()).doubleValue());
+                                                       else
+                                                               max.add(Double.NaN);
+                                                       if (iter.getValueBand().hasMin())
+                                                               min.add(((Number) iter.getValueBand().getMin()).doubleValue());
+                                                       else
+                                                               min.add(Double.NaN);
+                                                       if (iter.getValueBand().hasAvg())
+                                                               avg.add(((Number) iter.getValueBand().getAvg()).doubleValue());
+                                                       else
+                                                               avg.add(Double.NaN);
+                                               } else {
+                                                       throw new IllegalStateException("Value is not a number " + value);
+                                               }
+                                       } else if (value instanceof Boolean) {
+                                               values.add( (Boolean)value ? 1.0: 0.0);
+                                       } else {
+                                               throw new IllegalStateException("Value is not a number " + value);
+                                       }
+                                       
+                               }
+                               
+       
+                               // Read next values, and the following times
+                               if ( timeStep>0.0 ) {
+                                       bigTime = bigTime.add(bigTimeStep);
+                                       time = bigTime.doubleValue();
+                               } else {
+                                       // Get smallest end time that is larger than current time
+                                       Double nextTime = null;
+                                       //System.out.println(" time = "+time);
+                                       if(!iter.hasNext()) {
+                                               duplicateLastDataPoint(values);
+                                               times.add(end);
+                                               break;
+                                       }
+                                       Double itemNextTime = iter.getNextTime( time );
+                                       //System.out.println("  "+iter.toString()+" nextTime="+itemNextTime);
+                                       if ( nextTime == null || ( nextTime > itemNextTime && !itemNextTime.equals( time ) ) ) nextTime = itemNextTime; 
+                                       if ( nextTime == null || nextTime.equals( time ) ) break;
+                                       time = nextTime;
+                               }
+       
+                               boolean hasMore = false;
+       
+                               iter.proceedToTime(time);
+                               if (HistoryExportUtil.contains(iter, time)) hasMore = true;
+       
+                               if (!hasMore) {
+                                       if (time <= end) {
+                                               duplicateLastDataPoint(values);
+                                               times.add(end);
+                                       }
+                                       break;
+                               }
+       
+                       } while (time <= end);
+                       
+                       return buildJSONResponse("time", times.toArray(),"value",values.toArray(), "min", min.toArray(), "max", max.toArray(),"avg", avg.toArray());
+               } finally {
+                       if (hi != null) {
+                               try {
+                                       hi.close();
+                               } catch (Throwable err) {
+                               }
+                       }
+               }
+       }
+       
+       private static void duplicateLastDataPoint(TDoubleArrayList data) {
+               double lastValue = data.get(data.size() - 1);
+               //System.out.println("Duplicating last sample value " + lastValue + " @ " + timestamp);
+               data.add(lastValue);
+       }
+
+       private static Map<String, Object> buildJSONResponse(Object... keyValues) {
+               if ((keyValues.length % 2) != 0)
+                       throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues));
+               Map<String, Object> results = new HashMap<>(keyValues.length / 2);
+               for (int i = 0; i < keyValues.length; i += 2) {
+                       Object key = keyValues[i];
+                       Object value = keyValues[i + 1];
+                       if (!(key instanceof String))
+                               throw new IllegalArgumentException("Key with index " + i + " is not String");
+                       results.put((String) key, value);
+               }
+               return results;
+       }
+
+}
diff --git a/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestServer.java b/bundles/org.simantics.history.rest/src/org/simantics/history/rest/HistoryRestServer.java
new file mode 100644 (file)
index 0000000..00b4a1d
--- /dev/null
@@ -0,0 +1,137 @@
+package org.simantics.history.rest;
+
+import java.util.EnumSet;
+import java.util.concurrent.Semaphore;
+
+import javax.servlet.DispatcherType;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HistoryRestServer {
+private static final Logger LOGGER = LoggerFactory.getLogger(HistoryRestServer.class);
+    
+    private static HistoryRestServer INSTANCE = null;
+    private static Server server;
+    private static ServiceServerThread serverThread;
+    
+    private static String allowOriginUrl = null; 
+    
+    private static String address = null;
+    
+    private HistoryRestServer(int port) {
+        ResourceConfig config = new ResourceConfig();
+         // JSON serialization/deserialization
+         config.register(JacksonFeature.class);
+         // Actual API
+         config.register(HistoryRestApi.class);
+         
+         ServletHolder holder = new ServletHolder(new ServletContainer(config));
+         
+         server = new Server();
+         ServerConnector connector = new ServerConnector(server);
+         connector.setPort(port);
+         
+         server.setConnectors(new Connector[] { connector });
+         
+         ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
+         context.addServlet(holder, "/*");
+         
+         if (allowOriginUrl != null){
+               org.eclipse.jetty.servlets.CrossOriginFilter cfilter = new org.eclipse.jetty.servlets.CrossOriginFilter();
+               FilterHolder filterHolder = new FilterHolder(cfilter);
+               filterHolder.setInitParameter(org.eclipse.jetty.servlets.CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, allowOriginUrl);
+               context.addFilter(filterHolder, "/*", EnumSet.allOf(DispatcherType.class));
+         }
+    }
+    
+    
+    private static class ServiceServerThread extends Thread {
+
+       Semaphore s;
+        @Override
+        public void run() {
+            try {
+                server.start();
+                address = server.getURI().toString()+"history/";
+                if (s != null)
+                       s.release();
+                server.join();
+            } catch (Exception e) {
+                LOGGER.error("Could not start server ", e);
+            }
+        }
+        
+        public void startAndWait() {
+               s = new Semaphore(0);
+               this.start();
+               try {
+                       s.acquire();
+               } catch (InterruptedException e) {};
+        }
+    }
+    
+    private static synchronized HistoryRestServer getInstance(int port) {
+        try {
+            if (INSTANCE == null) {
+                INSTANCE = new HistoryRestServer(port);
+            }
+        } catch (Exception e) {
+            LOGGER.error("Could not initialize SCL REST server", e);
+        }
+        return INSTANCE;
+    }
+    
+    public static HistoryRestServer getRunningInstance() {
+       return INSTANCE;
+    }
+    
+    public static void setAllowOrigin(String url) {
+       allowOriginUrl = url;
+    }
+
+    public static synchronized void startAsync(int port) throws Exception {
+        // Ensure that an instance is created
+        getInstance(port);
+        if (serverThread == null && server != null) {
+            serverThread = new ServiceServerThread();
+            serverThread.start();
+        }
+    }
+    
+    public static synchronized void start(int port) throws Exception {
+        // Ensure that an instance is created
+        getInstance(port);
+        if (serverThread == null && server != null) {
+            serverThread = new ServiceServerThread();
+            serverThread.startAndWait();
+        }
+    }
+    
+    public static synchronized void stop() throws Exception {
+        if (server != null)
+            server.stop();
+        serverThread = null;
+        INSTANCE = null;
+        address = null;
+        
+    }
+    
+    public static synchronized boolean running() {
+       return INSTANCE != null;
+    }
+    
+    public static synchronized String address() {
+       return address;
+    }
+
+}
index be6940333a34486af59d3d1c16379daa23043254..d5c3942a6ba35caa4c796f54526763b18ea7f20b 100644 (file)
@@ -41,6 +41,11 @@ public class HistorySamplerItem2 implements Comparable<HistorySamplerItem2> {
                this.items = items;
                this.chartDataId = identityHashCode;
        }
+       
+       public void flush() throws HistoryException {
+               if (collector != null)
+                       collector.flush();
+       }
 
        public void open() throws HistoryException {
                accessor = history.openStream(items[0].id, "r");
index 88535ff46103c6d8dc6905083321a5e4221550c1..04b6311eba4b916233ab64527de7f3fcd9a0b507 100644 (file)
                <module>org.simantics.help.ontology</module>
                <module>org.simantics.help.ui</module>
                <module>org.simantics.history</module>
+               <module>org.simantics.history.rest</module>
                <module>org.simantics.image.ontology</module>
                <module>org.simantics.image.ui</module>
                <module>org.simantics.image2.ontology</module>
diff --git a/features/org.simantics.history.rest.feature/.project b/features/org.simantics.history.rest.feature/.project
new file mode 100644 (file)
index 0000000..a740f26
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.history.rest.feature</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.pde.FeatureBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.FeatureNature</nature>
+       </natures>
+</projectDescription>
diff --git a/features/org.simantics.history.rest.feature/build.properties b/features/org.simantics.history.rest.feature/build.properties
new file mode 100644 (file)
index 0000000..64f93a9
--- /dev/null
@@ -0,0 +1 @@
+bin.includes = feature.xml
diff --git a/features/org.simantics.history.rest.feature/feature.xml b/features/org.simantics.history.rest.feature/feature.xml
new file mode 100644 (file)
index 0000000..33dd73e
--- /dev/null
@@ -0,0 +1,252 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feature
+      id="org.simantics.history.rest.feature"
+      label="History REST Feature"
+      version="1.0.0.qualifier"
+      provider-name="Semantum">
+
+   <description url="http://www.example.com/description">
+      [Enter Feature Description here.]
+   </description>
+
+   <copyright url="http://www.example.com/copyright">
+      [Enter Copyright Description here.]
+   </copyright>
+
+   <license url="http://www.example.com/license">
+      [Enter License Description here.]
+   </license>
+
+   <requires>
+      <import plugin="org.simantics.history"/>
+      <import plugin="gnu.trove3"/>
+      <import plugin="org.simantics.charts"/>
+      <import plugin="org.simantics.db"/>
+      <import plugin="org.slf4j.api"/>
+   </requires>
+
+   <plugin
+         id="org.simantics.history.rest"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.core.jersey-server"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="javax.ws.rs-api"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.jetty.servlet"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.containers.jersey-container-servlet-core"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="javax.servlet-api"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.jetty.server"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.jetty.util"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.jetty.io"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.core.jackson-core"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.core.jackson-annotations"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.core.jackson-databind"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.media.jersey-media-json-jackson"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.media.jersey-media-multipart"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.slf4j.api"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.jvnet.mimepull"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.core.runtime"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.bundles.repackaged.jersey-guava"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.jaxrs.jackson-jaxrs-base"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="javax.validation.api"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.hk2.api"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.hk2.external.aopalliance-repackaged"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.hk2.locator"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.hk2.osgi-resource-locator"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.hk2.utils"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.core.jersey-common"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.ext.jersey-entity-filtering"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.glassfish.jersey.core.jersey-client"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="javassist"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="com.fasterxml.jackson.module.jackson-module-jaxb-annotations"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.eclipse.jetty.servlets"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+</feature>
index 0f4197b1a9a5dd432641361129feca4b2c19362f..4c19ccca4f224921948976fcb323a365064370e1 100644 (file)
         <module>org.simantics.g2d.feature</module>
         <module>org.simantics.help.feature</module>
         <module>org.simantics.help.addons.feature</module>
+        <module>org.simantics.history.rest.feature</module>
         <module>org.simantics.image.feature</module>
         <module>org.simantics.issues.feature</module>
         <module>org.simantics.issues.ui.feature</module>