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.DateUtilsBinaryUtils;
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.TextUtilsProperties;
import nl.wldelft.util.StringArrayUtils;
import nl.wldelft.util.TimeUnitTextUtils;
import nl.wldelft.util.TimeZoneUtilsTimeUnit;
import nl.wldelft.util.NumberTypeTimeZoneUtils;
import nl.wldelft.util.BinaryUtilsgeodatum.GeoDatum;
import nl.wldelft.util.io.VirtualInputDir;
import nl.wldelft.util.io.VirtualInputDirConsumer;
import nl.wldelft.util.io.XmlParser;
import nl.wldelft.util.timeseries.IrregularTimeStepDefaultTimeSeriesHeader;
import nl.wldelft.util.timeseries.ParameterTypeIrregularTimeStep;
import nl.wldelft.util.timeseries.SimpleEquidistantTimeStepOutOfDetectionRangeFlag;
import nl.wldelft.util.timeseries.TimeSeriesContentHandlerParameterType;
import nl.wldelft.util.timeseries.TimeStepRelativeEquidistantTimeStep;
import orgnl.wldelft.apacheutil.log4jtimeseries.LoggerSimpleEquidistantTimeStep;

import javaxnl.wldelft.xmlutil.streamtimeseries.XMLStreamConstantsTimeSeriesArray;
import javaxnl.wldelft.xmlutil.streamtimeseries.XMLStreamExceptionTimeSeriesContentHandler;
import javaxnl.wldelft.xmlutil.streamtimeseries.XMLStreamReaderTimeSeriesHeader;
import java.io.IOExceptionnl.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.ionio.EOFExceptionByteOrder;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.ListArrays;
import java.util.Calendar;
import java.util.LocaleList;
import java.util.TimeZoneLocale;
import java.nioutil.ByteOrderTimeZone;

public class PiTimeSeriesParser implements XmlParser<TimeSeriesContentHandler>, VirtualInputDirConsumer {
    private static final Loggerint logBUFFER_SIZE = Logger.getLogger(PiTimeSeriesParser.class);
2048;
    private static final int BUFFER_SIZEProperties.Builder propertyBuilder = 2048;
 new Properties.Builder();
    @Override
private char[] charBuffer = publicnull;
 void setVirtualInputDir(VirtualInputDir virtualInputDir) {
private float minValueResolution = Float.NaN;
    private  this.virtualInputDirfloat[][] axisValues = virtualInputDirnull;
    }
private float[] axisValueResolutions = Clasz.floats.emptyArray();
    private boolean enumaxesDirty HeaderElement= {false;
    private String[] domainParameterIds  type(F.R), locationId(F.R),= Clasz.strings.emptyArray();
    private String[] domainUnits  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),= Clasz.strings.emptyArray();
    private int domainCount = 0;

    @Override
    public void setVirtualInputDir(VirtualInputDir virtualInputDir) {
        missVal, longName, stationName, units,
this.virtualInputDir = virtualInputDir;
    }

