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 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 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 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 combined = new HashMap<>(); synchronized (cache) { for (long item : items) { Map 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 items) { try { HistoryCache cache = getOrCreateCache(run); Map 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 _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 buildJSONResponse(Object... keyValues) { if ((keyValues.length % 2) != 0) throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues)); Map 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; } }