Versions Compared

Key

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


Code Block

package nl.wldelft.fews.pi;

import nl.wldelft.util.DateUtils;
import nl.wldelft.util.ExceptionUtils;
import nl.wldelft.util.FastDateFormat;
import nl.wldelft.util.FileUtils;
import nl.wldelft.util.Period;
import nl.wldelft.util.TextUtils;
import nl.wldelft.util.TimeUnit;
import nl.wldelft.util.TimeZoneUtils;
import nl.wldelft.util.NumberType;
import nl.wldelft.util.BinaryUtils;
import nl.wldelft.util.io.VirtualInputDir;
import nl.wldelft.util.io.VirtualInputDirConsumer;
import nl.wldelft.util.io.XmlParser;
import nl.wldelft.util.timeseries.IrregularTimeStep;
import nl.wldelft.util.timeseries.ParameterType;
import nl.wldelft.util.timeseries.SimpleEquidistantTimeStep;
import nl.wldelft.util.timeseries.TimeSeriesContentHandler;
import nl.wldelft.util.timeseries.TimeStep;
import org.apache.log4j.Logger;

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

public class PiTimeSeriesParser implements XmlParser<TimeSeriesContentHandler>, VirtualInputDirConsumer {
    private static final Logger log = Logger.getLogger(PiTimeSeriesParser.class);

    private static final int BUFFER_SIZE = 2048;

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

    private enum HeaderElement {
        type(F.R), locationId(F.R),
        parameterId(F.R), qualifierId(F.M), ensembleId, ensembleMemberIndex,
        timeStep(F.R | F.A), startDate(F.R | F.A), endDate(F.R | F.A), forecastDate(F.A),
        missVal, longName, stationName, units,
        sourceOrganisation, sourceSystem, fileDescription,
        creationDate, creationTime, region, thresholds;

        interface F {/* ================================================================
 * 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.CharArrayUtils;
import nl.wldelft.util.Clasz;
import nl.wldelft.util.ExceptionUtils;
import nl.wldelft.util.FastDateFormat;
import nl.wldelft.util.FileUtils;
import nl.wldelft.util.FloatArrayUtils;
import nl.wldelft.util.NumberType;
import nl.wldelft.util.Period;
import nl.wldelft.util.Properties;
import nl.wldelft.util.StringArrayUtils;
import nl.wldelft.util.TextUtils;
import nl.wldelft.util.TimeUnit;
import nl.wldelft.util.TimeZoneUtils;
import nl.wldelft.util.geodatum.GeoDatum;
import nl.wldelft.util.io.VirtualInputDir;
import nl.wldelft.util.io.VirtualInputDirConsumer;
import nl.wldelft.util.io.XmlParser;
import nl.wldelft.util.timeseries.DefaultTimeSeriesHeader;
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;
        String user = null;
        String limitText = null;

        for (int i = 0, n = reader.getAttributeCount(); i < n; i++) {
            String localName = reader.getAttributeLocalName(i);
            String attributeValue = reader.getAttributeValue(i);
            if (dateText == null && TextUtils.equals(localName, "date")) {
                dateText = attributeValue;
            } else if (timeText == null && TextUtils.equals(localName, "time")) {
                timeText = attributeValue;
            } else if (valueText == null && TextUtils.equals(localName, "value")) {
                valueText = attributeValue;
            } else if (valueSource == null && TextUtils.equals(localName, "valueSource")) {
                valueSource = attributeValue;
            } else if (flagText == null && TextUtils.equals(localName, "flag")) {
                flagText = attributeValue;
            } else if (flagSource == null && TextUtils.equals(localName, "flagSource")) {
                flagSource = attributeValue;
            } else if (comment == null && TextUtils.equals(localName, "comment")) {
                comment = attributeValue;
            } else if (user == null && TextUtils.equals(localName, "user")) {
                user = attributeValue;
            } else if (startDateText == null && TextUtils.equals(localName, "startDate")) {
                startDateText = attributeValue;
            } else if (startTimeText == null && TextUtils.equals(localName, "startTime")) {
                startTimeText = attributeValue;
            } else if (endDateText == null && TextUtils.equals(localName, "endDate")) {
                endDateText = attributeValue;
            } else if (endTimeText == null && TextUtils.equals(localName, "endTime")) {
                endTimeText = attributeValue;
            } else if (minValueText == null && TextUtils.equals(localName, "minValue")) {
                minValueText = attributeValue;
            } else if (maxValueText == null && TextUtils.equals(localName, "maxValue")) {
                maxValueText = attributeValue;
            } else if (maxValueText == null && TextUtils.equals(localName, "detection")) {
                limitText = attributeValue;
            } else if (reader.getAttributePrefix(i) != null &&  TextUtils.equals(reader.getAttributePrefix(i), "fs")) {
                int index = timeSeriesContentHandler.addFlagSourceColumn(localName);
                timeSeriesContentHandler.setColumnFlagSource(index, attributeValue);
            } else {
                if (lenient) continue;
                throw 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");
        reader.nextTag();
    }

    private void parseValue(String valueText, String valueSource, String minValueText, String maxValueText, String limit) throws Exception {
        if (domainCount == 0) {
            try {
                float value = valueText == null ? Float.NaN : TextUtils.parseFloat(valueText);
                float minValue = minValueText == null ? value : TextUtils.parseFloat(minValueText);
                float maxValue = maxValueText == null ? value : TextUtils.parseFloat(maxValueText);
                // we can not use the automatic missing value detection of the content handler because the missing value is different for each time series
                if (value == missingValue) {
                    value = Float.NaN;
                } else {
                    timeSeriesContentHandler.setValueResolution(TextUtils.getValueResolution(valueText, '.'));
                }
                timeSeriesContentHandler.setValueAndRange(value, minValue, maxValue);
                timeSeriesContentHandler.setValueSource(TextUtils.equals(valueSource, "MAN") ? ValueSource.MANUAL : ValueSource.AUTOMATIC);
                timeSeriesContentHandler.setOutOfDetectionRangeFlag(OutOfDetectionRangeFlag.get(getOutOfDetectionFlag(limit)));
            } catch (NumberFormatException e) {
                throw new Exception("Value should be a float " + valueText);
            }
            reader.nextTag();
            return;
        }

        if (valueText != null)
            throw new Exception("Attribute value not allowed when having domain parameters, use event element text instead");
        int count = parseValueList();
        if (count == 0) {
            timeSeriesContentHandler.setValues(null);
            return;
        }

        int expectedCount = 1;
        for (int i = 0, n = domainCount; i < n; i++) {
            float[] axis = axisValues[i];

            if (axis == null)
                throw new Exception("Domain axis values for domain parameter " + domainParameterIds[i] + " are missing");

            expectedCount *= axis.length;
        }

        if (expectedCount != count)
            throw new Exception("Length of value list for event does not matches the axes lengths");

        if (axesDirty) {
            timeSeriesContentHandler.setDomainAxesValueResolutions(axisValueResolutions);
            timeSeriesContentHandler.setDomainAxesValues(axisValues);
            axesDirty = false;
        }

        if (count != floatBuffer.length) floatBuffer = FloatArrayUtils.resize(floatBuffer, expectedCount);
        timeSeriesContentHandler.setValueResolution(minValueResolution);
        timeSeriesContentHandler.setValues(floatBuffer);
    }

    private byte getOutOfDetectionFlag(String text) {
        if (text == null || text.isEmpty()) return TimeSeriesArray.INSIDE_DETECTION_RANGE;
        char ch = text.charAt(0);
        if (ch == '<') return TimeSeriesArray.BELOW_DETECTION_RANGE;
        if (ch == '>')  return TimeSeriesArray.ABOVE_DETECTION_RANGE;
        if (ch == '~') return TimeSeriesArray.VARYING;
        return TimeSeriesArray.INSIDE_DETECTION_RANGE;
    }

    private void parseFlagsUserComment(String flagText, String flagSource, String comment, String user) throws Exception {
        if (flagText == null) {
            timeSeriesContentHandler.setFlag(0);
        } else {
            try {
                timeSeriesContentHandler.setFlag(TextUtils.parseInt(flagText));
            } catch (NumberFormatException e) {
                throw new Exception("Flag should be an integer " + flagText);
            }
        }

        timeSeriesContentHandler.setComment(comment);
        timeSeriesContentHandler.setUser(user);
        timeSeriesContentHandler.setFlagSource(flagSource);
    }

    private void parseTime(String dateText, String timeText, String startDateText, String startTimeText, String endDateText, String endTimeText) throws Exception {
        if (timeText == null)
            throw new Exception("Attribute time is missing");

        if (dateText == null)
            throw new Exception("Attribute date is missing");

        try {
            long time = parseTime(dateText, timeText, null, null, Long.MIN_VALUE, lastTime, lastTimeAmbiguous);
            lastTime = time;
            intlastTimeAmbiguous A = 1 << 0timeAmbiguous;
 // attributes
          long startTime int R = 1 << 1; // required;
= parseTime(startDateText, startTimeText, dateText, timeText, time, lastStartTime, lastStartTimeAmbiguous);
            lastStartTime int= MstartTime;
 = 1 << 2; // multple;
      lastStartTimeAmbiguous = }timeAmbiguous;

        private final int flags;

 long endTime = parseTime(endDateText, endTimeText, dateText, dateText, time, lastEndTime, HeaderElement(lastEndTimeAmbiguous) {;
            this.flagslastEndTime = 0endTime;
        }

    lastEndTimeAmbiguous    HeaderElement(int flags) {= timeAmbiguous;
            this.flags = flagstimeSeriesContentHandler.setTimeAndRange(time, startTime, endTime);
        }

 catch (ParseException e) {
    public boolean isRequired() {
     throw new Exception("Can not parse " + returndateText (flags+ & F.R) != 0' ' + timeText);
        }

    }

    publicprivate booleanlong hasAttributesparseTime()String {
dateText, String timeText, String defaultDateText, String defaultTimeText, long defaultTime, long lastTime, boolean returnlastTimeAmbiguous) (flagsthrows & F.A) != 0;ParseException {
        }

if (dateText == null && timeText   public boolean isMultipleAllowed(== null) {
   return defaultTime;
        if return (flagsdateText & F.M) != 0;
== null) dateText = defaultDateText;
        if (timeText }
== null) timeText = }
defaultTimeText;

    // fastDateFormat is used toboolean keepuseMillis track of last time zone and lenient
= timeText.contains(".");

         private FastDateFormat fastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd", "HH:mm:ss", DateUtils.GMT, Locale.US, null);

    private boolean invalidHeaderTimeDetected = false;

    private HeaderElement currentHeaderElement = null;

    private static final HeaderElement[] HEADER_ELEMENTS = HeaderElement.class.getEnumConstants();
if (!fastDateFormat.getTimeZone().useDaylightTime()) return useMillis? fastDateFormatWithMillies.parseToMillis(dateText, timeText): fastDateFormat.parseToMillis(dateText, timeText);
        long t1;
        try {
    private PiTimeSeriesHeader header = new PiTimeSeriesHeader();
   t1 private List<String> qualfiers = new ArrayList<String>(= useMillis?fastDateFormatWithMillies.parseToMillis(dateText, timeText): fastDateFormat.parseToMillis(dateText, timeText);
     private long timeStepMillis =} 0;
catch (ParseException e) {
 private TimeStep timeStep = null;
    private long startTime =if Long.MIN_VALUE;
    private long endTime = Long.MIN_VALUE;
(!timeText.equals("02:00:00")) throw e;
          private float missingValue// = Float.NaN;
    private String creationDateText = null;
see FEWS-17782 also accept 02:00:00 on daylight saving time switch
      private String creationTimeText = null;

  try {
 private TimeSeriesContentHandler timeSeriesContentHandler = null;

    /**
     * For performance reasions the pi time series format alllows that the values are stored in
     * a separate bin file instead of embedded in the xml file.
return fastDateFormat.parseToMillis(dateText, "03:00:00");
            } catch (ParseException e1) {
                *throw Thee;
 bin file should have same name as the xml file except the}
 extension equals bin
     *}
 In this case all time series should beCalendar equidistant.
calendar = useMillis? fastDateFormatWithMillies.getCalendar()  */: fastDateFormat.getCalendar();
    private VirtualInputDir virtualInputDir = VirtualInputDir.NONEcalendar.setTimeInMillis(t1);
       private InputStreamint binaryInputStreamtimeOfDay = nullgetTimeOfDay(calendar);
       private byte[]long byteBuffert2 = null getOtherTime(calendar, t1, timeOfDay);
     private float[] floatBuffer = null;
    timeAmbiguous = t1 != t2;
   private int bufferPos = 0;
 long minTime  private int bufferCount = 0;

= Math.min(t1, t2);
       private XMLStreamReaderlong readermaxTime = nullMath.max(t1, t2);
    private String virtualFileName = null;

long res = minTime private> staticlastTime boolean? lenientminTime =: falsemaxTime;