    private sourceOrganisation,enum sourceSystem,HeaderElement fileDescription,{
        creationDatetype(F.R), creationTimemoduleInstanceId, region, thresholds;
locationId(F.R),
        interface F {
  parameterId(F.R), qualifierId(F.M), ensembleId, ensembleMemberIndex, ensembleMemberId,
        timeStep(F.R | int F.A = 1 << 0; // attributes
   ), startDate(F.R | F.A), endDate(F.R | F.A), forecastDate(F.A), approvedDate(F.A),
        missVal, intlongName, RstationName, =lat, 1lon, << 1; // required;
            int M = 1 << 2; // multple;
x, y, z, units, domainAxis(F.M | F.A),
        sourceOrganisation,    }
sourceSystem, fileDescription,
        privatecreationDate, finalcreationTime, int flags;
region, thresholds,
        HeaderElementfirstValueTime(F.A), lastValueTime(F.A), maxValue, minValue, valueCount, maxWarningLevelName;

        interface F {
            this.flagsint A = 1 << 0; // attributes
         }

   int R = 1 << HeaderElement(int flags) {1; // required;
            this.flagsint M = flags 1 << 2; // multiple;
        }

        private final  public boolean isRequiredint flags;

        HeaderElement() {
            return (flags & F.R) !this.flags = 0;
        }

        public boolean 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", DateUtilsTimeZoneUtils.GMT, Locale.US, null);

    private booleanFastDateFormat invalidHeaderTimeDetectedfastDateFormatWithMillies = false;

   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> qualfiersqualifiers = new ArrayList<String>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 TimeSeriesContentHandlerlong timeSeriesContentHandlerlastTime = nullLong.MIN_VALUE;

    /**
private long lastStartTime = Long.MIN_VALUE;
 * For performance reasionsprivate thelong pilastEndTime time series format alllows that the values are stored in= Long.MIN_VALUE;
    private boolean timeAmbiguous = false;
    private *boolean alastTimeAmbiguous separate= binfalse;
 file instead of embeddedprivate inboolean thelastStartTimeAmbiguous xml= file.false;
    private *boolean ThelastEndTimeAmbiguous bin file should have = 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 tolleranttolerant 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 temporaraytemporary 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 readTimeSeries();timeZone != null) {
        }

        readerthis.fastDateFormat.require(XMLStreamConstants.END_ELEMENT, null, "TimeSeries"setTimeZone(timeZone);
                readerthis.fastDateFormatWithMillies.nextsetTimeZone(timeZone);
        reader.require(XMLStreamConstants.END_DOCUMENT, null, null);

    }
            if (noTimeSeries()) continue;
      }

    private void readTimeSeries() throws Exception {;
        }

        reader.require(XMLStreamConstants.STARTEND_ELEMENT, null, "seriesTimeSeries");
        reader.nextTagnext();
        parseHeader(reader.require(XMLStreamConstants.END_DOCUMENT, null, null);

    }

    ifprivate boolean noTimeSeries(binaryInputStream == null) {
            whilereturn (reader.getEventType() == XMLStreamConstants.STARTEND_ELEMENT && TextUtils.equals(reader.getLocalName(), "event")) {.equals("TimeSeries");
    }

    private void readTimeSeries() throws Exception {
       parseEvent( reader.require(XMLStreamConstants.START_ELEMENT, null, "series");
        reader.nextTag();
    }

    parseHeader();
        if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {lastTime = Long.MIN_VALUE;
        timeAmbiguous = false;
      // skip comment
lastTimeAmbiguous = false;
        lastStartTimeAmbiguous = false;
    reader.require(XMLStreamConstants.START_ELEMENT, null, "comment");
   lastEndTimeAmbiguous = false;
           reader.getElementText(timeSeriesContentHandler.setProperties(Properties.NONE);
        if (eventDestination       reader.nextTag();== PiTimeSeriesSerializer.EventDestination.XML_EMBEDDED) {
            }
    while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
    }  else {
         String localName = readValuesFromBinFilereader.getLocalName();
         }
       if reader(TextUtils.require(XMLStreamConstants.END_ELEMENT, nullequals(localName, "seriesevent");) {
          reader.nextTag();
    }

    private void parseHeaderparseEvent() throws Exception {;
        reader.require(XMLStreamConstants.START_ELEMENT, null, "header");
      } else if (readerTextUtils.getAttributeCount() > 0equals(localName, "domainAxisValues")) {
            throw new Exception("Attributes are not allowed for header element "parseDomainAxisValues();
         }
        reader.nextTag();
} else if (TextUtils.equals(localName, "properties")) {
                    initHeaderparseProperties();
          do      }  else {
            detectHeaderElement();
        break;
    parseHeaderElement();
        } while (reader.getEventType() != XMLStreamConstants.END_ELEMENT);
 }
        if (header.getForecastTime() == Long.MIN_VALUE) header.setForecastTime(startTime); }
        initiateTimeStep();
} else {
      header.setTimeStep(timeStep);
      assert eventDestination if (!qualfiers.isEmpty()) header.setQualifierIds(qualfiers.toArray(new String[qualfiers.size()]))== PiTimeSeriesSerializer.EventDestination.SEPARATE_BINARY_FILE;
            readValuesFromBinFile();
        }
        if (reader.getEventType(creationDateText) !== nullXMLStreamConstants.START_ELEMENT) {
            // tryskip {comment
                long creationTime = fastDateFormat.parseToMillis(creationDateText, creationTimeTextreader.require(XMLStreamConstants.START_ELEMENT, null, "comment");
                header.setCreationTime(creationTimereader.getElementText();
            } catch (ParseException e) {reader.nextTag();
        }
        throw new Exception("Can not parse creation date/time " + creationDateText + ' ' + creationTimeText);
   reader.require(XMLStreamConstants.END_ELEMENT, null, "series");
        reader.nextTag();
    }

    private void parseDomainAxisValues() throws Exception }{
        }
String domainParameterId       timeSeriesContentHandler.setNewTimeSeriesHeader(header= reader.getAttributeValue(null, "parameterId");
        if (startTimedomainParameterId != Long.MIN_VALUE && endTime != Long.MIN_VALUEnull)
 {
           throw timeSeriesContentHandler.setEstimatedPeriod(new PeriodException(startTime, endTime));
    "Attribute parameterId for domainAxisValues is missing");

    }

    int index =  readerStringArrayUtils.require(XMLStreamConstants.END_ELEMENTindexOf(domainParameterIds, 0, nulldomainCount, "header"domainParameterId);
        if reader.nextTag();
(index == -1)
        }

    privatethrow voidnew parseEvent() throws Exception {Exception("parameterId " + domainParameterId + " for domainAxisValues not defined in header");

        assertif binaryInputStream(axisValues == null) axisValues = new float[domainCount][];
        if reader.require(XMLStreamConstants.START_ELEMENT, null, "event")axisValueResolutions.length < domainCount) axisValueResolutions = new float[domainCount];
        Stringint timeTextcount = reader.getAttributeValue(null, "time"parseValueList();
        String dateText = reader.getAttributeValue(reader.require(XMLStreamConstants.END_ELEMENT, null, "datedomainAxisValues");
        String valueText = reader.getAttributeValue(null, "value"nextTag();
        String flagText axisValues[index] = readerClasz.floats.getAttributeValue(nullcopyOfArrayRange(floatBuffer, 0, "flag"count);
        String commentTextaxisValueResolutions[index] = reader.getAttributeValue(null, "comment");
minValueResolution;
        ifaxesDirty (timeText == null)true;
    }

    private int   throw newparseValueList() throws Exception("Attribute time is missing");
 {
        if (dateTextcharBuffer == null)
 charBuffer = new char[100];
        char[] throwcharBuffer new Exception("Attribute date is missing");
= this.charBuffer;
        if (valueTextfloatBuffer == null)
 floatBuffer = new float[100];
        throwminValueResolution new Exception("Attribute value is missing");
= Float.POSITIVE_INFINITY;
        tryint {
i = 0;
        int  timeSeriesContentHandler.setTime(fastDateFormat.parseToMillis(dateText, timeText))length = 0;
        }int catchj (ParseException e) {= 0;
        while (reader.next() ==  throw new Exception("Can not parse " + dateText + ' ' + timeText)XMLStreamConstants.CHARACTERS) {
            length -= i;
        }

    assert length >= 0;
 if (flagText == null) {
       // shift the remaining part of  timeSeriesContentHandler.setFlag(0);
   the buffer to the start
     } else {
     if (length > 0) CharArrayUtils.arraycopy(charBuffer, i, charBuffer, try {0, length);
            int textLength =  timeSeriesContentHandlerreader.setFlag(TextUtils.parseInt(flagText)getTextLength();

             } catch (NumberFormatException eif (charBuffer.length < length + textLength) {
                throwcharBuffer new Exception("Flag should be an integer "= Clasz.chars.copyOfArrayRange(charBuffer, 0, length, length + flagTexttextLength);
            }
    this.charBuffer = charBuffer;
  }
          timeSeriesContentHandler.setComment(commentText);

}
          try {
 int n = reader.getTextCharacters(0, charBuffer, length, textLength);
     float value = TextUtils.parseFloat(valueText);
    assert n == textLength;
     // we can not use the automatic missinglength value detection of the content handler because the missing value is different for each time series+= textLength;
            i = 0;
            iffor (value; ==; missingValue) {
                int valuestart = Float.NaNCharArrayUtils.indexOfNonWhitespace(charBuffer, i, length - i);
            } else {
  if (start == -1) {
          timeSeriesContentHandler.setValueResolution(TextUtils.getValueResolution(valueText, '.'));
         i =  }length;
            timeSeriesContentHandler.setValue(value);
        break;
    timeSeriesContentHandler.applyCurrentFields();
        } catch (NumberFormatException e) {}
            throw   new Exception("Value should be a float " + valueText);
int end = CharArrayUtils.indexOfWhitespace(charBuffer, start, length - start);
         }
       if reader.nextTag();
(end == -1) {
           reader.require(XMLStreamConstants.END_ELEMENT, null, "event");
         i = reader.nextTag()start;
    }

    private long parseTime() throws Exception {
       break;
 String dateText = reader.getAttributeValue(null, "date");
        if (dateText == null) {}
            throw new Exception("Attribute " +i currentHeaderElement= +end;
                parseValue(charBuffer, j++, start, end "-date start is+ missing"1);
        }
    }
    String timeText = reader.getAttributeValue(null, "time"); }

        if (timeTexti =!= nulllength) {
     parseValue(charBuffer, j++, i, length - i);
       throw new Exception("Attribute " + currentHeaderElement +return j;
    }

    private void parseValue(char[] charBuffer, int pos, int start, int length) {
      "-time is missing");
float value = (float) TextUtils.parseDouble(charBuffer, start,   }

length, '.');
        long time;
        try {if (value == missingValue) value = Float.NaN;
        float valueResolution   time = fastDateFormatTextUtils.parseToMillis(dateText, timeTextgetValueResolution(charBuffer, start, length, '.');
        }if catch(valueResolution (ParseException< eminValueResolution) {
minValueResolution = valueResolution;
        if  throw new Exception("Not a valid data time for "
(floatBuffer.length == pos) floatBuffer = Clasz.floats.ensureCapacity(floatBuffer, pos + 1);
        floatBuffer[pos] = value;
    }


    private void parseHeader() +throws currentHeaderElementException +{
 ' ' + dateText + ' ' +domainCount timeText,= e)0;
        }

        reader.nextTag(Arrays.fill(domainParameterIds, null);
        return timeArrays.fill(domainUnits, null);
     }

   if private(axisValues long!= parseTimeStep(null) throws Exception {
   Arrays.fill(axisValues, null);
     String unit = reader.getAttributeValue(require(XMLStreamConstants.START_ELEMENT, null, "unitheader");
        if (unit == nullreader.getAttributeCount() > 0) {
            throw new Exception("AttributeAttributes unitare isnot missingallowed infor "header +element currentHeaderElement");
        }

        TimeUnit tu = TimeUnit.get(unitreader.nextTag();
        if (tu != null) {
   initHeader();
        do {
         String multiplierText = reader.getAttributeValue(null, "multiplier"detectHeaderElement();
            int multiplierparseHeaderElement();
        }    if (multiplierText == null) {while (reader.getEventType() != XMLStreamConstants.END_ELEMENT);

        if (header.getForecastTime() ==      multiplier = 1Long.MIN_VALUE) header.setForecastTime(startTime);
        if (!Double.isNaN(lat)) header.setGeometry(GeoDatum.WGS_1984.createLatLongZ(lat,  } else {lon, z));
        initiateTimeStep();
        try {header.setTimeStep(timeStep);
        if (!qualifiers.isEmpty()) header.setQualifierIds(Clasz.strings.newArrayFrom(qualifiers));
        if (creationDateText multiplier != Integer.parseInt(multiplierText);null) {
                } catch (NumberFormatException e) try {
                long creationTime   throw new Exception(ExceptionUtils.getMessage(e), e= fastDateFormat.parseToMillis(creationDateText, creationTimeText);
                }

header.setCreationTime(creationTime);
            } catch   if (multiplier == 0ParseException e) {
                    throw new Exception("Multiplier is 0");
       Can not parse creation date/time " + creationDateText + ' ' + creationTimeText);
         }
    }
        }

        if (startTime !=  String dividerText = reader.getAttributeValue(null, "divider");
            int divider;Long.MIN_VALUE && endTime != Long.MIN_VALUE) {
            iftimeSeriesContentHandler.setEstimatedPeriod(new Period(dividerText == null) {startTime, endTime));
        }
        dividerdomainParameterIds = 1 Clasz.strings.resizeArray(domainParameterIds, domainCount);
        domainUnits = Clasz.strings.resizeArray(domainUnits, domainCount);
 } else {
     header.setDomainParameterIds(domainParameterIds);
        header.setDomainUnits(domainUnits);
   try {
    timeSeriesContentHandler.setNewTimeSeriesHeader(header);

        reader.require(XMLStreamConstants.END_ELEMENT, null, "header");
      divider = Integerreader.parseIntnextTag(dividerText);
    }

    @SuppressWarnings("OverlyLongMethod")
    private void parseEvent()  } catch (NumberFormatException e)throws Exception {
        assert binaryInputStream == null;
         throw new Exception(ExceptionUtils.getMessage(e), ereader.require(XMLStreamConstants.START_ELEMENT, null, "event");
         timeSeriesContentHandler.clearFlagSourceColumns();

        }

String dateText = null;
        String timeText = null;
     if (divider == 0)String {
startDateText = null;
        String startTimeText = null;
       throw new Exception("dividplier is 0")String endDateText = null;
        String endTimeText = null;
     }
   String valueText = null;
      }
  String valueSource = null;
       reader.nextTag();
 String minValueText = null;
        return tu.getMillis() * multiplier / divider;
String maxValueText = null;
        String }flagText else= {null;
        String flagSource   reader.nextTag()= null;
        String comment =  return 0null;
         }String user = null;
    }

    privateString voidlimitText initHeader() {
= null;

        for header.clear();
        header.setFileDescription(virtualFileName);
        currentHeaderElement = null;
(int i = 0, n = reader.getAttributeCount(); i < n; i++) {
            String timeSteplocalName = nullreader.getAttributeLocalName(i);
            timeStepMillisString attributeValue = 0reader.getAttributeValue(i);
        startTime = Long.MIN_VALUE;
  if (dateText == null &&  endTime = Long.MIN_VALUE;TextUtils.equals(localName, "date")) {
        missingValue = Float.NaN;
      dateText  creationDateText = nullattributeValue;
         creationTimeText = "00:00:00";
        qualfiers.clear(); } else if (timeText == null && TextUtils.equals(localName, "time")) {
    }

    private void readValuesFromBinFile() throws Exception {
   timeText = attributeValue;
   TimeStep timeStep = header.getTimeStep();
      } else if (!timeStep.isRegular(valueText == null && TextUtils.equals(localName, "value")) {
            throw new Exception("Only equidistant timevalueText step= supportedattributeValue;
 when pi events are stored in bin file instead of xml");
 } else if (valueSource == null  }

&& TextUtils.equals(localName, "valueSource")) {
         boolean equidistantMillis = timeStep.isEquidistantMillis();
    valueSource = attributeValue;
  long stepMillis = equidistantMillis ?  timeStep.getStepMillis() : Long.MIN_VALUE;
  } else if (flagText ==  trynull && TextUtils.equals(localName, "flag")) {
            for   (long timeflagText = startTimeattributeValue;
 time <= endTime;) {
        } else if (flagSource == null &&  timeSeriesContentHandlerTextUtils.setTime(time);equals(localName, "flagSource")) {
                ifflagSource (bufferPos == bufferCount) fillBuffer()= attributeValue;
            } else if  float value = floatBuffer[bufferPos++];
    (comment == null && TextUtils.equals(localName, "comment")) {
            // we can not usecomment the= automaticattributeValue;
 missing value detection of the content handler because the missing value is} differentelse forif each(user time== series
null && TextUtils.equals(localName, "user")) {
            if (value == missingValue) valueuser = Float.NaNattributeValue;
            } else  if timeSeriesContentHandler.setValue(value);
       (startDateText == null && TextUtils.equals(localName, "startDate")) {
         timeSeriesContentHandler.applyCurrentFields();
       startDateText = attributeValue;
       if (equidistantMillis) {
   } else if (startTimeText == null && TextUtils.equals(localName, "startTime")) {
        time += stepMillis;
      startTimeText = attributeValue;
            continue;
} else if (endDateText == null && TextUtils.equals(localName,         }"endDate")) {
                timeendDateText = timeStep.nextTime(time)attributeValue;
            }
 else if (endTimeText == null   } catch (IOException e&& TextUtils.equals(localName, "endTime")) {
            throw    new Exception(ExceptionUtils.getMessage(e), e)endTimeText = attributeValue;
        }
    }

 else if (minValueText == privatenull void fillBuffer() throws IOException {
&& TextUtils.equals(localName, "minValue")) {
               int byteBufferCountminValueText = 0attributeValue;
        while (byteBufferCount % NumberType.FLOAT_SIZE != 0 || byteBufferCount } else if (maxValueText == 0null && TextUtils.equals(localName, "maxValue")) {
            int count = binaryInputStream.read(byteBuffer, byteBufferCount, BUFFER_SIZE * NumberType.FLOAT_SIZE - byteBufferCount) maxValueText = attributeValue;
            assert count != 0; // see read javadoc
} else if (maxValueText == null && TextUtils.equals(localName, "detection")) {
               if (countlimitText == -1) throw new EOFException("Bin file is too short");
 attributeValue;
            } else if (reader.getAttributePrefix(i) != null  byteBufferCount += count;&&  TextUtils.equals(reader.getAttributePrefix(i), "fs")) {
        }
        bufferCountint index = byteBufferCount / NumberType.FLOAT_SIZEtimeSeriesContentHandler.addFlagSourceColumn(localName);
        BinaryUtils.copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIAN  timeSeriesContentHandler.setColumnFlagSource(index, attributeValue);
        bufferPos = 0;
  } else }{

     private void initiateTimeStep() {
        timeStepif = IrregularTimeStep.INSTANCE; //default timestep

(lenient) continue;
          if (timeStepMillis == 0) {
  throw new Exception("Unknown or duplicate attribute " + localName  return+ " in event");
        }
    }
    if (timeStepMillis % TimeUnit.MINUTE_MILLIS != 0) {}

        parseTime(dateText, timeText, startDateText, startTimeText, if (!this.invalidHeaderTimeDetected) {endDateText, endTimeText);
        parseFlagsUserComment(flagText, flagSource, comment, user);
     if   (log.isDebugEnabled()) log.debug("Header timestep and/or start time has not rounded  minutes ! Irregular timestep wil be used."parseValue(valueText, valueSource, minValueText, maxValueText, limitText);
        timeSeriesContentHandler.applyCurrentFields();
        reader.require(XMLStreamConstants.END_ELEMENT, null, "event");
        reader.nextTag();
    }

    this.invalidHeaderTimeDetectedprivate = true;
            }void parseValue(String valueText, String valueSource, String minValueText, String maxValueText, String limit) throws Exception {
        if    timeStepMillis (domainCount == 0;) {
            try return;{
        }

        long timeZoneOffsetMillisfloat value = valueText = -startTime % timeStepMillis;
  = null ? Float.NaN : TextUtils.parseFloat(valueText);
      if (timeZoneOffsetMillis % TimeUnit.MINUTE_MILLIS != 0) {
    float minValue = minValueText == null ? value if: (!this.invalidHeaderTimeDetected) {TextUtils.parseFloat(minValueText);
                if (log.isDebugEnabled()) log.debug("Header timestep and/or start time has not rounded  minutes ! Irregular timestep wil be used.");
float maxValue = maxValueText == null ? value : TextUtils.parseFloat(maxValueText);
                // we can not use the automatic missing this.invalidHeaderTimeDetectedvalue =detection true;
of the content handler because the missing value is different for each time }series
            timeStepMillis = 0;
  if (value == missingValue) {
      return;
        }
      value  timeStep = SimpleEquidistantTimeStep.getInstance(timeStepMillis, timeZoneOffsetMillis);
Float.NaN;
         }

    private void parseTimeZone() throws} Exceptionelse {
        if (reader.getEventType() != XMLStreamConstants.START_ELEMENT) return;
        if timeSeriesContentHandler.setValueResolution(!TextUtils.equals(reader.getLocalName(), "timeZone")) return;
getValueResolution(valueText, '.'));
               try {}
             double offset = DoubletimeSeriesContentHandler.parseDouble(reader.getElementText()setValueAndRange(value, minValue, maxValue);
             TimeZone timeZoneFromDouble = TimeZoneUtils.createTimeZoneFromDouble(offsettimeSeriesContentHandler.setValueSource(TextUtils.equals(valueSource, "MAN") ? ValueSource.MANUAL : ValueSource.AUTOMATIC);
                thistimeSeriesContentHandler.fastDateFormat.setTimeZone(timeZoneFromDoublesetOutOfDetectionRangeFlag(OutOfDetectionRangeFlag.get(getOutOfDetectionFlag(limit)));

            } catch (NumberFormatException e) {
                throw new Exception("Not valid timeZone format", eValue should be a float " + valueText);
            }
            reader.require(XMLStreamConstants.END_ELEMENT, null, "timeZone"nextTag();
            reader.nextTag()return;
    }

    @SuppressWarnings({"OverlyLongMethod"})}

    private void parseHeaderElement() throws Exceptionif {
(valueText        switch (currentHeaderElement) {!= null)
            casethrow type:
                header.setParameterType(parseType(reader.getElementText())new Exception("Attribute value not allowed when having domain parameters, use event element text instead");
        int count       break= parseValueList();
        if (count ==  case locationId:0) {
                header.setLocationId(reader.getElementText()timeSeriesContentHandler.setValues(null);
            return;
    break;
    }

        caseint parameterId:
expectedCount = 1;
        for (int i = 0,  header.setParameterId(reader.getElementText());n = domainCount; i < n; i++) {
            float[] axis   break= axisValues[i];

            case qualifierId:
  if (axis == null)
              qualfiers.add(reader.getElementText());
  throw new Exception("Domain axis values for domain parameter " + domainParameterIds[i] + " are breakmissing");

            expectedCount case ensembleId:*= axis.length;
        }

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

     case ensembleMemberIndex:
  if (axesDirty) {
            headertimeSeriesContentHandler.setEnsembleMemberIndex(parseEnsembleMemberIndex(reader.getElementText()))setDomainAxesValueResolutions(axisValueResolutions);
            timeSeriesContentHandler.setDomainAxesValues(axisValues);
    break;
        axesDirty = false;
  case timeStep:
     }

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

    private byte getOutOfDetectionFlag(String  case startDate:text) {
        if (text == null || text.isEmpty())   startTime = parseTime()return TimeSeriesArray.INSIDE_DETECTION_RANGE;
        char ch = text.charAt(0);
      break;
  if (ch == '<') return TimeSeriesArray.BELOW_DETECTION_RANGE;
     case endDate:
  if (ch == '>')  return TimeSeriesArray.ABOVE_DETECTION_RANGE;
        endTimeif (ch == parseTime()'~') return TimeSeriesArray.VARYING;
        return TimeSeriesArray.INSIDE_DETECTION_RANGE;
    }

   break;
 private void parseFlagsUserComment(String flagText, String flagSource, String comment, String user) throws caseException forecastDate:{
        if (flagText == null) {
    header.setForecastTime(parseTime());
        timeSeriesContentHandler.setFlag(0);
        break;} else {
            casetry missVal:{
                missingValue = parseMissingValue(reader.getElementText(timeSeriesContentHandler.setFlag(TextUtils.parseInt(flagText));
            } catch (NumberFormatException e) break;{
            case longName:
   throw new Exception("Flag should be an integer "      header.setLongName(reader.getElementText()+ flagText);
            }
    break;
    }

        case stationName:timeSeriesContentHandler.setComment(comment);
        timeSeriesContentHandler.setUser(user);
        headertimeSeriesContentHandler.setLocationName(reader.getElementText()setFlagSource(flagSource);
    }

    private void parseTime(String dateText, String timeText, String startDateText, break;
String startTimeText, String endDateText, String endTimeText) throws      case units:Exception {
        if (timeText       header.setUnit(reader.getElementText());== null)
            throw new Exception("Attribute time is breakmissing");

        if (dateText == null)
 case sourceOrganisation:
          throw new Exception("Attribute date   header.setSourceOrganisation(reader.getElementText()is missing");

        try {
        break;
    long time = parseTime(dateText, timeText, null, null,  case sourceSystem:Long.MIN_VALUE, lastTime, lastTimeAmbiguous);
            lastTime    header.setSourceSystem(reader.getElementText())= time;
            lastTimeAmbiguous =   breaktimeAmbiguous;
            caselong fileDescription:
startTime = parseTime(startDateText, startTimeText, dateText, timeText, time,          header.setFileDescription(reader.getElementText()lastStartTime, lastStartTimeAmbiguous);
            lastStartTime =   breakstartTime;
            lastStartTimeAmbiguous case= creationDate:timeAmbiguous;
            long endTime =  creationDateText = reader.getElementText(parseTime(endDateText, endTimeText, dateText, dateText, time, lastEndTime, lastEndTimeAmbiguous);
            lastEndTime =   breakendTime;
            lastEndTimeAmbiguous case= creationTime:timeAmbiguous;
            timeSeriesContentHandler.setTimeAndRange(time,    creationTimeText = reader.getElementText(startTime, endTime);
        } catch (ParseException e) {
    break;
        throw new Exception("Can not caseparse region:
" + dateText + ' ' + timeText);
         header.setRegion(reader.getElementText());}
    }

    private long parseTime(String dateText, String timeText, String defaultDateText, break;
String defaultTimeText, long defaultTime, long lastTime, boolean lastTimeAmbiguous) throws ParseException {
  case thresholds:
     if (dateText == null && timeText == null) return   parseThresholds()defaultTime;
        if (dateText == null) dateText =   breakdefaultDateText;
        }
if (timeText == null) timeText = defaultTimeText;

  reader.require(XMLStreamConstants.END_ELEMENT, null, currentHeaderElement.name());
    boolean useMillis =  readertimeText.nextTagcontains(".");

    }

    private void parseThresholds() throws XMLStreamException {
        reader.nextTag(if (!fastDateFormat.getTimeZone().useDaylightTime()) return useMillis? fastDateFormatWithMillies.parseToMillis(dateText, timeText): fastDateFormat.parseToMillis(dateText, timeText);
        ArrayList<String> ids = new ArrayList<String>()long t1;
        ArrayList<String>try names{
 = new ArrayList<String>();
        ArrayList<String> stringValuest1 = new ArrayList<String>( useMillis?fastDateFormatWithMillies.parseToMillis(dateText, timeText): fastDateFormat.parseToMillis(dateText, timeText);
        do} catch (ParseException e) {
            if (reader!timeText.getEventType(equals("02:00:00")) == XMLStreamConstants.START_ELEMENT) {throw e;
            // see FEWS-17782 also String id = reader.getAttributeValue(null, "id");
  accept 02:00:00 on daylight saving time switch
              String name = reader.getAttributeValue(null, "name");try {
                String stringValue = reader.getAttributeValue(nullreturn fastDateFormat.parseToMillis(dateText, "value03:00:00");
            } catch (ParseException  ids.add(id);
e1) {
                 names.add(name);throw e;
                stringValues.add(stringValue);}
        }
    }
    Calendar calendar = useMillis? fastDateFormatWithMillies.getCalendar() :   readerfastDateFormat.nextTaggetCalendar();
        } while (!reader.getLocalName().equals(currentHeaderElement.name()))calendar.setTimeInMillis(t1);
        float[]int valuestimeOfDay = new float[stringValues.size()]getTimeOfDay(calendar);
        for (int ilong t2 = 0; i < values.length; i++) {getOtherTime(calendar, t1, timeOfDay);
        timeAmbiguous =   values[i] = Float.valueOf(stringValues.get(i))t1 != t2;
        }
long minTime = Math.min(t1, t2);
    header.setHighLevelThresholds(ids.toArray(new String[ids.size()]), names.toArray(new String[names.size()]), values);
    }

    private static float parseMissingValue(String gotString) throws Exception {    long maxTime = Math.max(t1, t2);
        long res = minTime > lastTime ? minTime : maxTime;
        if (!lastTimeAmbiguous) return res;
        // see FEWS-17782 also accept two times 02:00:00 instead of two times 01:00:00  on daylight saving time switch
        if (timeStepMillis != 0L && lastTime + timeStepMillis != res) return lastTime + timeStepMillis;
        if (timeStep != null && timeStep.isRegular() && timeStep.nextTime(lastTime) != res) return timeStep.nextTime(lastTime);
        return res;
    }

    private static int getTimeOfDay(Calendar calendar) {
        return (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));
    }

    private static long getOtherTime(Calendar calendar, long time, int timeOfDay) {
        try {
            calendar.setTimeInMillis(time + TimeUnit.HOUR_MILLIS);
            if (getTimeOfDay(calendar) == timeOfDay) return time + TimeUnit.HOUR_MILLIS;
            calendar.setTimeInMillis(time - TimeUnit.HOUR_MILLIS);
            if (getTimeOfDay(calendar) == timeOfDay) return time - TimeUnit.HOUR_MILLIS;
            return time;
        } finally {
            calendar.setTimeInMillis(time);
        }
    }

    private long parseTime() throws Exception {
        String dateText = reader.getAttributeValue(null, "date");
        if (dateText == null) {
            throw new Exception("Attribute " + currentHeaderElement +
                    "-date is missing");
        }
        String timeText = reader.getAttributeValue(null, "time");
        if (timeText == null) {
            throw new Exception("Attribute " + currentHeaderElement +
                    "-time is missing");
        }
        boolean useMillis = timeText.contains(".");
        long time;
        try {
            time = useMillis? fastDateFormatWithMillies.parseToMillis(dateText, timeText) : fastDateFormat.parseToMillis(dateText, timeText);
        } catch (ParseException e) {
            throw new Exception("Not a valid data time for "
                    + currentHeaderElement + ' ' + dateText + ' ' + timeText, e);
        }

        reader.nextTag();
        return time;
    }

    private void parseTimeStep() throws Exception {
        String times = reader.getAttributeValue(null, "times");

        if (times != null) {
            timeStep = PiCastorUtils.createTimesOfDayTimeStep(times, getTimeZone());
            reader.nextTag();
            return;
        }

        String unit = reader.getAttributeValue(null, "unit");
        TimeUnit tu = unit == null ? null : TimeUnit.get(unit);
        if (tu != null) {
            String multiplierText = reader.getAttributeValue(null, "multiplier");
            int multiplier;
            if (multiplierText == null) {
                multiplier = 1;
            } else {
                try {
                    multiplier = Integer.parseInt(multiplierText);
                } catch (NumberFormatException e) {
                    throw new Exception(ExceptionUtils.getMessage(e), e);
                }

                if (multiplier == 0) {
                    throw new Exception("Multiplier is 0");
                }
            }

            String dividerText = reader.getAttributeValue(null, "divider");
            int divider;
            if (dividerText == null) {
                divider = 1;
            } else {
                try {
                    divider = Integer.parseInt(dividerText);
                } catch (NumberFormatException e) {
                    throw new Exception(ExceptionUtils.getMessage(e), e);
                }

                if (divider == 0) {
                    throw new Exception("divider is 0");
                }
            }
            reader.nextTag();
            timeStepMillis =  tu.getMillis() * multiplier / divider;
            timeStep = null;
        } else {
            reader.nextTag();
            timeStepMillis = 0;
            timeStep = IrregularTimeStep.INSTANCE;
        }
    }

    private void initHeader() {
        header.clear();
        header.setFileDescription(virtualFileName);
        currentHeaderElement = null;
        timeStep = null;
        timeStepMillis = 0;
        startTime = Long.MIN_VALUE;
        endTime = Long.MIN_VALUE;
        missingValue = Float.NaN;
        creationDateText = null;
        creationTimeText = "00:00:00";
        qualifiers.clear();
        lat = Double.NaN;
        lon = Double.NaN;
        z = Double.NaN;
    }

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

        boolean equidistantMillis = timeStep.isEquidistantMillis();
        long stepMillis = equidistantMillis ?  timeStep.getStepMillis() : Long.MIN_VALUE;
        for (long time = startTime; time <= endTime;) {
            timeSeriesContentHandler.setTime(time);
            if (bufferPos == bufferCount) fillBuffer();
            float 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.setValue(value);
            timeSeriesContentHandler.applyCurrentFields();
            if (equidistantMillis) {
                time += stepMillis;
                continue;
            }
            time = timeStep.nextTime(time);
        }
    }

    private void fillBuffer() throws IOException {
        int byteBufferCount = 0;
        while (byteBufferCount % NumberType.FLOAT_SIZE != 0 || byteBufferCount == 0) {
            int count = binaryInputStream.read(byteBuffer, byteBufferCount, BUFFER_SIZE * NumberType.FLOAT_SIZE - byteBufferCount);
            assert count != 0; // see read javadoc
            if (count == -1) throw new EOFException("Bin file is too short");
            byteBufferCount += count;
        }
        bufferCount = byteBufferCount / NumberType.FLOAT_SIZE;
        BinaryUtils.copy(byteBuffer, 0, byteBufferCount, floatBuffer, 0, bufferCount, ByteOrder.LITTLE_ENDIAN);
        bufferPos = 0;
    }

    private void initiateTimeStep() {
        if (timeStep != null) {
            assert timeStepMillis == 0;
            return;
        }
        if (timeStepMillis == 0){
            //no timestep in header. Fix for backward compatibility
            timeStep = IrregularTimeStep.INSTANCE;
            return;
        }
        if (timeStepMillis >= TimeUnit.HOUR_MILLIS && getTimeZone().useDaylightTime()) {
            timeStep = IrregularTimeStep.INSTANCE;
            return;
        }

        long startTime = this.startTime ==  Long.MIN_VALUE ? 0L : this.startTime;
        if (timeStepMillis % TimeUnit.SECOND_MILLIS != 0) {
            timeStep = RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);
            return;
        }

        long timeZoneOffsetMillis = -startTime % timeStepMillis;
        if (timeZoneOffsetMillis % TimeUnit.MINUTE_MILLIS != 0) {
            timeStep = RelativeEquidistantTimeStep.getInstance(timeStepMillis, startTime);
            return;
        }

        timeStep = SimpleEquidistantTimeStep.getInstance(timeStepMillis, timeZoneOffsetMillis);
    }

    @SuppressWarnings({"OverlyLongMethod"})
    private void parseHeaderElement() throws Exception {
        switch (currentHeaderElement) {
            case type:
                header.setParameterType(parseType(reader.getElementText()));
                break;
            case moduleInstanceId:
                header.setModuleInstanceId(reader.getElementText());
                break;
            case locationId:
                // see FEWS-9858, when there is no location id the time series are assigned to all locations
                // this is a flaw in the pi_timeSeries.xsd, the location element is required but is allowed empty strings
                header.setLocationId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementText()), "none"));
                break;
            case parameterId:
                // see FEWS-9858, when there is no parameter id the time series are assigned to all locations
                // this is a flaw in the pi_timeSeries.xsd, the location element is required but is allowed empty strings
                header.setParameterId(TextUtils.defaultIfNull(TextUtils.trimToNull(reader.getElementText()), "none"));
                break;
            case qualifierId:
                qualifiers.add(reader.getElementText());
                break;
            case ensembleId:
                header.setEnsembleId(reader.getElementText());
                break;
            case ensembleMemberIndex:
                header.setEnsembleMemberIndex(parseEnsembleMemberIndex(reader.getElementText()));
                break;
            case ensembleMemberId:
                header.setEnsembleMemberId(reader.getElementText());
                break;
            case timeStep:
                parseTimeStep();
                break;
            case startDate:
                startTime = parseTime();
                break;
            case endDate:
                endTime = parseTime();
                break;
            case forecastDate:
                header.setForecastTime(parseTime());
                break;
            case approvedDate:
                header.setApprovedTime(parseTime());
                break;
            case missVal:
                missingValue = parseString(reader.getElementText());
                break;
            case longName:
                header.setLongName(reader.getElementText());
                break;
            case stationName:
                header.setLocationName(reader.getElementText());
                break;
            case units:
                header.setUnit(reader.getElementText());
                break;
            case domainAxis:
                parseDomainAxis();
                break;
            case sourceOrganisation:
                header.setSourceOrganisation(reader.getElementText());
                break;
            case sourceSystem:
                header.setSourceSystem(reader.getElementText());
                break;
            case fileDescription:
                header.setFileDescription(reader.getElementText());
                break;
            case creationDate:
                creationDateText = reader.getElementText();
                break;
            case creationTime:
                creationTimeText = reader.getElementText();
                break;
            case region:
                header.setRegion(reader.getElementText());
                break;
            case thresholds:
                parseThresholds();
                break;
            case lat:
                lat = parseString(reader.getElementText());
                break;
            case lon:
                lon = parseString(reader.getElementText());
                break;
            case x:
                reader.getElementText();
                break;
            case y:
                reader.getElementText();
                break;
            case z:
                z = parseString(reader.getElementText());
                break;
            case firstValueTime:
                reader.getElementText();
                break;
            case lastValueTime:
                reader.getElementText();
                break;
            case maxValue:
                reader.getElementText();
                break;
            case minValue:
                reader.getElementText();
                break;
            case valueCount:
                reader.getElementText();
                break;
            case maxWarningLevelName:
                reader.getElementText();
                break;

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

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

        domainParameterIds = Clasz.strings.ensureCapacity(domainParameterIds, domainCount + 1);
        domainUnits = Clasz.strings.ensureCapacity(domainUnits, domainCount + 1);
        domainParameterIds[domainCount] = parameterId;
        domainUnits[domainCount] = reader.getAttributeValue(null, "units");
        domainCount++;
        reader.nextTag();
    }

    private void parseThresholds() throws XMLStreamException {
        reader.nextTag();
        ArrayList<DefaultTimeSeriesHeader.DefaultThreshold> thresholds = new ArrayList<>();
        do {
            if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
                String id = reader.getAttributeValue(null, "id");
                String name = reader.getAttributeValue(null, "name");
                float value = TextUtils.parseFloat(reader.getAttributeValue(null, "value"));
                String groupId = reader.getAttributeValue(null, "groupId");
                String groupName = reader.getAttributeValue(null, "groupName");
                String[] groupIds = groupId == null ? Clasz.strings.emptyArray() : new String[] {groupId};
                String[] groupNames = groupName == null ? Clasz.strings.emptyArray() : new String[] {groupName};
                thresholds.add(new DefaultTimeSeriesHeader.DefaultThreshold(id, name, value, groupIds, groupNames));
            }
            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":
                    try {
                        if (time.contains(".")) {
                            propertyBuilder.addDateTime(key, fastDateFormatWithMillies.parseToMillis(date, time));
                        } else {
                            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 parseString(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);
            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 " + 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;
    }
}