From d49af86fbcfe8a1d4283d3f41391c54225f9e701 Mon Sep 17 00:00:00 2001 From: Stephen Colebourne Date: Tue, 3 Dec 2024 10:16:26 +0000 Subject: [PATCH] Create packed binary format * Define the input/output classes for the new binary format * Design is inspired by MessagePack, and partly compatible * First commit just introduces the format, without the reader/writer --- .../java/org/joda/beans/ser/bin/BeanPack.java | 334 ++++++++++++ .../org/joda/beans/ser/bin/BeanPackInput.java | 461 +++++++++++++++++ .../joda/beans/ser/bin/BeanPackOutput.java | 474 ++++++++++++++++++ .../beans/ser/bin/BeanPackVisualizer.java | 240 +++++++++ src/test/java/org/joda/beans/TestClone.java | 1 + .../java/org/joda/beans/TestMetaBeans.java | 2 +- 6 files changed, 1511 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/joda/beans/ser/bin/BeanPack.java create mode 100644 src/main/java/org/joda/beans/ser/bin/BeanPackInput.java create mode 100644 src/main/java/org/joda/beans/ser/bin/BeanPackOutput.java create mode 100644 src/main/java/org/joda/beans/ser/bin/BeanPackVisualizer.java diff --git a/src/main/java/org/joda/beans/ser/bin/BeanPack.java b/src/main/java/org/joda/beans/ser/bin/BeanPack.java new file mode 100644 index 00000000..5f68a5d8 --- /dev/null +++ b/src/main/java/org/joda/beans/ser/bin/BeanPack.java @@ -0,0 +1,334 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * 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.joda.beans.ser.bin; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Constants used in MsgPack binary serialization. + *

+ * This uses the v2.0 specification of MsgPack as of 2014-01-29. + */ +abstract class BeanPack { + + /** + * UTF-8 encoding. + */ + static final Charset UTF_8 = StandardCharsets.UTF_8; + /** + * The smallest length of string that is cached. + */ + static final int MIN_LENGTH_STR_VALUE = 3; + + // fixed-size numbers + /** + * Maximum fixed int (7 bits). + */ + static final int MAX_FIX_INT = 0x7F; + /** + * Minimum fixed int (4 bits). + */ + static final int MIN_FIX_INT = 0xFFFFFFF0; + + // maps + /** + * Min fixed map - up to length 12. + */ + static final int MIN_FIX_MAP = 0xFFFFFF80; + /** + * Max fixed map. + */ + static final int MAX_FIX_MAP = 0xFFFFFF8C; + /** + * Map - followed by the size as an unsigned byte. + */ + static final int MAP_8 = 0xFFFFFF8D; + /** + * Map - followed by the size as an unsigned short. + */ + static final int MAP_16 = 0xFFFFFF8E; + /** + * Map - followed by the size as an unsigned long. + */ + static final int MAP_32 = 0xFFFFFF8F; + + // arrays + /** + * Min fixed array - up to length 12. + */ + static final int MIN_FIX_ARRAY = 0xFFFFFF90; // must be same as MsgPack + /** + * Max fixed array. + */ + static final int MAX_FIX_ARRAY = 0xFFFFFF9C; + /** + * Array - followed by the size as an unsigned byte. + */ + static final int ARRAY_8 = 0xFFFFFF9D; + /** + * Array - followed by the size as an unsigned short. + */ + static final int ARRAY_16 = 0xFFFFFF9E; + /** + * Array - followed by the size as an unsigned long. + */ + static final int ARRAY_32 = 0xFFFFFF9F; + + // strings + /** + * Min fixed string - up to length 40. + */ + static final int MIN_FIX_STR = 0xFFFFFFA0; + /** + * Max fixed string. + */ + static final int MAX_FIX_STR = 0xFFFFFFC8; + /** + * String - followed by the size as an unsigned byte. + */ + static final int STR_8 = 0xFFFFFFC9; + /** + * String - followed by the size as an unsigned short. + */ + static final int STR_16 = 0xFFFFFFCA; + /** + * String - followed by the size as an unsigned int. + */ + static final int STR_32 = 0xFFFFFFCB; + + // primitives + /** + * Null. + */ + static final int NULL = 0xFFFFFFCC; + /** + * False. + */ + static final int FALSE = 0xFFFFFFCD; + /** + * True. + */ + static final int TRUE = 0xFFFFFFCE; + /** + * Unused. + */ + static final int UNUSED = 0xFFFFFFCF; + + // numbers + /** + * Float - followed by 4 bytes. + */ + static final int FLOAT_32 = 0xFFFFFFD0; + /** + * Double in 1-byte int format - followed by 1 byte signed int + */ + static final int DOUBLE_INT_8 = 0xFFFFFFD1; + /** + * Double - followed by 8 bytes. + */ + static final int DOUBLE_64 = 0xFFFFFFD2; + /** + * Char (unsigned) - followed by 2 bytes. + */ + static final int CHAR_16 = 0xFFFFFFD3; + /** + * Byte (signed) - followed by 1 byte. + */ + static final int BYTE_8 = 0xFFFFFFD4; + /** + * Short (signed) - followed by 2 bytes. + */ + static final int SHORT_16 = 0xFFFFFFD5; + /** + * Int (signed) - followed by 2 bytes. + */ + static final int INT_16 = 0xFFFFFFD6; + /** + * Int (signed) - followed by 4 bytes. + */ + static final int INT_32 = 0xFFFFFFD7; + /** + * Long (signed) - followed by 1 byte. + */ + static final int LONG_8 = 0xFFFFFFD8; + /** + * Long (signed) - followed by 2 bytes. + */ + static final int LONG_16 = 0xFFFFFFD9; + /** + * Long (signed) - followed by 4 bytes. + */ + static final int LONG_32 = 0xFFFFFFDA; + /** + * Long (signed) - followed by 8 bytes. + */ + static final int LONG_64 = 0xFFFFFFDB; + + // date/time + /** + * LocalDate (2 bytes) - packed format from year 2000 to 2169 inclusive, + * 11 bits for year-month from 2000, 5 bits for 1-based day-of-month. + */ + static final int DATE_PACKED = 0xFFFFFFDC; + /** + * LocalDate (5 bytes) - 27 bits for year-month, 5 bits for 1-based day-of-month. + */ + static final int DATE = 0xFFFFFFDD; + /** + * LocalTime (6 bytes) - 6 byte nano-of-day. + */ + static final int TIME = 0xFFFFFFDE; + /** + * Instant (12 bytes) - 8 for the seconds and 4 for the nanoseconds. + */ + static final int INSTANT = 0xFFFFFFDF; + /** + * Duration (12 bytes) - 8 for the seconds and 4 for the nanoseconds. + */ + static final int DURATION = 0xFFFFFFE0; + + // byte[]/double[] + /** + * byte[] - followed by the size as an unsigned short. + */ + static final int BIN_8 = 0xFFFFFFE1; + /** + * byte[] - followed by the size as an unsigned short. + */ + static final int BIN_16 = 0xFFFFFFE2; + /** + * byte[] - followed by the size as an unsigned int. + */ + static final int BIN_32 = 0xFFFFFFE3; + /** + * double[] - followed by the size as an unsigned byte. + */ + static final int DOUBLE_ARRAY_8 = 0xFFFFFFE4; + /** + * double[] - followed by the size as an unsigned short. + */ + static final int DOUBLE_ARRAY_16 = 0xFFFFFFE5; + /** + * double[] - followed by the size as an unsigned int. + */ + static final int DOUBLE_ARRAY_32 = 0xFFFFFFE6; + + // types and references + /** + * Type definition - followed by a 1 byte length, UTF-8 string and the actual value. + */ + static final int TYPE_DEFN_8 = 0xFFFFFFE7; + /** + * Type definition - followed by a 2 byte length, UTF-8 string and the actual value. + */ + static final int TYPE_DEFN_16 = 0xFFFFFFE8; + /** + * Reference to a type name - followed by a 1 byte int and the actual value. + */ + static final int TYPE_REF_8 = 0xFFFFFFE9; + /** + * Reference to a type name - followed by a 2 byte int and the actual value. + */ + static final int TYPE_REF_16 = 0xFFFFFFEA; + /** + * Bean with full definition - followed by a 1 byte int count of properties, then each property name, + * then each property value, nulls replacing non-serialized entries. + * Beans with 256 or more properties are not recorded as bean definitions. + */ + static final int BEAN_DEFN = 0xFFFFFFEB; + /** + * Value with full definition - followed by the value. + */ + static final int VALUE_DEFN = 0xFFFFFFEC; + /** + * Reference to a previous value - followed by a 1 byte int and the actual value. + */ + static final int VALUE_REF_8 = 0xFFFFFFED; + /** + * Reference to a previous value - followed by a 2 byte int and the actual value. + */ + static final int VALUE_REF_16 = 0xFFFFFFEE; + /** + * Reference to a previous value - followed by a 3 byte int and the actual value. + */ + static final int VALUE_REF_24 = 0xFFFFFFEF; + + //------------------------------------------------------------------------- + /** + * Set type code, followed by an array of values. + */ + static final int TYPE_CODE_LIST = -1; + /** + * Set type code, followed by an array of values. + */ + static final int TYPE_CODE_SET = -2; + /** + * Set type code, followed by an array of values. + */ + static final int TYPE_CODE_MAP = -3; + /** + * Optional type code, followed by a value, where a null value means empty. + */ + static final int TYPE_CODE_OPTIONAL = -4; + /** + * Multiset type code, followed by a map of values. + */ + static final int TYPE_CODE_MULTISET = -5; + /** + * Multimap type code, followed by a map of values. + */ + static final int TYPE_CODE_LIST_MULTIMAP = -6; + /** + * SetMultimap type code, followed by a map of values. + */ + static final int TYPE_CODE_SET_MULTIMAP = -7; + /** + * Bimap type code, followed by a map of values. + */ + static final int TYPE_CODE_BIMAP = -8; + /** + * Table type code. + */ + static final int TYPE_CODE_TABLE = -9; + /** + * Optional (Guava) type code, followed by a value, where a null value means empty. + */ + static final int TYPE_CODE_GUAVA_OPTIONAL = -10; + /** + * Grid type code. + */ + static final int TYPE_CODE_GRID = -11; + /** + * Object[] type code, followed by an array of values. + */ + static final int TYPE_CODE_OBJECT_ARRAY = -12; + /** + * String[] type code, followed by an array of values. + */ + static final int TYPE_CODE_STRING_ARRAY = -13; + + //----------------------------------------------------------------------- + /** + * Converts a byte to a hex string for debugging. + * + * @param b the byte + * @return the two character hex equivalent, not null + */ + static String toHex(int b) { + return String.format("%02X", (byte) b); + } +} diff --git a/src/main/java/org/joda/beans/ser/bin/BeanPackInput.java b/src/main/java/org/joda/beans/ser/bin/BeanPackInput.java new file mode 100644 index 00000000..df92c4bf --- /dev/null +++ b/src/main/java/org/joda/beans/ser/bin/BeanPackInput.java @@ -0,0 +1,461 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * 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.joda.beans.ser.bin; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Receives and processes BeanPack data. + *

+ * This interprets based on the data in the input, and does not interpret data into beans. + */ +abstract class BeanPackInput extends BeanPack { + + /** + * The stream to read. + */ + private final DataInputStream input; + /** + * The type definitions. + */ + private final List typeDefinitions = new ArrayList<>(); + /** + * The value definitions. + */ + private final List valueDefinitions = new ArrayList<>(); + + /** + * Creates an instance. + * + * @param bytes the bytes to read, not null + */ + BeanPackInput(byte[] bytes) { + this(new DataInputStream(new ByteArrayInputStream(bytes))); + } + + /** + * Creates an instance. + * + * @param stream the stream to read from, not null + */ + BeanPackInput(DataInputStream stream) { + this.input = stream; + } + + //----------------------------------------------------------------------- + /** + * Reads all the data in the stream, closing the stream. + */ + void readAll() { + try { + try { + acceptObject(); + } finally { + input.close(); + } + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + //------------------------------------------------------------------------- + private Object acceptObject() throws IOException { + var next = input.readByte(); + return readObject(next); + } + + //----------------------------------------------------------------------- + Object readObject(byte typeByte) throws IOException { + handleObjectStart(); + if (typeByte >= MIN_FIX_INT) { // no need to check for b <= MAX_FIX_INT + handleInt(typeByte); + return typeByte; + + } else if (typeByte >= MIN_FIX_STR && typeByte <= MAX_FIX_STR) { + return parseString(typeByte - MIN_FIX_STR); + + } else if (typeByte >= MIN_FIX_ARRAY && typeByte <= MAX_FIX_ARRAY) { + return parseArray(typeByte - MIN_FIX_ARRAY); + + } else if (typeByte >= MIN_FIX_MAP && typeByte <= MAX_FIX_MAP) { + return parseMap(typeByte - MIN_FIX_MAP); + + } else { + return switch (typeByte) { + case MAP_8 -> parseMap(input.readUnsignedByte()); + case MAP_16 -> parseMap(input.readUnsignedShort()); + case MAP_32 -> parseMap(input.readInt()); + case ARRAY_8 -> parseArray(input.readUnsignedByte()); + case ARRAY_16 -> parseArray(input.readUnsignedShort()); + case ARRAY_32 -> parseArray(input.readInt()); + case STR_8 -> parseString(input.readUnsignedByte()); + case STR_16 -> parseString(input.readUnsignedShort()); + case STR_32 -> parseString(input.readInt()); + case NULL -> parseNull(); + case FALSE -> parseBoolean(false); + case TRUE -> parseBoolean(true); + case UNUSED -> parseUnknown(typeByte); + case FLOAT_32 -> parseFloat(input.readFloat()); + case DOUBLE_INT_8 -> parseDouble(input.readByte()); + case DOUBLE_64 -> parseDouble(input.readDouble()); + case CHAR_16 -> parseChar(input.readChar()); + case BYTE_8 -> parseByte(input.readByte()); + case SHORT_16 -> parseShort(input.readShort()); + case INT_16 -> parseInt(input.readShort()); + case INT_32 -> parseInt(input.readInt()); + case LONG_8 -> parseLong(input.readByte()); + case LONG_16 -> parseLong(input.readShort()); + case LONG_32 -> parseLong(input.readInt()); + case LONG_64 -> parseLong(input.readLong()); + case DATE_PACKED -> parseDatePacked(); + case DATE -> parseDate(); + case TIME -> parseTime(); + case INSTANT -> parseInstant(); + case DURATION -> parseDuration(); + case BIN_8 -> parseByteArray(input.readUnsignedByte()); + case BIN_16 -> parseByteArray(input.readUnsignedShort()); + case BIN_32 -> parseByteArray(input.readInt()); + case DOUBLE_ARRAY_8 -> parseDoubleArray(input.readUnsignedByte()); + case DOUBLE_ARRAY_16 -> parseDoubleArray(input.readUnsignedShort()); + case DOUBLE_ARRAY_32 -> parseDoubleArray(input.readInt()); + case TYPE_DEFN_8 -> parseTypeName(input.readUnsignedByte()); + case TYPE_DEFN_16 -> parseTypeName(input.readUnsignedShort()); + case TYPE_REF_8 -> parseTypeReference(input.readByte()); + case TYPE_REF_16 -> parseTypeReference(input.readUnsignedShort()); + case BEAN_DEFN -> parseBean(input.readUnsignedByte()); + case VALUE_DEFN -> parseValueDefinition(); + case VALUE_REF_8 -> parseValueReference(input.readUnsignedByte()); + case VALUE_REF_16 -> parseValueReference(input.readUnsignedShort()); + case VALUE_REF_24 -> parseValueReference((input.readUnsignedByte() << 16) + input.readUnsignedShort()); + default -> parseUnknown(typeByte); + }; + } + } + + //------------------------------------------------------------------------- + private Object parseMap(int size) throws IOException { + handleMapHeader(size); + for (var i = 0; i < size; i++) { + readMapKey(); + readMapValue(); + } + handleMapFooter(); + return ""; // maps are not cached as values, so this map must actually be a bean + } + + Object readMapKey() throws IOException { + return acceptObject(); + } + + Object readMapValue() throws IOException { + return acceptObject(); + } + + private Object parseArray(int size) throws IOException { + handleArrayHeader(size); + for (var i = 0; i < size; i++) { + readArrayItem(); + } + handleArrayFooter(); + return ""; // maps are not cached as values, so this map must actually be a bean + } + + Object readArrayItem() throws IOException { + return acceptObject(); + } + + private Object parseBean(int size) throws IOException { + handleBeanHeader(size); + for (var i = 0; i < size * 2; i++) { + readBeanItem(); + } + handleBeanFooter(); + return ""; + } + + Object readBeanItem() throws IOException { + return acceptObject(); + } + + private Object readAnnotatedValue() throws IOException { + return acceptObject(); + } + + //------------------------------------------------------------------------- + private Object parseNull() throws IOException { + handleNull(); + return null; + } + + private Object parseBoolean(boolean value) throws IOException { + handleBoolean(value); + return value; + } + + private Object parseUnknown(byte value) throws IOException { + handleUnknown(value); + return null; + } + + private Object parseFloat(float value) throws IOException { + handleFloat(value); + return null; + } + + private Object parseDouble(double value) throws IOException { + handleDouble(value); + return null; + } + + private Object parseChar(char value) throws IOException { + handleChar(value); + return null; + } + + private Object parseByte(byte value) throws IOException { + handleByte(value); + return value; + } + + private Object parseShort(short value) throws IOException { + handleShort(value); + return value; + } + + private Object parseInt(int value) throws IOException { + handleInt(value); + return value; + } + + private Object parseLong(long value) throws IOException { + handleLong(value); + return value; + } + + private String parseString(int size) throws IOException { + if (size < 0) { + throw new IllegalStateException("String too large"); + } + var bytes = new byte[size]; + input.readFully(bytes); + var str = new String(bytes, UTF_8); + handleString(str); + if (str.length() >= MIN_LENGTH_STR_VALUE) { + valueDefinitions.add(str); + } + return str; + } + + private LocalDate parseDatePacked() throws IOException { + var packed = input.readUnsignedShort(); + var dom = packed & 31; + var ym = packed >> 5; + var date = LocalDate.of((ym / 12) + 2000, (ym % 12) + 1, dom); + handleDate(date); + return date; + } + + private LocalDate parseDate() throws IOException { + var upper = input.readInt(); + var lower = input.readUnsignedByte(); + var year = upper >> 1; + var month = ((upper & 1) << 3) + (lower >>> 5); + var dom = lower & 31; + var date = LocalDate.of(year, month, dom); + handleDate(date); + return date; + } + + private LocalTime parseTime() throws IOException { + var upper = input.readUnsignedShort(); + var lower = Integer.toUnsignedLong(input.readInt()); + var nod = ((long) upper << 32) + lower; + var time = LocalTime.ofNanoOfDay(nod); + handleTime(time); + return time; + } + + private Instant parseInstant() throws IOException { + var second = input.readLong(); + var nanos = input.readInt(); + var instant = Instant.ofEpochSecond(second, nanos); + handleInstant(instant); + return instant; + } + + private Duration parseDuration() throws IOException { + var seconds = input.readLong(); + var nanos = input.readInt(); + var duration = Duration.ofSeconds(seconds, nanos); + handleDuration(duration); + return parseDuration(); + } + + //----------------------------------------------------------------------- + private byte[] parseByteArray(int size) throws IOException { + var bytes = new byte[size]; + input.readFully(bytes); + handleBinary(bytes); + return bytes; + } + + private double[] parseDoubleArray(int size) throws IOException { + var values = new double[size]; + for (int i = 0; i < size; i++) { + values[i] = input.readDouble(); + } + handleDoubleArray(values); + return values; + } + + //------------------------------------------------------------------------- + private Object parseTypeName(int size) throws IOException { + var bytes = new byte[size]; + input.readFully(bytes); + var typeName = new String(bytes, UTF_8); + handleTypeName(typeName); + typeDefinitions.add(typeName); + return readAnnotatedValue(); + } + + private Object parseTypeReference(int ref) throws IOException { + var typeName = switch (ref) { + case TYPE_CODE_LIST -> "List"; + case TYPE_CODE_SET -> "Set"; + case TYPE_CODE_MAP -> "Map"; + case TYPE_CODE_OPTIONAL -> "Optional"; + case TYPE_CODE_MULTISET -> "Multiset"; + case TYPE_CODE_LIST_MULTIMAP -> "ListMultimap"; + case TYPE_CODE_SET_MULTIMAP -> "SetMultimap"; + case TYPE_CODE_BIMAP -> "BiMap"; + case TYPE_CODE_TABLE -> "Table"; + case TYPE_CODE_GUAVA_OPTIONAL -> "List"; + case TYPE_CODE_GRID -> "Grid"; + case TYPE_CODE_OBJECT_ARRAY -> "Object[]"; + case TYPE_CODE_STRING_ARRAY -> "String[]"; + default -> ref >= 0 ? typeDefinitions.get(ref) : "Unknown"; + }; + handleTypeReference(ref, typeName); + return readAnnotatedValue(); + } + + private Object parseValueDefinition() throws IOException { + handleValueDefinition(); + var value = readAnnotatedValue(); + valueDefinitions.add(value); + return value; + } + + private Object parseValueReference(int ref) { + var value = valueDefinitions.get(ref); + handleValueReference(ref, value); + return value; + } + + //------------------------------------------------------------------------- + void handleObjectStart() { + } + + void handleMapHeader(int size) { + } + + void handleMapFooter() { + } + + void handleArrayHeader(int size) { + } + + void handleArrayFooter() { + } + + void handleString(String str) { + } + + void handleNull() { + } + + void handleBoolean(boolean bool) { + } + + void handleFloat(float value) { + } + + void handleDouble(double value) { + } + + void handleChar(char value) { + } + + void handleByte(byte value) { + } + + void handleShort(short value) { + } + + void handleInt(int value) { + } + + void handleLong(long value) { + } + + void handleDate(LocalDate date) { + } + + void handleTime(LocalTime time) { + } + + void handleInstant(Instant instant) { + } + + void handleDuration(Duration duration) { + } + + void handleBinary(byte[] bytes) { + } + + void handleDoubleArray(double[] values) { + } + + void handleTypeName(String typeName) throws IOException { + } + + void handleTypeReference(int ref, String typeName) throws IOException { + } + + void handleBeanHeader(int propertyCount) throws IOException { + } + + void handleBeanFooter() throws IOException { + } + + void handleValueDefinition() throws IOException { + } + + void handleValueReference(int ref, Object value) { + } + + void handleUnknown(byte b) { + } + +} diff --git a/src/main/java/org/joda/beans/ser/bin/BeanPackOutput.java b/src/main/java/org/joda/beans/ser/bin/BeanPackOutput.java new file mode 100644 index 00000000..698eaf26 --- /dev/null +++ b/src/main/java/org/joda/beans/ser/bin/BeanPackOutput.java @@ -0,0 +1,474 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * 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.joda.beans.ser.bin; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; + +import org.joda.beans.ResolvedType; + +/** + * Outputter for BeanPack data, which is derived from ideas in MsgPack. + */ +final class BeanPackOutput extends BeanPack { + + /** + * Mask to check if value is a small positive integer, from 0 to 127 inclusive. + */ + private static final int MASK_SMALL_INT_POSITIVE = 0xFFFFFF80; + + /** + * The stream to write to. + */ + private final DataOutputStream output; + + /** + * Creates an instance. + * + * @param stream the stream to write to, not null + */ + BeanPackOutput(OutputStream stream) { + this.output = new DataOutputStream(stream); + } + + //----------------------------------------------------------------------- + /** + * Writes a null. + * + * @throws IOException if an error occurs + */ + void writeNull() throws IOException { + output.writeByte(NULL); + } + + /** + * Writes a boolean. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeBoolean(boolean value) throws IOException { + if (value) { + output.writeByte(TRUE); + } else { + output.writeByte(FALSE); + } + } + + /** + * Writes a float. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeFloat(float value) throws IOException { + output.writeByte(FLOAT_32); + output.writeFloat(value); + } + + /** + * Writes a double. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeDouble(double value) throws IOException { + var intValue = (int) value; + if (value == intValue && intValue <= Byte.MAX_VALUE && intValue >= Byte.MIN_VALUE && Double.compare(value, -0d) != 0) { + output.writeByte(DOUBLE_INT_8); + output.writeByte(intValue); + } else { + output.writeByte(DOUBLE_64); + output.writeDouble(value); + } + } + + /** + * Writes a char. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeChar(char value) throws IOException { + output.writeByte(CHAR_16); + output.writeChar(value); + } + + /** + * Writes a byte. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeByte(byte value) throws IOException { + output.writeByte(BYTE_8); + output.writeByte(value); + } + + /** + * Writes a short. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeShort(short value) throws IOException { + output.writeByte(SHORT_16); + output.writeShort(value); + } + + /** + * Writes an int. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeInt(int value) throws IOException { + if ((value & MASK_SMALL_INT_POSITIVE) == 0) { + output.writeByte(value); + } else if (value >= 0) { + if (value <= Short.MAX_VALUE) { + output.writeByte(INT_16); + output.writeShort((short) value); + } else { + output.writeByte(INT_32); + output.writeInt(value); + } + } else { + if (value >= MIN_FIX_INT) { + output.writeByte(value); + } else if (value >= Short.MIN_VALUE) { + output.writeByte(INT_16); + output.writeShort((short) value); + } else { + output.writeByte(INT_32); + output.writeInt(value); + } + } + } + + /** + * Writes a long. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeLong(long value) throws IOException { + if (value >= 0) { + if (value <= Byte.MAX_VALUE) { + output.writeByte(LONG_8); + output.writeByte((byte) value); + } else if (value <= Short.MAX_VALUE) { + output.writeByte(LONG_16); + output.writeShort((short) value); + } else if (value <= Integer.MAX_VALUE) { + output.writeByte(LONG_32); + output.writeInt((int) value); + } else { + output.writeByte(LONG_64); + output.writeLong(value); + } + } else { + if (value >= Byte.MIN_VALUE) { + output.writeByte(LONG_8); + output.writeByte((byte) value); + } else if (value >= Short.MIN_VALUE) { + output.writeByte(LONG_16); + output.writeShort((short) value); + } else if (value >= Integer.MIN_VALUE) { + output.writeByte(LONG_32); + output.writeInt((int) value); + } else { + output.writeByte(LONG_64); + output.writeLong(value); + } + } + } + + //------------------------------------------------------------------------- + /** + * Writes a date. + * + * @param date the date + * @throws IOException if an error occurs + */ + void writeDate(LocalDate date) throws IOException { + var year = date.getYear(); + var month = date.getMonthValue(); + var dom = date.getDayOfMonth(); + if (year >= 2000 && year <= 2169) { + var ym2000 = (year - 2000) * 12 + (month - 1); + output.write(DATE_PACKED); + var packed = (ym2000 << 5) + dom; + output.writeShort(packed); + } else { + output.write(DATE); + var packed = (((long) year) << 9) + (month << 5) + dom; + output.writeInt((int) (packed >> 8)); + output.writeByte((byte) (packed & 0xFF)); + } + } + + /** + * Writes a time. + * + * @param time the time + * @throws IOException if an error occurs + */ + void writeTime(LocalTime time) throws IOException { + var nod = time.toNanoOfDay(); + var upper = (int) (nod >>> 32); + var lower = (int) (nod & 0xFFFFFFFFL); + output.write(TIME); + output.writeShort(upper); + output.writeInt(lower); + } + + /** + * Writes an instant. + * + * @param instant the instant + * @throws IOException if an error occurs + */ + void writeInstant(Instant instant) throws IOException { + output.write(INSTANT); + output.writeLong(instant.getEpochSecond()); + output.writeInt(instant.getNano()); + } + + /** + * Writes a duration. + * + * @param duration the instant + * @throws IOException if an error occurs + */ + void writeDuration(Duration duration) throws IOException { + output.write(DURATION); + output.writeLong(duration.getSeconds()); + output.writeInt(duration.getNano()); + } + + //------------------------------------------------------------------------- + /** + * Writes a byte[]. + * + * @param bytes the bytes, not null + * @throws IOException if an error occurs + */ + void writeBytes(byte[] bytes) throws IOException { + // positive numbers only + var size = bytes.length; + if (size <= 0xFF) { + output.writeByte(BIN_8); + output.writeByte(size); + } else if (size <= 0xFFFF) { + output.writeByte(BIN_16); + output.writeShort(size); + } else { + output.writeByte(BIN_32); + output.writeInt(size); + } + output.write(bytes); + } + + /** + * Writes a double[]. + * + * @param values the values, not null + * @throws IOException if an error occurs + */ + void writeDoubles(double[] values) throws IOException { + // positive numbers only + var size = values.length; + if (size <= 0xFF) { + output.writeByte(DOUBLE_ARRAY_8); + output.writeByte(size); + } else if (size <= 0xFFFF) { + output.writeByte(DOUBLE_ARRAY_16); + output.writeShort(size); + } else { + output.writeByte(DOUBLE_ARRAY_32); + output.writeInt(size); + } + for (double value : values) { + output.writeDouble(value); + } + } + + //------------------------------------------------------------------------- + /** + * Writes a map header. + * + * @param size the size + * @throws IOException if an error occurs + */ + void writeMapHeader(int size) throws IOException { + // positive numbers only + if (size <= 12) { + output.writeByte(MIN_FIX_MAP + size); + } else if (size <= 0xFF) { + output.writeByte(MAP_8); + output.writeByte(size); + } else if (size <= 0xFFFF) { + output.writeByte(MAP_16); + output.writeShort(size); + } else { + output.writeByte(MAP_32); + output.writeInt(size); + } + } + + /** + * Writes an array header. + * + * @param size the size + * @throws IOException if an error occurs + */ + void writeArrayHeader(int size) throws IOException { + // positive numbers only + if (size <= 12) { + output.writeByte(MIN_FIX_ARRAY + size); + } else if (size <= 0xFF) { + output.writeByte(ARRAY_8); + output.writeByte(size); + } else if (size <= 0xFFFF) { + output.writeByte(ARRAY_16); + output.writeShort(size); + } else { + output.writeByte(ARRAY_32); + output.writeInt(size); + } + } + + /** + * Writes a String. + * + * @param value the value + * @throws IOException if an error occurs + */ + void writeString(String value) throws IOException { + // Java 21 performance testing showed manually converting to UTF-8 to be slower + var bytes = value.getBytes(UTF_8); + var size = bytes.length; + if (size <= (MAX_FIX_STR - MIN_FIX_STR)) { + output.writeByte(MIN_FIX_STR + size); + } else { + writeStringHeaderLarge(size); + } + output.write(bytes); + } + + // separate out larger strings, which may benefit hotspot + private void writeStringHeaderLarge(int size) throws IOException { + // positive numbers only + if (size <= 0xFF) { + output.writeByte(STR_8); + output.writeByte(size); + } else if (size <= 0xFFFF) { + output.writeByte(STR_16); + output.writeShort(size); + } else { + output.writeByte(STR_32); + output.writeInt(size); + } + } + + //------------------------------------------------------------------------- + /** + * Writes a type name. + *

+ * The type name is a class name, not a {@link ResolvedType}. + * + * @param className the class name + * @throws IOException if an error occurs + */ + void writeTypeName(String className) throws IOException { + // written directly, as this is not part of the value definition setup + var bytes = className.getBytes(UTF_8); + if (bytes.length <= 0xFF) { + output.write(TYPE_DEFN_8); + output.writeByte(bytes.length); + } else { // assume type name length will be < 0xFFFF + output.write(TYPE_DEFN_16); + output.writeShort(bytes.length); + } + output.write(bytes); + } + + /** + * Writes a type reference. + * + * @param ref the reference + * @throws IOException if an error occurs + */ + void writeTypeReference(int ref) throws IOException { + // allow negative numbers >= -128 + if (ref <= 0x7F) { + output.write(TYPE_REF_8); + output.writeByte(ref); + } else { + output.write(TYPE_REF_16); + output.writeShort(ref); + } + } + + /** + * Writes a bean definition header. + * + * @param propertyCount the count of properties, must be 0 to 255 + * @throws IOException if an error occurs + */ + void writeBeanDefinitionHeader(int propertyCount) throws IOException { + // 0 to 255 + output.write(BEAN_DEFN); + output.writeByte(propertyCount); + } + + /** + * Writes a value definition header. + * + * @throws IOException if an error occurs + */ + void writeValueDefinitionHeader() throws IOException { + output.write(VALUE_DEFN); + } + + /** + * Writes a value reference. + * + * @param ref the reference + * @throws IOException if an error occurs + */ + void writeValueReference(int ref) throws IOException { + // positive numbers only + if (ref <= 0xFF) { + output.write(VALUE_REF_8); + output.writeByte(ref); + } else if (ref <= 0xFFFF) { + output.write(VALUE_REF_16); + output.writeShort(ref); + } else { + output.write(VALUE_REF_24); + output.writeByte(ref >>> 16); + output.writeShort(ref & 0xFFFF); + } + } + +} diff --git a/src/main/java/org/joda/beans/ser/bin/BeanPackVisualizer.java b/src/main/java/org/joda/beans/ser/bin/BeanPackVisualizer.java new file mode 100644 index 00000000..c03daa3a --- /dev/null +++ b/src/main/java/org/joda/beans/ser/bin/BeanPackVisualizer.java @@ -0,0 +1,240 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * 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.joda.beans.ser.bin; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; + +/** + * Allows BeanPack data to be visualized. + */ +final class BeanPackVisualizer extends BeanPackInput { + + /** + * The current indent. + */ + private String indent = ""; + /** + * The buffer. + */ + private final StringBuilder buf = new StringBuilder(1024); + + /** + * Creates an instance. + * + * @param bytes the bytes to read, not null + */ + BeanPackVisualizer(byte[] bytes) { + super(bytes); + } + + //----------------------------------------------------------------------- + /** + * Visualizes the data in the stream. + */ + String visualizeData() { + try { + readAll(); + return buf.toString(); + } catch (Exception ex) { + return buf.append("!!ERROR!!").append(System.lineSeparator()).append(ex.toString()).toString(); + } + } + + //----------------------------------------------------------------------- + @Override + Object readMapKey() throws IOException { + indent = indent + "= "; + var value = super.readMapKey(); + indent = indent.substring(0, indent.length() - 2); + return value; + } + + @Override + Object readMapValue() throws IOException { + indent = indent + " "; + var value = super.readMapValue(); + indent = indent.substring(0, indent.length() - 2); + return value; + } + + @Override + Object readArrayItem() throws IOException { + indent = indent + "- "; + var value = super.readArrayItem(); + indent = indent.substring(0, indent.length() - 2); + return value; + } + + @Override + Object readBeanItem() throws IOException { + indent = indent + "- "; + var value = super.readBeanItem(); + indent = indent.substring(0, indent.length() - 2); + return value; + } + + @Override + void handleObjectStart() { + buf.append(indent); + indent = indent.replace("-", " ").replace("=", " "); + } + + //------------------------------------------------------------------------- + @Override + void handleNull() { + buf.append("null").append(System.lineSeparator()); + } + + @Override + void handleBoolean(boolean bool) { + buf.append(bool).append(System.lineSeparator()); + } + + @Override + void handleFloat(float value) { + buf.append("flt ").append(value).append(System.lineSeparator()); + } + + @Override + void handleDouble(double value) { + buf.append("dbl ").append(value).append(System.lineSeparator()); + } + + @Override + void handleChar(char value) { + buf.append("chr ").append(value).append(System.lineSeparator()); + } + + @Override + void handleByte(byte value) { + buf.append("byt ").append(value).append(System.lineSeparator()); + } + + @Override + void handleShort(short value) { + buf.append("sht ").append(value).append(System.lineSeparator()); + } + + @Override + void handleInt(int value) { + buf.append("int ").append(value).append(System.lineSeparator()); + } + + @Override + void handleLong(long value) { + buf.append("lng ").append(value).append(System.lineSeparator()); + } + + //------------------------------------------------------------------------- + @Override + void handleDate(LocalDate date) { + buf.append(date).append(System.lineSeparator()); + } + + @Override + void handleTime(LocalTime time) { + buf.append(time).append(System.lineSeparator()); + } + + @Override + void handleInstant(Instant instant) { + buf.append(instant).append(System.lineSeparator()); + } + + @Override + void handleDuration(Duration duration) { + buf.append(duration).append(System.lineSeparator()); + } + + //------------------------------------------------------------------------- + @Override + void handleMapHeader(int size) { + buf.append("map (").append(size).append(")").append(System.lineSeparator()); + } + + @Override + void handleArrayHeader(int size) { + buf.append("arr (").append(size).append(")").append(System.lineSeparator()); + } + + @Override + void handleString(String str) { + buf.append("str '").append(str).append('\'').append(System.lineSeparator()); + } + + @Override + void handleBinary(byte[] bytes) { + buf.append("bin '"); + for (byte b : bytes) { + buf.append(toHex(b)); + } + buf.append("'").append(System.lineSeparator()); + } + + @Override + void handleDoubleArray(double[] values) { + buf.append("dbl ["); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buf.append(System.lineSeparator()).append(" "); + } + for (int j = 0; j < 8 && i < values.length; j++, i++) { + if (j > 0) { + buf.append(','); + } + buf.append(values[i]); + } + } + buf.append("]").append(System.lineSeparator()); + } + + //------------------------------------------------------------------------- + @Override + void handleTypeName(String typeName) throws IOException { + buf.append("@type ").append(typeName).append(System.lineSeparator()); + } + + @Override + void handleTypeReference(int ref, String typeName) throws IOException { + var str = ref < 0 ? typeName : ref + " " + typeName; + buf.append("@typeref ").append(str).append(System.lineSeparator()); + } + + @Override + void handleBeanHeader(int propertyCount) throws IOException { + buf.append("bean (").append(propertyCount).append(")").append(System.lineSeparator()); + } + + @Override + void handleValueDefinition() throws IOException { + buf.append("@value ").append(System.lineSeparator()); + } + + @Override + void handleValueReference(int ref, Object value) { + buf.append("ref ").append(ref).append(" '").append(value).append('\'').append(System.lineSeparator()); + } + + //------------------------------------------------------------------------- + @Override + void handleUnknown(byte b) { + buf.append("Unknown - ").append(String.format("%02X ", b)).append(System.lineSeparator()); + } +} diff --git a/src/test/java/org/joda/beans/TestClone.java b/src/test/java/org/joda/beans/TestClone.java index 5897b243..a806b0a0 100644 --- a/src/test/java/org/joda/beans/TestClone.java +++ b/src/test/java/org/joda/beans/TestClone.java @@ -61,6 +61,7 @@ void test_bean() { @Test void test_noclone_on_mutable_bean_option() { + // test that clone() was not code-generated Class c = NoClone.class; Method[] noCloneMethods = c.getDeclaredMethods(); diff --git a/src/test/java/org/joda/beans/TestMetaBeans.java b/src/test/java/org/joda/beans/TestMetaBeans.java index 69a7e95c..f81a23dd 100644 --- a/src/test/java/org/joda/beans/TestMetaBeans.java +++ b/src/test/java/org/joda/beans/TestMetaBeans.java @@ -28,7 +28,7 @@ class TestMetaBeans { @Test - public void test_metaBeanProviderAnnotation() { + void test_metaBeanProviderAnnotation() { MetaBean metaBean = MetaBeans.lookup(AnnotatedBean.class); assertThat(metaBean).isInstanceOf(AnnotatedMetaBean.class); }