uk.ac.ucl.excites.sapelli.storage.model.columns.TimeStampColumn.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ucl.excites.sapelli.storage.model.columns.TimeStampColumn.java

Source

/**
 * Sapelli data collection platform: http://sapelli.org
 * 
 * Copyright 2012-2014 University College London - ExCiteS group
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package uk.ac.ucl.excites.sapelli.storage.model.columns;

import java.io.IOException;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;

import uk.ac.ucl.excites.sapelli.shared.io.BitInputStream;
import uk.ac.ucl.excites.sapelli.shared.io.BitOutputStream;
import uk.ac.ucl.excites.sapelli.shared.util.IntegerRangeMapping;
import uk.ac.ucl.excites.sapelli.shared.util.TimeUtils;
import uk.ac.ucl.excites.sapelli.storage.model.Column;
import uk.ac.ucl.excites.sapelli.storage.model.ComparableColumn;
import uk.ac.ucl.excites.sapelli.storage.model.VirtualColumn;
import uk.ac.ucl.excites.sapelli.storage.types.TimeStamp;
import uk.ac.ucl.excites.sapelli.storage.visitors.ColumnVisitor;

/**
 * A column to store TimeStamps.
 * Supports various ways to configure precision and (hence) size.
 * 
 * @author mstevens
 */
public class TimeStampColumn extends ComparableColumn<TimeStamp> {

    // STATICS-------------------------------------------------------
    static private final long serialVersionUID = 2L;

    static private final int TIMEZONE_QH_OFFSET_SIZE = 7; //bits

    static public final String LOCAL_PRETTY_VIRTUAL_COLUMN_NAME = "LocalYYYYMMDD_HHMMSS";
    static public final String UTC_OFFSET_VIRTUAL_COLUMN_NAME = "UCTOffsetH";
    static public final String RAW_TIMESTAMP_VIRTUAL_COLUMN_NAME = "UnixMS";

    /**
     * Returns a TimeStampColumn that can hold Java-style timestamps, i.e. signed 64 bit integers representing
     * the number of milliseconds since (or to, if negative) the Java/Unix epoch of 1970/01/01 00:00:00 UTC.
     * All times are stored as UTC (local timezone is NOT kept).
     * 
     * @param name
     * @param optional
     * @return
     */
    static public TimeStampColumn JavaMSTime(String name, boolean optional, boolean addVirtuals) {
        return new TimeStampColumn(name, new TimeStamp(Long.MIN_VALUE), 64 /*bits*/, true, false, true, optional,
                addVirtuals);
    }

    /**
     * Returns a TimeStampColumn that can hold Java-style timestamps, i.e. signed 64 bit integers representing
     * the number of milliseconds since (or to, if negative) the Java/Unix epoch of 1970/01/01 00:00:00 UTC.
     * Local timezone IS kept.
     * 
     * @param name
     * @param optional
     * @return
     */
    static public TimeStampColumn JavaMSLocalTime(String name, boolean optional, boolean addVirtuals) {
        return new TimeStampColumn(name, new TimeStamp(Long.MIN_VALUE), 64 /*bits*/, true, true, true, optional,
                addVirtuals);
    }

    /**
     * Returns a TimeStampColumn that can hold any millisecond-accurate timestamp in the 21st century, including a local timezone reference.
     * Takes up 49 bits.
     * 
     * @param name
     * @param optional
     * @return
     */
    static public TimeStampColumn Century21(String name, boolean optional, boolean addVirtuals) {
        return new TimeStampColumn(name, new TimeStamp(new DateTime(2000, 01, 01, 00, 00, 00, DateTimeZone.UTC)),
                new TimeStamp(new DateTime(2100, 01, 01, 00, 00, 00, DateTimeZone.UTC)), true, true, false,
                optional, addVirtuals);
    }

    /**
     * Returns a TimeStampColumn that can hold any second-accurate timestamp in the 21st century, including a local timezone reference
     * Takes up 39 bits.
     * 
     * @param name
     * @param optional
     * @return
     */
    static public TimeStampColumn Century21NoMS(String name, boolean optional, boolean addVirtuals) {
        return new TimeStampColumn(name, new TimeStamp(new DateTime(2000, 01, 01, 00, 00, 00, DateTimeZone.UTC)),
                new TimeStamp(new DateTime(2100, 01, 01, 00, 00, 00, DateTimeZone.UTC)), false, true, false,
                optional, addVirtuals);
    }

    /**
     * Returns a TimeStampColumn that only needs 30 bits.
     * This is achieved by using second-level accurate (instead of millisecond-level), by not storing the
     * local timezone, and by limiting the value range to a 34 year window starting on 2008/01/01
     * (taken because the first Android device was released in 2008).
     * 
     * @param name
     * @param optional
     * @return
     */
    static public TimeStampColumn Compact(String name, boolean optional, boolean addVirtuals) {
        return new TimeStampColumn(name, new TimeStamp(new DateTime(2008, 01, 01, 00, 00, 00, DateTimeZone.UTC)),
                30 /*bits*/, false, false, false, optional, addVirtuals);
    }

