/*
* NewFunction2.java
* Transform
*
* Copyright (c) 2001-2010 Flagstone Software Ltd. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Flagstone Software Ltd. nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.flagstone.transform.action;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.flagstone.transform.coder.Coder;
import com.flagstone.transform.coder.Context;
import com.flagstone.transform.coder.SWFDecoder;
import com.flagstone.transform.coder.SWFEncoder;
import com.flagstone.transform.coder.SWFFactory;
import com.flagstone.transform.exception.IllegalArgumentRangeException;
/**
* The NewFunction2 action is used to create a user-defined function with
* optimisations to improve performance.
*
* <p>
* NewFunction2 was added in Flash 7 to improve the performance of function
* calls by allowing pre-defined variables such as <em>_root</em>,
* <em>_parent</em>, <em>_global</em>, <em>super</em>, <em>this</em> and the
* <em>arguments</em> passed to the function to be pre-loaded to a set of up to
* 256 internal registers.
* </p>
*
* <p>
* The optimisation attribute is a compound code, containing a number of flags
* that control which variables are pre-loaded:
* </p>
*
* <table class="datasheet">
* <tr>
* <td valign="top">CreateSuper</td>
* <td>Create and initialise the <em>super</em> variable with the parent class
* of the function.</td>
* </tr>
* <tr>
* <td valign="top">CreateArguments</td>
* <td>Create the <em>arguments</em> variable which contains the arguments
* passed to the function.</td>
* </tr>
* <tr>
* <td valign="top">CreateThis</td>
* <td>Create and initialise the <em>this</em> variable with the object.</td>
* </tr>
* <tr>
* <td valign="top">LoadThis</td>
* <td>Pre-load the <em>this</em> variable into register number 1.</td>
* </tr>
* <tr>
* <td valign="top">LoadArguments</td>
* <td>Pre-load the <em>parent</em> variable into register number 2.</td>
* </tr>
* <tr>
* <td valign="top">LoadSuper</td>
* <td>Pre-load the <em>super</em> variable into register number 3.</td>
* </tr>
* <tr>
* <td valign="top">LoadRoot</td>
* <td>Pre-load the <em>_root</em> variable into register number 4.</td>
* </tr>
* <tr>
* <td valign="top">LoadParent</td>
* <td>Pre-load the <em>_parent</em> variable into register number 5.</td>
* </tr>
* <tr>
* <td valign="top">LoadGlobal</td>
* <td>Pre-load the <em>_global</em> variable into register number 5.</td>
* </tr>
* </table>
*
* <p>
* The register numbers that the predefined variables are assigned to are fixed.
* When specifying which of the functions arguments are also assigned to
* registers it is important avoid these locations otherwise the variables will
* be overwritten.
* </p>
*
* <p>
* User-defined functions are also used to create methods for user-defined
* objects. The name of the function is omitted and the function definition is
* assigned to a variable which allows it to be referenced at a alter time. See
* the example below.
* </p>
*
* <p>
* The arguments supplied to the function can be referenced by the name supplied
* in the arguments list.
* </p>
*
* <p>
* All the action objects added are owned by the function. They will be deleted
* when the function definition is deleted.
* </p>
*
* @see NewFunction
*/
public final class NewFunction2 implements Action {
/** Number of last internal register in the Flash Player. */
private static final int LAST_REGISTER = 255;
/**
* The Builder class is used to generate a new NewFunction2 object
* using a small set of convenience methods.
*/
public static final class Builder {
/** The name, if any, for the function. */
private transient String name = "";
/** The number of registers to allocate for use by the function. */
private transient int registerCount;
/** The set of optimizations to speed the function. */
private transient int optimizations;
/** The set of arguments, with optional register assignments. */
private final transient Map<String, Integer> arguments =
new LinkedHashMap<String, Integer>();
/** The list of actions that make up the function body. */
private final transient List<Action>actions = new ArrayList<Action>();
/**
* Set the name of the function. Must not be null or an empty string.
* The name defaults to an empty string so this method is not needed to
* define methods.
*
* @param aString the name of the function.
* @return this object.
*/
public Builder setName(final String aString) {
if (aString == null || aString.length() == 0) {
throw new IllegalArgumentException();
}
name = aString;
return this;
}
/**
* Set the number of registers to allocate for the function.
* @param count the number of registers. Must be in the range 0..255.
* @return this object.
*/
public Builder allocate(final int count) {
if ((count < 0) || (count > LAST_REGISTER)) {
throw new IllegalArgumentRangeException(0,
LAST_REGISTER, count);
}
registerCount = count;
return this;
}
/**
* Add an Optimization to be used by the function.
* @param opt an Optimization used to speed up the function execution.
* @return this object.
*/
public Builder optimize(final Optimization opt) {
optimizations |= opt.getValue();
return this;
}
/**
* Add the name of an argument to the list of arguments that will be
* passed to the function. Must not be null or an empty string.
* @param argName the name of the argument.
* @return this object.
*/
public Builder addArgument(final String argName) {
return addArgument(argName, 0);
}
/**
* Add the name of an argument and the number of the register where it
* will be stored to the list of arguments that will be
* passed to the function. The name must not be null or an empty string
* and the register number must be in the range 0..255. If the number
* is set to zero then the argument will not be stored in a register.
*
* @param argName the name of the argument.
* @param index the register number.
* @return this object.
*/
public Builder addArgument(final String argName, final int index) {
if (argName == null || argName.length() == 0) {
throw new IllegalArgumentException();
}
if ((index < 0) || (index > LAST_REGISTER)) {
throw new IllegalArgumentRangeException(0,
LAST_REGISTER, index);
}
arguments.put(argName, index);
return this;
}
/**
* Add an action to the list of actions that will make up the body of
* the function. Must not be null.
* @param action the action to add to the function body.
* @return this object.
*/
public Builder addAction(final Action action) {
if (action == null) {
throw new IllegalArgumentException();
}
actions.add(action);
return this;
}
/**
* Generate an NewFunction2 using the parameters defined in the
* Builder.
* @return an initialized NewFunction2 object.
*/
public NewFunction2 build() {
return new NewFunction2(this);
}
}
/** Format string used in toString() method. */
private static final String FORMAT = "NewFunction2: { name=%s; "
+ "registerCount=%d; optimizations=%s; arguments=%s; actions=%s}";
/**
* The set of optimisations that can be used to speed up the execution of
* functions.
*/
public enum Optimization {
/** Create the predefined variable, <em>super</em>. */
CREATE_SUPER(4),
/** Create the predefined variable, <em>arguments</em>. */
CREATE_ARGUMENTS(16),
/** Create and initialised the predefined variable, <em>this</em>. */
CREATE_THIS(64),
/** Load the predefine variable, <em>this</em>, into register 1. */
LOAD_THIS(128),
/** Load the predefine variable, <em>arguments</em>, into register 2. */
LOAD_ARGUMENTS(32),
/** Load the predefine variable, <em>super</em>, into register 3. */
LOAD_SUPER(8),
/** Load the predefine variable, <em>_root</em>, into register 4. */
LOAD_ROOT(2),
/** Load the predefine variable, <em>_parent</em>, into register 5. */
LOAD_PARENT(1),
/** Load the predefine variable, <em>_global</em>, into register 6. */
LOAD_GLOBAL(32768);
/** Table used to convert flags into Optimization values. */
private static final Map<Integer, Optimization> TABLE;
static {
TABLE = new LinkedHashMap<Integer, Optimization>();
for (final Optimization opt : values()) {
TABLE.put(opt.value, opt);
}
}
/** The encoded value for the Optimization. */
private final int value;
/**
* Creates and initializes an Optimization for an encoded value.
*
* @param val the encoded value for an Optimization.
*/
private Optimization(final int val) {
value = val;
}
/**
* Get the value used to represent the Optimization when encoded.
* @return the value used to encode the Optimization.
*/
public int getValue() {
return value;
}
}
/** Initial number of bytes when encoding. */
private static final int INITIAL_LENGTH = 5;
/** The name of the function or an empty string for methods. */
private final transient String name;
/** The number of registers to allocate for variables. */
private final transient int registerCount;
/** The set of flags identifying optimizations to be applied. */
private final transient int optimizations;
/** The set of arguments with optional assignment to registers. */
private final transient Map<String, Integer> arguments;
/** The set of actions that make up the function body. */
private final transient List<Action> actions;
/** The length of the action, minus the header, when it is encoded. */
private transient int length;
/** The length of the encoded function body. */
private transient int actionsLength;
/**
* Creates and initialises a NewFunction2 object using parameters defined
* in the Builder.
*
* @param builder a Builder object containing the parameters to generate
* the function definition.
*/
public NewFunction2(final Builder builder) {
name = builder.name;
registerCount = builder.registerCount;
optimizations = builder.optimizations;
arguments = new LinkedHashMap<String, Integer>(builder.arguments);
actions = new ArrayList<Action>(builder.actions);
}
/**
* Creates and initialises a NewFunction2 definition using values encoded
* in the Flash binary format.
*
* @param coder
* an SWFDecoder object that contains the encoded Flash data.
*
* @param context
* a Context object used to manage the decoders for different
* type of object and to pass information on how objects are
* decoded.
*
* @throws IOException
* if an error occurs while decoding the data.
*/
public NewFunction2(final SWFDecoder coder, final Context context)
throws IOException {
final SWFFactory<Action> decoder = context.getRegistry()
.getActionDecoder();
length = coder.readUnsignedShort();
name = coder.readString();
final int argumentCount = coder.readUnsignedShort();
registerCount = coder.readByte();
optimizations = (coder.readByte() << Coder.TO_UPPER_BYTE)
+ coder.readByte();
int index;
arguments = new LinkedHashMap<String, Integer>(argumentCount);
for (int i = 0; i < argumentCount; i++) {
index = coder.readByte();
arguments.put(coder.readString(), index);
}
actionsLength = coder.readUnsignedShort();
coder.mark();
length += actionsLength;
actions = new ArrayList<Action>();
while (coder.bytesRead() < actionsLength) {
decoder.getObject(actions, coder, context);
}
coder.check(actionsLength);
coder.unmark();
}
/**
* Creates a NewFunction2 with the specified name, argument names and
* actions to be executed. The order of the Strings in the argument list
* indicate the order in which the values will be popped off the stack when
* the function is executed. The first argument is popped from the stack
* first.
*
* @param aString
* the name of the function. Can be an empty string if the
* function is anonymous.
* @param count
* the number of registers to allocate for variables.
* @param opts
* the set of optimizations that will be applied to boost
* function performance.
* @param map
* the arguments and any register numbers they will be
* assigned to (zero for no assignment).
* @param list
* the list of actions that define the operation performed by
* the function.
*/
public NewFunction2(final String aString,
final int count,
final Set<Optimization>opts,
final Map<String, Integer> map,
final List<Action> list) {
if (aString == null || aString.length() == 0) {
throw new IllegalArgumentException();
}
name = aString;
if ((count < 0) || (count > LAST_REGISTER)) {
throw new IllegalArgumentRangeException(0,
LAST_REGISTER, count);
}
registerCount = count;
int value = 0;
for (Optimization opt : opts) {
value |= opt.getValue();
}
optimizations = value;
if (map == null) {
throw new IllegalArgumentException();
}
arguments = map;
if (list == null) {
throw new IllegalArgumentException();
}
actions = list;
}
/**
* Creates and initialises a NewFunction2 action using the values
* copied from another NewFunction2 action.
*
* @param object
* a NewFunction2 action from which the values will be
* copied. References to immutable objects will be shared.
*/
public NewFunction2(final NewFunction2 object) {
name = object.name;
registerCount = object.registerCount;
optimizations = object.optimizations;
arguments = new LinkedHashMap<String, Integer>(object.arguments);
actions = new ArrayList<Action>(object.actions);
}
/**
* Get the name of the function. If the function will be used as an object
* method then the name is an empty string.
*
* @return the name of the function or an empty string.
*/
public String getName() {
return name;
}
/**
* Get the number of registers to allocate for function variables.
*
* @return the number of registers to allocate.
*/
public int getRegisterCount() {
return registerCount;
}
/**
* Get the list of Optimizations that will be used.
*
* @return the set of optimizations to increase performance.
*/
public Set<Optimization> getOptimizations() {
final Set<Optimization> set = EnumSet.noneOf(Optimization.class);
for (final Optimization opt : EnumSet.allOf(Optimization.class)) {
if ((optimizations & opt.getValue()) != 0) {
set.add(opt);
}
}
return set;
}
/**
* Get the list of RegisterVariables that define the function arguments
* and whether they are assigned to internal registers or to local variables
* in memory.
*
* @return a copy of the function arguments with optional register
* assignments.
*/
public Map<String, Integer> getArguments() {
return new LinkedHashMap<String, Integer>(arguments);
}
/**
* Get the actions executed by the function.
*
* @return a copy of the list of actions that make up the function body.
*/
public List<Action> getActions() {
return new ArrayList<Action>(actions);
}
/** {@inheritDoc} */
public NewFunction2 copy() {
return this;
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.format(FORMAT, name, registerCount, optimizations,
arguments, actions);
}
/** {@inheritDoc} */
public int prepareToEncode(final Context context) {
length = INITIAL_LENGTH + context.strlen(name);
for (final String arg : arguments.keySet()) {
length += arg.getBytes().length + 2;
}
length += 2;
if (actions.isEmpty()) {
actionsLength = 1;
} else {
actionsLength = 0;
}
for (final Action action : actions) {
actionsLength += action.prepareToEncode(context);
}
length += actionsLength;
return Coder.ACTION_HEADER + length;
}
/** {@inheritDoc} */
public void encode(final SWFEncoder coder, final Context context)
throws IOException {
coder.writeByte(ActionTypes.NEW_FUNCTION_2);
coder.writeShort(length - actionsLength);
coder.writeString(name);
coder.writeShort(arguments.size());
coder.writeByte(registerCount);
coder.writeByte(optimizations >>> Coder.TO_LOWER_BYTE);
coder.writeByte(optimizations);
for (final String arg : arguments.keySet()) {
coder.writeByte(arguments.get(arg));
coder.writeString(arg);
}
coder.writeShort(actionsLength);
for (final Action action : actions) {
action.encode(coder, context);
}
if (actions.isEmpty()) {
coder.writeByte(0);
}
}
}