All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.highmobility.autoapi.property.Property Maven / Gradle / Ivy

/*
 * The MIT License
 *
 * Copyright (c) 2014- High-Mobility GmbH (https://high-mobility.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.highmobility.autoapi.property;

import com.highmobility.autoapi.Command;
import com.highmobility.autoapi.CommandParseException;
import com.highmobility.autoapi.exception.ParseException;
import com.highmobility.autoapi.value.Availability;
import com.highmobility.value.Bytes;

import javax.annotation.Nullable;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

import static com.highmobility.autoapi.AutoApiLogger.getLogger;

/**
 * Property is a representation of some AutoAPI data. It consists of 4 optional components: data, unit
 * timestamp, failure
 *
 * @param  The value type. Eg Integer, CheckControlMessage, Duration
 */
public class Property extends Bytes {
    /*
    Property is created/updated in 3 places:

    1:
    Incoming command:
    Base Command parses the components but doesn't know the type. Sub command updates its
    properties with base components.

    base command:
    properties[i] = new Property(bytes);
    findComponents(bytes)

    sub command update is needed because base does not know the property type:
     >> field.update(property) > copy the components to sub property, replace the base property
      with parsed one

    2:
    User creates the property herself and passes it to builder.
    IntegerProperty is updated later in the builder to set length/sign
    * new Property(GasFlapState) > update(GasFlapState, timestamp, null) >> goto 3
    or
    * new Property(null, null, failure) > update(null, null, failure) >> goto 3

    3:
    in control commands the object is created as field, but updated with real value (eg
    GasFlapState).
    Property field = new Property(GasFlapState.class, IDENTIFIER)

    ControlGasFlap((GasFlapState, timestamp, failure) {
        field.update(GasFlapState, timestamp, failure)
         > bytes = setBytes(GasFlapState, timestamp, failure);
         > findComponents()
    }
     */

    protected static final byte[] unknownBytes = new byte[]{0x00, 0x00, 0x00};

    @Nullable
    protected PropertyComponentValue value;
    @Nullable
    protected PropertyComponentTimestamp timestamp;
    @Nullable
    protected PropertyComponentFailure failure;
    @Nullable
    protected PropertyComponentAvailability availability;

    protected List components;

    private Class valueClass = null;

    public byte getPropertyIdentifier() {
        return bytes[0];
    }

    /**
     * @return The property value.
     */
    @Nullable
    public V getValue() {
        return value != null ? value.getValue() : null;
    }

    /**
     * @return All of the components.
     */
    public List getComponents() {
        return components;
    }

    /**
     * @param identifier The component identifier.
     * @return The component with the given identifier.
     */
    @Nullable
    public PropertyComponent getComponent(byte identifier) {
        for (int i = 0; i < components.size(); i++) {
            PropertyComponent component = components.get(i);
            if (component.getIdentifier() == identifier) return component;
        }
        return null;
    }

    /**
     * @return The value component.
     */
    @Nullable
    public PropertyComponentValue getValueComponent() {
        return value;
    }

    /**
     * @return The timestamp of the property.
     */
    @Nullable
    public Calendar getTimestamp() {
        if (timestamp == null) return null;
        return timestamp.getCalendar();
    }

    @Nullable
    PropertyComponentTimestamp getTimestampComponent() {
        return timestamp;
    }

    /**
     * @return The availability of the property.
     */
    @Nullable
    public Availability getAvailability() {
        return availability.getAvailability();
    }

    @Nullable
    PropertyComponentAvailability getAvailabilityComponent() {
        return availability;
    }

    /**
     * @return The failure of the property.
     */
    @Nullable
    public PropertyComponentFailure getFailureComponent() {
        return failure;
    }

    @Nullable
    Class getValueClass() {
        return valueClass;
    }

    /**
     * @return The length of the property components(not including the header).
     */
    public int getPropertyLength() {
        return getLength() - 3;
    }

    // MARK: incoming ctor

    // cannot create Bytes ctor because it would overlap with some Bytes subclasses that need to use
    // the class ctor.
    public Property(byte[] bytes) {
        if (bytes == null || bytes.length == 0) bytes = unknownBytes;
        if (bytes.length < 3) bytes = Arrays.copyOf(bytes, 3);
        this.bytes = bytes;
        findComponents();
    }

