package nl.wldelft.fews.pi; import com.sun.org.apache.xml.internal.serialize.OutputFormat; import com.sun.org.apache.xml.internal.serialize.XMLSerializer; import nl.wldelft.util.FastDateFormat; import nl.wldelft.util.FileUtils; import nl.wldelft.util.Period; import nl.wldelft.util.TimeUnit; import nl.wldelft.util.io.LineWriter; import nl.wldelft.util.io.LittleEndianDataOutputStream; import nl.wldelft.util.io.TextSerializer; import nl.wldelft.util.io.VirtualOutputDir; import nl.wldelft.util.io.VirtualOutputDirConsumer; import nl.wldelft.util.timeseries.TimeSeriesContent; import nl.wldelft.util.timeseries.TimeSeriesHeader; import nl.wldelft.util.timeseries.TimeStep; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; public class PiTimeSeriesSerializer implements TextSerializer<TimeSeriesContent>, VirtualOutputDirConsumer { public enum EventDestination {XML_EMBEDDED, SEPARATE_BINARY_FILE, ONLY_HEADERS} private EventDestination eventDestination = EventDestination.XML_EMBEDDED; private PiVersion version = PiVersion.VERSION_1_2; private final FastDateFormat dateFormat = new FastDateFormat("yyyy-MM-dd"); private final FastDateFormat timeFormat = new FastDateFormat("HH:mm:ss"); private TimeSeriesContent timeSeriesContent = null; private LineWriter writer = null; private ContentHandler xmlContentHandler = null; private VirtualOutputDir virtualOutputDir = null; private LittleEndianDataOutputStream binaryOutputSteam = null; private final AttributesImpl attributesBuffer = new AttributesImpl(); public PiTimeSeriesSerializer() { } public EventDestination getEventDestination() { return eventDestination; } public void setEventDestination(EventDestination eventDestination) { this.eventDestination = eventDestination; } public PiVersion getVersion() { return version; } public void setVersion(PiVersion version) { if (version == null) throw new IllegalArgumentException("version == null"); this.version = version; } @Override public void setVirtualOutputDir(VirtualOutputDir virtualOutputDir) { this.virtualOutputDir = virtualOutputDir; } @Override public void serialize(TimeSeriesContent timeSeriesContent, LineWriter writer, String virtualFileName) throws Exception { this.timeSeriesContent = timeSeriesContent; this.writer = writer; dateFormat.setTimeZone(this.timeSeriesContent.getDefaultTimeZone()); timeFormat.setTimeZone(this.timeSeriesContent.getDefaultTimeZone()); if (eventDestination == EventDestination.SEPARATE_BINARY_FILE) { if (virtualOutputDir == null) throw new IllegalStateException("virtualOutputDir == null"); binaryOutputSteam = new LittleEndianDataOutputStream(virtualOutputDir.getOutputStream(FileUtils.getPathWithOtherExtension(virtualFileName, "bin"))); try { serialize(); } finally { binaryOutputSteam.close(); } return; } binaryOutputSteam = null; serialize(); } private void serialize() throws Exception { XMLSerializer serializer = new XMLSerializer(); OutputFormat of = new OutputFormat("XML", "UTF-8", true); serializer.setOutputFormat(of); serializer.setOutputCharStream(writer); xmlContentHandler = serializer.asContentHandler(); xmlContentHandler.startDocument(); attributesBuffer.clear(); addAttribute("xmlns", "http://www.wldelft.nl/fews/PI"); addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); addAttribute("xsi:schemaLocation", PiSchemaLocations.get("pi_timeseries.xsd")); addAttribute("version", version.toString()); xmlContentHandler.startElement("", "TimeSeries", "TimeSeries", attributesBuffer); writeElement("timeZone", String.valueOf((double) timeSeriesContent.getDefaultTimeZone().getRawOffset() / (double) TimeUnit.HOUR_MILLIS)); for (int i = 0, n = timeSeriesContent.getTimeSeriesCount(); i < n; i++) { timeSeriesContent.setTimeSeriesIndex(i); xmlContentHandler.startElement(null, null, "series", null); writeHeader(); writeEvents(); xmlContentHandler.endElement(null, null, "series"); } xmlContentHandler.endElement(null, null, "TimeSeries"); xmlContentHandler.endDocument(); } private void writeEvents() throws Exception { for (int i = 0, n = timeSeriesContent.getContentTimeCount(); i < n; i++) { timeSeriesContent.setContentTimeIndex(i); if (!timeSeriesContent.isTimeAvailable()) continue; writeEvent(timeSeriesContent.getTime()); } } private void writeHeader() throws Exception { TimeSeriesHeader header = timeSeriesContent.getTimeSeriesHeader(); PiTimeSeriesHeader piHeader = header instanceof PiTimeSeriesHeader ? (PiTimeSeriesHeader) header : new PiTimeSeriesHeader(); xmlContentHandler.startElement(null, null, "header", null); writeElement("type", header.getParameterType() == null ? "instantaneous" : header.getParameterType().getName()); writeElement("locationId", header.getLocationId() == null ? "unknown" : header.getLocationId()); writeElement("parameterId", header.getParameterId() == null ? "unknown" : header.getParameterId()); if (version.ordinal() >= PiVersion.VERSION_1_4.ordinal()) { for (int i = 0, n = header.getQualifierCount(); i < n; i++) { writeElement("qualifierId", header.getQualifierId(i)); } } if (version.ordinal() >= PiVersion.VERSION_1_4.ordinal() && header.getEnsembleId() != null && !header.getEnsembleId().equals("main")) { writeOptionalElement("ensembleId", header.getEnsembleId()); writeOptionalElement("ensembleMemberIndex", header.getEnsembleMemberIndex()); } writeTimeStep(header); writePeriod(); if (version.ordinal() >= PiVersion.VERSION_1_5.ordinal()) writeTime("forecastDate", header.getForecastTime()); writeElement("missVal", Float.toString(timeSeriesContent.getDefaultMissingValue())); writeOptionalElement("longName", piHeader.getLongName()); writeOptionalElement("stationName", header.getLocationName()); writeOptionalElement("units", header.getUnit()); writeOptionalElement("sourceOrganisation", piHeader.getSourceOrganisation()); writeOptionalElement("sourceSystem", piHeader.getSourceSystem()); writeOptionalElement("fileDescription", piHeader.getFileDescription()); if (header.getCreationTime() != Long.MIN_VALUE) { writeElement("creationDate", dateFormat.format(header.getCreationTime())); writeElement("creationTime", timeFormat.format(header.getCreationTime())); } writeOptionalElement("region", piHeader.getRegion()); xmlContentHandler.endElement(null, null, "header"); } private void writePeriod() throws SAXException { TimeStep timeStep = timeSeriesContent.getTimeSeriesHeader().getTimeStep(); Period period = timeSeriesContent.getTimeSeriesPeriod(); Period headerPeriod; if (period == Period.NEVER) { // create a dummy period long now = timeStep.nearestTime(System.currentTimeMillis()); headerPeriod = new Period(now, now); } else { headerPeriod = period; } writeTime("startDate", headerPeriod.getStartTime()); writeTime("endDate", headerPeriod.getEndTime()); } private void writeTime(String name, long time) throws SAXException { if (time == Long.MIN_VALUE) return; attributesBuffer.clear(); addAttribute("date", dateFormat.format(time)); addAttribute("time", timeFormat.format(time)); writeAttributes(name); } private void writeTimeStep(TimeSeriesHeader header) throws SAXException { TimeStep timeStep = header.getTimeStep(); attributesBuffer.clear(); // todo add support for month time step if (timeStep.isEquidistantMillis()) { long seconds = timeStep.getStepMillis() / 1000; addAttribute("unit","second"); addAttribute("multiplier", String.valueOf(seconds)); } else { addAttribute("unit", "nonequidistant"); } writeAttributes("timeStep"); } private void writeEvent(long time) throws Exception { if (eventDestination == EventDestination.ONLY_HEADERS) return; if (eventDestination == EventDestination.SEPARATE_BINARY_FILE) { binaryOutputSteam.writeFloat(timeSeriesContent.getValue()); return; } attributesBuffer.clear(); addAttribute("date", dateFormat.format(time)); addAttribute("time", timeFormat.format(time)); addAttribute("value", timeSeriesContent.getValue('.')); addAttribute("flag", timeSeriesContent.getStringFlag()); if (version.ordinal() >= PiVersion.VERSION_1_3.ordinal()) { String comment = timeSeriesContent.getComment(); if (comment != null) addAttribute("comment", timeSeriesContent.getComment()); } writeAttributes("event"); } private void writeOptionalElement(String elementName, int index) throws SAXException { if (index == -1) return; writeElement(elementName, Integer.toString(index)); } private void writeOptionalElement(String elementName, String s) throws SAXException { if (s == null) return; if (s.trim().length() == 0) return; writeElement(elementName, s); } private void writeElement(String name, String value) throws SAXException { xmlContentHandler.startElement(null, null, name, null); xmlContentHandler.characters(value.toCharArray(), 0, value.length()); xmlContentHandler.endElement(null, null, name); } private void writeAttributes(String name) throws SAXException { xmlContentHandler.startElement("", name, name, attributesBuffer); xmlContentHandler.endElement(null, null, name); } private void addAttribute(String name, String value) { attributesBuffer.addAttribute("", name, name, "CDATA", value); } }