root/usr/src/lib/libdtrace_jni/java/src/org/opensolaris/os/dtrace/ScalarRecord.java
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 *
 * ident        "%Z%%M% %I%     %E% SMI"
 */
package org.opensolaris.os.dtrace;

import java.io.*;
import java.util.Arrays;
import java.beans.*;

/**
 * A traced D primitive generated by a DTrace action such as {@code
 * trace()} or {@code tracemem()}, or else an element in a composite
 * value generated by DTrace.
 * <p>
 * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
 *
 * @author Tom Erickson
 */
public final class ScalarRecord implements ValueRecord, Serializable {
    static final long serialVersionUID = -6920826443240176724L;
    static final int RAW_BYTES_INDENT = 5;

    static {
        try {
            BeanInfo info = Introspector.getBeanInfo(ScalarRecord.class);
            PersistenceDelegate persistenceDelegate =
                    new DefaultPersistenceDelegate(
                    new String[] {"value", "numberOfBytes"})
            {
                /*
                 * Need to prevent DefaultPersistenceDelegate from using
                 * overridden equals() method, resulting in a
                 * StackOverFlowError.  Revert to PersistenceDelegate
                 * implementation.  See
                 * http://forum.java.sun.com/thread.jspa?threadID=
                 * 477019&tstart=135
                 */
                protected boolean
                mutatesTo(Object oldInstance, Object newInstance)
                {
                    return (newInstance != null && oldInstance != null &&
                            oldInstance.getClass() == newInstance.getClass());
                }
            };
            BeanDescriptor d = info.getBeanDescriptor();
            d.setValue("persistenceDelegate", persistenceDelegate);
        } catch (IntrospectionException e) {
            System.out.println(e);
        }
    }

    /** @serial */
    private final Object value;
    /** @serial */
    private int numberOfBytes;

    /**
     * Creates a scalar record with the given DTrace primitive and the
     * number of bytes used to store the primitive in the native DTrace
     * buffer.  Since traced 8- and 16-bit integers are promoted (as
     * unsigned values) to 32-bit integers, it may be important for
     * output formatting to know the number of bytes used to represent
     * the primitive before promotion.
     *
     * @param v DTrace primitive data value
     * @param nativeByteCount number of bytes used to store the given
     * primitive in the native DTrace buffer
     * @throws NullPointerException if the given value is {@code null}
     * @throws IllegalArgumentException if the given number of bytes is
     * not consistent with the given primitive type or is not greater
     * than zero
     * @throws ClassCastException if the given value is not a DTrace
     * primitive type listed as a possible return value of {@link
     * #getValue()}
     */
    public
    ScalarRecord(Object v, int nativeByteCount)
    {
        value = v;
        numberOfBytes = nativeByteCount;
        validate();
    }

    private final void
    validate()
    {
        if (value == null) {
            throw new NullPointerException();
        }

        // Short-circuit-evaluate common cases first
        if (value instanceof Integer) {
            switch (numberOfBytes) {
                case 1:
                case 2:
                case 4:
                    break;
                default:
                    throw new IllegalArgumentException(
                            "number of bytes is " + numberOfBytes +
                            ", expected 1, 2, or 4 for Integer primitive");
            }
        } else if (value instanceof Long) {
            if (numberOfBytes != 8) {
                throw new IllegalArgumentException(
                        "number of bytes is " + numberOfBytes +
                        ", expected 8 for Long primitive");
            }
        } else if ((value instanceof String) || (value instanceof byte[])) {
            switch (numberOfBytes) {
                case 1:
                case 2:
                case 4:
                case 8:
                    throw new IllegalArgumentException(
                            "number of bytes is " + numberOfBytes +
                            ", expected a number other than " +
                            "1, 2, 4, or 8 for String or byte-array " +
                            "primitive");
            }
        } else if (value instanceof Number) {
            if (numberOfBytes <= 0) {
                throw new IllegalArgumentException(
                        "number of bytes is " + numberOfBytes +
                        ", must be greater than zero");
            }
        } else {
            throw new ClassCastException(value.getClass().getName() +
                    " value is not a D primitive");
        }
    }

    /**
     * Gets the traced D primitive value of this record.
     *
     * @return a non-null value whose type is one of the following:
     * <ul>
     * <li>{@link Number}</li>
     * <li>{@link String}</li>
     * <li>byte[]</li>
     * </ul>
     */
    public Object
    getValue()
    {
        return value;
    }

    /**
     * Gets the number of bytes used to store the primitive value of
     * this record in the native DTrace buffer.  Since traced 8- and
     * 16-bit integers are promoted (as unsigned values) to 32-bit
     * integers, it may be important for output formatting to know the
     * number of bytes used to represent the primitive before promotion.
     *
     * @return the number of bytes used to store the primitive value
     * of this record in the native DTrace buffer, guaranteed to be
     * greater than zero and consisitent with the type of the primitive
     * value
     */
    public int
    getNumberOfBytes()
    {
        return numberOfBytes;
    }