    protected void findComponents() {
        ArrayList builder = new ArrayList<>();

        for (int i = 3; i < bytes.length; i++) {
            int size = getUnsignedInt(bytes, i + 1, 2);
            byte componentIdentifier = bytes[i];
            Bytes componentBytes = getRange(i, i + 3 + size);

            try {
                switch (componentIdentifier) {
                    case 0x01:
                        value = new PropertyComponentValue<>(componentBytes, valueClass);
                        builder.add(value);
                        break;
                    case 0x02:
                        timestamp = new PropertyComponentTimestamp(componentBytes);
                        builder.add(timestamp);
                        break;
                    case 0x03:
                        failure = new PropertyComponentFailure(componentBytes);
                        builder.add(failure);
                        break;
                    case 0x05:
                        availability = new PropertyComponentAvailability(componentBytes);
                        builder.add(availability);
                        break;
                }
            } catch (Exception e) {
                builder.add(new PropertyComponent(componentBytes));
                printFailedToParse(e, componentBytes);
            }

            i += size + 2;
        }

        components = builder;
    }

    public Property(@Nullable V value) {
        update((byte) 0, value, null, null, null);
    }

    public Property update(Property p) throws CommandParseException {
        if (valueClass == null)
            throw new IllegalArgumentException("Initialise with a class to update.");

        this.bytes = p.getByteArray();
        this.value = p.value;

        if (this.value != null) {
            try {
                this.value.setClass(valueClass);
            } catch (Exception e) {
                getLogger().debug(String.format("Invalid bytes %s for property: %s\n%s", p, valueClass.getSimpleName(), e));
            }
        }

        this.components = p.components;
        this.timestamp = p.timestamp;
        this.failure = p.failure;
        this.availability = p.availability;

        return this;
    }

    // MARK: builder ctor

    public Property(byte identifier,
                    @Nullable V value,
                    @Nullable Calendar timestamp,
                    @Nullable PropertyComponentFailure failure,
                    @Nullable PropertyComponentAvailability availability) {
        update(identifier, value, timestamp, failure, availability);
    }

    // MARK: internal ctor

    public Property(int identifier, V value) {
        this((byte) identifier, value);
    }

    public Property(byte identifier, V value) {
        update(identifier, value, null, null, null);
    }

    public Property(Class valueClass, int identifier) {
        this(valueClass, ((byte) identifier));
    }

    public Property(Class valueClass, byte identifier) {
        this.bytes = new byte[]{identifier, 0, 0};
        this.valueClass = valueClass;
    }

    public Property(Class valueClass, Property property) throws CommandParseException {
        this.valueClass = valueClass;
        if (property == null || property.getLength() == 0) this.bytes = unknownBytes;
        if (property.getLength() < 3) this.bytes = Arrays.copyOf(property.getByteArray(), 3);
        else this.bytes = property.getByteArray();
        update(property);
    }

    public Property update(V value) {
        return update(bytes[0], value, null, null, null);
    }

    public Property addValueComponent(Bytes valueComponentValue) {
        try {
            byte[] valueLength = Property.intToBytes(valueComponentValue.getLength(), 2);
            Bytes value = new Bytes(3 + valueComponentValue.getLength());
            value.set(0, (byte) 0x01);
            value.set(1, valueLength);
            value.set(3, valueComponentValue);
            this.value = new PropertyComponentValue<>(value, valueClass);
        } catch (CommandParseException e) {
            throw new IllegalArgumentException();
        }
        createBytesFromComponents(getPropertyIdentifier());
        return this;
    }

    private Property update(byte identifier,
                            @Nullable V value,
                            @Nullable Calendar timestamp,
                            @Nullable PropertyComponentFailure failure,
                            @Nullable PropertyComponentAvailability availability) {
        if (value != null && failure == null) this.value = new PropertyComponentValue<>(value);
        if (timestamp != null) this.timestamp = new PropertyComponentTimestamp(timestamp);
        if (failure != null) this.failure = failure;
        if (availability != null) this.availability = availability;

        createBytesFromComponents(identifier);

        return this;
    }

