com.github.emabrey.rtmp4j.amf.io.AMFOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for com.github.emabrey.rtmp4j.amf.io.AMFOutputStream.java

Source

/*
 * The MIT License
 *
 * Copyright 2013 Emily Mabrey <emilymabrey93@gmail.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.github.emabrey.rtmp4j.amf.io;

import com.github.emabrey.rtmp4j.amf.AMFMarker;
import com.github.emabrey.rtmp4j.amf.AMFUtil;
import com.github.emabrey.rtmp4j.amf.io.internal.AMF0ReferencePool;
import com.github.emabrey.rtmp4j.amf.io.internal.AMFSimpleTypeOutputStream;
import com.github.emabrey.rtmp4j.amf.types.AMF0ComplexTypeDataStructure;
import com.github.emabrey.rtmp4j.amf.types.AMF0ECMAArray;
import com.github.emabrey.rtmp4j.amf.types.AMF0Object;
import com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray;
import com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.AMF0StrictArrayInternalData;
import com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.ARRAY_DENSITY;
import com.github.emabrey.rtmp4j.amf.types.AMF0TypedObject;
import com.github.emabrey.rtmp4j.localization.Messages;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Map.Entry;
import java.util.Set;
import org.joou.UInteger;
import org.joou.UShort;
import static org.joou.Unsigned.uint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

/**
 * {@code OutputStream} that allows one to output the simple AMF types and the
 * complex AMF types.
 * <p>
 * Methods with a suffix of 0 are AMF0 types, and methods with a suffix of 3 are
 * AMF3 types, however there are currently no AMF3 implementations.
 *
 * @author Emily Mabrey <emilymabrey93@gmail.com>
 */
public class AMFOutputStream extends AMFSimpleTypeOutputStream {

    private static final Logger LOG = LoggerFactory.getLogger(AMFOutputStream.class);

    /**
     * The reference pool used by this {@code OutputStream}, this pool should be
     * reset between headers, or when appropriate, by calling the
     * {@code resetReferences} method of this class.
     */
    private final AMF0ReferencePool referencePool = new AMF0ReferencePool();

    /**
     * Creates a new {@code AMFOutputStream}, the newly created stream will
     * write-through to the given stream.
     *
     * @param out A destination {@code OutputStream}.
     */
    public AMFOutputStream(OutputStream out) {
        super(out);
    }

    /**
     * Write an AMF version 0 ECMAArray to the underlying {@code OutputStream}.
     *
     * @param array The AMF version 0 ECMAArray to write.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    public final void writeECMAArray0(final AMF0ECMAArray array) throws IOException {

        LOG.trace("Write AMF0ECMAArray");

        if (writeComplexTypeAsEvaluatedReference0(array)) {
            //The object was encoded via reference, so no additional encoding is needed
            return;
        }

        final ImmutableSet<Entry<String, Object>> arrayEntries = array.getImmutableViewOfFields();

        writeUByte(AMFMarker.Version0.ECMAArray);

        //Write the array size data            
        final UInteger arraySize = uint(arrayEntries.size());
        writeUInteger(arraySize);

        writeObjectEntries0(arrayEntries);
        writeObjectTerminator0();
    }

    /**
     * Write an AMF version 0 TypedObject to the underlying
     * {@code OutputStream}.
     *
     * @param typedObject The AMF version 0 Typed Object to write.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    public final void writeTypedObject0(final AMF0TypedObject typedObject) throws IOException {

        LOG.trace("Write AMF0TypedObject");

        if (writeComplexTypeAsEvaluatedReference0(typedObject)) {
            //The object was encoded via reference, so no additional encoding is needed
            return;
        }

        writeUByte(AMFMarker.Version0.Object);

        //Output the UTF8 type data
        final byte[] objectTypeBytes = AMFUtil.generateRawUTF8Bytes(typedObject.getType());
        write(objectTypeBytes);

        writeObjectEntries0(typedObject.getImmutableViewOfFields());
        writeObjectTerminator0();
    }

    /**
     * Write an AMF version 0 Object to the underlying {@code OutputStream}.
     *
     * @param amfObject The AMF version 0 Object to write.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    public final void writeObject0(final AMF0Object amfObject) throws IOException {

        LOG.trace("Write AMF0Object");

        if (writeComplexTypeAsEvaluatedReference0(amfObject)) {
            //The object was encoded via reference, so no additional encoding is needed
            return;
        }

        writeUByte(AMFMarker.Version0.Object);

        writeObjectEntries0(amfObject.getImmutableViewOfFields());
        writeObjectTerminator0();
    }

    /**
     * Write an AMF version 0 StrictArray to the underlying
     * {@code OutputStream}.
     *
     * @param array The AMF version 0 StrictArray to write.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    public final void writeStrictArray0(final AMF0StrictArray array) throws IOException {

        LOG.trace("Write AMF0 StrictArray");

        if (writeComplexTypeAsEvaluatedReference0(array)) {
            //The object was encoded via reference, so no additional encoding is needed
            return;
        }

        final AMF0StrictArrayInternalData objectData = array.getInternalData();

        synchronized (objectData.internalStateLock) {

            writeUByte(AMFMarker.Version0.StrictArray);

            //Write the array size data
            final UInteger arraySize = uint(array.getArrayCapacity());
            writeUInteger(arraySize);

            if (objectData.arrayDensity == ARRAY_DENSITY.DENSE) {
                writeDenseStrictArray0(objectData);
            } else {
                writeSparseStrictArray0(objectData);
            }

            writeObjectTerminator0();
        }
    }

    /**
     * Resets the internal reference pool of this stream to the default starting
     * value. This should be called when ever an AMF context switch occurs, such
     * as when a new AMF header is encountered.
     */
    public void resetReferences() {
        referencePool.resetPool();
    }

    /**
     * Outputs a StrictArray with data laid out in a {@code sparse} fashion.
     * This method must be externally synchronized by the caller to prevent
     * modification of the given
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.AMF0StrictArrayInternalData}.
     *
     * @param arrayData The
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.AMF0StrictArrayInternalData}
     * to write in a {@code sparse} fashion.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    protected final void writeSparseStrictArray0(final AMF0StrictArrayInternalData arrayData) throws IOException {

        LOG.trace("Writing StrictArray as sparse array");

        for (int currentIndex = 0; currentIndex < arrayData.elements.length; currentIndex++) {

            writeNumber0(currentIndex);

            if (arrayData.changedElements.contains(currentIndex)) {
                final Object indexValue = arrayData.elements[currentIndex];
                writeUnknown0(indexValue);
            } else {
                writeUndefined0();
            }
        }
    }

    /**
     * Outputs a StrictArray with data laid out in a {@code dense} fashion. This
     * method must be externally synchronized by the caller to prevent
     * modification of the given
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.AMF0StrictArrayInternalData}.
     *
     * @param arrayData The
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0StrictArray.AMF0StrictArrayInternalData}
     * to write in a {@code dense} fashion.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    protected final void writeDenseStrictArray0(AMF0StrictArrayInternalData arrayData) throws IOException {

        LOG.trace("Writing StrictArray as dense array");

        if (arrayData.elements.length != arrayData.changedElements.size()) {
            throw new IOException(Messages.AMF0OutputStreamMsg.denseArrayElementsAreNotCorreclyArranged());
        }

        for (int currentIndex = 0; currentIndex < arrayData.elements.length; currentIndex++) {

            writeNumber0(currentIndex);
            final Object indexValue = arrayData.elements[currentIndex];
            writeUnknown0(indexValue);
        }
    }

    /**
     * Attempts to write the given {@code Object} as an AMF0 type. This is
     * accomplished by first verifying the {@code Object}'s class with a call to {@link com.github.emabrey.rtmp4j.amf.AMFUtil#isValidAMF0Object(java.lang.Object)
     * }, and then by calling the appropriate write method as a result of
     * casting the given unknown {@code Object} to the matching acceptable
     * class.
     *
     * @param object The object to verify via the {@link com.github.emabrey.rtmp4j.amf.AMFUtil#isValidAMF0Object(java.lang.Object)
     * } method and to then encode/write to the underlying stream.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    protected final void writeUnknown0(final Object object) throws IOException {

        if (!AMFUtil.isValidAMF0Object(object)) {
            //Check objects class against whitelist of acceptable Java classes
            throw new IOException(Messages.AMF0OutputStreamMsg.objectCannotBeEncoded());
        }

        if (object == null) {
            writeNull0();
        } else if (object instanceof Double) {
            final Double objectAsDouble = (Double) object;
            writeNumber0(objectAsDouble);
        } else if (object instanceof Boolean) {
            final Boolean objectAsBoolean = (Boolean) object;
            writeBoolean0(objectAsBoolean);
        } else if (object instanceof String) {
            final String objectAsString = (String) object;
            writeUnknownString0(objectAsString);
        } else if (object instanceof Date) {
            final Date objectAsDate = (Date) object;
            writeDate0(objectAsDate);
        } else if (object instanceof Document) {
            final Document objectAsDocument = (Document) object;
            writeXMLDocument0(objectAsDocument);
        } else if (object instanceof AMF0ECMAArray) {
            final AMF0ECMAArray array = (AMF0ECMAArray) object;
            writeECMAArray0(array);
        } else if (object instanceof AMF0TypedObject) {
            final AMF0TypedObject typedObject = (AMF0TypedObject) object;
            writeTypedObject0(typedObject);
        } else if (object instanceof AMF0Object) {
            //The AMF0Object must occur after AMF0ECMAArray and AMF0TypedObject,
            //Or it will swallow those subclasses
            final AMF0Object amfObject = (AMF0Object) object;
            writeObject0(amfObject);
        } else if (object instanceof AMF0StrictArray) {
            final AMF0StrictArray array = (AMF0StrictArray) object;
            writeStrictArray0(array);
        } else {
            throw new IOException(Messages.AMF0OutputStreamMsg.unknownObjectCaseMismatch());
        }
    }

    /**
     * Write the given
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0ComplexTypeDataStructure}
     * as a reference if needed; the reference writing is needed if the object
     * has been previously encountered in the object graph (the previous object
     * encodings are maintained by the {@code referencePool} variable of this
     * class).
     *
     * @param complexTypeObject The
     * {@link com.github.emabrey.rtmp4j.amf.types.AMF0ComplexTypeDataStructure},
     * that needs to be evaluated for reference encoding.
     *
     * @return {@code True} if the object was encoded as a reference,
     * {@code False} otherwise.
     *
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    protected final boolean writeComplexTypeAsEvaluatedReference0(
            final AMF0ComplexTypeDataStructure complexTypeObject) throws IOException {
        final Object referencePoolResult = referencePool.evaluateComplexObject(complexTypeObject);
        if (!referencePoolResult.equals(complexTypeObject)) {
            //We have encountered a reference
            final UShort ID = (UShort) referencePoolResult;
            writeReference0(ID);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Attempts to write the given {@code String} as an AMF version 0 String,
     * and should that fail because the given {@code String} is too large for
     * the smaller String type, this method then writes the given {@code String}
     * as the larger AMF version 0 LongString.
     *
     * @param string The {@code String} to encoded in the most efficient AMF0
     * type possible.
     *
     * @throws IOException If an IO error occurs in the underlying stream.
     */
    protected final void writeUnknownString0(final String string) throws IOException {
        if (!writeString0(string)) {
            writeLongString0(string);
        }
    }

    /**
     * Write the given AMF0 object fields as key-value object pairs given , with
     * the keys encoded as marker-less UTF-8 and the values encoded according to
     * their AMF0 type.
     *
     * @param objectFields The object fields to encode as key-value pairs.
     * @throws IOException If an IO error occurs in the underlying stream or
     * within the reference pool.
     */
    protected final void writeObjectEntries0(final Set<Entry<String, Object>> objectFields) throws IOException {
        //Outputs the object values as key value pairs
        for (Entry<String, Object> objectField : objectFields) {
            final String fieldKey = objectField.getKey();
            final Object fieldValue = objectField.getValue();

            final byte[] fieldKeyBytes = AMFUtil.generateRawUTF8Bytes(fieldKey);

            write(fieldKeyBytes);
            writeUnknown0(fieldValue);
        }
    }

    /**
     * Write out the object termination sequence ({@code 0x000009}) which is
     * composed of an empty UTF-8 key-value pair and the ObjectEnd marker.
     *
     * @throws IOException If an IO error occurs in the underlying stream.
     */
    protected final void writeObjectTerminator0() throws IOException {
        //Object Terminator (Empty String keypair + Object End)
        write(0x00);
        write(0x00);
        writeObjectEnd0();
    }
}