com.github.jknack.handlebars.internal.js.RhinoHandlebars.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jknack.handlebars.internal.js.RhinoHandlebars.java

Source

/**
 * Copyright (c) 2012-2013 Edgar Espina
 *
 * This file is part of Handlebars.java.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jknack.handlebars.internal.js;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang3.ClassUtils;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.ToolErrorReporter;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.HelperRegistry;
import com.github.jknack.handlebars.Options;
import com.github.jknack.handlebars.internal.Files;
import com.github.jknack.handlebars.js.HandlebarsJs;

/**
 * An implementation of {@link HandlebarsJs} on top of Rhino.
 *
 * @author edgar.espina.
 * @since 1.1.0
 */
public class RhinoHandlebars extends HandlebarsJs {

    /**
     * Wrap a {@link Context handlebars context} as a Rhino Js Object.
     *
     * @author edgar.espina
     * @since 1.1.0
     */
    @SuppressWarnings("serial")
    public static class JsContext extends ScriptableObject {

        /**
         * The {@link Handlebars} context.
         */
        private Context context;

        /**
         * Creates a new {@link JsContext}.
         *
         * @param context The {@link Handlebars} context.
         */
        public JsContext(final Context context) {
            this.context = context;
        }

        @Override
        public String getClassName() {
            return "Object";
        }

        @Override
        public Object get(final String name, final Scriptable start) {
            return context.get(name);
        }

        @Override
        public Object get(final int index, final Scriptable start) {
            return context.get("" + index);
        }

        @Override
        public Object[] getIds() {
            Set<Entry<String, Object>> propertySet = context.propertySet();
            Object[] ids = new Object[propertySet.size()];
            int idx = 0;
            for (Entry<String, Object> entry : propertySet) {
                ids[idx++] = entry.getKey();
            }
            return ids;
        }

    }

    /**
     * The JavaScript helper contract.
     *
     * @author edgar.espina
     * @since 1.1.0
     */
    public interface JsHelper {

        /**
         * Apply the helper to the context.
         *
         * @param context The context object.
         * @param arg0 The helper first argument.
         * @param options The options object.
         * @return A string result.
         */
        Object apply(Object context, Object arg0, OptionsJs options);
    }

    /**
     * The Handlebars.js options.
     *
     * @author edgar.espina
     * @since 1.1.0
     */
    public static class OptionsJs {
        /**
         * Handlebars.java options.
         */
        private Options options;

        /**
         * The options hash as JS Rhino object.
         */
        public NativeObject hash;

        /**
         * The helper params as JS Rhino object.
         */
        public NativeArray params;

        /**
         * Creates a new {@link HandlebarsJs} options.
         *
         * @param options The {@link Handlebars} options.
         */
        public OptionsJs(final Options options) {
            this.options = options;
            this.hash = hash(options.hash);
            this.params = new NativeArray(options.params);
        }

        /**
         * Apply the {@link #options#fn(Object)} template using the provided context.
         *
         * @param context The context to use.
         * @return The resulting text.
         * @throws IOException If a resource cannot be loaded.
         */
        public CharSequence fn(final Object context) throws IOException {
            return options.fn(context);
        }

        /**
         * Apply the {@link #options#inverse(Object)} template using the provided context.
         *
         * @param context The context to use.
         * @return The resulting text.
         * @throws IOException If a resource cannot be loaded.
         */
        public CharSequence inverse(final Object context) throws IOException {
            return options.inverse(context);
        }
    }

    /**
     * The JavaScript helpers environment for Rhino.
     */
    private static final String HELPERS_ENV = envSource("/helpers.rhino.js");

    /**
     * Creates a new {@link RhinoHandlebars}.
     *
     * @param helperRegistry The handlebars object.
     */
    public RhinoHandlebars(final HelperRegistry helperRegistry) {
        super(helperRegistry);
    }

    /**
     * Register a helper in the helper registry.
     *
     * @param name The helper's name. Required.
     * @param helper The helper object. Required.
     */
    public void registerHelper(final String name, final JsHelper helper) {
        registry.registerHelper(name, new Helper<Object>() {
            @SuppressWarnings({ "rawtypes", "unchecked" })
            @Override
            public CharSequence apply(final Object context, final Options options) throws IOException {
                JsContext jsContext = new JsContext(options.context);
                Object arg0 = context;
                Integer paramSize = options.data(Context.PARAM_SIZE);
                if (paramSize == 0) {
                    arg0 = "___NOT_SET_";
                } else if (!isSupportedType(arg0)) {
                    if (Map.class.isInstance(arg0)) {
                        arg0 = hash((Map) arg0);
                    } else if (Collection.class.isInstance(arg0)) {
                        arg0 = new NativeArray(((Collection) arg0).toArray(new Object[0]));
                    }
                }
                Object result = helper.apply(jsContext, arg0, new OptionsJs(options));
                if (result instanceof CharSequence) {
                    return (CharSequence) result;
                }
                return result == null ? null : result.toString();
            }
        });
    }

    /**
     * True if the object is natively supported by Rhino.
     *
     * @param object An object.
     * @return True if the object is natively supported by Rhino.
     */
    private static boolean isSupportedType(final Object object) {
        if (object == null) {
            return true;
        }
        if (CharSequence.class.isInstance(object)) {
            return true;
        }
        return ClassUtils.isPrimitiveOrWrapper(object.getClass());
    }

    @Override
    public void registerHelpers(final String filename, final String source) throws Exception {

        org.mozilla.javascript.Context ctx = null;
        try {
            ctx = newContext();

            Scriptable sharedScope = helpersEnvScope(ctx);
            sharedScope.put("Handlebars_java", sharedScope, this);
            Scriptable scope = ctx.newObject(sharedScope);
            scope.setParentScope(null);
            scope.setPrototype(sharedScope);

            ctx.evaluateString(scope, source, filename, 1, null);
        } finally {
            if (ctx != null) {
                org.mozilla.javascript.Context.exit();
            }
        }
    }

    /**
     * Creates a new Rhino Context.
     *
     * @return A Rhino Context.
     */
    private org.mozilla.javascript.Context newContext() {
        org.mozilla.javascript.Context ctx = org.mozilla.javascript.Context.enter();
        ctx.setOptimizationLevel(-1);
        ctx.setErrorReporter(new ToolErrorReporter(false));
        ctx.setLanguageVersion(org.mozilla.javascript.Context.VERSION_1_8);
        return ctx;
    }

    /**
     * Creates a initialize the helpers.rhino.js scope.
     *
     * @param ctx A rhino context.
     * @return A handlebars.js scope. Shared between executions.
     */
    private Scriptable helpersEnvScope(final org.mozilla.javascript.Context ctx) {
        Scriptable env = ctx.initStandardObjects();
        ctx.evaluateString(env, HELPERS_ENV, "helpers.rhino.js", 1, null);
        return env;
    }

    /**
     * Load the helper environment.
     *
     * @param location The classpath location.
     * @return The helper environment.
     */
    private static String envSource(final String location) {
        try {
            return Files.read(location);
        } catch (IOException ex) {
            throw new IllegalStateException("Unable to read " + location, ex);
        }
    }

    /**
     * Convert a map to a JS Rhino object.
     *
     * @param map The map.
     * @return A JS Rhino object.
     */
    private static NativeObject hash(final Map<String, Object> map) {
        NativeObject hash = new NativeObject();
        for (Entry<String, Object> prop : map.entrySet()) {
            hash.defineProperty(prop.getKey(), prop.getValue(), NativeObject.READONLY);
        }
        return hash;
    }
}