    protected void createBytesFromComponents(byte propertyIdentifier) {
        int componentBytesLength = 0;
        int componentsSize = 0;

        if (value != null) {
            componentBytesLength += value.getLength();
            componentsSize++;
        }

        if (timestamp != null) {
            componentBytesLength += timestamp.getLength();
            componentsSize++;
        }

        if (failure != null) {
            componentBytesLength += failure.getLength();
            componentsSize++;
        }

        if (availability != null) {
            componentBytesLength += availability.getLength();
            componentsSize++;
        }

        ArrayList componentsBuilder = new ArrayList<>(componentsSize);
        Bytes builder = new Bytes(3 + componentBytesLength);

        builder.set(0, propertyIdentifier);
        builder.set(1, Property.intToBytes(componentBytesLength, 2));

        int pointer = 3;

        if (value != null) {
            builder.set(pointer, value);
            pointer += value.getLength();
            componentsBuilder.add(value);
        }

        if (timestamp != null) {
            builder.set(pointer, timestamp);
            pointer += timestamp.getLength();
            componentsBuilder.add(timestamp);
        }

        if (failure != null) {
            builder.set(pointer, failure);
            pointer += failure.getLength();
            componentsBuilder.add(failure);
        }

        if (availability != null) {
            builder.set(pointer, availability);
            componentsBuilder.add(availability);
        }

        bytes = builder.getByteArray();
        components = componentsBuilder;
    }

    /**
     * Set a new identifier for the property
     *
     * @param identifier The identifier.
     * @return Self.
     */
    public Property setIdentifier(byte identifier) {
        bytes[0] = identifier;
        return this;
    }

    public Property setIdentifier(int identifier) {
        setIdentifier((byte) identifier);
        return this;
    }

    public boolean isUniversalProperty() {
        byte propertyIdentifier = getPropertyIdentifier();
        return propertyIdentifier == Command.SIGNATURE_IDENTIFIER ||
                propertyIdentifier == Command.NONCE_IDENTIFIER ||
                propertyIdentifier == Command.TIMESTAMP_IDENTIFIER ||
                propertyIdentifier == Command.VIN_IDENTIFIER ||
                propertyIdentifier == Command.BRAND_IDENTIFIER;
    }

    public void printFailedToParse(Exception e, Bytes component) {
        String componentString = (component != null ? " | Component " + component : "");

        String exceptionString = (e != null ?
                (" | " + e.getClass().getSimpleName() + ": " + e.getMessage()) : "");

        getLogger().error(String.format("Failed to parse property: " + toString() + componentString + exceptionString + "\n%s", e));
    }

    // MARK: ctor helpers

    public static long getLong(byte[] b, int at) throws IllegalArgumentException {
        return getLong(b, at, 8);
    }

    public static long getLong(byte[] b, int at, int length) throws IllegalArgumentException {
        if (b.length - at < length) throw new IllegalArgumentException();

        long result = 0;
        for (int i = at; i < at + length; i++) {
            result <<= 8;
            result |= (b[i] & 0xFF);
        }

        return result;
    }

    public static long getLong(byte[] b) throws IllegalArgumentException {
        return getLong(b, 0, b.length);
    }

    public static byte[] longToBytes(long l) {
        byte[] result = new byte[8];
        for (int i = 7; i >= 0; i--) {
            result[i] = (byte) (l & 0xFF);
            l >>= 8;
        }
        return result;
    }

    public static float getFloat(Bytes bytes) throws IllegalArgumentException {
        return getFloat(bytes.getByteArray());
    }

    public static float getFloat(byte[] bytes) throws IllegalArgumentException {
        if (bytes.length < 4) throw new IllegalArgumentException();
        return Float.intBitsToFloat(getUnsignedInt(bytes, 0, 4));
    }

    public static float getFloat(byte[] bytes, int at) throws IllegalArgumentException {
        int intValue = getUnsignedInt(bytes, at, 4);
        return Float.intBitsToFloat(intValue);
    }

    public static float getFloat(Bytes bytes, int at) throws IllegalArgumentException {
        int intValue = getUnsignedInt(bytes.getByteArray(), at, 4);
        return Float.intBitsToFloat(intValue);
    }

    public static byte[] floatToBytes(float value) {
        return ByteBuffer.allocate(4).putFloat(value).array();
    }

    public static double getDouble(Bytes bytes) throws IllegalArgumentException {
        return getDouble(bytes.getByteArray());
    }

    public static double getDouble(byte[] bytes) throws IllegalArgumentException {
        if (bytes.length < 8) throw new IllegalArgumentException();
        return Double.longBitsToDouble(getLong(bytes));
    }

    public static double getDouble(byte[] bytes, int at) throws IllegalArgumentException {
        return Double.longBitsToDouble(getLong(bytes, at));
    }

    public static double getDouble(Bytes bytes, int at) throws IllegalArgumentException {
        return getDouble(bytes.getByteArray(), at);
    }

    public static byte[] doubleToBytes(double value) {
        long bits = Double.doubleToLongBits(value);
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.putLong(bits);
        return buffer.array();
    }

    public static int floatToIntPercentage(float value) {
        return Math.round(value * 100f);
    }

    public static byte floatToIntPercentageByte(float value) {
        return (byte) Math.round(value * 100f);
    }

    public static float getPercentage(byte value) {
        return getUnsignedInt(value) / 100f;
    }

    public static int getUnsignedInt(byte value) {
        return value & 0xFF;
    }

    public static int getUnsignedInt(Bytes bytes) throws IllegalArgumentException {
        return getUnsignedInt(bytes.getByteArray());
    }

    public static int getUnsignedInt(byte[] bytes) throws IllegalArgumentException {
        return getUnsignedInt(bytes, 0, bytes.length);
    }

    public static int getUnsignedInt(Bytes bytes, int at, int length) throws
            IllegalArgumentException {
        return getUnsignedInt(bytes.getByteArray(), at, length);
    }

    public static int getUnsignedInt(byte[] bytes, int at, int length) throws
            IllegalArgumentException {
        if (bytes.length >= at + length) {
            if (length == 4) {
                return ((0xFF & bytes[at]) << 24) | ((0xFF & bytes[at + 1]) << 16) |
                        ((0xFF & bytes[at + 2]) << 8) | (0xFF & bytes[at + 3]);
            } else if (length == 3) {
                return (bytes[at] & 0xff) << 16 | (bytes[at + 1] & 0xff) << 8 | (bytes[at + 2]
                        & 0xff);
            } else if (length == 2) {
                return ((bytes[at] & 0xff) << 8) | (bytes[at + 1] & 0xff);
            } else if (length == 1) {
                return bytes[at] & 0xFF;
            }
        }

        throw new IllegalArgumentException();
    }

    public static int getSignedInt(byte value) {
        return value;
    }

    public static int getSignedInt(Bytes bytes) {
        return getSignedInt(bytes.getByteArray());
    }

    public static int getSignedInt(byte[] bytes) {
        if (bytes.length == 1) return getSignedInt(bytes[0]);
        else if (bytes.length >= 2) {
            return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getShort();
        }

        throw new IllegalArgumentException();
    }

    public static int getSignedInt(byte[] bytes, int at, int length) {
        if (bytes.length >= 2) return bytes[at] << 8 | bytes[at + 1];
        throw new IllegalArgumentException();
    }

    /**
     * This works for both negative and positive ints.
     *
     * @param value  the valueBytes converted to byte[]
     * @param length the returned byte[] length
     * @return the allBytes representing the valueBytes
     * @throws IllegalArgumentException when input is invalid
     */
    public static byte[] intToBytes(int value, int length) throws IllegalArgumentException {
        if (length == 1) return new byte[]{(byte) value};

        byte[] bytes = BigInteger.valueOf(value).toByteArray();

        if (bytes.length == length) {
            return bytes;
        } else if (bytes.length < length) {
            // put the allBytes to last elements
            byte[] withZeroBytes = new byte[length];

            for (int i = 0; i < bytes.length; i++) {
                withZeroBytes[length - 1 - i] = bytes[bytes.length - 1 - i];
            }

            return withZeroBytes;
        } else {
            throw new IllegalArgumentException();
        }
    }

