Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
/* ================================================================
 * Delft FEWS 
 * ================================================================
 *
 * Project Info:  http://www.wldelft.nl/soft/fews/index.html
 * Project Lead:  Karel Heynert (karel.heynert@wldelft.nl)
 *
 * (C) Copyright 2003, by WL | Delft Hydraulics
 *                        P.O. Box 177
 *                        2600 MH  Delft
 *                        The Netherlands
 *                        http://www.wldelft.nl
 *
 * DELFT-FEWS is a sophisticated collection of modules designed 
 * for building a FEWS customised to the specific requirements 
 * of individual agencies. An open modelling approach allows users
 * to add their own modules in an efficient way.
 *
 * ----------------------------------------------------------------
 * PiTimeSeriesParser.java
 * ----------------------------------------------------------------
 * (C) Copyright 2003, by WL | Delft Hydraulics
 *
 * Original Author:  Erik de Rooij
 * Original Author:  Onno van den Akker
 */

package nl.wldelft.fews.pi;

import nl.wldelft.util.*BinaryUtils;
import nl.wldelft.util.coverage.PointGeometryCharArrayUtils;
import nl.wldelft.util.geodatum.GeoDatumClasz;
import nl.wldelft.util.io.VirtualInputDirExceptionUtils;
import nl.wldelft.util.io.VirtualInputDirConsumerFastDateFormat;
import nl.wldelft.util.io.XmlParserFileUtils;
import nl.wldelft.util.timeseries.*FloatArrayUtils;

import javaxnl.xmlwldelft.streamutil.XMLStreamConstantsNumberType;
import javaxnl.xmlwldelft.streamutil.XMLStreamExceptionPeriod;
import javaxnl.xmlwldelft.streamutil.XMLStreamReaderProperties;
import javanl.wldelft.ioutil.EOFExceptionStringArrayUtils;
import javanl.wldelft.ioutil.IOExceptionTextUtils;
import javanl.wldelft.ioutil.InputStreamTimeUnit;
import javanl.wldelft.nioutil.ByteOrderTimeZoneUtils;
import java.text.ParseExceptionnl.wldelft.util.geodatum.GeoDatum;
import javanl.wldelft.util.io.ArrayListVirtualInputDir;
import javanl.wldelft.util.Listio.VirtualInputDirConsumer;
import javanl.wldelft.util.io.LocaleXmlParser;
import javanl.wldelft.util.TimeZonetimeseries.DefaultTimeSeriesHeader;

public class PiTimeSeriesParser implements XmlParser<TimeSeriesContentHandler>, VirtualInputDirConsumer {
    private static final int BUFFER_SIZE = 2048;
    private final Properties.Builder propertyBuilder = new Properties.Builder();

    @Override
    public void setVirtualInputDir(VirtualInputDir virtualInputDir) {import nl.wldelft.util.timeseries.IrregularTimeStep;
import nl.wldelft.util.timeseries.OutOfDetectionRangeFlag;
import nl.wldelft.util.timeseries.ParameterType;
import nl.wldelft.util.timeseries.RelativeEquidistantTimeStep;
import nl.wldelft.util.timeseries.SimpleEquidistantTimeStep;
import nl.wldelft.util.timeseries.TimeSeriesArray;
import nl.wldelft.util.timeseries.TimeSeriesContentHandler;
import nl.wldelft.util.timeseries.TimeSeriesHeader;
import nl.wldelft.util.timeseries.TimeStep;
import nl.wldelft.util.timeseries.ValueSource;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

public class PiTimeSeriesParser implements XmlParser<TimeSeriesContentHandler>, VirtualInputDirConsumer {
    private static final int BUFFER_SIZE = 2048;
    private final Properties.Builder propertyBuilder = new Properties.Builder();
    private char[] charBuffer = null;
    private float minValueResolution = Float.NaN;
    private float[][] axisValues = null;
    private float[] axisValueResolutions = Clasz.floats.emptyArray();
    private boolean axesDirty = false;
    private String[] domainParameterIds = Clasz.strings.emptyArray();
    private String[] domainUnits = Clasz.strings.emptyArray();
    private int domainCount = 0;

    @Override
    public void setVirtualInputDir(VirtualInputDir virtualInputDir) {
        this.virtualInputDir = virtualInputDir;
    }

    private enum HeaderElement {
        type(F.R), moduleInstanceId, locationId(F.R),
        parameterId(F.R), qualifierId(F.M), ensembleId, ensembleMemberIndex, ensembleMemberId,
        timeStep(F.R | F.A), startDate(F.R | F.A), endDate(F.R | F.A), forecastDate(F.A), approvedDate(F.A),
        missVal, longName, stationName, lat, lon, x, y, z, units, domainAxis(F.M | F.A),
        sourceOrganisation, sourceSystem, fileDescription,
        creationDate, creationTime, region, thresholds,
        firstValueTime(F.A), lastValueTime(F.A), maxValue, minValue, valueCount, maxWarningLevelName;

        interface F {
            int A = 1 << 0; // attributes
            int R = 1 << 1; // required;
            int M = 1 << 2; // multiple;
        }

        private final int flags;

        HeaderElement() {
            this.flags = 0;
        }

        HeaderElement(int flags) {
            this.flags = flags;
        }

        public boolean isRequired() {
            return (flags & F.R) != 0;
        }

        public boolean hasAttributes() {
            return (flags & F.A) != 0;
        }

        public boolean isMultipleAllowed() {
            return (flags & F.M) != 0;
        }
    }


    // fastDateFormat is used to keep track of last time zone and lenient
    private FastDateFormat fastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd", "HH:mm:ss", TimeZoneUtils.GMT, Locale.US, null);
    private FastDateFormat fastDateFormatWithMillies = FastDateFormat.getInstance("yyyy-MM-dd", "HH:mm:ss.SSS", TimeZoneUtils.GMT, Locale.US, null);

    private HeaderElement currentHeaderElement = null;

    private static final HeaderElement[] HEADER_ELEMENTS = HeaderElement.class.getEnumConstants();

    private PiTimeSeriesHeader header = new PiTimeSeriesHeader();
    private List<String> qualifiers = new ArrayList<>();
    private long timeStepMillis = 0;
    private TimeStep timeStep = null;
    private long startTime = Long.MIN_VALUE;
    private long endTime = Long.MIN_VALUE;
    private float missingValue = Float.NaN;
    private String creationDateText = null;
    private String creationTimeText = null;
    private long lastTime = Long.MIN_VALUE;
    private long lastStartTime = Long.MIN_VALUE;
    private long lastEndTime = Long.MIN_VALUE;
    private boolean timeAmbiguous = false;
    private boolean lastTimeAmbiguous = false;
    private boolean lastStartTimeAmbiguous = false;
    private boolean lastEndTimeAmbiguous = false;

    private TimeSeriesContentHandler timeSeriesContentHandler = null;

    private PiTimeSeriesSerializer.EventDestination eventDestination = null;

    /**
     * For performance reasons the pi time series format allows that the values are stored in
     * a separate bin file instead of embedded in the xml file.
     * The bin file should have same name as the xml file except the extension equals bin
     * In this case all time series should be equidistant.
     */
    private VirtualInputDir virtualInputDir = VirtualInputDir.NONE;
    private InputStream binaryInputStream = null;
    private byte[] byteBuffer = null;
    private float[] floatBuffer = null;
    private int bufferPos = 0;
    private int bufferCount = 0;

    private XMLStreamReader reader = null;
    private String virtualFileName = null;

    private static boolean lenient = false;
    private double lat = Double.NaN;
    private double lon = Double.NaN;
    private double z = Double.NaN;

    /**
     * For backwards compatibility. Earlier versions of the PiTimeSeriesParser were tolerant about the date/time format
     * and the case insensitive for header element names.
     * This parser should not accept files that are not valid according to pi_timeseries.xsd
     * When old adapters are not working you can UseLenientPiTimeSeriesParser temporary till the adapter is fixed
     *
     * @param lenient
     */
    public static void setLenient(boolean lenient) {
        PiTimeSeriesParser.lenient = lenient;
    }

    public static boolean isLenient() {
        return lenient;
    }

    public PiTimeSeriesParser() {
        fastDateFormat.setLenient(lenient);
        fastDateFormatWithMillies.setLenient(lenient);
    }

    @Override
    public void parse(XMLStreamReader reader, String virtualFileName, TimeSeriesContentHandler timeSeriesContentHandler) throws Exception {
        this.reader = reader;
        this.virtualFileName = virtualFileName;
        this.timeSeriesContentHandler = timeSeriesContentHandler;

        String virtualBinFileName = FileUtils.getPathWithOtherExtension(virtualFileName, "bin");

        // time zone can be overruled by one or more time zone elements in the pi file
        this.fastDateFormat.setTimeZone(timeSeriesContentHandler.getDefaultTimeZone());
        this.fastDateFormatWithMillies.setTimeZone(timeSeriesContentHandler.getDefaultTimeZone());

        if (!virtualInputDir.exists(virtualBinFileName)) {
            eventDestination = PiTimeSeriesSerializer.EventDestination.XML_EMBEDDED;
            parse();
            return;
        }

        eventDestination = PiTimeSeriesSerializer.EventDestination.SEPARATE_BINARY_FILE;
        binaryInputStream = virtualInputDir.getInputStream(virtualBinFileName);
        try {
            if (byteBuffer == null) {
                byteBuffer = new byte[BUFFER_SIZE * NumberType.FLOAT_SIZE];
                floatBuffer = new float[BUFFER_SIZE];
            }
            parse();
            boolean eof = bufferPos == bufferCount && binaryInputStream.read() == -1;
            if (!eof)
                throw new IOException("More values available in bin file than expected based on time step and start and end time\n" + FileUtils.getPathWithOtherExtension(virtualFileName, "bin"));

        } finally {
            bufferPos = 0;
            bufferCount = 0;
            binaryInputStream.close();
            binaryInputStream = null;
        }
    }

    private void parse() throws Exception {
        reader.require(XMLStreamConstants.START_DOCUMENT, null, null);
        reader.nextTag();
        reader.require(XMLStreamConstants.START_ELEMENT, null, "TimeSeries");
        reader.nextTag();

        while (reader.getEventType() != XMLStreamConstants.END_ELEMENT) {
            TimeZone timeZone = PiParserUtils.parseTimeZone(reader); //returns null if the timeZone is not present in the file
            if (timeZone != null) {
                this.fastDateFormat.setTimeZone(timeZone);
                this.fastDateFormatWithMillies.setTimeZone(timeZone);
            }
            if (noTimeSeries()) continue;
            readTimeSeries();
        }

        reader.require(XMLStreamConstants.END_ELEMENT, null, "TimeSeries");
        reader.next();
        reader.require(XMLStreamConstants.END_DOCUMENT, null, null);

    }

    private boolean noTimeSeries() {
        return reader.getEventType() == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equals("TimeSeries");
    }

    private void readTimeSeries() throws Exception {
        reader.require(XMLStreamConstants.START_ELEMENT, null, "series");
        reader.nextTag();
        parseHeader();
        lastTime = Long.MIN_VALUE;
        timeAmbiguous = false;
        lastTimeAmbiguous = false;
        lastStartTimeAmbiguous = false;
        lastEndTimeAmbiguous = false;
        timeSeriesContentHandler.setProperties(Properties.NONE);
        if (eventDestination == PiTimeSeriesSerializer.EventDestination.XML_EMBEDDED) {
            while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
                String localName = reader.getLocalName();
                if (TextUtils.equals(localName, "event")) {
                    parseEvent();
                } else if (TextUtils.equals(localName, "domainAxisValues")) {
                    parseDomainAxisValues();
                } else if (TextUtils.equals(localName, "properties")) {
                    parseProperties();
                }  else {
                    break;
                }
            }
        } else {
            assert eventDestination == PiTimeSeriesSerializer.EventDestination.SEPARATE_BINARY_FILE;
            readValuesFromBinFile();
        }
        if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
            // skip comment
            reader.require(XMLStreamConstants.START_ELEMENT, null, "comment");
            reader.getElementText();
            reader.nextTag();
        }
        reader.require(XMLStreamConstants.END_ELEMENT, null, "series");
        reader.nextTag();
    }