     /**
   if (!lastTimeAmbiguous) *return Forres;
 backwards compatibility. Earlier versions of the PiTimeSeriesParser were// tollerantsee aboutFEWS-17782 thealso date/time format
     * and the case insensitive for header element names.
accept two times 02:00:00 instead of two times 01:00:00  on daylight saving time switch
        *if This(timeStepMillis parser!= should0L not&& acceptlastTime files+ thattimeStepMillis are!= notres) validreturn accordinglastTime to pi_timeseries.xsd+ timeStepMillis;
     * When old adaptersif are(timeStep not!= workingnull you can UseLenientPiTimeSeriesParser temporaray till the adapter is fixed&& timeStep.isRegular() && timeStep.nextTime(lastTime) != res) return timeStep.nextTime(lastTime);
     *
     * @param lenientreturn res;
     */}

    publicprivate static voidint setLenientgetTimeOfDay(booleanCalendar lenientcalendar) {
        PiTimeSeriesParser.lenient = lenient;
    }

    public PiTimeSeriesParser() {
        fastDateFormat.setLenient(lenientreturn (int) (calendar.get(Calendar.HOUR_OF_DAY) * TimeUnit.HOUR_MILLIS + calendar.get(Calendar.MINUTE) * TimeUnit.MINUTE_MILLIS + calendar.get(Calendar.SECOND) * TimeUnit.SECOND_MILLIS + calendar.get(Calendar.MILLISECOND));
    }

    @Override
private static   public void parse(XMLStreamReader reader, String virtualFileName, TimeSeriesContentHandler timeSeriesContentHandler) throws Exception {
long getOtherTime(Calendar calendar, long time, int timeOfDay) {
        try {
         this.reader = reader;
   calendar.setTimeInMillis(time + TimeUnit.HOUR_MILLIS);
          this.virtualFileName = virtualFileName;
        this.timeSeriesContentHandler = timeSeriesContentHandler;

if (getTimeOfDay(calendar) == timeOfDay) return time + TimeUnit.HOUR_MILLIS;
         String virtualBinFileName = FileUtilscalendar.getPathWithOtherExtension(virtualFileName, "bin");
setTimeInMillis(time - TimeUnit.HOUR_MILLIS);
        // time zone can be overruled by one or more time zone elements in the pi file
if (getTimeOfDay(calendar) == timeOfDay) return time - TimeUnit.HOUR_MILLIS;
             this.fastDateFormat.setTimeZone(timeSeriesContentHandler.getDefaultTimeZone());
return time;
        if (!virtualInputDir.exists(virtualBinFileName))} finally {
            parsecalendar.setTimeInMillis(time);
        }
    return;}

    private long parseTime() throws Exception }{

        String binaryInputStreamdateText = virtualInputDirreader.getInputStream(virtualBinFileNamegetAttributeValue(null, "date");
        try {
            if (byteBufferdateText == null) {
            throw new Exception("Attribute " byteBuffer+ =currentHeaderElement new byte[BUFFER_SIZE * NumberType.FLOAT_SIZE];
+
                   floatBuffer ="-date new float[BUFFER_SIZE];
    is missing");
        }
        String timeText =  parse(reader.getAttributeValue(null, "time");
        if (timeText == null) boolean{
 eof = bufferPos == bufferCount && binaryInputStream.read() == -1;
   throw new Exception("Attribute " + currentHeaderElement +
   if (!eof)
                throw"-time newis IOException("More values available in bin file than expected based on time step and start and end time\n" + FileUtils.getPathWithOtherExtension(virtualFileName, "bin"));
missing");
        }
        boolean useMillis = timeText.contains(".");
        } finally {long time;
        try {
   bufferPos = 0;
       time = useMillis? fastDateFormatWithMillies.parseToMillis(dateText, timeText) bufferCount = 0: fastDateFormat.parseToMillis(dateText, timeText);
        } catch (ParseException  binaryInputStream.close();
e) {
            throw new Exception("Not a valid data binaryInputStreamtime =for null;"
        }
     }

    private void parse() throws+ ExceptioncurrentHeaderElement {
+ ' ' + dateText + '  reader.require(XMLStreamConstants.START_DOCUMENT, null, null' + timeText, e);
        reader.nextTag();}

        reader.require(XMLStreamConstants.START_ELEMENT, null, "TimeSeries"nextTag();
        reader.nextTag()return time;

    }

    private while (reader.getEventTypevoid parseTimeStep() != XMLStreamConstants.END_ELEMENT)throws Exception {
        String times =  parseTimeZone(reader.getAttributeValue(null, "times");

        if (times !=  readTimeSeries();
   null) {
     }

       timeStep reader= PiCastorUtils.require(XMLStreamConstants.END_ELEMENT, null, "TimeSeries"createTimesOfDayTimeStep(times, getTimeZone());
            reader.nextnextTag();
          reader.require(XMLStreamConstants.END_DOCUMENT, null, null)return;

    }

    private}

 void readTimeSeries() throws Exception {
   String unit  =  reader.require(XMLStreamConstants.START_ELEMENT, getAttributeValue(null, "seriesunit");
         reader.nextTag();
        parseHeader(TimeUnit tu = unit == null ? null : TimeUnit.get(unit);
        if (binaryInputStreamtu =!= null) {
            while (reader.getEventType() == XMLStreamConstants.START_ELEMENT && TextUtils.equals(reader.getLocalName(), "event")) {
   String multiplierText = reader.getAttributeValue(null, "multiplier");
            int multiplier;
            if parseEvent();multiplierText == null) {
            }

      multiplier = 1;
          if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) } else {
                try {
  //   skip comment
              multiplier = readerInteger.require(XMLStreamConstants.START_ELEMENT, null, "comment"parseInt(multiplierText);
                } reader.getElementText();
catch (NumberFormatException e) {
                    throw new readerException(ExceptionUtils.nextTaggetMessage(e), e);
  
          }
        } else {}

            readValuesFromBinFile();
    if (multiplier == 0) }{
        reader.require(XMLStreamConstants.END_ELEMENT, null, "series");
        reader.nextTag();
   throw }

    private void parseHeader() throws Exception {
new Exception("Multiplier is 0");
              reader.require(XMLStreamConstants.START_ELEMENT, null, "header"); }
        if (reader.getAttributeCount() > 0) {}

            String throwdividerText new= Exception("Attributes are not allowed for header element "reader.getAttributeValue(null, "divider");
        }
        reader.nextTag()int divider;
        initHeader();
    if (dividerText ==  donull) {
            detectHeaderElement();
    divider =   1;
    parseHeaderElement();
        } while (reader.getEventType() != XMLStreamConstants.END_ELEMENT);

else {
        if (header.getForecastTime() == Long.MIN_VALUE) header.setForecastTime(startTime);
    try {
   initiateTimeStep();
        header.setTimeStep(timeStep);
        if (!qualfiers.isEmpty()) header.setQualifierIds(qualfiers.toArray(new String[qualfiers.size()])) divider = Integer.parseInt(dividerText);
        if (creationDateText != null) {
    } catch (NumberFormatException e) {
    try {
               throw long creationTime = fastDateFormat.parseToMillis(creationDateText, creationTimeTextnew Exception(ExceptionUtils.getMessage(e), e);
                header.setCreationTime(creationTime);
}

               } catchif (ParseException edivider == 0) {
                    throw new Exception("Candivider not parse creation date/time " + creationDateText + ' ' + creationTimeTextis 0");
            }
    }
    }
        timeSeriesContentHandler.setNewTimeSeriesHeader(header);
}
         if (startTime != Long.MIN_VALUE && endTime != Long.MIN_VALUE) {
reader.nextTag();
            timeStepMillis =  timeSeriesContentHandlertu.setEstimatedPeriod(new Period(startTime, endTime))getMillis() * multiplier / divider;
        }
    timeStep = null;
        reader.require(XMLStreamConstants.END_ELEMENT, null, "header");
} else {
            reader.nextTag();
    }

     private void parseEvent() throwstimeStepMillis Exception= {0;
           assert binaryInputStreamtimeStep == nullIrregularTimeStep.INSTANCE;
        reader.require(XMLStreamConstants.START_ELEMENT, null, "event");
 }
    }

    private void initHeader() {
       String timeText = reader.getAttributeValue(null, "time"header.clear();
        String dateText = reader.getAttributeValue(null, "date"header.setFileDescription(virtualFileName);
        String valueTextcurrentHeaderElement = reader.getAttributeValue(null, "value");
        String flagTexttimeStep = reader.getAttributeValue(null, "flag");
        String commentTexttimeStepMillis = reader.getAttributeValue(null, "comment");
0;
        ifstartTime (timeText == null)Long.MIN_VALUE;
        endTime = Long.MIN_VALUE;
     throw new Exception("Attribute timemissingValue is missing");
= Float.NaN;
        if (dateTextcreationDateText == null);
        creationTimeText = "00:00:00";
   throw new Exception("Attribute date is missing" qualifiers.clear();

        if (valueTextlat == null)
 Double.NaN;
        lon = Double.NaN;
     throw new Exception("Attribute valuez is missing")= Double.NaN;
    }

    private void readValuesFromBinFile() throws tryException {
        TimeStep timeStep =  timeSeriesContentHandler.setTime(fastDateFormat.parseToMillis(dateText, timeText)header.getTimeStep();
        } catch (ParseException eif (!timeStep.isRegular()) {
            throw new Exception("Can not parse " + dateText + ' ' + timeTextOnly equidistant time step supported when pi events are stored in bin file instead of xml");
        }

        ifboolean (flagTextequidistantMillis == null) {
timeStep.isEquidistantMillis();
        long stepMillis = equidistantMillis ?  timeSeriesContentHandlertimeStep.setFlaggetStepMillis(0);
        } else) : Long.MIN_VALUE;
        for (long time = startTime; time <= endTime;) {
            try {timeSeriesContentHandler.setTime(time);
            if (bufferPos ==  timeSeriesContentHandler.setFlag(TextUtils.parseInt(flagText)bufferCount) fillBuffer();
            float  } catch (NumberFormatException e) {
                throw new Exception("Flag should be an integer " + flagText);
value = floatBuffer[bufferPos++];
            // we can not use the automatic missing value detection of the content handler because the missing value is different for each time series
            if (value }
== missingValue) value = Float.NaN;
    }
        timeSeriesContentHandler.setCommentsetValue(commentTextvalue);

         try {
  timeSeriesContentHandler.applyCurrentFields();
          float value =if TextUtils.parseFloat(valueTextequidistantMillis); {
            // we can not usetime the automatic missing value detection of the content handler because the missing value is different for each time series
        += stepMillis;
                continue;
    if (value == missingValue) {
    }
            valuetime = Float.NaNtimeStep.nextTime(time);
        }
    } else

    private void fillBuffer() throws IOException {
        int byteBufferCount = 0;
        while timeSeriesContentHandler.setValueResolution(TextUtils.getValueResolution(valueText, '.'));
     (byteBufferCount % NumberType.FLOAT_SIZE != 0 || byteBufferCount == 0) {
       }
     int count = binaryInputStream.read(byteBuffer, byteBufferCount, BUFFER_SIZE * timeSeriesContentHandler.setValue(valueNumberType.FLOAT_SIZE - byteBufferCount);
            assert count != 0; // see  timeSeriesContentHandler.applyCurrentFields();read javadoc
        }    catchif (NumberFormatException ecount == -1) {
throw new EOFException("Bin file is too short");
      throw new Exception("Value should be a float "byteBufferCount += valueText)count;
        }
        bufferCount = byteBufferCount / reader.nextTag()NumberType.FLOAT_SIZE;
        readerBinaryUtils.require(XMLStreamConstants.END_ELEMENT, null, "event"copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIAN);
        reader.nextTag()bufferPos = 0;
    }

    private longvoid parseTimeinitiateTimeStep() throws Exception {
        Stringif dateText(timeStep != reader.getAttributeValue(null, "date");
 null) {
          if (dateText  assert timeStepMillis == null) {
0;
            return;
  throw new Exception("Attribute " + currentHeaderElement +}
        if (timeStepMillis == 0){
         "-date is missing");
        } //no timestep in header. Fix for backward compatibility
        String    timeTexttimeStep = reader.getAttributeValue(null, "time")IrregularTimeStep.INSTANCE;
        if (timeText == null) {return;
        }
     throw new Exception("Attribute " + currentHeaderElement +
   if (timeStepMillis >= TimeUnit.HOUR_MILLIS && getTimeZone().useDaylightTime()) {
            timeStep = IrregularTimeStep.INSTANCE;
          "-time is missing")return;
        }

        long time;
 startTime = this.startTime ==  Long.MIN_VALUE ? 0L : this.startTime;
        if (timeStepMillis % TimeUnit.SECOND_MILLIS != try0) {
            timetimeStep = fastDateFormatRelativeEquidistantTimeStep.parseToMillisgetInstance(dateTexttimeStepMillis, timeTextstartTime);
        } catch (ParseException e) {return;
        }

     throw new Exception("Not along validtimeZoneOffsetMillis data= time-startTime for% "
   timeStepMillis;
        if (timeZoneOffsetMillis % TimeUnit.MINUTE_MILLIS != 0) {
            timeStep =    + currentHeaderElement + ' ' + dateText + ' ' + timeText, e)RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);
            return;
        }

        timeStep = readerSimpleEquidistantTimeStep.nextTaggetInstance(timeStepMillis, timeZoneOffsetMillis);
        return time;}

    @SuppressWarnings({"OverlyLongMethod"})

    private longvoid parseTimeStepparseHeaderElement() throws Exception {
         String unit = reader.getAttributeValue(null, "unit");
switch (currentHeaderElement) {
            case type:
             if (unit == null) { header.setParameterType(parseType(reader.getElementText()));
            throw new Exception("Attribute unit isbreak;
 missing in " + currentHeaderElement);
       case }

moduleInstanceId:
             TimeUnit tu = TimeUnit.get(unitheader.setModuleInstanceId(reader.getElementText());
            if (tu != null) {break;
            Stringcase multiplierTextlocationId:
 = reader.getAttributeValue(null, "multiplier");
            int multiplier;
// see FEWS-9858, when there is no location id the time series ifare (multiplierTextassigned ==to null)all {locations
                // multiplierthis =is 1;
a flaw in the pi_timeSeries.xsd, the location element is required but is }allowed elseempty {strings
                try {header.setLocationId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementText()), "none"));
                    multiplier = Integer.parseInt(multiplierText)break;
                } catch (NumberFormatException e) {case parameterId:
                // see FEWS-9858, when throwthere new Exception(ExceptionUtils.getMessage(e), e);
         is no parameter id the time series are assigned to all locations
       }

         // this is a flaw in  if (multiplier == 0) {
      the pi_timeSeries.xsd, the location element is required but is allowed empty strings
              throw new Exception("Multiplier is 0" header.setParameterId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementText()), "none"));
                }break;
            case }qualifierId:

             String dividerText = qualifiers.add(reader.getAttributeValue(null, "divider"getElementText());
            int divider;
   break;
         if (dividerText == null)case {ensembleId:
                divider = 1header.setEnsembleId(reader.getElementText());
            } else {
   break;
            case try {ensembleMemberIndex:
                    divider = Integer.parseInt(dividerTextheader.setEnsembleMemberIndex(parseEnsembleMemberIndex(reader.getElementText()));
                } catch (NumberFormatException e) {
break;
            case ensembleMemberId:
              throw new Exceptionheader.setEnsembleMemberId(ExceptionUtilsreader.getMessagegetElementText(e), e);
                }break;

            case timeStep:
    if (divider == 0) {
        parseTimeStep();
            throw new Exception("dividplier is 0")break;
            case startDate:
   }
            }
 startTime = parseTime();
         reader.nextTag();
       break;
     return tu.getMillis() * multiplier / divider;
  case endDate:
     } else {
         endTime =  reader.nextTagparseTime();
               return 0break;
        }
    }

case forecastDate:
     private void initHeader() {
        header.clearsetForecastTime(parseTime());
        header.setFileDescription(virtualFileName);
        currentHeaderElement = nullbreak;
        timeStep = null;
   case approvedDate:
    timeStepMillis = 0;
        startTime = Long.MIN_VALUEheader.setApprovedTime(parseTime());
         endTime      = Long.MIN_VALUEbreak;
         missingValue = Float.NaN;
 case missVal:
      creationDateText = null;
        creationTimeTextmissingValue = "00:00:00";
parseString(reader.getElementText());
            qualfiers.clear();
    }
break;
    private void readValuesFromBinFile() throws Exception {
   case longName:
    TimeStep timeStep = header.getTimeStep();
        if header.setLongName(!timeStepreader.isRegulargetElementText()) {;
            throw new Exception("Only equidistant timebreak;
 step supported when pi events are stored in bin file instead of xml");
  case stationName:
      }

        boolean equidistantMillis = timeStep.isEquidistantMillis( header.setLocationName(reader.getElementText());
        long stepMillis = equidistantMillis ?  timeStep.getStepMillis() : Long.MIN_VALUEbreak;
        try  {
  case units:
         for (long time = startTime; time <= endTime;) { header.setUnit(reader.getElementText());
                timeSeriesContentHandler.setTime(time)break;
                if (bufferPos == bufferCount) fillBuffer();case domainAxis:
                float value = floatBuffer[bufferPos++]parseDomainAxis();
                //break;
 we can not use the automatic missing value detection of the contentcase handlersourceOrganisation:
 because the missing value is different for each time series
      header.setSourceOrganisation(reader.getElementText());
          if (value == missingValue) value = Float.NaNbreak;
                timeSeriesContentHandler.setValue(value);case sourceSystem:
                timeSeriesContentHandler.applyCurrentFields(header.setSourceSystem(reader.getElementText());
                if (equidistantMillis) {break;
            case fileDescription:
       time += stepMillis;
       header.setFileDescription(reader.getElementText());
             continue;
   break;
            case }creationDate:
                timecreationDateText = timeStepreader.nextTimegetElementText(time);
            }
        } catch (IOException e) {break;
            throw new Exception(ExceptionUtils.getMessage(e), e);case creationTime:
        }
    }

    privatecreationTimeText void= fillBufferreader.getElementText() throws IOException {;
        int byteBufferCount = 0;
     break;
   while (byteBufferCount % NumberType.FLOAT_SIZE != 0 || byteBufferCount == 0)case {region:
            int  count = binaryInputStreamheader.read(byteBuffer, byteBufferCount, BUFFER_SIZE * NumberType.FLOAT_SIZE - byteBufferCountsetRegion(reader.getElementText());
            assert  count != 0break;
    // see read javadoc
     case thresholds:
      if (count == -1) throw new EOFException("Bin file is too short"parseThresholds();
              byteBufferCount += countbreak;
        }
    case lat:
   bufferCount = byteBufferCount / NumberType.FLOAT_SIZE;
        BinaryUtils.copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIAN lat = parseString(reader.getElementText());
         bufferPos  = 0;
    }break;

     private void initiateTimeStep() {
    case lon:
   timeStep = IrregularTimeStep.INSTANCE; //default timestep

        if (timeStepMillislon == 0) {
parseString(reader.getElementText());
                returnbreak;
            case }x:
        if (timeStepMillis % TimeUnit.MINUTE_MILLIS != 0) {
  reader.getElementText();
          if (!this.invalidHeaderTimeDetected) {
    break;
            if (log.isDebugEnabled()) log.debug("Header timestep and/or start time has not rounded  minutes ! Irregular timestep wil be used."case y:
                reader.getElementText();
                this.invalidHeaderTimeDetected = truebreak;
            }case z:
            timeStepMillis = 0;
  z = parseString(reader.getElementText());
        return;
        }
break;
        long timeZoneOffsetMillis = -startTime %case timeStepMillis;firstValueTime:
        if (timeZoneOffsetMillis % TimeUnit.MINUTE_MILLIS != 0) {
  reader.getElementText();
          if (!this.invalidHeaderTimeDetected) {
    break;
            if (log.isDebugEnabled()) log.debug("Header timestep and/or start time has not rounded  minutes ! Irregular timestep wil be used.");
  case lastValueTime:
                reader.getElementText();
              this.invalidHeaderTimeDetected = truebreak;
            case }maxValue:
              timeStepMillis = 0reader.getElementText();
            return;
        }break;
        timeStep = SimpleEquidistantTimeStep.getInstance(timeStepMillis, timeZoneOffsetMillis);
  case  }

  minValue:
  private void parseTimeZone() throws Exception {
        if (reader.getEventTypegetElementText();
 != XMLStreamConstants.START_ELEMENT) return;
            if (!TextUtils.equals(reader.getLocalName(), "timeZone")) returnbreak;
            trycase {valueCount:
             double offset = Double.parseDouble(reader.getElementText());
             TimeZone timeZoneFromDouble = TimeZoneUtils.createTimeZoneFromDouble(offset) break;
             this.fastDateFormat.setTimeZone(timeZoneFromDouble);

case maxWarningLevelName:
          } catch (NumberFormatException e) {
  reader.getElementText();
          throw new Exception("Not valid timeZone format", e)break;

        }
        reader.require(XMLStreamConstants.END_ELEMENT, null, "timeZone"currentHeaderElement.name());
        reader.nextTag();
    }

    @SuppressWarnings({"OverlyLongMethod"})
    private void parseHeaderElementparseDomainAxis() throws Exception {
        switchString (currentHeaderElement) {
            case type:
                header.setParameterType(parseType(reader.getElementText()));
   parameterId = reader.getAttributeValue(null, "parameterId");
        if (parameterId    break;== null)
            casethrow locationId:
      new Exception("Attribute parameterId for domainUnits is missing");

        domainParameterIds = headerClasz.setLocationId(readerstrings.getElementText()ensureCapacity(domainParameterIds, domainCount + 1);
        domainUnits = Clasz.strings.ensureCapacity(domainUnits, domainCount     break+ 1);
        domainParameterIds[domainCount] = parameterId;
  case parameterId:
     domainUnits[domainCount] = reader.getAttributeValue(null, "units");
        header.setParameterId(reader.getElementText())domainCount++;
        reader.nextTag();
    }

    break;
private void parseThresholds() throws XMLStreamException {
       case qualifierId: reader.nextTag();
        ArrayList<DefaultTimeSeriesHeader.DefaultThreshold> thresholds =      qualfiers.add(reader.getElementText()new ArrayList<>();
        do {
       break;
     if (reader.getEventType() ==   XMLStreamConstants.START_ELEMENT) {
  case ensembleId:
             String id = header.setEnsembleId(reader.getElementText()getAttributeValue(null, "id");
                break;
String name = reader.getAttributeValue(null, "name");
        case ensembleMemberIndex:
       float value        header.setEnsembleMemberIndex(parseEnsembleMemberIndex= TextUtils.parseFloat(reader.getElementText()));
      getAttributeValue(null, "value"));
          break;
      String groupId = reader.getAttributeValue(null, "groupId");
  case timeStep:
             String   timeStepMillisgroupName = parseTimeStep(reader.getAttributeValue(null, "groupName");
                 break;
            case startDate:
String[] groupIds = groupId == null ? Clasz.strings.emptyArray() : new String[] {groupId};
                String[] groupNames = startTimegroupName = parseTime()= null ? Clasz.strings.emptyArray() : new String[] {groupName};
                break;
      thresholds.add(new DefaultTimeSeriesHeader.DefaultThreshold(id, name, value, groupIds, groupNames));
      case endDate:
     }
           endTime = parseTime reader.nextTag();
        }        break;while (!reader.getLocalName().equals(currentHeaderElement.name()));

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

    private void parseProperties() throws XMLStreamException {
        headerreader.setForecastTime(parseTime()require(XMLStreamConstants.START_ELEMENT, null, "properties");
        reader.nextTag();
        breakpropertyBuilder.clear();
        while    case missVal:(!TextUtils.equals(reader.getLocalName(), "properties")){
                missingValue = parseMissingValueif (reader.getElementTextgetEventType());
 != XMLStreamConstants.START_ELEMENT) {
             break;
   // eg <int       case longName:key="a" value=12><int>
                header.setLongName(reader.getElementTextnextTag());
                breakcontinue;
            case stationName:}
            String key =  header.setLocationName(reader.getElementText()getAttributeValue(null, "key");
            String value =  breakreader.getAttributeValue(null, "value");
            String  case units:date = reader.getAttributeValue(null, "date");
            String time  = header.setUnit(reader.getElementText()getAttributeValue(null, "time");
            switch (reader.getLocalName()) {
    break;
            case sourceOrganisation"string":
                    headerpropertyBuilder.setSourceOrganisation(reader.getElementText()addString(key, value);
                    break;
                case sourceSystem"int":
                    headerpropertyBuilder.setSourceSystem(reader.getElementText(addInt(key, TextUtils.parseInt(value));
                    break;
                case fileDescription"float":
                    headerpropertyBuilder.setFileDescription(reader.getElementText(addFloat(key, TextUtils.parseFloat(value));
                    break;
                case creationDate"double":
                   creationDateText = reader.getElementText( propertyBuilder.addDouble(key, TextUtils.parseDouble(value));
                    break;

                case creationTime"bool":
                  creationTimeText = reader.getElementText(  propertyBuilder.addBoolean(key, Boolean.parseBoolean(value));
                    break;
                case region"dateTime":
                     header.setRegion(reader.getElementText());
try {
                        if  break;(time.contains(".")) {
            case thresholds:
                parseThresholds(propertyBuilder.addDateTime(key, fastDateFormatWithMillies.parseToMillis(date, time));
                break;
        } else {
        reader.require(XMLStreamConstants.END_ELEMENT, null, currentHeaderElement.name());
        reader.nextTag();
     }

    private void parseThresholds() throws XMLStreamException {
 propertyBuilder.addDateTime(key, fastDateFormat.parseToMillis(date, time));
            reader.nextTag();
        ArrayList<String> ids = new ArrayList<String>();}
        ArrayList<String> names = new ArrayList<String>();
        ArrayList<String> stringValues = new ArrayList<String>()break;
        do {
           } ifcatch (reader.getEventType() == XMLStreamConstants.START_ELEMENTParseException e) {
                String id = reader.getAttributeValue(null, "id");
    throw new XMLStreamException("Invalid date time "+ date + ' '   String name = reader.getAttributeValue(null, "name"+ time);
                String stringValue = reader.getAttributeValue(null, "value"); }
                ids.add(id);default:
                names.add(name);
    throw new XMLStreamException("Invalid property type "       stringValues.add(stringValue+ reader.getLocalName());
            }
            reader.nextTag();
        } while (!reader.getLocalName().equals(currentHeaderElement.name()

        timeSeriesContentHandler.setProperties(propertyBuilder.build());

        float[] values = new float[stringValues.size()]reader.require(XMLStreamConstants.END_ELEMENT, null, "properties");
        reader.nextTag();
    }

    for (int i = 0; i < values.length; i++) private static float parseString(String gotString) throws Exception {
        // <element name="missVal"  values[i] = Float.valueOf(stringValues.get(i));
type="double" default="NaN">
        // }
when default is used in schema for  header.setHighLevelThresholds(ids.toArray(new String[ids.size()]), names.toArray(new String[names.size()]), values);
    }

element the consequence is that empty strings are allowed 
      private static float parseMissingValue(String gotString) throws Exception {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);
            assert element != null; // contract of valueOf
        } 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" + localName);
        }

        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;
    }
}