    // DYNAMICS------------------------------------------------------
    protected IntegerRangeMapping timeMapping;
    protected boolean keepMS;
    protected boolean keepLocalTimezone;
    protected boolean strict;

    /**
     * @param name
     * @param lowBound earliest allowed DateTime (inclusive)
     * @param highBound latest allowed DateTime (exclusive)
     * @param keepMS whether to use millisecond-level (true) or second-level (false) accuracy
     * @param keepLocalTimezone whether or not to remember to local timezone
     * @param strictHighBound whether highBound date should be strictly respected (true) or not (false; meaning that the column will accept any TimeStamp that fits in the allocated number of bits)
     * @param optional
     */
    public TimeStampColumn(String name, TimeStamp lowBound, TimeStamp highBound, boolean keepMS,
            boolean keepLocalTimezone, boolean strictHighBound, boolean optional, boolean addVirtuals) {
        this(name,
                new IntegerRangeMapping(Math.round(lowBound.getMsSinceEpoch() / (keepMS ? 1 : 1000d)),
                        Math.round(highBound.getMsSinceEpoch() / (keepMS ? 1 : 1000d))),
                keepMS, keepLocalTimezone, strictHighBound, optional, addVirtuals);
    }

    /**
     * @param name
     * @param lowBound earliest allowed DateTime (inclusive)
     * @param the number of bits to use
     * @param keepMS whether to use millisecond-level (true) or second-level (false) accuracy
     * @param keepLocalTimezone whether or not to remember to local timezone
     * @param strictHighBound whether highBound date should be strictly respected (true) or not (false; meaning that the column will accept any DateTime that fits in the allocated number of bits)
     * @param optional
     */
    public TimeStampColumn(String name, TimeStamp lowBound, int sizeBits, boolean keepMS, boolean keepLocalTimezone,
            boolean strictHighBound, boolean optional, boolean addVirtuals) {
        this(name,
                IntegerRangeMapping.ForSize(Math.round(lowBound.getMsSinceEpoch() / (keepMS ? 1 : 1000d)),
                        sizeBits - (keepLocalTimezone ? TIMEZONE_QH_OFFSET_SIZE : 0)),
                keepMS, keepLocalTimezone, strictHighBound, optional, addVirtuals);
    }

    private TimeStampColumn(String name, IntegerRangeMapping timeMapping, boolean keepMS, boolean keepLocalTimezone,
            boolean strictHighBound, boolean optional, boolean addVirtuals) {
        super(name, optional);
        this.timeMapping = timeMapping;
        this.keepMS = keepMS;
        this.keepLocalTimezone = keepLocalTimezone;
        this.strict = strictHighBound;

        if (addVirtuals) {
            // Add virtual version with second-accurate local(!) time in "pretty ISO" format ("yyyy-MM-dd HH:mm:ss"), which should be correctly interpreted by (most) Excel installations.
            this.addVirtualVersion(StringColumn.ForCharacterCount(LOCAL_PRETTY_VIRTUAL_COLUMN_NAME, optional, 19),
                    new VirtualColumn.ValueMapper<String, TimeStamp>() {
                        private static final long serialVersionUID = 2L;

                        @Override
                        public String mapValue(TimeStamp nonNullValue) {
                            return TimeUtils.PrettyTimestampWithoutMSFormatter.print(nonNullValue.toDateTime());
                        }

                        @Override
                        public int hashCode() {
                            return LOCAL_PRETTY_VIRTUAL_COLUMN_NAME.hashCode();
                        }
                    });

            // Add virtual version with offset in number of hours (signed float) of the local timezone w.r.t. UTC:
            this.addVirtualVersion(new FloatColumn(UTC_OFFSET_VIRTUAL_COLUMN_NAME, optional, true, false),
                    new VirtualColumn.ValueMapper<Double, TimeStamp>() {
                        private static final long serialVersionUID = 2L;

                        @Override
                        public Double mapValue(TimeStamp nonNullValue) {
                            return Double.valueOf(nonNullValue.getHourOffsetWrtUTC());
                        }

                        @Override
                        public int hashCode() {
                            return UTC_OFFSET_VIRTUAL_COLUMN_NAME.hashCode();
                        }
                    });

            // Add virtual version with raw millisecond timestamp (= elapsed ms since the UNIX/Java epoch of 1970-01-01T00:00:00Z; 'Z' meaning UTC)
            this.addVirtualVersion(new IntegerColumn(RAW_TIMESTAMP_VIRTUAL_COLUMN_NAME, optional, true, Long.SIZE),
                    new VirtualColumn.ValueMapper<Long, TimeStamp>() {
                        private static final long serialVersionUID = 2L;

                        @Override
                        public Long mapValue(TimeStamp nonNullValue) {
                            return nonNullValue.getMsSinceEpoch();
                        }

                        @Override
                        public int hashCode() {
                            return RAW_TIMESTAMP_VIRTUAL_COLUMN_NAME.hashCode();
                        }
                    });
        }
    }

    @Override
    public TimeStampColumn copy() {
        TimeStampColumn cpy = new TimeStampColumn(name, timeMapping, keepMS, keepLocalTimezone, strict, optional,
                false);
        for (VirtualColumn<?, TimeStamp> v : getVirtualVersions())
            cpy.addVirtualVersion(v.copy());
        return cpy;
    }

    @Override
    public TimeStamp parse(String value) throws IllegalArgumentException {
        try {
            return parse(value, TimeUtils.ISOWithMSFormatter);
        } catch (IllegalArgumentException iae) {
            return parse(value, TimeUtils.ISOWithoutMSFormatter); //for compatibility with old XML exports which used ISO without milliseconds when keepMS=false
        }
    }

    protected TimeStamp parse(String value, DateTimeFormatter formatter) throws IllegalArgumentException {
        return new TimeStamp(formatter.withOffsetParsed().parseDateTime(value)); // always parse UTC offset (even when keepLocalTimezone=false, that only affects binary storage)
    }

    @Override
    public String toString(TimeStamp value) {
        DateTime dt = value.toDateTime();
        // Note: we always keep milliseconds & UTC offset (keepMS & keepLocalTimeZone only affect binary storage)
        return TimeUtils.ISOWithMSFormatter.withZone(dt.getZone()).print(dt);
    }

    @Override
    protected void write(TimeStamp value, BitOutputStream bitStream) throws IOException {
        timeMapping.write(Math.round(value.getMsSinceEpoch() / (keepMS ? 1 : 1000d)), bitStream);
        if (keepLocalTimezone)
            bitStream.write(value.getQuarterHourOffsetWrtUTC(), TIMEZONE_QH_OFFSET_SIZE, true);
    }

    /**
     * Note: when local time zone is not kept TimeStamps that are read from binary input will be in UTC.
     * 
     * @see uk.ac.ucl.excites.sapelli.storage.model.Column#read(uk.ac.ucl.excites.sapelli.shared.io.BitInputStream)
     */
    @Override
    protected TimeStamp read(BitInputStream bitStream) throws IOException {
        long msSinceJavaEpoch = timeMapping.readLong(bitStream) * (keepMS ? 1 : 1000);
        return new TimeStamp(msSinceJavaEpoch,
                keepLocalTimezone
                        ? TimeStamp.getDateTimeZoneFor((int) bitStream.readInteger(TIMEZONE_QH_OFFSET_SIZE, true))
                        : DateTimeZone.UTC);
    }

    @Override
    protected void validate(TimeStamp value) throws IllegalArgumentException {
        DateTimeFormatter formatter = TimeUtils.ISOWithMSFormatter;
        if (!timeMapping.inRange(Math.round(value.getMsSinceEpoch() / (keepMS ? 1 : 1000d)), strict))
            throw new IllegalArgumentException(
                    "The given DateTime (" + value.format(formatter) + ") is outside the allowed range (from "
                            + getLowBound().format(formatter) + " to " + getHighBound().format(formatter) + ").");
    }

    @Override
    protected int _getMinimumSize() {
        return timeMapping.size() + (keepLocalTimezone ? TIMEZONE_QH_OFFSET_SIZE : 0);
    }

    @Override
    protected int _getMaximumSize() {
        return _getMinimumSize(); //size is fixed
    }

    public TimeStamp getLowBound() {
        return new TimeStamp(timeMapping.lowBound().longValue() * (keepMS ? 1 : 1000));
    }

    public TimeStamp getHighBound() {
        return new TimeStamp(timeMapping.highBound(strict).longValue() * (keepMS ? 1 : 1000));
    }

    @Override
    protected boolean equalRestrictions(Column<TimeStamp> otherColumn) {
        if (otherColumn instanceof TimeStampColumn) {
            TimeStampColumn other = (TimeStampColumn) otherColumn;
            return this.timeMapping.equals(other.timeMapping) && this.keepLocalTimezone == other.keepLocalTimezone
                    && this.keepMS == other.keepMS && this.strict == other.strict;
        } else
            return false;
    }

    @Override
    protected TimeStamp copy(TimeStamp value) {
        return new TimeStamp(value);
    }

    @Override
    public void accept(ColumnVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    protected int compareNonNullValues(TimeStamp lhs, TimeStamp rhs) {
        return lhs.compareTo(rhs);
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 31 * hash + (timeMapping == null ? 0 : timeMapping.hashCode());
        hash = 31 * hash + (keepMS ? 0 : 1);
        hash = 31 * hash + (keepLocalTimezone ? 0 : 1);
        hash = 31 * hash + (strict ? 0 : 1);
        return hash;
    }

    @Override
    public Class<TimeStamp> getType() {
        return TimeStamp.class;
    }

}