org.lwes.ArrayEvent.java Source code

Java tutorial

Introduction

Here is the source code for org.lwes.ArrayEvent.java

Source

/*======================================================================*
 * Licensed under the New BSD License (the "License"); you may not use  *
 * this file except in compliance with the License.  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. See accompanying LICENSE file.        *
 *======================================================================*/
package org.lwes;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang3.mutable.MutableInt;
import org.lwes.serializer.Deserializer;
import org.lwes.serializer.DeserializerState;
import org.lwes.serializer.Serializer;
import org.lwes.util.EncodedString;

public final class ArrayEvent extends DefaultEvent {

    private static final int SERIALIZED_ENCODING_LENGTH;
    private byte[] bytes = new byte[MAX_MESSAGE_SIZE];
    private final DeserializerState tempState = new DeserializerState();
    private int length = 3;
    private short encoding = DEFAULT_ENCODING;
    private static Map<ArrayEventStats, MutableInt> STATS = new EnumMap<ArrayEventStats, MutableInt>(
            ArrayEventStats.class);

    static {
        byte[] temp = new byte[256];
        SERIALIZED_ENCODING_LENGTH = Serializer.serializeATTRIBUTEWORD(ENCODING, temp, 0);
        resetStats();
    }

    //  * EVENTWORD,<UINT16 number of elements>,ATTRIBUTEWORD,TYPETOKEN,
    //  * (UINT16|INT16|UINT32|INT32|UINT64|INT64|BOOLEAN|STRING)
    //  * ...ATTRIBUTEWORD,TYPETOKEN(UINT16|INT16|UINT32|INT32|
    //  * UINT64|INT64|BOOLEAN|STRING)

    public ArrayEvent() {
        length = getValueListIndex();
        setEncoding(DEFAULT_ENCODING);
        final MutableInt creations = STATS.get(ArrayEventStats.CREATIONS);
        final MutableInt deletions = STATS.get(ArrayEventStats.DELETIONS);
        final MutableInt highwater = STATS.get(ArrayEventStats.HIGHWATER);
        creations.increment();
        highwater.setValue(Math.max(highwater.intValue(), creations.intValue() - deletions.intValue()));
    }

    public ArrayEvent(String name) throws EventSystemException {
        this();
        setEventName(name);
    }

    public ArrayEvent(byte[] bytes) {
        this();
        this.length = bytes.length;
        System.arraycopy(bytes, 0, this.bytes, 0, length);
        resetCaches();
    }

    private ArrayEvent(byte[] bytes, int offset, int length, int excess) {
        this();
        this.bytes = Arrays.copyOfRange(bytes, offset, length + excess);
        this.length = length;
        resetCaches();
    }

    private ArrayEvent(byte[] bytes, int length, short encoding) {
        this();
        System.arraycopy(bytes, 0, this.bytes, 0, length);
        this.length = length;
        this.encoding = encoding;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        STATS.get(ArrayEventStats.DELETIONS).increment();
    }

    @Override
    public void reset() {
        Arrays.fill(bytes, (byte) 0);
        length = getValueListIndex();
        tempState.reset();
        encoding = DEFAULT_ENCODING;
    }

    @Override
    public void clear(String key) {
        final int fieldIndex = find(key);
        if (fieldIndex < 0) {
            return;
        }
        final int tokenIndex = getTokenIndexFromFieldIndex(fieldIndex);
        final FieldType type = FieldType.byToken(bytes[tokenIndex]);
        final int valueIndex = tokenIndex + 1;
        final int nextIndex = valueIndex + getValueByteSize(type, valueIndex);
        shiftTail(nextIndex, fieldIndex);
        setNumEventAttributes(getNumEventAttributes() - 1);
    }

    @Override
    public void setEventName(String name) {
        checkShortStringLength(name, encoding, MAX_EVENT_NAME_SIZE);
        final String oldName = getEventName();
        final String defaultEncodingString = ENCODING_STRINGS[DEFAULT_ENCODING].getEncodingString();
        try {
            final byte[] oldBytes = oldName.getBytes(defaultEncodingString);
            final byte[] newBytes = name.getBytes(defaultEncodingString);
            if (oldBytes != newBytes) {
                final int numFields = getNumEventAttributes();
                final int oldValueListIndex = getValueListIndex();
                final int newValueListIndex = oldValueListIndex + newBytes.length - oldBytes.length;
                Serializer.serializeUBYTE((short) newBytes.length, bytes, 0);
                shiftTail(oldValueListIndex, newValueListIndex);
                int offset = Serializer.serializeEVENTWORD(name, bytes, 0);
                Serializer.serializeUINT16(numFields, bytes, offset);
            }
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unknown Encoding: " + defaultEncodingString);
        }
    }

    /**
     * For now, no in-place set() operations occur. set() places the field at the
     * end of the datagram, with the exception of ENCODING, which must be first.
     */
    @Override
    public void set(String key, FieldType type, Object value) {
        checkShortStringLength(key, encoding, MAX_FIELD_NAME_SIZE);
        if (ENCODING.equals(key)) {
            if (type == FieldType.INT16) {
                setEncoding((Short) value);
            } else {
                throw new EventSystemException("Attempted to set " + ENCODING + " with type " + type + " when "
                        + FieldType.INT16 + " is required.");
            }
        } else {
            if (type == FieldType.STRING || type == FieldType.STRING_ARRAY) {
                if (find(ENCODING) < 0) {
                    setEncoding(encoding);
                }
            }
            final int fieldIndex = find(key);
            if (fieldIndex >= 0) {
                // Found the field.  Can we modify it in place?
                final int tokenIndex = getTokenIndexFromFieldIndex(fieldIndex);
                final FieldType oldType = FieldType.byToken(bytes[tokenIndex]);
                if (oldType == type && type.isConstantSize()) {
                    // Modify the value in place, requiring no shifts.
                    Serializer.serializeValue(type, value, encoding, bytes, tokenIndex + 1);
                    return;
                }
                clear(key);
            }
            if (value != null) {
                appendField(key, type, value);
            }
        }
    }

    /**
     * Sets this field at the end of the datagram, updating cached values. The
     * field must not exist in the event beforehand.
     *
     * @param key   the attribute name, but must not be the ENCODING field
     * @param type  the type of this value
     * @param value the value to store
     */
    private void appendField(String key, FieldType type, Object value) {
        final int length0 = length;
        try {
            length += Serializer.serializeATTRIBUTEWORD(key, bytes, length);
            length += Serializer.serializeBYTE(type.token, bytes, length);
            length += Serializer.serializeValue(type, value, encoding, bytes, length);
            setNumEventAttributes(getNumEventAttributes() + 1);
        } catch (ArrayIndexOutOfBoundsException e) {
            // Overran the end of the serialized array, so this field does not fit.  Reset and alert.
            length = length0;
            throw new EventSystemException("Attempted to write " + type + " field [" + value
                    + "] on an event of length " + length0 + ", causing an overrun");
        }
    }

    @Override
    public void setEncoding(short encoding) {
        if (encoding < 0 || encoding >= ENCODING_STRINGS.length) {
            throw new IllegalArgumentException("Unable to set " + ENCODING + " to " + encoding
                    + "; acceptable range is 0<=enc<" + ENCODING_STRINGS.length);
        }

        this.encoding = encoding;
        final int fieldCountIndex = getFieldCountIndex();
        final int numFields = deserializeUINT16(fieldCountIndex);

        tempState.set(fieldCountIndex + 2);
        if (numFields == 0) {
            // We had no fields at all; just set ENCODING.
            appendField(ENCODING, FieldType.INT16, encoding);
            return;
        } else if (ENCODING.equals(Deserializer.deserializeATTRIBUTEWORD(tempState, bytes))) {
            if (FieldType.INT16.token == Deserializer.deserializeBYTE(tempState, bytes)) {
                // Encoding was already the first field and the right type.  Just change the value.
                Serializer.serializeINT16(encoding, bytes, tempState.currentIndex());
                return;
            } else {
                // Encoding was the first field, but had the wrong type.  Clear it and recreate below.
            }
        }

        // If we've gotten this far, ensure that no ENCODING exists elsewhere in the
        // event and then insert it as the first field.
        clear(ENCODING);
        int index = fieldCountIndex + 2;
        shiftTail(index, index + SERIALIZED_ENCODING_LENGTH + 3);
        index += Serializer.serializeATTRIBUTEWORD(ENCODING, bytes, index);
        index += Serializer.serializeBYTE(FieldType.INT16.token, bytes, index);
        index += Serializer.serializeINT16(encoding, bytes, index);
        setNumEventAttributes(getNumEventAttributes() + 1);
    }

    private int getFieldCountIndex() {
        return getEventWordLength(0);
    }

    private int getValueListIndex() {
        return getFieldCountIndex() + 2;
    }

    @Override
    public String getEventName() {
        tempState.set(0);
        return Deserializer.deserializeEVENTWORD(tempState, bytes);
    }

    @Override
    public int getNumEventAttributes() {
        return deserializeUINT16(getFieldCountIndex());
    }

