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

com.bazaarvoice.emodb.hadoop.io.Row Maven / Gradle / Ivy

There is a newer version: 6.2.3
Show newest version
package com.bazaarvoice.emodb.hadoop.io;

import com.bazaarvoice.emodb.common.json.JsonHelper;
import com.bazaarvoice.emodb.hadoop.json.IntrinsicsOnly;
import com.bazaarvoice.emodb.sor.api.Intrinsic;
import com.google.common.base.Charsets;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables;
import org.apache.hadoop.hdfs.util.ByteBufferOutputStream;
import org.apache.hadoop.io.BinaryComparable;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.io.WritableUtils;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

import static com.google.common.base.Preconditions.checkState;

/**
 * Hadoop representation of an EmoDB row.
 */
public class Row extends BytesWritable {

    // Map representation of the content.  Null if it hasn't been resolved
    private Map _map;
    // UTF-8 representation of the content.  Limit == 0 when the content has not been resolved.
    private ByteBuffer _text = ByteBuffer.allocate(0);

    public Row() {
        // empty
    }

    /** Generates a row from a JSON string. */
    public Row(String json) {
        set(json);
    }

    /** Generates a row from a UTF-8 representation of a JSON string.  The entire array is read. */
    public Row(byte[] utf8Bytes) {
        set(utf8Bytes);
    }

    /** Generates a row from a UTF-8 representation of a JSON string.  Only the first "length" bytes of the array are read. */
    public Row(byte[] bytes, int length) {
        set(bytes, 0, length);
    }

    /** Generates a row from a map representation of the content. */
    public Row(Map map) {
        set(map);
    }

    public void set(String json) {
        set(json.getBytes(Charsets.UTF_8));
    }

    @Override
    public void set(BytesWritable other) {
        if (other instanceof Row) {
            set(other.getBytes(), 0, other.getLength());
        } else {
            throw new IllegalArgumentException("Cannot set content from " + other.getClass());
        }
    }

    public void set(byte[] utf8) {
        set(utf8, 0, utf8.length);
    }

    @Override
    public void set(byte[] utf8, int start, int len) {
        // Reuse the buffer unless does not have sufficient capacity.
        if (len > _text.capacity()) {
            _text = ByteBuffer.allocate(len);
        }
        // Read the bytes into the buffer
        _text.clear();
        _text.put(utf8, start, len);
        // Flip the buffer to set the limit and move the position back to zero
        _text.flip();
        // Set the map to null, forcing it to be lazily loaded if getMap() is eventually called.
        _map = null;
    }

    public void set(Map map) {
        _map = map;
        // Reset the text length to zero, forcing it to be lazily loaded if the text is eventually needed.
        _text.position(0);
        _text.limit(0);
    }

    /**
     * Ensures that either the UTF-8 text has been set directly or by indirectly converting the Map contents to JSON.
     */
    private void ensureTextSet() {
        if (_text.limit() == 0) {
            checkState(_map != null, "Neither JSON text nor map has been set");
            _text.clear();
            // First try reading the JSON directly into be buffer.
            try {
                JsonHelper.writeJson(new ByteBufferOutputStream(_text), _map);
                // Set the limit and move the position back to zero.
                _text.flip();
            } catch (Exception e) {
                if (Iterables.tryFind(Throwables.getCausalChain(e), Predicates.instanceOf(BufferOverflowException.class)).isPresent()) {
                    // Buffer was insufficient.  Allocate a new array and read the bytes into it.
                    byte[] utf8 = JsonHelper.asUtf8Bytes(_map);
                    _text = ByteBuffer.wrap(utf8);
                } else {
                    throw Throwables.propagate(e);
                }
            }
        }
    }

    /**
     * Gets an EmoDB Map representation of the row's contents.
     * Note:  For performance reasons the original map is returned to the caller, not a copy, and therefore mutability
     *        is not guaranteed.  However, the caller should NOT modify the Map's contents since it is
     *        not deterministic whether those changes will be present when the Row is serialized.  To update this
     *        instance the caller should generate a new map and update the contents by calling {@link #set(java.util.Map)}.
     */
    public Map getMap() {
        if (_map == null) {
            checkState(_text.limit() != 0, "Neither JSON text nor map has been set");
            //noinspection unchecked
            _map = JsonHelper.fromUtf8Bytes(getBytes(), 0, getLength(), Map.class);
        }
        return _map;
    }

    public String getJson() {
        ensureTextSet();
        return new String(_text.array(), 0, _text.limit(), Charsets.UTF_8);
    }

    public String getTable() {
        return Intrinsic.getTable(getMap());
    }

    public String getId() {
        return Intrinsic.getId(getMap());
    }

    public long getVersion() {
        return Intrinsic.getVersion(getMap());
    }

    public String getSignature() {
        return Intrinsic.getSignature(getMap());
    }

    public Date getFirstUpdateAt() {
        return Intrinsic.getFirstUpdateAt(getMap());
    }

    public Date getLastUpdateAt() {
        return Intrinsic.getLastUpdateAt(getMap());
    }

    /**
     * Override the parent to compare the Map values since order of the fields in a JSON string is irrelevant.
     */
    @Override
    public boolean equals(Object o) {
        return this == o || o != null && o instanceof Row && Objects.equals(getMap(), ((Row) o).getMap());
    }

    @Override
    public int compareTo(BinaryComparable o) {
        if (this == o) {
            return 0;
        }
        if (!(o instanceof Row)) {
            throw new IllegalArgumentException("Cannot compare row to " + o.getClass());
        }
        Row other = (Row) o;
        return ComparisonChain.start()
                .compare(getTable(), other.getTable())
                .compare(getId(), other.getId())
                .compare(getVersion(), other.getVersion())
                .result();
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getTable());
    }

    /**
     * Sets this instance's content from the input.
     */
    @Override
    public void readFields(DataInput in)
            throws IOException {
        // Read the length as a variable int
        int length = WritableUtils.readVInt(in);
        // If necessary increase the buffer capacity
        if (length > _text.capacity()) {
            _text = ByteBuffer.allocate(length);
        }
        // For efficiency read directly into the buffer's array
        in.readFully(_text.array(), 0, length);
        // Since we bypassed putting into the buffer set the position and limit directly
        _text.position(0);
        _text.limit(length);
        // Set the map to null since the contents may have changed.
        _map = null;
    }

    /**
     * Writes this instance's content to the output.  Note that the format for Row is identical to Text, so even though
     * the classes are unrelated a Text object can read the bytes written by a Row as a JSON string.
     */
    @Override
    public void write(DataOutput out)
            throws IOException {
        ensureTextSet();
        // Write a variable int with the length
        WritableUtils.writeVInt(out, _text.limit());
        // Write the bytes.  For efficiency directly access the buffer's array.
        out.write(_text.array(), 0, _text.limit());
    }

    @Override
    public byte[] copyBytes() {
        ensureTextSet();
        byte[] bytes = new byte[_text.limit()];
        _text.get(bytes);
        _text.position(0);
        return bytes;
    }

    @Override
    public byte[] getBytes() {
        ensureTextSet();
        return _text.array();
    }

    @Override
    public int getLength() {
        ensureTextSet();
        return _text.limit();
    }

    @Override
    public void setSize(int size) {
        _text.limit(size);
    }

    @Override
    public int getCapacity() {
        return _text.capacity();
    }

    @Override
    public void setCapacity(int newCapacity) {
        if (_text.capacity() != newCapacity) {
            // Allocate a buffer with the requested capacity
            ByteBuffer newText = ByteBuffer.allocate(newCapacity);
            // If the current buffer is larger decrease the limit to match the new capacity
            if (_text.limit() > newCapacity) {
                _text.limit(newCapacity);
                _map = null;
            }
            newText.put(_text);
            newText.flip();
            _text = newText;
        }
    }

    @Override
    public String toString() {
        return getJson();
    }

    /**
     * Register a comparator which works specifically with Rows.
     */
    public static class Comparator extends WritableComparator {
        public Comparator() {
            super(Row.class);
        }

        @Override
        public int compare(byte[] b1, int s1, int l1,
                           byte[] b2, int s2, int l2) {
            try {
                int n1 = WritableUtils.decodeVIntSize(b1[s1]);
                int n2 = WritableUtils.decodeVIntSize(b2[s2]);
                IntrinsicsOnly v1 = JsonHelper.fromUtf8Bytes(b1, s1 + n1, l1 - n1, IntrinsicsOnly.class);
                IntrinsicsOnly v2 = JsonHelper.fromUtf8Bytes(b1, s2 + n2, l2 - n2, IntrinsicsOnly.class);

                return ComparisonChain.start()
                        .compare(v1.table, v2.table)
                        .compare(v1.id, v2.id)
                        .compare(v1.version, v2.version)
                        .result();
            } catch (Exception e) {
                throw Throwables.propagate(e);
            }
        }
    }

    private static final Comparator _comparatorInstance;

    static {
        // register this comparator
        WritableComparator.define(Row.class, _comparatorInstance = new Comparator());
    }

    /**
     * Override compareTo() to use the Row comparator.
     */
    @Override
    public int compareTo(byte[] other, int off, int len) {
        ensureTextSet();
        return _comparatorInstance.compare(
                getBytes(), 0, getLength(),
                other, off, len);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy