diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java b/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java new file mode 100644 index 000000000..cf9e55ee4 --- /dev/null +++ b/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java @@ -0,0 +1,38 @@ +// +// MessagePack for Java +// +// 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 org.msgpack.core; + +/** + * Thrown when format of an extension type is invalid + */ +public class MessageExtensionFormatException + extends MessageFormatException +{ + public MessageExtensionFormatException(Throwable e) + { + super(e); + } + + public MessageExtensionFormatException(String message) + { + super(message); + } + + public MessageExtensionFormatException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java index ed8b1e405..edd449b34 100644 --- a/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java +++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java @@ -165,6 +165,8 @@ public static final boolean isFixedRaw(byte b) public static final byte MAP32 = (byte) 0xdf; public static final byte NEGFIXINT_PREFIX = (byte) 0xe0; + + public static final byte EXT_TIMESTAMP = (byte) -1; } private MessagePack() diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java index 7c6700bd3..079cf366f 100644 --- a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java +++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java @@ -32,6 +32,8 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; +import java.time.Instant; +import java.util.Date; import static org.msgpack.core.MessagePack.Code.ARRAY16; import static org.msgpack.core.MessagePack.Code.ARRAY32; @@ -41,6 +43,7 @@ import static org.msgpack.core.MessagePack.Code.EXT16; import static org.msgpack.core.MessagePack.Code.EXT32; import static org.msgpack.core.MessagePack.Code.EXT8; +import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP; import static org.msgpack.core.MessagePack.Code.FALSE; import static org.msgpack.core.MessagePack.Code.FIXARRAY_PREFIX; import static org.msgpack.core.MessagePack.Code.FIXEXT1; @@ -790,6 +793,121 @@ else if (s.length() < (1 << 16)) { return this; } + /** + * Writes a Timestamp value. + * + *
+ * This method writes a timestamp value using timestamp format family. + * + * @param date the timestamp to be written + * @return this + * @throws IOException when underlying output throws IOException + */ + public MessagePacker packTimestamp(Date date) + throws IOException + { + long epochMilli = date.getTime(); + long sec = Math.floorDiv(epochMilli, 1000L); + int nsec = ((int) (epochMilli - sec * 1000L)) * 1000; // 0 <= nsec < 1,000,000,000 < 2^30 + return packTimestampImpl(sec, nsec); + } + + /** + * Writes a Timestamp value. + * + *
+ * This method writes a timestamp value using timestamp format family. + * + * @param instant the timestamp to be written + * @return this + * @throws IOException when underlying output throws IOException + */ + public MessagePacker packTimestamp(Instant instant) + throws IOException + { + return packTimestampImpl(instant.getEpochSecond(), instant.getNano()); + } + + /** + * Writes a Timestamp value. + * + *
+ * This method writes a timestamp value using timestamp format family. + * + * @param epochSecond the number of seconds from 1970-01-01T00:00:00Z + * @param nanoAdjustment the nanosecond adjustment to the number of seconds, positive or negative + * @return this + * @throws IOException when underlying output throws IOException + * @throws ArithmeticException when epochSecond plus nanoAdjustment in seconds exceeds the range of long + */ + public MessagePacker packTimestampEpochSecond(long epochSecond, int nanoAdjustment) + throws IOException, ArithmeticException + { + long sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L)); + int nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L); + return packTimestampImpl(sec, nsec); + } + + private MessagePacker packTimestampImpl(long sec, int nsec) + throws IOException + { + if (sec >>> 34 == 0) { + // sec can be serialized in 34 bits. + long data64 = (nsec << 34) | sec; + if ((data64 & 0xffffffff00000000L) == 0L) { + // sec can be serialized in 32 bits and nsec is 0. + // use timestamp 32 + writeTimestamp32((int) sec); + } + else { + // sec exceeded 32 bits or nsec is not 0. + // use timestamp 64 + writeTimestamp64(data64); + } + } + else { + // use timestamp 96 format + writeTimestamp96(sec, nsec); + } + return this; + } + + private void writeTimestamp32(int sec) + throws IOException + { + // timestamp 32 in fixext 4 + ensureCapacity(6); + buffer.putByte(position++, FIXEXT4); + buffer.putByte(position++, EXT_TIMESTAMP); + buffer.putInt(position, sec); + position += 4; + } + + private void writeTimestamp64(long data64) + throws IOException + { + // timestamp 64 in fixext 8 + ensureCapacity(10); + buffer.putByte(position++, FIXEXT8); + buffer.putByte(position++, EXT_TIMESTAMP); + buffer.putLong(position, data64); + position += 8; + } + + private void writeTimestamp96(long sec, int nsec) + throws IOException + { + // timestamp 96 in ext 8 + ensureCapacity(15); + buffer.putByte(position++, EXT8); + buffer.putByte(position++, (byte) 12); // length of nsec and sec + buffer.putByte(position++, EXT_TIMESTAMP); + buffer.putInt(position, nsec); + position += 4; + buffer.putLong(position, sec); + position += 8; + } + /** * Writes header of an Array value. *
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java index 82ed9d15f..058d4a360 100644 --- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java +++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java @@ -32,11 +32,14 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; +import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP; import static org.msgpack.core.Preconditions.checkNotNull; /** @@ -596,6 +599,12 @@ private static MessagePackException unexpected(String expected, byte b) } } + private static MessagePackException unexpectedExtension(String expected, int expectedType, int actualType) + { + return new MessageTypeException(String.format("Expected extension type %s (%d), but got extension type %d", + expected, expectedType, actualType)); + } + public ImmutableValue unpackValue() throws IOException { @@ -644,7 +653,12 @@ public ImmutableValue unpackValue() } case EXTENSION: { ExtensionTypeHeader extHeader = unpackExtensionTypeHeader(); - return ValueFactory.newExtension(extHeader.getType(), readPayload(extHeader.getLength())); + switch (extHeader.getType()) { + case EXT_TIMESTAMP: + return ValueFactory.newTimestamp(readPayload(extHeader.getLength())); + default: + return ValueFactory.newExtension(extHeader.getType(), readPayload(extHeader.getLength())); + } } default: throw new MessageNeverUsedFormatException("Unknown value type"); @@ -1232,6 +1246,70 @@ private String decodeStringFastPath(int length) } } + public Instant unpackInstant() + throws IOException + { + ExtensionTypeHeader ext = unpackExtensionTypeHeader(); + if (ext.getType() != EXT_TIMESTAMP) { + throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType()); + } + switch (ext.getLength()) { + case 4: { + int u32 = readInt(); + return Instant.ofEpochSecond(u32); + } + case 8: { + long data64 = readLong(); + int nsec = (int) (data64 >>> 34); + long sec = data64 & 0x00000003ffffffffL; + return Instant.ofEpochSecond(sec, nsec); + } + case 12: { + int nsec = readInt(); + long sec = readLong(); + return Instant.ofEpochSecond(sec, nsec); + } + default: + throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes", + EXT_TIMESTAMP, ext.getLength())); + } + } + + public Date unpackDate() + throws IOException + { + return new Date(unpackTimestampMillis()); + } + + public long unpackTimestampMillis() + throws IOException + { + ExtensionTypeHeader ext = unpackExtensionTypeHeader(); + if (ext.getType() != EXT_TIMESTAMP) { + throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType()); + } + switch (ext.getLength()) { + case 4: { + int u32 = readInt(); + return (u32 & 0xffffffffL) * 1000L; + } + case 8: { + long data64 = readLong(); + int nsec = (int) (data64 >>> 34); + long sec = data64 & 0x00000003ffffffffL; + return sec * 1000L + nsec / 1000L; + } + case 12: { + int nsec = readInt(); + long sec = readLong(); + return sec * 1000L + nsec / 1000L; + } + default: + throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes", + EXT_TIMESTAMP, ext.getLength())); + } + } + /** * Reads header of an array. * diff --git a/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java new file mode 100644 index 000000000..bd4a901bb --- /dev/null +++ b/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java @@ -0,0 +1,26 @@ +// +// MessagePack for Java +// +// 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 org.msgpack.value; + +/** + * Immutable representation of MessagePack's Timestamp type. + * + * @see org.msgpack.value.TimestampValue + */ +public interface ImmutableTimestampValue + extends TimestampValue, ImmutableValue +{ +} diff --git a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java new file mode 100644 index 000000000..caae015a1 --- /dev/null +++ b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java @@ -0,0 +1,38 @@ +// +// MessagePack for Java +// +// 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 org.msgpack.value; + +import java.time.Instant; +import java.util.Date; + +/** + * Representation of MessagePack's Timestamp type. + * + * MessagePack's Timestamp type can represent a timestamp. + */ +public interface TimestampValue + extends ExtensionValue +{ + long getEpochSecond(); + + int getNano(); + + long toEpochMilli(); + + Instant toInstant(); + + Date toDate(); +} diff --git a/msgpack-core/src/main/java/org/msgpack/value/Value.java b/msgpack-core/src/main/java/org/msgpack/value/Value.java index 546dfbbf4..a95fb8d3e 100644 --- a/msgpack-core/src/main/java/org/msgpack/value/Value.java +++ b/msgpack-core/src/main/java/org/msgpack/value/Value.java @@ -16,6 +16,7 @@ package org.msgpack.value; import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageTypeCastException; import java.io.IOException; diff --git a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java index 912dc55fb..6b02dd71c 100644 --- a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java +++ b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java @@ -25,6 +25,7 @@ import org.msgpack.value.impl.ImmutableMapValueImpl; import org.msgpack.value.impl.ImmutableNilValueImpl; import org.msgpack.value.impl.ImmutableStringValueImpl; +import org.msgpack.value.impl.ImmutableTimestampValueImpl; import java.math.BigInteger; import java.util.AbstractMap; @@ -294,4 +295,14 @@ public static ImmutableExtensionValue newExtension(byte type, byte[] data) { return new ImmutableExtensionValueImpl(type, data); } + + public static ImmutableTimestampValue newTimestamp(byte[] data) + { + return new ImmutableTimestampValueImpl(data); + } + + public static ImmutableTimestampValue newTimestamp(long epochSecond, int nanoAdjustment) + { + return new ImmutableTimestampValueImpl(epochSecond, nanoAdjustment); + } } diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java new file mode 100644 index 000000000..082dd37b4 --- /dev/null +++ b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java @@ -0,0 +1,226 @@ +// +// MessagePack for Java +// +// 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 org.msgpack.value.impl; + +import org.msgpack.core.MessageExtensionFormatException; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.buffer.MessageBuffer; +import org.msgpack.value.ExtensionValue; +import org.msgpack.value.ImmutableExtensionValue; +import org.msgpack.value.ImmutableTimestampValue; +import org.msgpack.value.TimestampValue; +import org.msgpack.value.Value; +import org.msgpack.value.ValueType; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; + +import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP; + +/** + * {@code ImmutableTimestampValueImpl} Implements {@code ImmutableTimestampValue} using a {@code byte} and a {@code byte[]} fields. + * + * @see TimestampValue + */ +public class ImmutableTimestampValueImpl + extends AbstractImmutableValue + implements ImmutableExtensionValue, ImmutableTimestampValue +{ + private final long sec; + + private final int nsec; + + private byte[] data; + + public ImmutableTimestampValueImpl(long epochSecond, int nanoAdjustment) + { + this.sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L)); + this.nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L); + this.data = null; + } + + public ImmutableTimestampValueImpl(byte[] data) + { + // See MessageUnpacker.unpackInstant + this.data = data; + switch (data.length) { + case 4: { + this.sec = MessageBuffer.wrap(data).getInt(0); + this.nsec = 0; + break; + } + case 8: { + long data64 = MessageBuffer.wrap(data).getLong(0); + this.nsec = (int) (data64 >>> 34); + this.sec = data64 & 0x00000003ffffffffL; + break; + } + case 12: { + MessageBuffer buffer = MessageBuffer.wrap(data); + this.nsec = buffer.getInt(0); + this.sec = buffer.getLong(4); + break; + } + default: + throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes", + EXT_TIMESTAMP, data.length)); + } + } + + @Override + public byte getType() + { + return EXT_TIMESTAMP; + } + + @Override + public ValueType getValueType() + { + // Note: Future version should return ValueType.TIMESTAMP instead. + return ValueType.EXTENSION; + } + + @Override + public ImmutableTimestampValue immutableValue() + { + return this; + } + + @Override + public ImmutableExtensionValue asExtensionValue() + { + return this; + } + + @Override + public byte[] getData() + { + if (data == null) { + // See MessagePacker.packTimestampImpl + byte[] bytes; + if (sec >>> 34 == 0) { + long data64 = (nsec << 34) | sec; + if ((data64 & 0xffffffff00000000L) == 0L) { + bytes = new byte[4]; + MessageBuffer.wrap(bytes).putInt(0, (int) sec); + } + else { + bytes = new byte[8]; + MessageBuffer.wrap(bytes).putLong(0, data64); + } + } + else { + bytes = new byte[12]; + MessageBuffer buffer = MessageBuffer.wrap(bytes); + buffer.putInt(0, nsec); + buffer.putLong(4, sec); + } + data = bytes; + } + return data; + } + + @Override + public long getEpochSecond() + { + return sec; + } + + @Override + public int getNano() + { + return nsec; + } + + @Override + public long toEpochMilli() + { + return Math.addExact(Math.multiplyExact(getEpochSecond(), 1000L), getNano() / 1000000L); + } + + @Override + public Instant toInstant() + { + return Instant.ofEpochSecond(getEpochSecond(), getNano()); + } + + @Override + public Date toDate() + { + return new Date(toEpochMilli()); + } + + @Override + public void writeTo(MessagePacker packer) + throws IOException + { + packer.packTimestampEpochSecond(sec, nsec); + } + + @Override + public boolean equals(Object o) + { + // Implements same behavior with ImmutableExtensionValueImpl. + if (o == this) { + return true; + } + if (!(o instanceof Value)) { + return false; + } + Value v = (Value) o; + + if (!v.isExtensionValue()) { + return false; + } + ExtensionValue ev = v.asExtensionValue(); + + // Here should use isTimestampValue and asTimestampValue instead. However, because + // adding these methods to Value interface can't keep backward compatibility without + // using "default" keyword since Java 7, here uses instanceof of and cast instead. + if (ev instanceof TimestampValue) { + TimestampValue tv = (TimestampValue) ev; + return getEpochSecond() == tv.getEpochSecond() && getNano() == tv.getNano(); + } + else { + return EXT_TIMESTAMP == ev.getType() && Arrays.equals(getData(), ev.getData()); + } + } + + @Override + public int hashCode() + { + // Implements same behavior with ImmutableExtensionValueImpl. + int hash = 31 + EXT_TIMESTAMP; + for (byte e : getData()) { + hash = 31 * hash + e; + } + return hash; + } + + @Override + public String toJson() + { + return "\"" + toInstant().toString() + "\""; + } + + @Override + public String toString() + { + return toInstant().toString(); + } +}