    private void setNumEventAttributes(int count) {
        Serializer.serializeUINT16(count, bytes, getFieldCountIndex());
    }

    @Override
    public Enumeration<String> getEventAttributeNames() {
        return Collections.enumeration(getEventAttributes());
    }

    @Override
    public SortedSet<String> getEventAttributes() {
        final SortedSet<String> fields = new TreeSet<String>();
        for (tempState.set(getValueListIndex()); tempState.currentIndex() < length;) {
            fields.add(Deserializer.deserializeATTRIBUTEWORD(tempState, bytes));
            final FieldType type = FieldType.byToken(Deserializer.deserializeBYTE(tempState, bytes));
            tempState.incr(getValueByteSize(type, tempState.currentIndex()));
        }
        if (tempState.currentIndex() > length) {
            throw new IllegalStateException("Overran the end of the byte array");
        }
        return fields;
    }

    @Override
    public FieldType getType(String attributeName) {
        final int fieldIndex = find(attributeName);
        if (fieldIndex < 0) {
            return null;
        }

        final int tokenIndex = getTokenIndexFromFieldIndex(fieldIndex);
        return FieldType.byToken(bytes[tokenIndex]);
    }

    @Override
    public Object get(String attributeName) {
        final int fieldIndex = find(attributeName);
        if (fieldIndex < 0) {
            return null;
        }

        final int tokenIndex = getTokenIndexFromFieldIndex(fieldIndex);
        final FieldType type = FieldType.byToken(bytes[tokenIndex]);
        return get(type, tokenIndex + 1);
    }

    private Object get(FieldType type, int valueIndex) {
        tempState.set(valueIndex);
        return get(type, tempState);
    }

    private Object get(FieldType type, DeserializerState state) {
        return Deserializer.deserializeValue(state, bytes, type, encoding);
    }

    @Override
    public short getEncoding() {
        return encoding;
    }

    /**
     * This reads the encoding from the serialized event, without using the cached
     * this.encoding value.
     */
    private short readEncoding() {
        final Short encodingValue = getInt16(ENCODING);
        return encodingValue == null ? DEFAULT_ENCODING : encodingValue;
    }

    @Override
    public int serialize(byte[] bytes, int offset) {
        System.arraycopy(this.bytes, 0, bytes, offset, length);
        return length;
    }

    @Override
    public int serialize(DataOutput output) throws IOException {
        output.write(this.bytes, 0, length);
        return length;
    }

    @Override
    public void deserialize(byte[] bytes, int offset, int length) {
        this.length = length;
        Arrays.fill(this.bytes, length, MAX_MESSAGE_SIZE, (byte) 0);
        System.arraycopy(bytes, offset, this.bytes, 0, length);
        resetCaches();
    }

    @Override
    public void deserialize(DataInput stream, int length) throws IOException {
        this.length = length;
        stream.readFully(bytes, 0, length);
        resetCaches();
    }

    private void resetCaches() {
        this.encoding = readEncoding();
    }

    @Override
    public int getBytesSize() {
        return length;
    }

    @Override
    public Event copy() {
        STATS.get(ArrayEventStats.COPIES).increment();
        return new ArrayEvent(bytes, length, encoding);
    }

    private int find(String key) {
        int count = 0;
        try {
            final byte[] keyBytes = EncodedString.getBytes(key, ENCODING_STRINGS[DEFAULT_ENCODING]);
            for (tempState.set(getValueListIndex()); tempState.currentIndex() < length;) {
                ++count;
                final int keyIndex = tempState.currentIndex();
                final int keyLength = bytes[keyIndex] & 0xff;
                if (arrayEquals(bytes, keyIndex + 1, keyLength, keyBytes, 0, keyBytes.length)) {
                    return keyIndex;
                } else {
                    // Wrong field.  Skip it, the type token, and the value.
                    tempState.incr(1 + keyLength); // field name
                    final FieldType type = FieldType.byToken(bytes[tempState.currentIndex()]);
                    tempState.incr(1); // type token
                    // Skip the value without deserializing it
                    tempState.incr(getValueByteSize(type, tempState.currentIndex()));
                }
            }
            if (tempState.currentIndex() > length) {
                throw new IllegalStateException(
                        "Overran the end of the byte array: " + tempState.currentIndex() + " " + length);
            }
            return -1;
        } finally {
            STATS.get(ArrayEventStats.FINDS).increment();
            STATS.get(ArrayEventStats.PARSES).add(count);
        }
    }

    private int getEventWordLength(int index) {
        return 1 + deserializeUBYTE(index);
    }

    private int deserializeUBYTE(int index) {
        return (bytes[index] & 0xff);
    }

    private int deserializeUINT16(int index) {
        return (((bytes[index] & 0xff) << 8) | (bytes[index + 1] & 0xff));
    }

    public int getValueByteSize(FieldType type, int valueIndex) {
        if (type.isConstantSize()) {
            return type.getConstantSize();
        }
        if (type == FieldType.STRING) {
            return 2 + deserializeUINT16(valueIndex);
        }
        if (type.isArray()) {
            final FieldType componentType = type.getComponentType();

            if (type.isNullableArray()) {
                // array_len + bitset_len + bitset + array
                DeserializerState ds = new DeserializerState();
                ds.incr(valueIndex + 2); // array length
                final int count = Deserializer.deserializeBitSetCount(ds, bytes);
                if (componentType.isConstantSize()) {
                    ds.incr(componentType.getConstantSize() * count);
                } else {
                    // If the field is not constant-width, we must walk it.  If there are N
                    // bits set in the BitSet, consume N objects of the component type.
                    for (int i = 0; i < count; i++) {
                        ds.incr(getValueByteSize(componentType, ds.currentIndex()));
                    }
                }
                return ds.currentIndex() - valueIndex;
            }

            if (componentType.isConstantSize()) {
                return 2 + deserializeUINT16(valueIndex) * componentType.getConstantSize();
            } else {
                DeserializerState ds = new DeserializerState();
                ds.incr(valueIndex); // array length
                final int count = Deserializer.deserializeUINT16(ds, bytes);
                for (int i = 0; i < count; i++) {
                    ds.incr(getValueByteSize(componentType, ds.currentIndex()));
                }
                return ds.currentIndex() - valueIndex;
            }
        }
        throw new IllegalStateException("Unrecognized type: " + type);
    }

    /**
     * This method repositions all content from the specified 'from' index through
     * the length of the used byte array onto the 'to' position.  It also updates
     * or invalidates cached values, as appropriate.  'from' and 'to' must be on
     * the first byte of a field or at the end of the serialized event.
     */
    private void shiftTail(int from, int to) {
        STATS.get(ArrayEventStats.SHIFTS).increment();
        final int move = to - from;
        if (move == 0) {
            return;
        }
        System.arraycopy(bytes, from, bytes, to, length - from);
        length += move;
    }

    private int getTokenIndexFromFieldIndex(int fieldIndex) {
        return fieldIndex + getEventWordLength(fieldIndex);
    }

    @Override
    public void copyFrom(Event event) {
        STATS.get(ArrayEventStats.COPIES).increment();
        reset();
        if (event instanceof ArrayEvent) {
            final ArrayEvent ae = (ArrayEvent) event;
            System.arraycopy(ae.bytes, 0, bytes, 0, ae.length);
            length = ae.length;
            tempState.reset();
            encoding = ae.encoding;
        } else {
            super.copyFrom(event);
        }
    }

    public static Map<ArrayEventStats, MutableInt> getStats() {
        return STATS;
    }

    public static Map<ArrayEventStats, Integer> getStatsSnapshot() {
        final Map<ArrayEventStats, Integer> statsCopy = new EnumMap<ArrayEventStats, Integer>(
                ArrayEventStats.class);
        for (Entry<ArrayEventStats, MutableInt> entry : STATS.entrySet()) {
            statsCopy.put(entry.getKey(), entry.getValue().intValue());
        }
        return statsCopy;
    }

    public static void resetStats() {
        for (ArrayEventStats counter : ArrayEventStats.values()) {
            STATS.put(counter, new MutableInt());
        }
    }

    /**
     * These two ArrayEvent objects swap all of their fields.
     * <p/>
     * Why would one want to do this? If one must set "this" to the value of
     * "event", but it's acceptable to modify "event" in the process, then
     * swap() accomplishes the copy faster than copyFrom(event) can.
     * <p/>
     * Typical events seem to take about 6ms for copyFrom() but only 100ns for
     * swap().  However, if you're not doing enough copies that the performance
     * difference matters, you should probably use copyFrom().
     */
    public void swap(ArrayEvent event) {
        if (this == event) {
            throw new IllegalArgumentException("Attempted to swap an event with itself");
        }
        final byte[] tempBytes = bytes;
        final int tempLength = length;
        final short tempEncoding = encoding;
        this.bytes = event.bytes;
        this.length = event.length;
        this.encoding = event.encoding;
        event.bytes = tempBytes;
        event.length = tempLength;
        event.encoding = tempEncoding;
        STATS.get(ArrayEventStats.SWAPS).increment();
    }

    public static enum ArrayEventStats {
        CREATIONS, DELETIONS, HIGHWATER, SHIFTS, FINDS, PARSES, COPIES, SWAPS;
    }

    /**
     * Return a new ArrayEvent with an unusually small buffer. The maximum length
     * for this trimmed ArrayEvent is its initial length. This should only be used
     * for testing operations, when we might want to store a large number of
     * events in memory at once.
     */
    public ArrayEvent trim(int excess) {
        final int overrun = length + excess - MAX_MESSAGE_SIZE;
        if (overrun > 0) {
            throw new IllegalArgumentException("Attempted to create an event " + overrun + " bytes too long");
        }
        return new ArrayEvent(bytes, 0, length, excess);
    }

    private boolean arrayEquals(byte[] b1, int o1, int l1, byte[] b2, int o2, int l2) {
        if (l1 != l2) {
            return false;
        }
        if (b1 == b2 && o1 == o2) {
            return true;
        }
        if (b1 == null || b2 == null) {
            return false;
        }

        for (int i = 0; i < l1; ++i) {
            if (b1[o1 + i] != b2[o2 + i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * This method shows detailed information about the internal state of the
     * event, and was designed as a "Detail Formatter" for tracing execution
     * under Eclipse.  It may be useful for other IDEs or other uses.
     */
    public String toStringDetailed() {
        final StringBuilder buf = new StringBuilder();
        try {
            buf.append(String.format("Event name:        \"%s\"\n", getEventName()));
            buf.append(String.format("Serialized length: %d\n", length));
            buf.append(String.format("tempState index:   %d\n", tempState.currentIndex()));
            buf.append(
                    String.format("Encoding:          %s\n", Event.ENCODING_STRINGS[encoding].getEncodingString()));
            buf.append(String.format("Number of fields:  %d\n", getNumEventAttributes()));
            final DeserializerState ds = new DeserializerState();
            ds.set(getValueListIndex());
            while (ds.currentIndex() < length) {
                String field;
                FieldType type;
                Object value;
                try {
                    field = Deserializer.deserializeATTRIBUTEWORD(ds, bytes);
                } catch (Exception e) {
                    throw new Exception("Error when reading field name: " + e.getMessage());
                }
                try {
                    type = FieldType.byToken(Deserializer.deserializeBYTE(ds, bytes));
                } catch (Exception e) {
                    throw new Exception("Error when reading field name: " + e.getMessage());
                }
                try {
                    value = Deserializer.deserializeValue(ds, bytes, type, encoding);
                } catch (Exception e) {
                    throw new Exception("Error when reading field name: " + e.getMessage());
                }
                if (value.getClass().isArray()) {
                    value = Arrays.deepToString(new Object[] { value });
                }
                buf.append(String.format("  field \"%s\" (%s): %s\n", field, type, value));
            }
        } catch (Exception e) {
            buf.append("\nEXCEPTION: ").append(e.getMessage());
        }
        return buf.toString();
    }

    @Override
    public Iterator<FieldAccessor> iterator() {
        return new Iterator<FieldAccessor>() {
            private final ArrayEventFieldAccessor accessor = new ArrayEventFieldAccessor();

            public boolean hasNext() {
                return accessor.nextFieldIndex < length;
            }

            public FieldAccessor next() {
                accessor.advance();
                return accessor;
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    private final class ArrayEventFieldAccessor extends DefaultFieldAccessor {
        private transient final DeserializerState accessorTempState = new DeserializerState();
        private int nextFieldIndex = getValueListIndex(), currentFieldIndex = Integer.MIN_VALUE,
                currentValueIndex = Integer.MIN_VALUE;

        public void advance() {
            // Deserialize name,type eagerly; deserialize value lazily. 
            currentFieldIndex = nextFieldIndex;
            accessorTempState.set(currentFieldIndex);
            setName(Deserializer.deserializeATTRIBUTEWORD(accessorTempState, bytes));
            setType(FieldType.byToken(Deserializer.deserializeBYTE(accessorTempState, bytes)));
            // Clear any existing value, to indicate that we have not cached it yet.
            setValue(null);
            // Remember where the current value starts.
            currentValueIndex = accessorTempState.currentIndex();
            // Remember where the next field (or end of event) is.
            nextFieldIndex = currentValueIndex + getValueByteSize(getType(), currentValueIndex);
        }

        @Override
        public Object getValue() {
            Object value = super.getValue();
            if (value == null) {
                // The value has not been cached yet.  Do so.
                value = get(getType(), currentValueIndex);
                setValue(value);
            }
            return value;
        }
    }
}