root/usr/src/lib/libdtrace_jni/java/src/org/opensolaris/os/dtrace/PrintaRecord.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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
package org.opensolaris.os.dtrace;

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

/**
 * A record generated by the DTrace {@code printa()} action.  Lists the
 * aggregations passed to {@code printa()} and records the formatted
 * output associated with each {@link Tuple}.  If multiple aggregations
 * were passed to the {@code printa()} action that generated this
 * record, then the DTrace library tabulates the output, using a default
 * format if no format string was specified.  By default, the output
 * string associated with a given {@code Tuple} includes a value from
 * each aggregation, or zero wherever an aggregation has no value
 * associated with that {@code Tuple}.  For example, the D statements
 * <pre><code>
 *     &#64;a[123] = sum(1);
 *     &#64;b[456] = sum(2);
 *     printa(&#64;a, &#64;b, &#64;c);
 * </code></pre>
 * produce output for the tuples "123" and "456" similar to the
 * following:
 * <pre><code>
 *      123     1       0       0
 *      456     0       2       0
 * </code></pre>
 * The first column after the tuple contains values from {@code @a},
 * the next column contains values from {@code @b}, and the last
 * column contains zeros because {@code @c} has neither a value
 * associated with "123" nor a value associated with "456".
 * <p>
 * If a format string is passed to {@code printa()}, it may limit the
 * aggregation data available in this record.  For example, if the
 * format string specifies a value placeholder for only one of two
 * aggregations passed to {@code printa()}, then the resulting {@code
 * PrintaRecord} will contain only one {@code Aggregation}.  If no value
 * placeholder is specified, or if the aggregation tuple is not
 * completely specified, the resulting {@code PrintaRecord} will contain
 * no aggregation data.  However, the formatted output generated by the
 * DTrace library is available in all cases.  For details about
 * {@code printa()} format strings, see the <a
 * href=http://dtrace.org/guide/chp-fmt.html#chp-fmt-printa>
 * <b>{@code printa()}</b></a> section of the <b>Output
 * Formatting</b> chapter of the <i>Dynamic Tracing Guide</i>.
 * <p>
 * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
 *
 * @author Tom Erickson
 */
public final class PrintaRecord implements Record, Serializable,
        Comparable <PrintaRecord> {
    static final long serialVersionUID = -4174277639915895694L;

    static {
        try {
            BeanInfo info = Introspector.getBeanInfo(PrintaRecord.class);
            PersistenceDelegate persistenceDelegate =
                    new DefaultPersistenceDelegate(
                    new String[] {"snaptime", "aggregations",
                    "formattedStrings", "tuples", "output"})
            {
                /*
                 * 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 long snaptime;
    /** @serial */
    private List <Aggregation> aggregations;
    /** @serial */
    private Map <Tuple, String> formattedStrings;
    /** @serial */
    private List <Tuple> tuples;
    private transient StringBuilder outputBuffer;
    private transient String output;
    private transient boolean formatted;

    /**
     * Package level access, called by ProbeData.
     */
    PrintaRecord(long snaptimeNanos, boolean isFormatString)
    {
        snaptime = snaptimeNanos;
        aggregations = new ArrayList <Aggregation> ();
        formattedStrings = new HashMap <Tuple, String> ();
        tuples = new ArrayList <Tuple> ();
        outputBuffer = new StringBuilder();
        formatted = isFormatString;
        validate();
    }

    /**
     * Creates a record with the given snaptime, aggregations, and
     * formatted output.
     *
     * @param snaptimeNanos  nanosecond timestamp of the snapshot used
     * to create this {@code printa()} record
     * @param aggs  aggregations passed to the {@code printa()} action
     * that generated this record
     * @param formattedOutput  the formatted output, if any, associated
     * with each {@code Tuple} occurring in the aggregations belonging
     * to this record, one formatted string per {@code Tuple}, or an
     * empty or {@code null} map if an incomplete {@code printa()}
     * format string caused aggregation tuples to be omitted from this
     * record
     * @param orderedTuples list of aggregation tuples in the same order
     * generated by the native DTrace library (determined by the various
     * "aggsort" options such as {@link Option#aggsortkey})
     * @param formattedOutputString {@code printa()} formatted string
     * output in the same order generated by the native DTrace library
     * (determined by the various "aggsort" options such as
     * {@link Option#aggsortkey})
     * @throws NullPointerException if the given collection of
     * aggregations is {@code null}, or if the given ordered lists of
     * tuples or formatted strings are {@code null}
     * @throws IllegalArgumentException if the given snaptime is
     * negative
     */
    public
    PrintaRecord(long snaptimeNanos, Collection <Aggregation> aggs,
            Map <Tuple, String> formattedOutput,
            List <Tuple> orderedTuples,
            String formattedOutputString)
    {
        snaptime = snaptimeNanos;
        if (aggs != null) {
            aggregations = new ArrayList <Aggregation> (aggs.size());
            aggregations.addAll(aggs);
        }
        if (formattedOutput != null) {
            formattedStrings = new HashMap <Tuple, String>
                    (formattedOutput);
        }
        if (orderedTuples != null) {
            tuples = new ArrayList <Tuple> (orderedTuples.size());
            tuples.addAll(orderedTuples);
        }
        output = formattedOutputString;
        validate();
    }

    private final void
    validate()
    {
        if (snaptime < 0) {
            throw new IllegalArgumentException("snaptime is negative");
        }
        if (aggregations == null) {
            throw new NullPointerException("aggregations list is null");
        }
        Aggregation a;
        for (int i = 0, len = aggregations.size(); i < len; ++i) {
            a = aggregations.get(i);
            if (a == null) {
                throw new NullPointerException(
                        "null aggregation at index " + i);
            }
        }
        if (tuples == null) {
            throw new NullPointerException("ordered tuple list is null");
        }
        if (output == null && outputBuffer == null) {
            throw new NullPointerException("formatted output is null");
        }
    }

    /**
     * Gets the nanosecond timestamp of the aggregate snapshot used to
     * create this {@code printa()} record.
     *
     * @return nanosecond timestamp
     */
    public long
    getSnaptime()
    {
        return snaptime;
    }

    private Aggregation
    getAggregationImpl(String name)
    {
        if (name == null) {
            return null;
        }
        for (Aggregation a : aggregations) {
            if (name.equals(a.getName())) {
                return a;
            }
        }
        return null;
    }

    /**
     * Gets the named aggregation.
     *
     * @return the named aggregation passed to {@code printa()}, or
     * {@code null} if the named aggregation is not passed to {@code
     * printa()}, or if it is omitted due to an incomplete {@code
     * printa()} format string, or if it is empty (a future release of
     * this API may represent an empty DTrace aggregation as a non-null
     * {@code Aggregation} with no records; users of this API should not
     * rely on a non-null return value to indicate a non-zero record
     * count)
     */
    public Aggregation
    getAggregation(String name)
    {
        name = Aggregate.filterUnnamedAggregationName(name);
        return getAggregationImpl(name);
    }

    /**
     * Gets a list of the aggregations passed to the {@code printa()}
     * action that generated this record.  The returned list is a copy,
     * and modifying it has no effect on this record.  Supports XML
     * persistence.
     *
     * @return non-null, possibly empty list of aggregations belonging
     * to this record (empty aggregations are excluded)
     */
    public List <Aggregation>
    getAggregations()
    {
        return new ArrayList <Aggregation> (aggregations);
    }

    /**
     * Gets the formatted string, if any, associated with the given
     * aggregation tuple.
     *
     * @param key aggregation tuple
     * @return the formatted string associated with the given
     * aggregation tuple, or {@code null} if the given tuple does not
     * exist in the aggregations belonging to this record or if it
     * is omitted from this record due to an incomplete {@code printa()}
     * format string
     * @see #getFormattedStrings()
     * @see #getOutput()
     */
    public String
    getFormattedString(Tuple key)
    {
        if (formattedStrings == null) {
            return null;
        }
        return formattedStrings.get(key);
    }

    /**
     * Gets the formatted output, if any, associated with each {@code
     * Tuple} occurring in the aggregations belonging to this record,
     * one formatted string per {@code Tuple}.  Gets an empty map if
     * aggregation tuples are omitted from this record due to an
     * incomplete {@code printa()} format string.  The returned map is a
     * copy and modifying it has no effect on this record.  Supports XML
     * persistence.
     *
     * @return a map of aggregation tuples and their associated
     * formatted output strings, empty if aggregation tuples are omitted
     * from this record due to an incomplete {@code printa(}) format
     * string
     * @see #getFormattedString(Tuple key)
     * @see #getOutput()
     */
    public Map <Tuple, String>
    getFormattedStrings()
    {
        if (formattedStrings == null) {
            return new HashMap <Tuple, String> ();
        }
        return new HashMap <Tuple, String> (formattedStrings);
    }

    /**
     * Gets an ordered list of this record's aggregation tuples.  The
     * returned list is a copy, and modifying it has no effect on this
     * record.  Supports XML persistence.
     *
     * @return a non-null list of this record's aggregation tuples in
     * the order they were generated by the native DTrace library, as
     * determined by the {@link Option#aggsortkey}, {@link
     * Option#aggsortrev}, {@link Option#aggsortpos}, and {@link
     * Option#aggsortkeypos} options
     */
    public List <Tuple>
    getTuples()
    {
        return new ArrayList <Tuple> (tuples);
    }

    /**
     * Gets this record's formatted output.  Supports XML persistence.
     *
     * @return non-null formatted output in the order generated by the
     * native DTrace library, as determined by the {@link
     * Option#aggsortkey}, {@link Option#aggsortrev}, {@link
     * Option#aggsortpos}, and {@link Option#aggsortkeypos} options
     */
    public String
    getOutput()
    {
        if (output == null) {
            output = outputBuffer.toString();
            outputBuffer = null;
            if ((output.length() == 0) && !formatted) {
                output = "\n";
            }
        }
        return output;
    }

    /**
     * Package level access, called by ProbeData.
     *
     * @throws NullPointerException if aggregationName is null
     * @throws IllegalStateException if this PrintaRecord has an
     * aggregation matching the given name and it already has an
     * AggregationRecord with the same tuple key as the given record.
     */
    void
    addRecord(String aggregationName, long aggid, AggregationRecord record)
    {
        if (formattedStrings == null) {
            // printa() format string does not completely specify tuple
            return;
        }

        aggregationName = Aggregate.filterUnnamedAggregationName(
                aggregationName);
        Aggregation aggregation = getAggregationImpl(aggregationName);
        if (aggregation == null) {
            aggregation = new Aggregation(aggregationName, aggid);
            aggregations.add(aggregation);
        }
        try {
            aggregation.addRecord(record);
        } catch (IllegalArgumentException e) {
            Map <Tuple, AggregationRecord> map = aggregation.asMap();
            AggregationRecord r = map.get(record.getTuple());
            //
            // The printa() format string may specify the value of the
            // aggregating action multiple times.  While that changes
            // the resulting formatted string associated with the tuple,
            // we ignore the attempt to add the redundant record to the
            // aggregation.
            //
            if (!r.equals(record)) {
                throw e;
            }
        }
    }

    //
    // Called from native code when the tuple is not completely
    // specified in the printa() format string.
    //
    void
    invalidate()
    {
        formattedStrings = null;
        aggregations.clear();
        tuples.clear();
    }

    void
    addFormattedString(Tuple tuple, String formattedString)
    {
        if (tuple != null && formattedStrings != null) {
            if (formattedStrings.containsKey(tuple)) {
                throw new IllegalArgumentException("A formatted string " +
                        "for tuple " + tuple + " already exists.");
            } else {
                formattedStrings.put(tuple, formattedString);
                tuples.add(tuple);
            }
        }
        outputBuffer.append(formattedString);
    }

    /**
     * Compares the specified object with this {@code PrintaRecord} for
     * equality. Returns {@code true} if and only if the specified
     * object is also a {@code PrintaRecord} and both records have the
     * same aggregations and the same formatted strings in the same
     * order (by aggregation tuple).
     *
     * @return {@code true} if and only if the specified object is also
     * a {@code PrintaRecord} and both records have the same
     * aggregations and the same formatted strings in the same order (by
     * aggregation tuple)
     */
    @Override
    public boolean
    equals(Object o)
    {
        if (o instanceof PrintaRecord) {
            PrintaRecord r = (PrintaRecord)o;
            return (aggregations.equals(r.aggregations) &&
                    ((formattedStrings == null || formattedStrings.isEmpty())
                    ? (r.formattedStrings == null ||
                        r.formattedStrings.isEmpty())
                    : formattedStrings.equals(r.formattedStrings)) &&
                    tuples.equals(r.tuples));
        }

        return false;
    }

    /**
     * Overridden to ensure that equal instances have equal hash codes.
     */
    @Override
    public int
    hashCode()
    {
        int hash = 17;
        hash = (hash * 37) + aggregations.hashCode();
        hash = (hash * 37) + ((formattedStrings == null ||
            formattedStrings.isEmpty()) ? 0 :
            formattedStrings.hashCode());
        hash = (hash * 37) + tuples.hashCode();
        return hash;
    }

    /**
     * Compares the formatted {@link #getOutput() output} of this record
     * with that of the given record. Note that ordering {@code printa}
     * records by their output string values is incompatible with {@link
     * #equals(Object o) equals()}, which also checks the underlying
     * aggregation data for equality.
     *
     * @return a negative number, 0, or a positive number as this
     * record's formatted output is lexicographically less than, equal
     * to, or greater than the given record's formatted output
     */
    public int
    compareTo(PrintaRecord r)
    {
        return getOutput().compareTo(r.getOutput());
    }

    /**
     * Serialize this {@code PrintaRecord} instance.
     *
     * @serialData Serialized fields are emitted, followed by the
     * formatted output string.
     */
    private void
    writeObject(ObjectOutputStream s) throws IOException
    {
        s.defaultWriteObject();
        if (output == null) {
            s.writeObject(outputBuffer.toString());
        } else {
            s.writeObject(output);
        }
    }

    private void
    readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException
    {
        s.defaultReadObject();
        output = (String)s.readObject();
        // make defensive copy
        if (aggregations != null) {
            List <Aggregation> copy = new ArrayList <Aggregation>
                    (aggregations.size());
            copy.addAll(aggregations);
            aggregations = copy;
        }
        if (formattedStrings != null) {
            formattedStrings = new HashMap <Tuple, String> (formattedStrings);
        }
        if (tuples != null) {
            List <Tuple> copy = new ArrayList <Tuple> (tuples.size());
            copy.addAll(tuples);
            tuples = copy;
        }
        // check constructor invariants only after defensive copy
        try {
            validate();
        } catch (Exception e) {
            InvalidObjectException x = new InvalidObjectException(
                    e.getMessage());
            x.initCause(e);
            throw x;
        }
    }

    /**
     * Gets a string representation of this instance useful for logging
     * and not intended for display.  The exact details of the
     * representation are unspecified and subject to change, but the
     * following format may be regarded as typical:
     * <pre><code>
     * class-name[property1 = value1, property2 = value2]
     * </code></pre>
     */
    public String
    toString()
    {
        StringBuilder buf = new StringBuilder();
        buf.append(PrintaRecord.class.getName());
        buf.append("[snaptime = ");
        buf.append(snaptime);
        buf.append(", aggregations = ");
        buf.append(aggregations);
        buf.append(", formattedStrings = ");
        buf.append(formattedStrings);
        buf.append(", tuples = ");
        buf.append(tuples);
        buf.append(", output = ");
        buf.append(getOutput());
        buf.append(']');
        return buf.toString();
    }
}