    private void parseDomainAxisValues() throws Exception {
        String domainParameterId = reader.getAttributeValue(null, "parameterId");
        if (domainParameterId == null)
            throw new Exception("Attribute parameterId for domainAxisValues is missing");

        int index = StringArrayUtils.indexOf(domainParameterIds, 0, domainCount, domainParameterId);
        if (index == -1)
            throw new Exception("parameterId " + domainParameterId + " for domainAxisValues not defined in header");

        if (axisValues == null) axisValues = new float[domainCount][];
        if (axisValueResolutions.length < domainCount) axisValueResolutions = new float[domainCount];
        int count = parseValueList();
        reader.require(XMLStreamConstants.END_ELEMENT, null, "domainAxisValues");
        reader.nextTag();
        axisValues[index] = Clasz.floats.copyOfArrayRange(floatBuffer, 0, count);
        axisValueResolutions[index] = minValueResolution;
        axesDirty = true;
    }

    private int parseValueList() throws Exception {
        if (charBuffer == null) charBuffer = new char[100];
        char[] charBuffer = this.charBuffer;
        if (floatBuffer == null) floatBuffer = new float[100];
        minValueResolution = Float.POSITIVE_INFINITY;
        int i = 0;
        int length = 0;
        int j = 0;
        while (reader.next() == XMLStreamConstants.CHARACTERS) {
            length -= i;
            assert length >= 0;
            // shift the remaining part of the buffer to the start
            if (length > 0) CharArrayUtils.arraycopy(charBuffer, i, charBuffer, 0, length);
            int textLength = reader.getTextLength();

            if (charBuffer.length < length + textLength) {
                charBuffer = Clasz.chars.copyOfArrayRange(charBuffer, 0, length, length + textLength);
                this.charBuffer = charBuffer;
            }
            int n = reader.getTextCharacters(0, charBuffer, length, textLength);
            assert n == textLength;
            length += textLength;
            i = 0;
            for (; ; ) {
                int start = CharArrayUtils.indexOfNonWhitespace(charBuffer, i, length - i);
                if (start == -1) {
                    i = length;
                    break;
                }
                int end = CharArrayUtils.indexOfWhitespace(charBuffer, start, length - start);
                if (end == -1) {
                    i = start;
                    break;
                }
                i = end;
                parseValue(charBuffer, j++, start, end - start + 1);
            }
        }

        if (i != length) parseValue(charBuffer, j++, i, length - i);
        return j;
    }

    private void parseValue(char[] charBuffer, int pos, int start, int length) {
        float value = (float) TextUtils.parseDouble(charBuffer, start, length, '.');
        if (value == missingValue) value = Float.NaN;
        float valueResolution = TextUtils.getValueResolution(charBuffer, start, length, '.');
        if (valueResolution < minValueResolution) minValueResolution = valueResolution;
        if (floatBuffer.length == pos) floatBuffer = Clasz.floats.ensureCapacity(floatBuffer, pos + 1);
        floatBuffer[pos] = value;
    }


    private void parseHeader() throws Exception {
        domainCount = 0;
        Arrays.fill(domainParameterIds, null);
        Arrays.fill(domainUnits, null);
        if (axisValues != null) Arrays.fill(axisValues, null);
        reader.require(XMLStreamConstants.START_ELEMENT, null, "header");
        if (reader.getAttributeCount() > 0) {
            throw new Exception("Attributes are not allowed for header element ");
        }
        reader.nextTag();
        initHeader();
        do {
            detectHeaderElement();
            parseHeaderElement();
        } while (reader.getEventType() != XMLStreamConstants.END_ELEMENT);

        if (header.getForecastTime() == Long.MIN_VALUE) header.setForecastTime(startTime);
        if (!Double.isNaN(lat)) header.setGeometry(GeoDatum.WGS_1984.createLatLongZ(lat, lon, z));
        initiateTimeStep();
        header.setTimeStep(timeStep);
        if (!qualifiers.isEmpty()) header.setQualifierIds(Clasz.strings.newArrayFrom(qualifiers));
        if (creationDateText != null) {
            try {
                long creationTime = fastDateFormat.parseToMillis(creationDateText, creationTimeText);
                header.setCreationTime(creationTime);
            } catch (ParseException e) {
                throw new Exception("Can not parse creation date/time " + creationDateText + ' ' + creationTimeText);
            }
        }
        if (startTime != Long.MIN_VALUE && endTime != Long.MIN_VALUE) {
            timeSeriesContentHandler.setEstimatedPeriod(new Period(startTime, endTime));
        }
        domainParameterIds = Clasz.strings.resizeArray(domainParameterIds, domainCount);
        domainUnits = Clasz.strings.resizeArray(domainUnits, domainCount);
        header.setDomainParameterIds(domainParameterIds);
        header.setDomainUnits(domainUnits);
        timeSeriesContentHandler.setNewTimeSeriesHeader(header);

        reader.require(XMLStreamConstants.END_ELEMENT, null, "header");
        reader.nextTag();
    }

    @SuppressWarnings("OverlyLongMethod")
    private void parseEvent() throws Exception {
        assert binaryInputStream == null;
        reader.require(XMLStreamConstants.START_ELEMENT, null, "event");
        timeSeriesContentHandler.clearFlagSourceColumns();

        String dateText = null;
        String timeText = null;
        String startDateText = null;
        String startTimeText = null;
        String endDateText = null;
        String endTimeText = null;
        String valueText = null;
        String valueSource = null;
        String minValueText = null;
        String maxValueText = null;
        String flagText = null;
        String flagSource = null;
        String comment = null;
        this.virtualInputDirString user = virtualInputDirnull;
        String limitText = }null;