    public static Boolean getBool(byte value) {
        return value != 0x00;
    }

    public static byte boolToByte(boolean value) {
        return (byte) (value ? 0x01 : 0x00);
    }

    public static byte getByte(boolean value) {
        return (byte) (value == false ? 0x00 : 0x01);
    }

    public static String getString(Bytes bytes) {
        return getString(bytes.getByteArray());
    }

    public static String getString(byte[] bytes) {
        try {
            return new String(bytes, PropertyComponentValue.CHARSET);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            throw new ParseException();
        }
    }

    public static String getString(Bytes bytes, int at, int length) {
        return getString(bytes.getByteArray(), at, length);
    }

    public static String getString(byte[] bytes, int at, int length) {
        return getString(Arrays.copyOfRange(bytes, at, at + length));
    }

    public static byte[] stringToBytes(String string) {
        try {
            return string.getBytes(PropertyComponentValue.CHARSET);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            throw new ParseException();
        }
    }

    public static int getUtf8Length(CharSequence sequence) {
        int count = 0;
        for (int i = 0, len = sequence.length(); i < len; i++) {
            char ch = sequence.charAt(i);
            if (ch <= 0x7F) {
                count++;
            } else if (ch <= 0x7FF) {
                count += 2;
            } else if (Character.isHighSurrogate(ch)) {
                count += 4;
                ++i;
            } else {
                count += 3;
            }
        }
        return count;
    }


    // 5 allBytes eg yy mm dd mm ss. year is from 2000
    public static Date getDate(byte[] bytes) throws IllegalArgumentException {
        for (int i = 0; i < bytes.length; i++) {
            if (i == bytes.length - 1 && bytes[i] == 0x00) return null; // all allBytes are 0x00
            else if (bytes[i] != 0x00) break; // one byte is not 0x00, some date is set
        }

        Calendar c = Calendar.getInstance();

        if (bytes.length == 5) {
            c.set(2000 + bytes[0], bytes[1] - 1, bytes[2], bytes[3], bytes[4], 0x00);
        } else if (bytes.length >= 6) {
            c.set(2000 + bytes[0], bytes[1] - 1, bytes[2], bytes[3], bytes[4], bytes[5]);
        } else {
            throw new IllegalArgumentException();
        }

        return c.getTime();
    }

    public static Calendar getCalendar(Property property) throws IllegalArgumentException {
        return getCalendar(property.getValueComponent().getValueBytes());
    }

    public static Calendar getCalendar(Bytes bytes) throws IllegalArgumentException {
        return getCalendar(bytes.getByteArray());
    }

    public static Calendar getCalendar(byte[] bytes) throws IllegalArgumentException {
        return getCalendar(bytes, 0, bytes.length);
    }

    public static Calendar getCalendar(byte[] bytes, int at) throws IllegalArgumentException {
        return getCalendar(bytes, at, PropertyComponentValue.CALENDAR_SIZE);
    }

    public static Calendar getCalendar(byte[] bytes, int at, int length) throws IllegalArgumentException {
        Calendar c;
        if (bytes.length >= at + length) {
            c = new GregorianCalendar();
            long epoch = Property.getLong(bytes, at, length);
            c.setTimeInMillis(epoch);
        } else {
            throw new IllegalArgumentException();
        }

        c.getTime(); // this is needed to set the right time...
        return c;
    }

    public static byte[] calendarToBytes(Calendar calendar) {
        return Property.longToBytes(calendar.getTimeInMillis());
    }

    public static int[] getIntegerArray(Bytes valueBytes) {
        int length = valueBytes.getLength();

        int[] value = new int[length];
        for (int i = 0; i < length; i++) {
            value[i] = Property.getUnsignedInt(valueBytes.get(i));
        }

        return value;
    }

    public static Bytes integerArrayToBytes(int[] value) {
        Bytes result = new Bytes(value.length);

        for (int i = 0; i < value.length; i++) {
            byte byteValue = Property.intToBytes(value[i], 1)[0];
            result.set(i, byteValue);
        }

        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy