/**
* Copyright (c) 2012-2015 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.tools.ToolErrorReporter;
import com.github.jknack.handlebars.Context;
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 {
/**
* The optimization level of rhino, default -1.
* Please refer to https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Optimization
*/
private int optimizationLevel = -1;
/**
* Better integration between java collections/arrays and js arrays. It check for data types
* at access time and convert them when necessary.
*
* @author edgar
*/
@SuppressWarnings("serial")
private static class BetterNativeArray extends NativeArray {
/** The context object. */
private Context context;
/** Internal state of array. */
private Map<Object, Object> state = new HashMap<Object, Object>();
/**
* A JS array.
*
* @param array Array.
* @param context Handlebars context.
*/
public BetterNativeArray(final Object[] array, final Context context) {
super(array);
this.context = context;
}
/**
* A JS collection.
*
* @param collection collection.
* @param context Handlebars context.
*/
public BetterNativeArray(final Collection<Object> collection, final Context context) {
this(collection.toArray(new Object[collection.size()]), context);
}
@Override
public Object get(final int index, final Scriptable start) {
Object value = state.get(index);
if (value != null) {
return value;
}
value = super.get(index, start);
value = toJsObject(value, context);
state.put(index, value);
return value;
}
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
String sep = ",";
for (Object v : this) {
buff.append(v).append(sep);
}
if (buff.length() > 0) {
buff.setLength(buff.length() - sep.length());
}
return buff.toString();
}
}
/**
* Better integration between java objects and js object. It check for data types at access time
* and convert them if necessary.
*
* @author edgar
*/
@SuppressWarnings("serial")
private static class BetterNativeObject extends NativeObject {
/** Handlebars context. */
private Context context;
/** Internal state. */
private Map<Object, Object> state = new HashMap<Object, Object>();
/**
* Creates a new {@link BetterNativeObject}.
*
* @param context Handlebars context.
*/
public BetterNativeObject(final Context context) {
this.context = context;
}
@Override
public Object get(final String name, final Scriptable start) {
Object value = state.get(name);
if (value != null) {
return value;
}
value = super.get(name, start);
value = toJsObject(value, context);
state.put(name, value);
return value;
}
}
/**
* 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 HandlebarsJs} options.
*/
public OptionsJs(final Options options) {
this.options = options;
this.hash = hash(options.hash, options.context);
this.params = new BetterNativeArray(options.params, options.context);
}
/**
* 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#fn()} template using the provided context.
*
* @return The resulting text.
* @throws IOException If a resource cannot be loaded.
*/
public CharSequence fn() throws IOException {
return options.fn();
}
/**
* 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);
}
/**
* Apply the {@link #options#inverse()} template using the provided context.
*
* @return The resulting text.
* @throws IOException If a resource cannot be loaded.
*/
public CharSequence inverse() throws IOException {
return options.inverse();
}
}
/**
* 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);
}
/**
* Creates a new {@link RhinoHandlebars}.
*
* @param helperRegistry The handlebars object.
* @param optimizationLevel The optimization level of rhino.
*/
public RhinoHandlebars(final HelperRegistry helperRegistry, final int optimizationLevel) {
super(helperRegistry);
this.optimizationLevel = optimizationLevel;
}
/**
* 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({"unchecked", "rawtypes" })
@Override
public Object apply(final Object context, final Options options) throws IOException {
Object jsContext = toJsObject(options.context.model(), options.context);
Object arg0 = context;
Integer paramSize = options.data(Context.PARAM_SIZE);
if (paramSize == 0) {
arg0 = "___NOT_SET_";
} else {
arg0 = toJsObject(arg0, options.context);
}
Object result = helper.apply(jsContext, arg0, new OptionsJs(options));
if (result instanceof NativeArray) {
return new BetterNativeArray((List) result, options.context);
}
return result;
}
});
}
@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);
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(optimizationLevel);
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();
env.put("Handlebars_java", env, this);
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.
* @param context Handlebars context.
* @return A JS Rhino object.
*/
private static NativeObject hash(final Map<?, Object> map, final Context context) {
NativeObject hash = new BetterNativeObject(context);
for (Entry<?, Object> prop : map.entrySet()) {
hash.defineProperty(prop.getKey().toString(), prop.getValue(), NativeObject.READONLY);
}
return hash;
}
/**
* Convert a Java Object to Js Object if necessary.
*
* @param object Source object.
* @param parent Handlebars context.
* @return A Rhino js object.
*/
@SuppressWarnings({"unchecked", "rawtypes" })
private static Object toJsObject(final Object object, final Context parent) {
if (object == null) {
return null;
}
if (object == Scriptable.NOT_FOUND) {
return Scriptable.NOT_FOUND;
}
if (object instanceof Number) {
return object;
}
if (object instanceof Boolean) {
return object;
}
if (object instanceof CharSequence || object instanceof Character) {
return object.toString();
}
if (Map.class.isInstance(object)) {
return hash((Map) object, parent);
} else if (Collection.class.isInstance(object)) {
return new BetterNativeArray((Collection) object, parent);
} else if (object.getClass().isArray()) {
Object[] array = (Object[]) object;
return new BetterNativeArray(array, parent);
} else if (object instanceof NativeArray) {
return new BetterNativeArray((NativeArray) object, parent);
} else if (object instanceof Scriptable) {
return object;
}
Context context = object instanceof Context
? (Context) object : Context.newContext(parent, object);
return toJsObject(context);
}
/**
* Convert a Java Object to Js Object if necessary.
*
* @param context Handlebars context.
* @return A Rhino js object.
*/
private static Object toJsObject(final Context context) {
Map<String, Object> hash = new HashMap<String, Object>();
for (Entry<String, Object> property : context.propertySet()) {
hash.put(property.getKey(), property.getValue());
}
return hash(hash, context);
}
}