    private enum HeaderElement {
 for (int i = 0, n = type(F.R), locationId(F.R),
reader.getAttributeCount(); i < n; i++) {
           parameterId(F.R), qualifierId(F.M), ensembleId, ensembleMemberIndex, ensembleMemberId,
  String localName = reader.getAttributeLocalName(i);
       timeStep(F.R | F.A), startDate(F.R | F.A), endDate(F.R | F.A), forecastDate(F.A),
String attributeValue = reader.getAttributeValue(i);
          missVal,  longName, stationName, lat, lon, x, y, z, units,
if (dateText == null && TextUtils.equals(localName, "date")) {
               sourceOrganisation, sourceSystem, fileDescription, dateText = attributeValue;
        creationDate, creationTime, region, thresholds;

 } else if (timeText == null  interface F&& TextUtils.equals(localName, "time")) {
             int A = 1timeText <<= 0attributeValue;
 // attributes
          } else intif R(valueText == 1 << 1; // required;
 null && TextUtils.equals(localName, "value")) {
               int MvalueText = 1attributeValue;
 << 2; // multiple;
        }

 else if (valueSource == null &&  private final int flags;

TextUtils.equals(localName, "valueSource")) {
        HeaderElement() {
       valueSource =  attributeValue;
  this.flags = 0;
        }

 else if (flagText == null   HeaderElement(int flags&& TextUtils.equals(localName, "flag")) {
                this.flagsflagText = flagsattributeValue;
         }

   } else if (flagSource == null public boolean isRequired(&& TextUtils.equals(localName, "flagSource")) {
            return  (flags & F.R)flagSource != 0attributeValue;
         }

   } else if (comment == publicnull boolean hasAttributes(&& TextUtils.equals(localName, "comment")) {
            return  (flags & F.A)comment != 0attributeValue;
        }

    } else if (user == publicnull boolean isMultipleAllowed(&& TextUtils.equals(localName, "user")) {
             return (flags & F.M)user != 0attributeValue;
            } else if (startDateText == null && TextUtils.equals(localName, "startDate")) {
           }


    // fastDateFormatstartDateText is= usedattributeValue;
 to keep track of last time zone and lenient
   } privateelse FastDateFormatif fastDateFormat(startTimeText == FastDateFormat.getInstance("yyyy-MM-dd", "HH:mm:ss", TimeZoneUtils.GMT, Locale.US, null);

 null && TextUtils.equals(localName, "startTime")) {
              private HeaderElement currentHeaderElementstartTimeText = nullattributeValue;

     private static final HeaderElement[] HEADER_ELEMENTS = HeaderElement.class.getEnumConstants();

    private PiTimeSeriesHeader header = new PiTimeSeriesHeader(); } else if (endDateText == null && TextUtils.equals(localName, "endDate")) {
    private  List<String> qualifiers = new ArrayList<>();
    private long timeStepMillisendDateText = 0attributeValue;
    private  TimeStep timeStep = null;
   } privateelse longif startTime(endTimeText = Long.MIN_VALUE;
    private long endTime = Long.MIN_VALUE;
= null && TextUtils.equals(localName, "endTime")) {
      private float missingValue = Float.NaN;
    private String creationDateTextendTimeText = nullattributeValue;
    private  String creationTimeText = null;

   } privateelse TimeSeriesContentHandlerif timeSeriesContentHandler(minValueText == null;

    private PiTimeSeriesSerializer.EventDestination eventDestination = null;

&& TextUtils.equals(localName, "minValue")) {
     /**
     * For performance reasons the pi timeminValueText series= formatattributeValue;
 allows that the values are stored in
     *} aelse separateif bin(maxValueText file== insteadnull of embedded in the xml file.
&& TextUtils.equals(localName, "maxValue")) {
       * The bin file should have same name as themaxValueText xml= fileattributeValue;
 except the extension equals bin
     * In this} caseelse allif time(maxValueText series== shouldnull be&& equidistant.
TextUtils.equals(localName, "detection")) {
      */
        private VirtualInputDir virtualInputDirlimitText = VirtualInputDir.NONEattributeValue;
     private InputStream binaryInputStream = null;
   } privateelse byte[] byteBuffer if (reader.getAttributePrefix(i) != null;
 &&   private float[] floatBuffer = null;
TextUtils.equals(reader.getAttributePrefix(i), "fs")) {
       private int bufferPos = 0;
    private int bufferCountindex = 0timeSeriesContentHandler.addFlagSourceColumn(localName);

     private XMLStreamReader reader = null;
    private String virtualFileName = null timeSeriesContentHandler.setColumnFlagSource(index, attributeValue);

    private static boolean lenient = false;
    private} doubleelse lat{
 = Double.NaN;
    private double lon = Double.NaN;
    private double zif = Double.NaN(lenient) continue;

     /**
     * For backwards compatibility. Earlier versions ofthrow the PiTimeSeriesParser were tolerant about the date/time format
     * and the case insensitive for header element names.
     * This parser should not accept files that are not valid according to pi_timeseries.xsd
     * When old adapters are not working you can UseLenientPiTimeSeriesParser temporary till the adapter is fixed
     *
     * @param lenient
     */
    public static void setLenient(boolean lenient) {new Exception("Unknown or duplicate attribute " + localName + " in event");
            }
        }

        parseTime(dateText, timeText, startDateText, startTimeText, endDateText, endTimeText);
        parseFlagsUserComment(flagText, flagSource, comment, user);
        parseValue(valueText, valueSource, minValueText, maxValueText, limitText);
        timeSeriesContentHandler.applyCurrentFields();
        reader.require(XMLStreamConstants.END_ELEMENT, null, "event");
        PiTimeSeriesParser.lenient = lenientreader.nextTag();
    }

    public static boolean isLenient() {
        return lenient;private void parseValue(String valueText, String valueSource, String minValueText, String maxValueText, String limit) throws Exception {
    }

    publicif PiTimeSeriesParser(domainCount == 0) {
         fastDateFormat.setLenient(lenient);
   try }{

    @Override
    public void parse(XMLStreamReader reader, String virtualFileName, TimeSeriesContentHandler timeSeriesContentHandler) throws Exception {
 float value = valueText == null ? thisFloat.readerNaN = reader: TextUtils.parseFloat(valueText);
        this.virtualFileName  = virtualFileName;
     float minValue = this.timeSeriesContentHandlerminValueText == timeSeriesContentHandler;

null ? value : TextUtils.parseFloat(minValueText);
    String virtualBinFileName = FileUtils.getPathWithOtherExtension(virtualFileName, "bin");

        //float timemaxValue zone= canmaxValueText be== overrulednull by? onevalue or more time zone elements in the pi file
: TextUtils.parseFloat(maxValueText);
               this.fastDateFormat.setTimeZone(timeSeriesContentHandler.getDefaultTimeZone());

        if (!virtualInputDir.exists(virtualBinFileName)) {
            eventDestination = PiTimeSeriesSerializer.EventDestination.XML_EMBEDDED; // we can not use the automatic missing value detection of the content handler because the missing value is different for each time series
            parse();
    if (value == missingValue) {
    return;
        }

        eventDestinationvalue = PiTimeSeriesSerializer.EventDestination.SEPARATE_BINARY_FILEFloat.NaN;
        binaryInputStream = virtualInputDir.getInputStream(virtualBinFileName);
       } tryelse {
            if (byteBuffer == null) {
    timeSeriesContentHandler.setValueResolution(TextUtils.getValueResolution(valueText, '.'));
           byteBuffer = new byte[BUFFER_SIZE * NumberType.FLOAT_SIZE]; }
                floatBuffer = new float[BUFFER_SIZE]timeSeriesContentHandler.setValueAndRange(value, minValue, maxValue);
            }
    timeSeriesContentHandler.setValueSource(TextUtils.equals(valueSource, "MAN") ? ValueSource.MANUAL     parse(: ValueSource.AUTOMATIC);
            boolean eof = bufferPos == bufferCount && binaryInputStream.read() == -1;
 timeSeriesContentHandler.setOutOfDetectionRangeFlag(OutOfDetectionRangeFlag.get(getOutOfDetectionFlag(limit)));
            } ifcatch (!eof)NumberFormatException e) {
                throw new IOException("More values available in bin file than expected based on time step and start and end time\n" + FileUtils.getPathWithOtherExtension(virtualFileName, "bin"));

Exception("Value should be a float " + valueText);
            }
            reader.nextTag();
          } finally {
return;
        }

        if bufferPos(valueText != 0;null)
            bufferCountthrow = 0;
            binaryInputStream.close(new Exception("Attribute value not allowed when having domain parameters, use event element text instead");
        int  count  binaryInputStream = nullparseValueList();
        }
if (count == 0) }

    private void parse() throws Exception {
{
            readertimeSeriesContentHandler.require(XMLStreamConstants.START_DOCUMENT, null, nullsetValues(null);
        reader.nextTag()    return;
         reader.require(XMLStreamConstants.START_ELEMENT, null, "TimeSeries");
}

        int expectedCount = reader.nextTag()1;

        while (reader.getEventType() != XMLStreamConstants.END_ELEMENTfor (int i = 0, n = domainCount; i < n; i++) {
            float[] axis  parseTimeZone();
= axisValues[i];

            if readTimeSeries(axis == null);
          }

      throw new reader.require(XMLStreamConstants.END_ELEMENT, null, "TimeSeries");
        reader.next();
Exception("Domain axis values for domain parameter " + domainParameterIds[i] + " are missing");

        reader.require(XMLStreamConstants.END_DOCUMENT, null, null);

  expectedCount  }

 *= axis.length;
   private void readTimeSeries() throws Exception {}

        reader.require(XMLStreamConstants.START_ELEMENT, null, "series");
if (expectedCount != count)
         reader.nextTag();
   throw new Exception("Length of value parseHeader();
list for event does not matches the  timeSeriesContentHandler.setProperties(Properties.NONEaxes lengths");

        if (eventDestination == PiTimeSeriesSerializer.EventDestination.XML_EMBEDDEDaxesDirty) {
            while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
timeSeriesContentHandler.setDomainAxesValueResolutions(axisValueResolutions);
            timeSeriesContentHandler.setDomainAxesValues(axisValues);
           String localNameaxesDirty = reader.getLocalName()false;
        }

        if (TextUtils.equals(localName, "event")) {count != floatBuffer.length) floatBuffer = FloatArrayUtils.resize(floatBuffer, expectedCount);
        timeSeriesContentHandler.setValueResolution(minValueResolution);
            parseEvent(timeSeriesContentHandler.setValues(floatBuffer);
    }

    private byte getOutOfDetectionFlag(String text) {
      } else if (TextUtils.equals(localName, "properties")) {text == null || text.isEmpty()) return TimeSeriesArray.INSIDE_DETECTION_RANGE;
        char ch           parseProperties(= text.charAt(0);
        if (ch == '<') return TimeSeriesArray.BELOW_DETECTION_RANGE;
     }  else {
if (ch == '>')  return TimeSeriesArray.ABOVE_DETECTION_RANGE;
        if (ch == '~') return  breakTimeSeriesArray.VARYING;
            return TimeSeriesArray.INSIDE_DETECTION_RANGE;
    }

    private void parseFlagsUserComment(String flagText, String flagSource, String comment, }

String user) throws Exception {
        if (reader.getEventType()flagText == XMLStreamConstants.START_ELEMENTnull) {
                // skip commenttimeSeriesContentHandler.setFlag(0);
        }        reader.require(XMLStreamConstants.START_ELEMENT, null, "comment");else {
                reader.getElementText();try {
                reader.nextTag(timeSeriesContentHandler.setFlag(TextUtils.parseInt(flagText));
            } catch (NumberFormatException e) {
        } else {
      throw new Exception("Flag should be an assertinteger eventDestination" == PiTimeSeriesSerializer.EventDestination.SEPARATE_BINARY_FILE+ flagText);
            readValuesFromBinFile();}
        }

        readertimeSeriesContentHandler.require(XMLStreamConstants.END_ELEMENT, null, "series"setComment(comment);
        timeSeriesContentHandler.setUser(user);
        readertimeSeriesContentHandler.nextTagsetFlagSource(flagSource);
    }

    private void parseHeader() throws Exception {
        reader.require(XMLStreamConstants.START_ELEMENT, null, "header");parseTime(String dateText, String timeText, String startDateText, String startTimeText, String endDateText, String endTimeText) throws Exception {
        if (reader.getAttributeCount() > 0) {timeText == null)
            throw new Exception("AttributesAttribute aretime not allowed for header element is missing");

        }
if (dateText == null)
         reader.nextTag();
   throw new Exception("Attribute date is initHeader(missing");

        dotry {
            detectHeaderElement(long time = parseTime(dateText, timeText, null, null, Long.MIN_VALUE, lastTime, lastTimeAmbiguous);
            parseHeaderElement()lastTime = time;
        }   while (reader.getEventType() != XMLStreamConstants.END_ELEMENT);

 lastTimeAmbiguous = timeAmbiguous;
            long startTime if= (header.getForecastTime() == Long.MIN_VALUE) header.setForecastTime(startTime);
parseTime(startDateText, startTimeText, dateText, timeText, time, lastStartTime, lastStartTimeAmbiguous);
         if (!Double.isNaN(lat)) header.setGeometry(new PointGeometry(GeoDatum.WGS_1984.createLatLongZ(lat, lon, z))) lastStartTime = startTime;
        initiateTimeStep()     lastStartTimeAmbiguous = timeAmbiguous;
        header.setTimeStep(timeStep);
    long endTime =  if (!qualifiers.isEmpty()) header.setQualifierIds(qualifiers.toArray(new String[qualifiers.size()]));
    parseTime(endDateText, endTimeText, dateText, dateText, time, lastEndTime, lastEndTimeAmbiguous);
    if (creationDateText != null) {
    lastEndTime = endTime;
      try {
     lastEndTimeAmbiguous = timeAmbiguous;
         long creationTime = fastDateFormattimeSeriesContentHandler.parseToMillis(creationDateTextsetTimeAndRange(time, startTime, creationTimeTextendTime);
        } catch (ParseException e) {
        header.setCreationTime(creationTime);
    throw new Exception("Can not parse " + dateText + }' catch' (ParseException+ etimeText) {;
        }
    }

    throwprivate newlong Exception("Can not parse creation date/time " + creationDateText + ' ' + creationTimeText);
   parseTime(String dateText, String timeText, String defaultDateText, String defaultTimeText, long defaultTime, long lastTime, boolean lastTimeAmbiguous) throws ParseException {
        if }
(dateText == null && timeText == null) return }defaultTime;
        if (startTimedateText !== Long.MIN_VALUE && endTime != Long.MIN_VALUE) {null) dateText = defaultDateText;
        if (timeText ==  timeSeriesContentHandler.setEstimatedPeriod(new Period(startTime, endTime));null) timeText = defaultTimeText;

        }
boolean useMillis       timeSeriesContentHandler.setNewTimeSeriesHeader(header= timeText.contains(".");

        if reader(!fastDateFormat.requiregetTimeZone(XMLStreamConstants.END_ELEMENT, null, "header");
        reader.nextTag();
).useDaylightTime()) return useMillis? fastDateFormatWithMillies.parseToMillis(dateText, timeText): fastDateFormat.parseToMillis(dateText, timeText);
     }

    @SuppressWarnings("OverlyLongMethod")long t1;
    private void parseEvent() throws Exceptiontry {
        assert binaryInputStream == null;
   t1 = useMillis?fastDateFormatWithMillies.parseToMillis(dateText, timeText):  readerfastDateFormat.require(XMLStreamConstants.START_ELEMENT, null, "event"parseToMillis(dateText, timeText);

        String} dateTextcatch = null;
        String timeText = null;
 (ParseException e) {
       String valueText = null;
  if (!timeText.equals("02:00:00"))     String flagText = nullthrow e;
        String flagSource = null;
 // see FEWS-17782 also accept 02:00:00 on Stringdaylight commentsaving =time null;switch
          String user =try null;{

          for (int i = 0, n =return readerfastDateFormat.getAttributeCount(parseToMillis(dateText, "03:00:00"); i
 < n; i++) {
        } catch (ParseException e1) String{
 localName = reader.getAttributeLocalName(i);
            String attributeValue = reader.getAttributeValue(i)throw e;
            }
 if (dateText == null && TextUtils.equals(localName, "date")) {}
            Calendar calendar =  dateText = attributeValueuseMillis? fastDateFormatWithMillies.getCalendar() : fastDateFormat.getCalendar();
        calendar.setTimeInMillis(t1);
    } else if (timeText ==int nulltimeOfDay && TextUtils.equals(localName, "time")) {= getTimeOfDay(calendar);
        long t2 = getOtherTime(calendar, t1, timeOfDay);
    timeText = attributeValue;
  timeAmbiguous = t1 != t2;
      } else iflong (valueTextminTime == null && TextUtils.equals(localName, "value")) {
        Math.min(t1, t2);
        long maxTime = Math.max(t1, t2);
        long valueTextres = attributeValue;
minTime > lastTime ? minTime : maxTime;
      } else if (flagText == null && TextUtils.equals(localName, "flag")) {
!lastTimeAmbiguous) return res;
        // see FEWS-17782 also accept two times 02:00:00 instead of two times flagText01:00:00 = attributeValue;
on daylight saving time switch
        } else if (flagSourcetimeStepMillis !== null0L && TextUtils.equals(localName, "flagSource")) { lastTime + timeStepMillis != res) return lastTime + timeStepMillis;
        if (timeStep != null && timeStep.isRegular() && timeStep.nextTime(lastTime) != flagSourceres) = attributeValuereturn timeStep.nextTime(lastTime);
        return res;
    }

 else if (comment ==private nullstatic && TextUtils.equals(localName, "comment"))int getTimeOfDay(Calendar calendar) {
        return (int) (calendar.get(Calendar.HOUR_OF_DAY) * TimeUnit.HOUR_MILLIS + calendar.get(Calendar.MINUTE) * comment = attributeValue;
            } else if (user == null && TextUtils.equals(localName, "user")) {TimeUnit.MINUTE_MILLIS + calendar.get(Calendar.SECOND) * TimeUnit.SECOND_MILLIS + calendar.get(Calendar.MILLISECOND));
    }

    private static long getOtherTime(Calendar calendar, long time, int user = attributeValue;timeOfDay) {
        try    } else {
                throw new Exception("Unknown attribute " + localName + " in event");
            }
    calendar.setTimeInMillis(time + TimeUnit.HOUR_MILLIS);
    }

        if (timeTextgetTimeOfDay(calendar) == null)
  timeOfDay) return time + TimeUnit.HOUR_MILLIS;
          throw new Exception("Attribute calendar.setTimeInMillis(time is missing"- TimeUnit.HOUR_MILLIS);

            if (getTimeOfDay(dateTextcalendar) == nulltimeOfDay)
 return time - TimeUnit.HOUR_MILLIS;
        throw new Exception("Attribute date isreturn missing")time;

        } finally if{
 (valueText == null)
         calendar.setTimeInMillis(time);
   throw new Exception("Attribute value is missing");

}
    }

    private long putValueparseTime(dateText,) timeText,throws valueText, flagText, flagSource, comment, user);
Exception {
        String dateText = reader.nextTag(getAttributeValue(null, "date");
        reader.require(XMLStreamConstants.END_ELEMENT, null, "event");if (dateText == null) {
        reader.nextTag();
    }
    throw new Exception("Attribute " + currentHeaderElement +
    private void putValue(String dateText, String timeText, String valueText, String flagText, String flagSource, String comment, String user) throws"-date Exception {is missing");
        try {}
        String timeText =  timeSeriesContentHandlerreader.setTime(fastDateFormat.parseToMillis(dateTextgetAttributeValue(null, timeText"time"));
        }if catch (ParseException e(timeText == null) {
            throw new Exception("CanAttribute not" parse+ "currentHeaderElement +
 dateText     + ' ' + timeText);
        }

  "-time is missing");
    if (flagText == null) {}
        boolean useMillis   timeSeriesContentHandler.setFlag(0= timeText.contains(".");
        } else {
    long time;
        try {
            time = useMillis?  timeSeriesContentHandlerfastDateFormatWithMillies.setFlag(TextUtils.parseInt(flagText))parseToMillis(dateText, timeText) : fastDateFormat.parseToMillis(dateText, timeText);
            } catch (NumberFormatExceptionParseException e) {
                throw new Exception("FlagNot a shouldvalid bedata antime integerfor "
   + flagText);
            }
    + currentHeaderElement + ' }

' + dateText + ' ' +  timeSeriesContentHandler.setComment(comment);
timeText, e);
        }

        timeSeriesContentHandlerreader.setUsernextTag(user);
         timeSeriesContentHandler.setFlagSource(flagSource)return time;
    }

    private void parseTimeStep() throws tryException {
        String times   float value = TextUtilsreader.parseFloat(valueTextgetAttributeValue(null, "times");

        if (times !=  // we can not use the automatic missing value detection of the content handler because the missing value is different for each time seriesnull) {
            timeStep = PiCastorUtils.createTimesOfDayTimeStep(times, getTimeZone());
            if (value == missingValue) {reader.nextTag();
            return;
    value = Float.NaN;
  }

        String unit } else {= reader.getAttributeValue(null, "unit");
        TimeUnit tu = unit == null ?  timeSeriesContentHandler.setValueResolution(TextUtils.getValueResolution(valueText, '.'))null : TimeUnit.get(unit);
        if (tu != null) }{
            timeSeriesContentHandler.setValue(valueString multiplierText = reader.getAttributeValue(null, "multiplier");
            timeSeriesContentHandler.applyCurrentFields()int multiplier;
           } catchif (NumberFormatException emultiplierText == null) {
            throw new Exception("Value should bemultiplier a float " + valueText)= 1;
        }
    }

    private long parseTime() throws Exception {
    else {
     String dateText = reader.getAttributeValue(null, "date");
       try if{
 (dateText == null) {
            throw new Exception("Attribute " +multiplier currentHeaderElement += Integer.parseInt(multiplierText);
                } catch (NumberFormatException e) {
 "-date is missing");
        }
        String timeTextthrow =new readerException(ExceptionUtils.getAttributeValuegetMessage(nulle), "time"e);
            if (timeText == null) {}

               throw newif Exception("Attribute " + currentHeaderElement +
(multiplier == 0) {
                    throw new Exception("-timeMultiplier is missing0");
         }

       }
  long time;
        try {}

            timeString dividerText = fastDateFormatreader.parseToMillisgetAttributeValue(dateText, timeText);
        } catch (ParseException e) {
null, "divider");
            int throwdivider;
 new Exception("Not a valid data time for "
    if (dividerText == null) {
            + currentHeaderElement + ' 'divider += dateText1;
 + ' ' + timeText, e);
      } else }{

        reader.nextTag();
        returntry time;{
    }

    private long parseTimeStep() throws Exception {
        String unitdivider = readerInteger.getAttributeValue(null, "unit"parseInt(dividerText);
        if (unit == null) {
            throw new Exception("Attribute unit is missing in " + currentHeaderElement);
} catch (NumberFormatException e) {
           }

        TimeUnit tuthrow =new TimeUnitException(ExceptionUtils.get(unitgetMessage(e), e);
        if (tu != null) {        }

            String multiplierText = reader.getAttributeValue(null, "multiplier");
     if (divider == 0) {
        int multiplier;
           throw ifnew Exception(multiplierText"divider ==is null0");
  {
              }
  multiplier = 1;
        }
    } else {
      reader.nextTag();
          try {
 timeStepMillis =  tu.getMillis() * multiplier / divider;
            multipliertimeStep = Integer.parseInt(multiplierText);
null;
        } else {
          } catch reader.nextTag(NumberFormatException e);
 {
           timeStepMillis = 0;
       throw new Exception(ExceptionUtils.getMessage(e), e);
  timeStep = IrregularTimeStep.INSTANCE;
        }
    }

    private void initHeader() {
        header.clear();
     if (multiplier == 0) { header.setFileDescription(virtualFileName);
        currentHeaderElement = null;
        timeStep = thrownull;
 new Exception("Multiplier is 0");
    timeStepMillis = 0;
        startTime  }
= Long.MIN_VALUE;
        endTime = Long.MIN_VALUE;
     }

   missingValue = Float.NaN;
       String dividerTextcreationDateText = reader.getAttributeValue(null, "divider");
        creationTimeText = "00:00:00";
  int divider;
     qualifiers.clear();
       if (dividerTextlat == null) {
 Double.NaN;
        lon = Double.NaN;
        dividerz = 1Double.NaN;
    }

    private void readValuesFromBinFile() throws } elseException {
        TimeStep timeStep = header.getTimeStep();
         tryif (!timeStep.isRegular()) {
            throw new Exception("Only equidistant time step supported when dividerpi = Integer.parseInt(dividerText);
        events are stored in bin file instead of xml");
        }

 catch (NumberFormatException e) {
    boolean equidistantMillis = timeStep.isEquidistantMillis();
        long stepMillis = equidistantMillis ? throw new Exception(ExceptionUtils.getMessage(e), e)timeStep.getStepMillis() : Long.MIN_VALUE;
        for (long time = startTime; time <= endTime;) }{

            timeSeriesContentHandler.setTime(time);
    if (divider == 0) {
    if (bufferPos == bufferCount) fillBuffer();
            throwfloat new Exception("divider is 0")value = floatBuffer[bufferPos++];
            // we can not }
use the automatic missing value detection of the content handler because  }
            reader.nextTag();the missing value is different for each time series
            returnif tu.getMillis((value == missingValue) *value multiplier / divider= Float.NaN;
         }  else { timeSeriesContentHandler.setValue(value);
            readertimeSeriesContentHandler.nextTagapplyCurrentFields();
            if return 0;(equidistantMillis) {
        }
    }

    privatetime void initHeader() {+= stepMillis;
        header.clear();
        header.setFileDescription(virtualFileName)continue;
        currentHeaderElement   = null;
}
            timeSteptime = nulltimeStep.nextTime(time);
        timeStepMillis}
   = 0;}

    private void fillBuffer() throws startTime = Long.MIN_VALUE;IOException {
        endTimeint byteBufferCount = Long.MIN_VALUE0;
        while missingValue(byteBufferCount =% Float.NaN;
        creationDateText = null;
NumberType.FLOAT_SIZE != 0 || byteBufferCount == 0) {
           creationTimeText = "00:00:00";
        qualifiers.clear(int count = binaryInputStream.read(byteBuffer, byteBufferCount, BUFFER_SIZE * NumberType.FLOAT_SIZE - byteBufferCount);
        lat = Double.NaN;
  assert count != 0; // see lonread = Double.NaN;
javadoc
         z = Double.NaN; 
if (count ==  }

    private void readValuesFromBinFile() throws Exception {
-1) throw new EOFException("Bin file is too short");
           TimeStep timeStepbyteBufferCount += header.getTimeStep()count;
        if (!timeStep.isRegular()) {}
        bufferCount = byteBufferCount  throw new Exception("Only equidistant time step supported when pi events are stored in bin file instead of xml"/ NumberType.FLOAT_SIZE;
        BinaryUtils.copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIAN);
        }
bufferPos = 0;
    }

    booleanprivate equidistantMillis = timeStep.isEquidistantMillisvoid initiateTimeStep(); {
        longif stepMillis(timeStep != equidistantMillis ?  timeStep.getStepMillis() : Long.MIN_VALUE;null) {
        for (long time = startTime;assert timetimeStepMillis <== endTime0;) {
            timeSeriesContentHandler.setTime(time)return;
        }
        if (bufferPostimeStepMillis == bufferCount0) fillBuffer();
{
            //no timestep in header. floatFix valuefor = floatBuffer[bufferPos++];backward compatibility
            timeStep = IrregularTimeStep.INSTANCE;
       // we can not use thereturn;
 automatic missing value detection of the content handler}
 because the missing value is different for eachif time series
    (timeStepMillis >= TimeUnit.HOUR_MILLIS && getTimeZone().useDaylightTime()) {
        if (value == missingValue) valuetimeStep = FloatIrregularTimeStep.NaNINSTANCE;
            timeSeriesContentHandler.setValue(value)return;
        }

    timeSeriesContentHandler.applyCurrentFields();
    long startTime = this.startTime ==  Long.MIN_VALUE ? if0L (equidistantMillis) {: this.startTime;
        if (timeStepMillis % TimeUnit.SECOND_MILLIS !=    time += stepMillis;0) {
            timeStep =   continueRelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);
            }return;
         }

   time = timeStep.nextTime(time);
   long timeZoneOffsetMillis = -startTime % }timeStepMillis;
     }

   if private(timeZoneOffsetMillis void fillBuffer() throws IOException% TimeUnit.MINUTE_MILLIS != 0) {
          int  byteBufferCounttimeStep = 0 RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);
        while (byteBufferCount % NumberType.FLOAT_SIZE != 0 || byteBufferCount == 0) {
   return;
        }

          inttimeStep count = binaryInputStreamSimpleEquidistantTimeStep.readgetInstance(byteBuffer, byteBufferCount, BUFFER_SIZE * NumberType.FLOAT_SIZE - byteBufferCount);timeStepMillis, timeZoneOffsetMillis);
    }

    @SuppressWarnings({"OverlyLongMethod"})
    private void parseHeaderElement() throws Exception {
   assert count != 0; // seeswitch read(currentHeaderElement) javadoc{
            case  if (count == -1) throw new EOFException("Bin file is too short");
type:
                header.setParameterType(parseType(reader.getElementText()));
              byteBufferCount += countbreak;
         }
   case moduleInstanceId:
    bufferCount = byteBufferCount / NumberType.FLOAT_SIZE;
        BinaryUtilsheader.copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIANsetModuleInstanceId(reader.getElementText());
          bufferPos = 0;
    }break;

    private void initiateTimeStep() {
     case  locationId:
 if (timeStepMillis == 0)  {
          // see timeStep = IrregularTimeStep.INSTANCE;
            return;FEWS-9858, when there is no location id the time series are assigned to all locations
        }

        long startTime =// this.startTime ==is a Long.MIN_VALUE ? 0L : this.startTime;
        if (timeStepMillis % TimeUnit.SECOND_MILLIS != 0) {
flaw in the pi_timeSeries.xsd, the location element is required but is allowed empty strings
                timeStep = RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTimeheader.setLocationId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementText()), "none"));
            return;
     break;
   }

        long timeZoneOffsetMilliscase =parameterId:
 -startTime % timeStepMillis;
        if (timeZoneOffsetMillis % TimeUnit.MINUTE_MILLIS != 0) {
            timeStep = RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);// see FEWS-9858, when there is no parameter id the time series are assigned to all locations
            return;
    // this is a }

flaw in the pi_timeSeries.xsd, the location element is timeSteprequired = SimpleEquidistantTimeStep.getInstance(timeStepMillis, timeZoneOffsetMillis);
    }
but is allowed empty strings
    private void parseTimeZone() throws Exception {
        if header.setParameterId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getEventTypegetElementText()) != XMLStreamConstants.START_ELEMENT) return;
, "none"));
            if (!TextUtils.equals(reader.getLocalName(), "timeZone")) return   break;
        try {
    case qualifierId:
             String timeZoneText = qualifiers.add(reader.getElementText());
            // element name="timeZone" type="fews:TimeZoneSimpleType" default="0.0" minOccurs="0"/> break;
            // when default is used in schema for element the consequence is that empty strings are allowedcase ensembleId:
                header.setEnsembleId(reader.getElementText());
            double offset = timeZoneText.isEmpty() ? 0d : Double.parseDouble(timeZoneText);
 break;
            case ensembleMemberIndex:
 TimeZone timeZoneFromDouble = TimeZoneUtils.createTimeZoneFromDouble(offset);
            this.fastDateFormat.setTimeZone(timeZoneFromDoubleheader.setEnsembleMemberIndex(parseEnsembleMemberIndex(reader.getElementText()));

        } catch (NumberFormatException e) {
     break;
       throw new Exception("Not valid timeZone format", e);case ensembleMemberId:
        }
        header.setEnsembleMemberId(reader.require(XMLStreamConstants.END_ELEMENT, null, "timeZone"getElementText());
        reader.nextTag()        break;
    }

       @SuppressWarnings({"OverlyLongMethod"})
   case timeStep:
  private void parseHeaderElement() throws Exception {
        switch parseTimeStep(currentHeaderElement);
 {
              case type:break;
            case startDate:
   header.setParameterType(parseType(reader.getElementText()));
             startTime =  breakparseTime();
            case locationId:
   break;
            case //endDate:
 see FEWS-9858, when there is no location id the time series are assigned to all locations
endTime = parseTime();
              // this isbreak;
 a flaw in the pi_timeSeries.xsd, the location element is required but is allowed empty stringscase forecastDate:
                header.setLocationId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementTextsetForecastTime(parseTime()), "none"));
                break;
            case parameterIdapprovedDate:
                // see FEWS-9858, when there is no parameter id the time series are assigned to all locations
    header.setApprovedTime(parseTime());
            // this is a flawbreak;
 in the pi_timeSeries.xsd, the location element is required but is allowed emptycase stringsmissVal:
                header.setParameterId(TextUtils.defaultIfNull(TextUtils.trimToNull(missingValue = parseString(reader.getElementText()), "none"));
                break;
            case qualifierIdlongName:
                qualifiersheader.addsetLongName(reader.getElementText());
                break;
            case ensembleIdstationName:
                header.setEnsembleIdsetLocationName(reader.getElementText());
                break;
            case ensembleMemberIndexunits:
                header.setEnsembleMemberIndex(parseEnsembleMemberIndexsetUnit(reader.getElementText()));
                break;
            case ensembleMemberIddomainAxis:
                header.setEnsembleMemberId(reader.getElementText())parseDomainAxis();
                break;
            case timeStepsourceOrganisation:
                timeStepMillis = parseTimeStep(header.setSourceOrganisation(reader.getElementText());
                break;
            case startDatesourceSystem:
                startTime = parseTimeheader.setSourceSystem(reader.getElementText());
                break;
            case endDatefileDescription:
                endTime = parseTimeheader.setFileDescription(reader.getElementText());
                break;
            case forecastDatecreationDate:
                header.setForecastTime(parseTime()creationDateText = reader.getElementText();
                break;
            case missValcreationTime:
                missingValuecreationTimeText = parseValue(reader.getElementText());
                break;
            case longNameregion:
                header.setLongNamesetRegion(reader.getElementText());
                break;
            case stationNamethresholds:
                header.setLocationName(reader.getElementTextparseThresholds());
                break;
            case unitslat:
                 header.setUnitlat = parseString(reader.getElementText());
                break;
            case sourceOrganisationlon:
                 header.setSourceOrganisationlon = parseString(reader.getElementText());
                break;
            case sourceSystemx:
                header.setSourceSystem(reader.getElementText());
                break;
            case fileDescriptiony:
                header.setFileDescription(reader.getElementText());
                break;
            case creationDatez:
                creationDateTextz = parseString(reader.getElementText());
                break;
            case creationTimefirstValueTime:
                creationTimeText = reader.getElementText();
                break;
            case regionlastValueTime:
                header.setRegion(reader.getElementText());
                break;
            case thresholdsmaxValue:
                parseThresholdsreader.getElementText();
                break;
            case latminValue:
                lat = parseValue(reader.getElementText());
                break;
            case lonvalueCount:
                lon = parseValue(reader.getElementText());
                break;
            case xmaxWarningLevelName:
                reader.getElementText();
                break;

        }
    case y:
        reader.require(XMLStreamConstants.END_ELEMENT, null, currentHeaderElement.name());
        reader.getElementTextnextTag();
    }

    private void parseDomainAxis() throws Exception    break;{
        String parameterId =  case z:reader.getAttributeValue(null, "parameterId");
        if        z = parseValue(reader.getElementText());(parameterId == null)
            throw new Exception("Attribute parameterId break;
for domainUnits is missing");

     }
   domainParameterIds     reader.require(XMLStreamConstants.END_ELEMENT, null, currentHeaderElement.name()= Clasz.strings.ensureCapacity(domainParameterIds, domainCount + 1);
        domainUnits reader.nextTag(= Clasz.strings.ensureCapacity(domainUnits, domainCount + 1);
    }

    private void parseThresholds() throws XMLStreamException {
domainParameterIds[domainCount] = parameterId;
        domainUnits[domainCount] = reader.nextTag(getAttributeValue(null, "units");
        ArrayList<String> ids = new ArrayList<>()domainCount++;
        ArrayList<String> names = new ArrayList<>reader.nextTag();
    }

    ArrayList<String>private stringValues = new ArrayList<>void parseThresholds();
 throws XMLStreamException {
     ArrayList<String> groupIds = new ArrayList<> reader.nextTag();
        ArrayList<String>ArrayList<DefaultTimeSeriesHeader.DefaultThreshold> groupNamesthresholds = new ArrayList<>();
        do {
            if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
                ids.add(String id = reader.getAttributeValue(null, "id"));
                names.add(reader.getAttributeValue(null, "name"));
             String name = stringValues.add(reader.getAttributeValue(null, "valuename"));
                float value = groupIdsTextUtils.addparseFloat(reader.getAttributeValue(null, "groupIdvalue"));
                groupNames.add(String groupId = reader.getAttributeValue(null, "groupNamegroupId"));
            }
    String groupName   =    reader.nextTag(getAttributeValue(null, "groupName");
        } while (!reader.getLocalName().equals(currentHeaderElement.name()));
        floatString[] groupIds = valuesgroupId == null new float[stringValues.size()];
        for (int i = 0; i < values.length; i++) {
? Clasz.strings.emptyArray() : new String[] {groupId};
                valuesString[i] groupNames = Float.parseFloat(stringValues.get(i))groupName == null ? Clasz.strings.emptyArray() : new String[] {groupName};
        }

        headerthresholds.setHighLevelThresholdsadd(ids.toArray(new String[idsDefaultTimeSeriesHeader.sizeDefaultThreshold()])id, names.toArray(new String[names.size()]), values,
name, value, groupIds, groupNames));
            }
        groupIds.toArray(new String[groupIds.size()]), groupNames.toArray(new String[groupNames.size()])    reader.nextTag();
        } while (!reader.getLocalName().equals(currentHeaderElement.name()));

        header.setHighLevelThresholds(TimeSeriesHeader.Threshold.clasz.newArrayFrom(thresholds));
    }

    private void parseProperties() throws XMLStreamException {
        reader.require(XMLStreamConstants.START_ELEMENT, null, "properties");
        reader.nextTag();
        propertyBuilder.clear();
        while (!TextUtils.equals(reader.getLocalName(), "properties")){
            if (reader.getEventType() != XMLStreamConstants.START_ELEMENT) {
                // eg <int key="a" value=12><int>
                reader.nextTag();
                continue;
            }
            String key = reader.getAttributeValue(null, "key");
            String value = reader.getAttributeValue(null, "value");
            String date = reader.getAttributeValue(null, "date");
            String time = reader.getAttributeValue(null, "time");
            switch (reader.getLocalName()) {
                case "string" :
                    propertyBuilder.addString(key, value);
                    break;
                case "int":
                    propertyBuilder.addInt(key, TextUtils.parseInt(value));
                    break;
                case "float":
                    propertyBuilder.addFloat(key, TextUtils.parseFloat(value));
                    break;
                case "double":
                    propertyBuilder.addDouble(key, TextUtils.parseDouble(value));
                    break;
                case "bool":
                    propertyBuilder.addBoolean(key, Boolean.parseBoolean(value));
;
                    break;
                case "dateTime":
         break;
           try {
    case "bool":
                   if propertyBuilder(time.addBoolean(key, Boolean.parseBoolean(value));contains(".")) {
                    break;
        propertyBuilder.addDateTime(key, fastDateFormatWithMillies.parseToMillis(date, time));
        case "dateTime":
               } else {
   try {
                        propertyBuilder.addDateTime(key, fastDateFormat.parseToMillis(date, time));
                        }
                        break;
                    } catch (ParseException e) {
                        throw new XMLStreamException("Invalid date time "+ date + ' ' + time);
                    }
                default:
                    throw new XMLStreamException("Invalid property type " + reader.getLocalName());
            }
            reader.nextTag();
        }

        timeSeriesContentHandler.setProperties(propertyBuilder.build());

        reader.require(XMLStreamConstants.END_ELEMENT, null, "properties");
        reader.nextTag();
    }

    private static float parseValueparseString(String gotString) throws Exception {
        // <element name="missVal" type="double" default="NaN">
        // when default is used in schema for element the consequence is that empty strings are allowed 
        if (gotString.isEmpty()) return Float.NaN;
        try {
            return TextUtils.parseFloat(gotString);
        } catch (NumberFormatException e) {
            throw new Exception(ExceptionUtils.getMessage(e), e);
        }
    }

    private static int parseEnsembleMemberIndex(String gotString) throws Exception {
        int index = Integer.parseInt(gotString);
        if (index < 0) {
            throw new Exception("Negative ensemble member index not allowed " + gotString);
        }
        return index;
    }

    private static ParameterType parseType(String gotString) throws Exception {
        ParameterType type = ParameterType.get(gotString);
        if (type == null) {
            throw new Exception("Type in header should be instantaneous or accumulative and not " + gotString);
        }
        return type;
    }

    private void detectHeaderElement() throws Exception {
        if (reader.getEventType() != XMLStreamConstants.START_ELEMENT)
            throw new Exception("header element expected");

        String localName = reader.getLocalName();
        HeaderElement element;
        try {
            element = Enum.valueOf(HeaderElement.class, localName);
        } catch (Exception e) {
            throw new Exception("Unknown header element: " + localName);
        }

        if (currentHeaderElement == element && currentHeaderElement.isMultipleAllowed()) return;

        if (currentHeaderElement != null && element.ordinal() < currentHeaderElement.ordinal()) {
            throw new Exception("Header elements in wrong order: " + localName);
        }

        if (currentHeaderElement == HeaderElement.ensembleMemberIndex && element == HeaderElement.ensembleMemberId) {
            throw new Exception("Duplicate header element, both ensembleMemberIndex and ensembleMemberId in header");
        }

        if (currentHeaderElement == element) {
            throw new Exception("Duplicate header element: " + localName);
        }

        if (reader.getAttributeCount() > 0 && !element.hasAttributes()) {
            throw new Exception("Attributes are not allowed for header element " + localName);
        }

        int nextOrdinal = currentHeaderElement == null ? 0 : currentHeaderElement.ordinal() + 1;

        // order is correct and no duplicate so currentHeaderElement can not be last header element
        assert nextOrdinal < HEADER_ELEMENTS.length;
        HeaderElement nextHeaderElement = HEADER_ELEMENTS[nextOrdinal];
        if (nextHeaderElement.isRequired() && nextHeaderElement != element) {
            throw new Exception("Required header item missing: " + nextHeaderElement);
        }

        currentHeaderElement = element;
    }

    public TimeZone getTimeZone() {
        //time zone for both fastDateFormat and fastDateFormatWithMillies should be the same.
        return fastDateFormat.getTimeZone();
    }

    @SuppressWarnings("UnusedDeclaration")
    public PiTimeSeriesSerializer.EventDestination getEventDestination() {
        return eventDestination;
    }

    @SuppressWarnings("UnusedDeclaration")
    public float getMissingValue() {
        return missingValue;
    }
}