Java tutorial
/* * 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(); } }