name.martingeisse.common.javascript.JavascriptAssembler.java Source code

Java tutorial

Introduction

Here is the source code for name.martingeisse.common.javascript.JavascriptAssembler.java

Source

/**
 * Copyright (c) 2010 Martin Geisse
 *
 * This file is distributed under the terms of the MIT license.
 */

package name.martingeisse.common.javascript;

import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.ReadableInstant;
import org.joda.time.ReadablePartial;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * This class is used to assemble Javascript source code.
 */
public class JavascriptAssembler extends SourceCodeAssembler {

    /**
     * Application code can set this field to install a default date
     * formatter used by new instances of {@link JavascriptAssembler}.
     * Note however that access to this field is not synchronized automatically.
     */
    public static DateTimeFormatter defaultDateFormatter = DateTimeFormat.mediumDate().withZone(DateTimeZone.UTC);

    /**
     * Application code can set this field to install a default datetime
     * formatter used by new instances of {@link JavascriptAssembler}.
     * Note however that access to this field is not synchronized automatically.
     */
    public static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormat.mediumDateTime()
            .withZone(DateTimeZone.UTC);

    /**
     * 
     */
    private boolean firstCollectionElement;

    /**
     * the dateFormatter
     */
    private DateTimeFormatter dateFormatter;

    /**
     * the dateFormatter
     */
    private DateTimeFormatter dateTimeFormatter;

    /**
     * Constructor.
     */
    public JavascriptAssembler() {
        firstCollectionElement = false;
        dateFormatter = defaultDateFormatter;
        dateTimeFormatter = defaultDateTimeFormatter;
    }

    /**
     * Getter method for the dateFormatter.
     * @return the dateFormatter
     */
    public DateTimeFormatter getDateFormatter() {
        return dateFormatter;
    }

    /**
     * Setter method for the dateFormatter.
     * @param dateFormatter the dateFormatter to set
     */
    public void setDateFormatter(DateTimeFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    /**
     * Getter method for the dateTimeFormatter.
     * @return the dateTimeFormatter
     */
    public DateTimeFormatter getDateTimeFormatter() {
        return dateTimeFormatter;
    }

    /**
     * Setter method for the dateTimeFormatter.
     * @param dateTimeFormatter the dateTimeFormatter to set
     */
    public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter) {
        this.dateTimeFormatter = dateTimeFormatter;
    }

    /**
     * Appends the specified code fragment to the builder.
     * @param code the code fragment to append
     * @return this
     */
    public final JavascriptAssembler append(String code) {
        if (code == null) {
            throw new IllegalArgumentException("code argument is null");
        }
        getBuilder().append(code);
        return this;
    }

    /**
     * Appends the specified identifier to the builder.
     * @param name the identifier to append
     * @return this
     */
    public final JavascriptAssembler appendIdentifier(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name argument is null");
        }
        JavascriptAssemblerUtil.appendIdentifier(getBuilder(), name);
        return this;
    }

    /**
     * Appends the null literal to the builder.
     * @return this
     */
    public final JavascriptAssembler appendNullLiteral() {
        getBuilder().append("null");
        return this;
    }

    /**
     * Appends the specified string literal to the builder.
     * @param value the value of the literal to append (must not be null)
     * @return this
     */
    public final JavascriptAssembler appendStringLiteral(String value) {
        if (value == null) {
            throw new IllegalArgumentException("value argument is null");
        }
        JavascriptAssemblerUtil.appendStringLiteral(getBuilder(), value);
        return this;
    }

    /**
     * Appends the specified string literal to the builder, or the null literal if
     * the argument is null.
     * @param value the value of the literal to append (may be null)
     * @return this
     */
    public final JavascriptAssembler appendStringLiteralOrNull(String value) {
        if (value == null) {
            appendNullLiteral();
        } else {
            JavascriptAssemblerUtil.appendStringLiteral(getBuilder(), value);
        }
        return this;
    }

    /**
     * Appends the specified boolean literal to the builder.
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendBooleanLiteral(boolean value) {
        JavascriptAssemblerUtil.appendBooleanLiteral(getBuilder(), value);
        return this;
    }

    /**
     * Appends the specified numeric literal to the builder.
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendNumericLiteral(int value) {
        getBuilder().append(value);
        return this;
    }

    /**
     * Appends the specified numeric literal to the builder.
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendNumericLiteral(long value) {
        getBuilder().append(value);
        return this;
    }

    /**
     * Appends the specified numeric literal to the builder.
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendNumericLiteral(double value) {
        getBuilder().append(value);
        return this;
    }

    /**
     * Appends the specified numeric literal to the builder.
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendNumericLiteral(Number value) {
        if (value == null) {
            throw new IllegalArgumentException("value argument is null");
        }
        getBuilder().append(value.toString());
        return this;
    }

    /**
     * Appends the specified numeric literal to the builder, or the null literal if
     * the argument is null.
     * @param value the value of the literal to append (may be null)
     * @return this
     */
    public final JavascriptAssembler appendNumericLiteralOrNull(Number value) {
        if (value == null) {
            appendNullLiteral();
        } else {
            appendNumericLiteral(value);
        }
        return this;
    }

    /**
     * Appends the date contained in the specified instant according
     * to the date formatter that is set for this assembler.
     * @param value the instant to extract the date from
     * @return this
     */
    public final JavascriptAssembler appendDateLiteral(ReadableInstant value) {
        appendJodaLiteral(value, dateFormatter);
        return this;
    }

    /**
     * Appends the date contained in the specified instant according
     * to the date formatter that is set for this assembler,
     * or the null literal if the argument is null.
     * @param value the instant to extract the date from (may be null)
     * @return this
     */
    public final JavascriptAssembler appendDateLiteralOrNull(ReadableInstant value) {
        appendJodaLiteralOrNull(value, dateFormatter);
        return this;
    }

    /**
     * Appends the datetime contained in the specified instant according
     * to the datetime formatter that is set for this assembler.
     * @param value the instant to extract the datetime from
     * @return this
     */
    public final JavascriptAssembler appendDateTimeLiteral(ReadableInstant value) {
        appendJodaLiteral(value, dateTimeFormatter);
        return this;
    }

    /**
     * Appends the datetime contained in the specified instant according
     * to the datetime formatter that is set for this assembler,
     * or the null literal if the argument is null.
     * @param value the instant to extract the datetime from (may be null)
     * @return this
     */
    public final JavascriptAssembler appendDateTimeLiteralOrNull(ReadableInstant value) {
        appendJodaLiteralOrNull(value, dateTimeFormatter);
        return this;
    }

    /**
     * Uses the specified Joda-Time instant and formatter to append a string literal.
     * 
     * @param value the instant to append
     * @param formatter the formatter used to turn the instant into a string
     * @return this
     */
    public final JavascriptAssembler appendJodaLiteral(ReadableInstant value, DateTimeFormatter formatter) {
        if (value == null) {
            throw new IllegalArgumentException("value argument is null");
        }
        appendStringLiteral(formatter.print(value));
        return this;
    }

    /**
     * Uses the specified Joda-Time instant and formatter to append a string literal,
     * or the null literal if the argument is null.
     * 
     * @param value the instant to append (may be null)
     * @param formatter the formatter used to turn the instant into a string
     * @return this
     */
    public final JavascriptAssembler appendJodaLiteralOrNull(ReadableInstant value, DateTimeFormatter formatter) {
        if (value == null) {
            appendNullLiteral();
        } else {
            appendStringLiteral(formatter.print(value));
        }
        return this;
    }

    /**
     * Appends the specified date according to the date formatter
     * that is set for this assembler.
     * @param value the date
     * @return this
     */
    public final JavascriptAssembler appendDateLiteral(ReadablePartial value) {
        appendJodaLiteral(value, dateFormatter);
        return this;
    }

    /**
     * Appends the specified date according to the date formatter
     * that is set for this assembler,
     * or the null literal if the argument is null.
     * @param value the date (may be null)
     * @return this
     */
    public final JavascriptAssembler appendDateLiteralOrNull(ReadablePartial value) {
        appendJodaLiteralOrNull(value, dateFormatter);
        return this;
    }

    /**
     * Appends the specified datetime according to the datetime formatter
     * that is set for this assembler.
     * @param value the datetime
     * @return this
     */
    public final JavascriptAssembler appendDateTimeLiteral(ReadablePartial value) {
        appendJodaLiteral(value, dateTimeFormatter);
        return this;
    }

    /**
     * Appends the specified datetime according to the datetime formatter
     * that is set for this assembler,
     * or the null literal if the argument is null.
     * @param value the datetime (may be null)
     * @return this
     */
    public final JavascriptAssembler appendDateTimeLiteralOrNull(ReadablePartial value) {
        appendJodaLiteralOrNull(value, dateTimeFormatter);
        return this;
    }

    /**
     * Uses the specified Joda-Time partial and formatter to append a string literal.
     * 
     * @param value the partial to append
     * @param formatter the formatter used to turn the partial into a string
     * @return this
     */
    public final JavascriptAssembler appendJodaLiteral(ReadablePartial value, DateTimeFormatter formatter) {
        if (value == null) {
            throw new IllegalArgumentException("value argument is null");
        }
        appendStringLiteral(formatter.print(value));
        return this;
    }

    /**
     * Uses the specified Joda-Time partial and formatter to append a string literal,
     * or the null literal if the argument is null.
     * 
     * @param value the partial to append (may be null)
     * @param formatter the formatter used to turn the partial into a string
     * @return this
     */
    public final JavascriptAssembler appendJodaLiteralOrNull(ReadablePartial value, DateTimeFormatter formatter) {
        if (value == null) {
            appendNullLiteral();
        } else {
            appendStringLiteral(formatter.print(value));
        }
        return this;
    }

    /**
     * Appends the specified object property name to the builder.
     * Note that no escaping is done for the name, so the name
     * must conform to identifier syntax.
     * @param name the name of the property
     * @return this
     */
    public final JavascriptAssembler appendPropertyName(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name argument is null");
        }
        JavascriptAssemblerUtil.appendStringLiteral(getBuilder(), name);
        return this;
    }

    /**
     * Common handling to begin a list or object.
     * @param mark the punctuation mark to use
     */
    private void beginCollection(char mark) {
        getBuilder().append(mark);
        firstCollectionElement = true;
    }

    /**
     * Common handling to end a list or object.
     * @param mark the punctuation mark to use
     */
    private void endCollection(char mark) {
        getBuilder().append(mark);

        /**
         * This statement is necessary to handle the special
         * case that the first element of a collection is an
         * empty collection. Without this statement, the inner
         * collection sets the first-flag to true and never resets
         * it since it is empty. The outer collection then
         * misinterprets this and omits the comma for the second
         * element.
         */
        firstCollectionElement = false;
    }

    /**
     * Common handling to start a new collection element.
     * This method handles comma-separation of elements.
     */
    private void prepareCollectionElement() {
        if (firstCollectionElement) {
            firstCollectionElement = false;
        } else {
            getBuilder().append(", ");
        }
    }

    /**
     * Begins a new list expression.
     * @return this
     */
    public final JavascriptAssembler beginList() {
        beginCollection('[');
        return this;
    }

    /**
     * Ends the current list expression.
     * @return this
     */
    public final JavascriptAssembler endList() {
        endCollection(']');
        return this;
    }

    /**
     * This method must be called before each list element.
     * @return this
     */
    public final JavascriptAssembler prepareListElement() {
        prepareCollectionElement();
        return this;
    }

    /**
     * Begins a new object expression.
     * @return this
     */
    public final JavascriptAssembler beginObject() {
        beginCollection('{');
        return this;
    }

    /**
     * Ends the current object expression.
     * @return this
     */
    public final JavascriptAssembler endObject() {
        endCollection('}');
        return this;
    }

    /**
     * This method must be called before each object property.
     * @param name the name of the property
     * @return this
     */
    public final JavascriptAssembler prepareObjectProperty(String name) {
        prepareCollectionElement();
        appendPropertyName(name);
        getBuilder().append(": ");
        return this;
    }

    /**
     * Appends the specified primitive-typed literal to the builder.
     * Supported Java types are: {@link String}, {@link Number},
     * {@link Boolean}, and null values.
     * 
     * @param value the value of the literal to append
     * @return this
     */
    public final JavascriptAssembler appendPrimitive(Object value) {
        if (value == null) {
            appendNullLiteral();
        } else if (value instanceof String) {
            appendStringLiteral((String) value);
        } else if (value instanceof Number) {
            appendNumericLiteral((Number) value);
        } else if (value instanceof Boolean) {
            appendBooleanLiteral((Boolean) value);
        } else if (value instanceof ReadableInstant) {
            appendDateTimeLiteral((ReadableInstant) value);
        } else if (value instanceof LocalDate) {
            appendDateLiteral((LocalDate) value);
        } else if (value instanceof LocalDateTime) {
            appendDateTimeLiteral((LocalDateTime) value);
        } else if (value instanceof ReadablePartial) {
            appendDateTimeLiteral((ReadablePartial) value);
        } else {
            appendCustomPrimitive(value);
        }
        return this;
    }

    /**
     * This method is invoked by {@link #appendPrimitive(Object)} for
     * unknown types. It should either convert the value to a valid
     * JSON expression or, if the value has an unknown type, call
     * {@link #unknownPrimitiveTypeException(Object)} and throw the
     * returned exception.
     * 
     * The default implementation knows no custom type and thus always
     * throws the exception.
     * 
     * @param value the value of the literal to append
     */
    protected void appendCustomPrimitive(Object value) {
        throw unknownPrimitiveTypeException(value);
    }

    /**
     * Creates an {@link IllegalArgumentException} about a value with an
     * unknown primitive type that was passed to {@link #appendPrimitive(Object)}.
     * @param value the value
     * @return the exception
     */
    protected final IllegalArgumentException unknownPrimitiveTypeException(Object value) {
        return new IllegalArgumentException("not a JSON-compatible primitive value: " + value);
    }

    /**
     * Appends an array of primitive values as a Javascript array, using
     * {@link #appendPrimitive(Object)} for each element.
     * @param value the array to append
     * @return this
     */
    public final JavascriptAssembler appendPrimitiveArray(Object[] value) {
        beginList();
        for (Object element : value) {
            prepareListElement();
            appendPrimitive(element);
        }
        endList();
        return this;
    }

    /**
     * Appends an {@link Iterable} of primitive values as a Javascript array, using
     * {@link #appendPrimitive(Object)} for each element.
     * @param value the iterable to append
     * @return this
     */
    public final JavascriptAssembler appendPrimitiveArray(Iterable<?> value) {
        beginList();
        for (Object element : value) {
            prepareListElement();
            appendPrimitive(element);
        }
        endList();
        return this;
    }

    /**
     * Begins a nested variable scope by using an anonymous function.
     * This is useful in two ways. First, it shields the outer scope
     * from the variables defined in the inner scope.
     * 
     * Second, it creates a new scope each time the scope statement is
     * executed, which is necessary to capture the state of the current
     * iteration in a for loop when using a closure within the loop.
     * (Blocks, and especially the iteration block of a for loop, do
     * not create nested variable scopes themselves in Javascript). The
     * correct way to use a nested scope for this purpose is to use
     * this method within the for loop, define variables for all state
     * to be captured, initialize them with values from the outer scope,
     * then define the actual closure and have if refer only to variables
     * from the nested scope.
     * 
     * Consider the following nonworking example:
     * 
     * functions = [];
     * for (var i=0; i<3; i++) {
     *   var value = i;
     *     functions[i] = function() {
     *       alert('value: ' + value);
     *   }
     * }
     * 
     * Even though the value of i is copied into the "value" variable,
     * the for loop does not create a nested scope on its own, so "value"
     * refers to the same variable in all three iterations, hence all
     * functions output the value 2 which is the last value written into
     * the "value" variable.
     * 
     * The following example works:
     * 
     * functions = [];
     * for (var i=0; i<3; i++) {
     * 
     *   // this line is generated by beginNestedScope()
     *   (function() {
     * 
     *     // the caller must manually copy all state that shall be captured
     *     // into variables from the nested scope ...
     *     var value = i;
     *     
     *     // ... then build the actual closure
     *     functions[i] = function() {
     *       alert('value: ' + value);
     *     }
     *     
     *   // this line is generated by endNestedScope()
     *   })();
     *   
     * }
     * 
     * This method creates complete lines including indentation and
     * also increments indentation for the nested scope.
     * 
     * @return this
     */
    public final JavascriptAssembler beginNestedScope() {
        appendIndentedLine("(function() {");
        incrementIndentation();
        return this;
    }

    /**
     * Ends a nested scope started by beginNestedScope(). See that method for
     * a discussion of the purpose of nested scopes.
     * 
     * @return this
     */
    public final JavascriptAssembler endNestedScope() {
        decrementIdentation();
        appendIndentedLine("})();");
        return this;
    }

}