    /**
     * Compares the specified object with this record for equality.
     * Defines equality as having the same value.
     *
     * @return {@code true} if and only if the specified object is also
     * a {@code ScalarRecord} and the values returned by the {@link
     * #getValue()} methods of both instances are equal, {@code false}
     * otherwise.  Values are compared using {@link
     * java.lang.Object#equals(Object o) Object.equals()}, unless they
     * are arrays of raw bytes, in which case they are compared using
     * {@link java.util.Arrays#equals(byte[] a, byte[] a2)}.
     */
    @Override
    public boolean
    equals(Object o)
    {
        if (o instanceof ScalarRecord) {
            ScalarRecord r = (ScalarRecord)o;
            if (value instanceof byte[]) {
                if (r.value instanceof byte[]) {
                    byte[] a1 = (byte[])value;
                    byte[] a2 = (byte[])r.value;
                    return Arrays.equals(a1, a2);
                }
                return false;
            }
            return value.equals(r.value);
        }
        return false;
    }

    /**
     * Overridden to ensure that equal instances have equal hashcodes.
     *
     * @return {@link java.lang.Object#hashCode()} of {@link
     * #getValue()}, or {@link java.util.Arrays#hashCode(byte[] a)} if
     * the value is a raw byte array
     */
    @Override
    public int
    hashCode()
    {
        if (value instanceof byte[]) {
            return Arrays.hashCode((byte[])value);
        }
        return value.hashCode();
    }

    private static final int BYTE_SIGN_BIT = 1 << 7;

    /**
     * Static utility for treating a byte as unsigned by converting it
     * to int without sign extending.
     */
    static int
    unsignedByte(byte b)
    {
        if (b < 0) {
            b ^= (byte)BYTE_SIGN_BIT;
            return ((int)b) | BYTE_SIGN_BIT;
        }
        return (int)b;
    }

    static String
    hexString(int n, int width)
    {
        String s = Integer.toHexString(n);
        int len = s.length();
        if (width < len) {
            s = s.substring(len - width);
        } else if (width > len) {
            s = (spaces(width - len) + s);
        }
        return s;
    }

    static String
    spaces(int n)
    {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < n; ++i) {
            buf.append(' ');
        }
        return buf.toString();
    }

    /**
     * Represents a byte array as a table of unsigned byte values in hex,
     * 16 per row ending in their corresponding character
     * representations (or a period (&#46;) for each unprintable
     * character).  Uses default indentation.
     *
     * @see ScalarRecord#rawBytesString(byte[] bytes, int indent)
     */
    static String
    rawBytesString(byte[] bytes)
    {
        return rawBytesString(bytes, RAW_BYTES_INDENT);
    }

    /**
     * Represents a byte array as a table of unsigned byte values in hex,
     * 16 per row ending in their corresponding character
     * representations (or a period (&#46;) for each unprintable
     * character).  The table begins and ends with a newline, includes a
     * header row, and uses a newline at the end of each row.
     *
     * @param bytes array of raw bytes treated as unsigned when
     * converted to hex display
     * @param indent number of spaces to indent each line of the
     * returned string
     * @return table representation of 16 bytes per row as hex and
     * character values
     */
    static String
    rawBytesString(byte[] bytes, int indent)
    {
        // ported from libdtrace/common/dt_consume.c dt_print_bytes()
        int i, j;
        int u;
        StringBuilder buf = new StringBuilder();
        String leftMargin = spaces(indent);
        buf.append('\n');
        buf.append(leftMargin);
        buf.append("      ");
        for (i = 0; i < 16; i++) {
            buf.append("  ");
            buf.append("0123456789abcdef".charAt(i));
        }
        buf.append("  0123456789abcdef\n");
        int nbytes = bytes.length;
        String hex;
        for (i = 0; i < nbytes; i += 16) {
            buf.append(leftMargin);
            buf.append(hexString(i, 5));
            buf.append(':');

            for (j = i; (j < (i + 16)) && (j < nbytes); ++j) {
                buf.append(hexString(unsignedByte(bytes[j]), 3));
            }

            while ((j++ % 16) != 0) {
                buf.append("   ");
            }

            buf.append("  ");

            for (j = i; (j < (i + 16)) && (j < nbytes); ++j) {
                u = unsignedByte(bytes[j]);
                if ((u < ' ') || (u > '~')) {
                    buf.append('.');
                } else {
                    buf.append((char) u);
                }
            }

            buf.append('\n');
        }

        return buf.toString();
    }

    static String
    valueToString(Object value)
    {
        String s;
        if (value instanceof byte[]) {
            s = rawBytesString((byte[])value);
        } else {
            s = value.toString();
        }
        return s;
    }

    private void
    readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException
    {
        s.defaultReadObject();
        // check class invariants
        try {
            validate();
        } catch (Exception e) {
            InvalidObjectException x = new InvalidObjectException(
                    e.getMessage());
            x.initCause(e);
            throw x;
        }
    }

    /**
     * Gets the natural string representation of the traced D primitive.
     *
     * @return the value of {@link Object#toString} when called on
     * {@link #getValue()}; or if the value is an array of raw bytes, a
     * table displaying 16 bytes per row in unsigned hex followed by the
     * ASCII character representations of those bytes (each unprintable
     * character is represented by a period (.))
     */
    public String
    toString()
    {
        return ScalarRecord.valueToString(getValue());
    }
}