/* * This file is part of Gradoop. * * Gradoop is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Gradoop is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Gradoop. If not, see <http://www.gnu.org/licenses/>. */ package org.gradoop.common.model.impl.properties; import edu.umd.cs.findbugs.annotations.SuppressWarnings; import org.apache.flink.core.memory.DataInputView; import org.apache.flink.core.memory.DataInputViewStreamWrapper; import org.apache.flink.core.memory.DataOutputView; import org.apache.flink.core.memory.DataOutputViewStreamWrapper; import org.apache.flink.types.Value; import org.apache.hadoop.hbase.util.Bytes; import org.gradoop.common.model.impl.id.GradoopId; import org.gradoop.common.storage.exceptions.UnsupportedTypeException; import org.gradoop.common.util.GConstants; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Represents a single property value in the EPGM. * * A property value wraps a value that implements a supported data type. */ public class PropertyValue implements Value, Serializable, Comparable<PropertyValue> { /** * Represents a property value that is {@code null}. */ public static final PropertyValue NULL_VALUE = PropertyValue.create(null); /** * Class version for serialization. */ private static final long serialVersionUID = 1L; /** * {@code <property-type>} for empty property value (i.e. {@code null}) */ public static final transient byte TYPE_NULL = 0x00; /** * {@code <property-type>} for {@link java.lang.Boolean} */ public static final transient byte TYPE_BOOLEAN = 0x01; /** * {@code <property-type>} for {@link java.lang.Integer} */ public static final transient byte TYPE_INTEGER = 0x02; /** * {@code <property-type>} for {@link java.lang.Long} */ public static final transient byte TYPE_LONG = 0x03; /** * {@code <property-type>} for {@link java.lang.Float} */ public static final transient byte TYPE_FLOAT = 0x04; /** * {@code <property-type>} for {@link java.lang.Double} */ public static final transient byte TYPE_DOUBLE = 0x05; /** * {@code <property-type>} for {@link java.lang.String} */ public static final transient byte TYPE_STRING = 0x06; /** * {@code <property-type>} for {@link java.lang.String} */ public static final transient byte TYPE_BIG_DECIMAL = 0x07; /** * {@code <property-type>} for {@link org.gradoop.common.model.impl.id.GradoopId} */ public static final transient byte TYPE_GRADOOP_ID = 0x08; /** * {@code <property-type>} for {@link java.util.HashMap} */ public static final transient byte TYPE_MAP = 0x09; /** * {@code <property-type>} for {@link java.util.List} */ public static final transient byte TYPE_LIST = 0x0a; /** * {@code <property-type>} for {@link java.util.List} */ public static final transient byte TYPE_DATE = 0x0b; /** * {@code <property-type>} for {@link java.util.List} */ public static final transient byte TYPE_TIME = 0x0c; /** * {@code <property-type>} for {@link java.util.List} */ public static final transient byte TYPE_DATETIME = 0x0d; /** * Value offset in byte */ public static final transient byte OFFSET = 0x01; /** * Stores the type and the value */ private byte[] rawBytes; /** * Default constructor. */ public PropertyValue() { } /** * Creates a new property value from the given value. * * If the given object type is not supported, an * {@link UnsupportedTypeException} will be thrown. * * @param value value with supported type */ private PropertyValue(Object value) { setObject(value); } /** * Creates a new property value from the given byte array. * * @param bytes byte array */ private PropertyValue(byte[] bytes) { rawBytes = bytes; } /** * Creates a new Property Value from the given object. * * If the given object type is not supported, an * {@link UnsupportedTypeException} will be thrown. * * @param value value with supported type * @return property value */ public static PropertyValue create(Object value) { return new PropertyValue(value); } //---------------------------------------------------------------------------- // Type checking //---------------------------------------------------------------------------- /** * True, if the value represents {@code null}. * * @return true, if {@code null} value */ public boolean isNull() { return rawBytes[0] == TYPE_NULL; } /** * True, if the wrapped value is of type {@code boolean}. * * @return true, if {@code boolean} value */ public boolean isBoolean() { return rawBytes[0] == TYPE_BOOLEAN; } /** * True, if the wrapped value is of type {@code int}. * * @return true, if {@code int} value */ public boolean isInt() { return rawBytes[0] == TYPE_INTEGER; } /** * True, if the wrapped value is of type {@code long}. * * @return true, if {@code long} value */ public boolean isLong() { return rawBytes[0] == TYPE_LONG; } /** * True, if the wrapped value is of type {@code float}. * * @return true, if {@code float} value */ public boolean isFloat() { return rawBytes[0] == TYPE_FLOAT; } /** * True, if the wrapped value is of type {@code double}. * * @return true, if {@code double} value */ public boolean isDouble() { return rawBytes[0] == TYPE_DOUBLE; } /** * True, if the wrapped value is of type {@code String}. * * @return true, if {@code String} value */ public boolean isString() { return rawBytes[0] == TYPE_STRING; } /** * True, if the wrapped value is of type {@code BigDecimal}. * * @return true, if {@code BigDecimal} value * @see BigDecimal */ public boolean isBigDecimal() { return rawBytes[0] == TYPE_BIG_DECIMAL; } /** * True, if the wrapped value is of type {@code GradoopId}. * * @return true, if {@code GradoopId} value */ public boolean isGradoopId() { return rawBytes[0] == TYPE_GRADOOP_ID; } /** * True, if the wrapped value is of type {@code Map}. * * @return true, if {@code Map} value */ public boolean isMap() { return rawBytes[0] == TYPE_MAP; } /** * True, if the wrapped value is of type {@code List}. * * @return true, if {@code List} value */ public boolean isList() { return rawBytes[0] == TYPE_LIST; } /** * True, if the wrapped value is of type {@code LocalDate}. * * @return true, if {@code LocalDate} value */ public boolean isDate() { return rawBytes[0] == TYPE_DATE; } /** * True, if the wrapped value is of type {@code LocalTime}. * * @return true, if {@code LocalTime} value */ public boolean isTime() { return rawBytes[0] == TYPE_TIME; } /** * True, if the wrapped value is of type {@code LocalDateTime}. * * @return true, if {@code LocalDateTime} value */ public boolean isDateTime() { return rawBytes[0] == TYPE_DATETIME; } //---------------------------------------------------------------------------- // Getter //---------------------------------------------------------------------------- /** * Returns the wrapped value as object. * * @return value or {@code null} if the value is empty */ public Object getObject() { return isBoolean() ? getBoolean() : isInt() ? getInt() : isLong() ? getLong() : isFloat() ? getFloat() : isDouble() ? getDouble() : isString() ? getString() : isBigDecimal() ? getBigDecimal() : isGradoopId() ? getGradoopId() : isMap() ? getMap() : isList() ? getList() : isDate() ? getDate() : isTime() ? getTime() : isDateTime() ? getDateTime() : null; } /** * Returns the wrapped value as {@code boolean}. * * @return {@code boolean} value */ public boolean getBoolean() { return rawBytes[1] == -1; } /** * Returns the wrapped value as {@code int}. * * @return {@code int} value */ public int getInt() { return Bytes.toInt(rawBytes, OFFSET); } /** * Returns the wrapped value as {@code long}. * * @return {@code long} value */ public long getLong() { return Bytes.toLong(rawBytes, OFFSET); } /** * Returns the wrapped value as {@code float}. * * @return {@code float} value */ public float getFloat() { return Bytes.toFloat(rawBytes, OFFSET); } /** * Returns the wrapped value as {@code double}. * * @return {@code double} value */ public double getDouble() { return Bytes.toDouble(rawBytes, OFFSET); } /** * Returns the wrapped value as {@code String}. * * @return {@code String} value */ public String getString() { return Bytes.toString(rawBytes, OFFSET, rawBytes.length - OFFSET); } /** * Returns the wrapped value as {@code BigDecimal}. * * @return {@code BigDecimal} value * @see BigDecimal */ public BigDecimal getBigDecimal() { BigDecimal decimal; if (isBigDecimal()) { decimal = Bytes.toBigDecimal(rawBytes, OFFSET, rawBytes.length - OFFSET); } else if (isFloat()) { decimal = BigDecimal.valueOf(Bytes.toFloat(rawBytes, OFFSET)); } else if (isDouble()) { decimal = BigDecimal.valueOf(Bytes.toDouble(rawBytes, OFFSET)); } else if (isInt()) { decimal = BigDecimal.valueOf(Bytes.toInt(rawBytes, OFFSET)); } else if (isLong()) { decimal = BigDecimal.valueOf(Bytes.toLong(rawBytes, OFFSET)); } else if (isString()) { decimal = new BigDecimal( Bytes.toString(rawBytes, OFFSET, rawBytes.length - OFFSET)); } else { throw new ClassCastException( "Cannot covert " + this.getType().getSimpleName() + " to " + Double.class.getSimpleName()); } return decimal; } /** * Returns the wrapped value as {@code GradoopId}. * * @return {@code GradoopId} value */ public GradoopId getGradoopId() { return GradoopId.fromByteArray( Arrays.copyOfRange(rawBytes, OFFSET, GradoopId.ID_SIZE + OFFSET)); } /** * Returns the wrapped Map as {@code Map<PropertyValue, PropertyValue>}. * * @return {@code Map<PropertyValue, PropertyValue>} value */ public Map<PropertyValue, PropertyValue> getMap() { PropertyValue key; PropertyValue value; Map<PropertyValue, PropertyValue> map = new HashMap<>(); ByteArrayInputStream byteStream = new ByteArrayInputStream(rawBytes); DataInputStream inputStream = new DataInputStream(byteStream); DataInputView inputView = new DataInputViewStreamWrapper(inputStream); try { if (inputStream.skipBytes(OFFSET) != OFFSET) { throw new RuntimeException("Malformed entry in PropertyValue List"); } while (inputStream.available() > 0) { key = new PropertyValue(); key.read(inputView); value = new PropertyValue(); value.read(inputView); map.put(key, value); } } catch (IOException e) { throw new RuntimeException("Error reading PropertyValue"); } return map; } /** * Returns the wrapped List as {@code List<PropertyValue>}. * * @return {@code List<PropertyValue>} value */ public List<PropertyValue> getList() { PropertyValue entry; List<PropertyValue> list = new ArrayList<>(); ByteArrayInputStream byteStream = new ByteArrayInputStream(rawBytes); DataInputStream inputStream = new DataInputStream(byteStream); DataInputView inputView = new DataInputViewStreamWrapper(inputStream); try { if (inputStream.skipBytes(OFFSET) != OFFSET) { throw new RuntimeException("Malformed entry in PropertyValue List"); } while (inputStream.available() > 0) { entry = new PropertyValue(); entry.read(inputView); list.add(entry); } } catch (IOException e) { throw new RuntimeException("Error reading PropertyValue"); } return list; } /** * Returns the wrapped List as {@code LocalDate}. * * @return {@code LocalDate} value */ public LocalDate getDate() { return DateTimeSerializer.deserializeDate( Arrays.copyOfRange(rawBytes, OFFSET, DateTimeSerializer.SIZEOF_DATE + OFFSET)); } /** * Returns the wrapped List as {@code LocalTime}. * * @return {@code LocalTime} value */ public LocalTime getTime() { return DateTimeSerializer.deserializeTime( Arrays.copyOfRange(rawBytes, OFFSET, DateTimeSerializer.SIZEOF_TIME + OFFSET)); } /** * Returns the wrapped List as {@code LocalDateTime}. * * @return {@code LocalDateTime} value */ public LocalDateTime getDateTime() { return DateTimeSerializer.deserializeDateTime( Arrays.copyOfRange(rawBytes, OFFSET, DateTimeSerializer.SIZEOF_DATETIME + OFFSET)); } //---------------------------------------------------------------------------- // Setter //---------------------------------------------------------------------------- /** * Sets the given value as internal value if it has a supported type. * * @param value value * @throws UnsupportedTypeException */ public void setObject(Object value) { if (value == null) { rawBytes = new byte[] {TYPE_NULL}; } else if (value instanceof Boolean) { setBoolean((Boolean) value); } else if (value instanceof Integer) { setInt((Integer) value); } else if (value instanceof Long) { setLong((Long) value); } else if (value instanceof Float) { setFloat((Float) value); } else if (value instanceof Double) { setDouble((Double) value); } else if (value instanceof String) { setString((String) value); } else if (value instanceof BigDecimal) { setBigDecimal((BigDecimal) value); } else if (value instanceof GradoopId) { setGradoopId((GradoopId) value); } else if (value instanceof Map) { setMap((Map) value); } else if (value instanceof List) { setList((List) value); } else if (value instanceof LocalDate) { setDate((LocalDate) value); } else if (value instanceof LocalTime) { setTime((LocalTime) value); } else if (value instanceof LocalDateTime) { setDateTime((LocalDateTime) value); } else { throw new UnsupportedTypeException(value.getClass()); } } /** * Sets the wrapped value as {@code boolean} value. * * @param booleanValue value */ public void setBoolean(boolean booleanValue) { rawBytes = new byte[OFFSET + Bytes.SIZEOF_BOOLEAN]; rawBytes[0] = TYPE_BOOLEAN; Bytes.putByte(rawBytes, OFFSET, (byte) (booleanValue ? -1 : 0)); } /** * Sets the wrapped value as {@code int} value. * * @param intValue intValue */ public void setInt(int intValue) { rawBytes = new byte[OFFSET + Bytes.SIZEOF_INT]; rawBytes[0] = TYPE_INTEGER; Bytes.putInt(rawBytes, OFFSET, intValue); } /** * Sets the wrapped value as {@code long} value. * * @param longValue value */ public void setLong(long longValue) { rawBytes = new byte[OFFSET + Bytes.SIZEOF_LONG]; rawBytes[0] = TYPE_LONG; Bytes.putLong(rawBytes, OFFSET, longValue); } /** * Sets the wrapped value as {@code float} value. * * @param floatValue value */ public void setFloat(float floatValue) { rawBytes = new byte[OFFSET + Bytes.SIZEOF_FLOAT]; rawBytes[0] = TYPE_FLOAT; Bytes.putFloat(rawBytes, OFFSET, floatValue); } /** * Sets the wrapped value as {@code double} value. * * @param doubleValue value */ public void setDouble(double doubleValue) { rawBytes = new byte[OFFSET + Bytes.SIZEOF_DOUBLE]; rawBytes[0] = TYPE_DOUBLE; Bytes.putDouble(rawBytes, OFFSET, doubleValue); } /** * Sets the wrapped value as {@code String} value. * * @param stringValue value */ public void setString(String stringValue) { byte[] valueBytes = Bytes.toBytes(stringValue); rawBytes = new byte[OFFSET + valueBytes.length]; rawBytes[0] = TYPE_STRING; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } /** * Sets the wrapped value as {@code BigDecimal} value. * * @param bigDecimalValue value */ public void setBigDecimal(BigDecimal bigDecimalValue) { byte[] valueBytes = Bytes.toBytes(bigDecimalValue); rawBytes = new byte[OFFSET + valueBytes.length]; rawBytes[0] = TYPE_BIG_DECIMAL; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } /** * Sets the wrapped value as {@code GradoopId} value. * * @param gradoopIdValue value */ public void setGradoopId(GradoopId gradoopIdValue) { byte[] valueBytes = gradoopIdValue.toByteArray(); rawBytes = new byte[OFFSET + GradoopId.ID_SIZE]; rawBytes[0] = TYPE_GRADOOP_ID; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } /** * Sets the wrapped value as {@code Map} value. * * @param map value */ public void setMap(Map<PropertyValue, PropertyValue> map) { int size = map.keySet().stream().mapToInt(PropertyValue::byteSize).sum() + map.values().stream().mapToInt(PropertyValue::byteSize).sum() + OFFSET; ByteArrayOutputStream byteStream = new ByteArrayOutputStream(size); DataOutputStream outputStream = new DataOutputStream(byteStream); DataOutputView outputView = new DataOutputViewStreamWrapper(outputStream); try { outputStream.write(TYPE_MAP); for (Map.Entry<PropertyValue, PropertyValue> entry : map.entrySet()) { entry.getKey().write(outputView); entry.getValue().write(outputView); } } catch (IOException e) { throw new RuntimeException("Error writing PropertyValue"); } this.rawBytes = byteStream.toByteArray(); } /** * Sets the wrapped value as {@code List} value. * * @param list value */ public void setList(List<PropertyValue> list) { int size = list.stream().mapToInt(PropertyValue::byteSize).sum() + OFFSET; ByteArrayOutputStream byteStream = new ByteArrayOutputStream(size); DataOutputStream outputStream = new DataOutputStream(byteStream); DataOutputView outputView = new DataOutputViewStreamWrapper(outputStream); try { outputStream.write(TYPE_LIST); for (PropertyValue entry : list) { entry.write(outputView); } } catch (IOException e) { throw new RuntimeException("Error writing PropertyValue"); } this.rawBytes = byteStream.toByteArray(); } /** * Sets the wrapped value as {@code LocalDate} value. * * @param date value */ public void setDate(LocalDate date) { byte[] valueBytes = DateTimeSerializer.serializeDate(date); rawBytes = new byte[OFFSET + DateTimeSerializer.SIZEOF_DATE]; rawBytes[0] = TYPE_DATE; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } /** * Sets the wrapped value as {@code LocalTime} value. * * @param time value */ public void setTime(LocalTime time) { byte[] valueBytes = DateTimeSerializer.serializeTime(time); rawBytes = new byte[OFFSET + DateTimeSerializer.SIZEOF_TIME]; rawBytes[0] = TYPE_TIME; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } /** * Sets the wrapped value as {@code LocalDateTime} value. * * @param dateTime value */ public void setDateTime(LocalDateTime dateTime) { byte[] valueBytes = DateTimeSerializer.serializeDateTime(dateTime); rawBytes = new byte[OFFSET + DateTimeSerializer.SIZEOF_DATETIME]; rawBytes[0] = TYPE_DATETIME; Bytes.putBytes(rawBytes, OFFSET, valueBytes, 0, valueBytes.length); } //---------------------------------------------------------------------------- // Util //---------------------------------------------------------------------------- public Class<?> getType() { return rawBytes[0] == TYPE_BOOLEAN ? Boolean.class : rawBytes[0] == TYPE_INTEGER ? Integer.class : rawBytes[0] == TYPE_LONG ? Long.class : rawBytes[0] == TYPE_FLOAT ? Float.class : rawBytes[0] == TYPE_DOUBLE ? Double.class : rawBytes[0] == TYPE_STRING ? String.class : rawBytes[0] == TYPE_BIG_DECIMAL ? BigDecimal.class : rawBytes[0] == TYPE_GRADOOP_ID ? GradoopId.class : rawBytes[0] == TYPE_MAP ? Map.class : rawBytes[0] == TYPE_LIST ? LocalDate.class : rawBytes[0] == TYPE_DATE ? LocalTime.class : rawBytes[0] == TYPE_TIME ? LocalDateTime.class : rawBytes[0] == TYPE_DATETIME ? List.class : null; } public int getByteSize() { return rawBytes.length; } @SuppressWarnings("EI_EXPOSE_REP") public byte[] getRawBytes() { return this.rawBytes; } @SuppressWarnings("EI_EXPOSE_REP") public void setBytes(byte[] bytes) { this.rawBytes = bytes; } public static PropertyValue fromRawBytes(byte[] rawBytes) { return new PropertyValue(rawBytes); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PropertyValue)) { return false; } PropertyValue that = (PropertyValue) o; return Arrays.equals(rawBytes, that.rawBytes); } @Override public int hashCode() { return Arrays.hashCode(rawBytes); } @Override public int compareTo(PropertyValue o) { int result; if (this.isNull() && o.isNull()) { result = 0; } else if (this.isBoolean() && o.isBoolean()) { result = Boolean.compare(this.getBoolean(), o.getBoolean()); } else if (this.isInt() && o.isInt()) { result = Integer.compare(this.getInt(), o.getInt()); } else if (this.isLong() && o.isLong()) { result = Long.compare(this.getLong(), o.getLong()); } else if (this.isFloat() && o.isFloat()) { result = Float.compare(this.getFloat(), o.getFloat()); } else if (this.isDouble() && o.isDouble()) { result = Double.compare(this.getDouble(), o.getDouble()); } else if (this.isString() && o.isString()) { result = this.getString().compareTo(o.getString()); } else if (this.isBigDecimal() && o.isBigDecimal()) { result = this.getBigDecimal().compareTo(o.getBigDecimal()); } else if (this.isGradoopId() && o.isGradoopId()) { result = this.getGradoopId().compareTo(o.getGradoopId()); } else if (this.isMap() || o.isMap() || this.isList() || o.isList()) { throw new UnsupportedOperationException(String.format( "Method compareTo() is not supported for %s, %s", this.getClass(), o.getClass())); } else { throw new IllegalArgumentException(String.format( "Incompatible types: %s, %s", this.getClass(), o.getClass())); } return result; } /** * Returns the byte size of the properties internal representation * @return byte size */ public int byteSize() { return rawBytes.length; } /** * Byte representation: * * byte 1 : type info * * for dynamic length types (e.g. String and BigDecimal) * byte 2 : length (short) * byte 3 : length (short) * byte 4 - end : value bytes * * for fixed length types (e.g. int, long, float, ...) * byte 2 - end : value bytes * * @param outputView data output to write data to * @throws IOException */ @Override public void write(DataOutputView outputView) throws IOException { // null? // type outputView.writeByte(rawBytes[0]); // dynamic type? if (rawBytes[0] == TYPE_STRING || rawBytes[0] == TYPE_BIG_DECIMAL || rawBytes[0] == TYPE_MAP || rawBytes[0] == TYPE_LIST) { // write length outputView.writeShort(rawBytes.length - OFFSET); } // write data outputView.write(rawBytes, OFFSET, rawBytes.length - OFFSET); } @Override public void read(DataInputView inputView) throws IOException { short length = 0; // type byte type = inputView.readByte(); // dynamic type? if (type == TYPE_STRING || type == TYPE_BIG_DECIMAL || type == TYPE_MAP || type == TYPE_LIST) { // read length length = inputView.readShort(); } else if (type == TYPE_NULL) { length = 0; } else if (type == TYPE_BOOLEAN) { length = Bytes.SIZEOF_BOOLEAN; } else if (type == TYPE_INTEGER) { length = Bytes.SIZEOF_INT; } else if (type == TYPE_LONG) { length = Bytes.SIZEOF_LONG; } else if (type == TYPE_FLOAT) { length = Bytes.SIZEOF_FLOAT; } else if (type == TYPE_DOUBLE) { length = Bytes.SIZEOF_DOUBLE; } else if (type == TYPE_GRADOOP_ID) { length = GradoopId.ID_SIZE; } else if (type == TYPE_DATE) { length = DateTimeSerializer.SIZEOF_DATE; } else if (type == TYPE_TIME) { length = DateTimeSerializer.SIZEOF_TIME; } else if (type == TYPE_DATETIME) { length = DateTimeSerializer.SIZEOF_DATETIME; } // init new array rawBytes = new byte[OFFSET + length]; // read type info rawBytes[0] = type; // read data for (int i = OFFSET; i < rawBytes.length; i++) { rawBytes[i] = inputView.readByte(); } } @Override public String toString() { return getObject() != null ? getObject().toString() : GConstants.NULL_STRING; } }