com.github.jknack.handlebars.internal.BaseTemplate.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jknack.handlebars.internal.BaseTemplate.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;

import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.split;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.HandlebarsError;
import com.github.jknack.handlebars.HandlebarsException;
import com.github.jknack.handlebars.TagType;
import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.TypeSafeTemplate;

/**
 * Base class for {@link Template}.
 *
 * @author edgar.espina
 * @since 0.1.0
 */
abstract class BaseTemplate implements Template {

    /**
     * The line of this template.
     */
    protected int line;

    /**
     * The column of this template.
     */
    protected int column;

    /**
     * The file's name.
     */
    protected String filename;

    /**
     * A Handlebars.js lock.
     */
    private final Object jsLock = new Object();

    /**
     * A pre-compiled JavaScript function.
     */
    private String javaScript;

    /**
     * {@inheritDoc}
     */
    @Override
    public final String apply(final Object context) throws IOException {
        return apply(wrap(context));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void apply(final Object context, final Writer writer) throws IOException {
        apply(wrap(context), writer);
    }

    @Override
    public String apply(final Context context) throws IOException {
        FastStringWriter writer = new FastStringWriter();
        try {
            apply(context, writer);
            return writer.toString();
        } finally {
            writer.close();
        }
    }

    @Override
    public void apply(final Context context, final Writer writer) throws IOException {
        notNull(writer, "A writer is required.");
        Context wrapped = wrap(context);
        try {
            merge(wrapped, writer);
        } catch (HandlebarsException ex) {
            throw ex;
        } catch (Exception ex) {
            String evidence = toString();
            String reason = ex.toString();
            String message = filename + ":" + line + ":" + column + ": " + reason + "\n";
            message += "    " + join(split(evidence, "\n"), "\n    ");
            HandlebarsError error = new HandlebarsError(filename, line, column, reason, evidence, message);
            HandlebarsException hex = new HandlebarsException(error, ex);
            // Override the stack-trace
            hex.setStackTrace(ex.getStackTrace());
            throw hex;
        } finally {
            if (wrapped != context) {
                wrapped.destroy();
            }
        }
    }

    /**
     * Wrap the candidate object as a Context, or creates a new context.
     *
     * @param candidate The candidate object.
     * @return A context.
     */
    private static Context wrap(final Object candidate) {
        if (candidate instanceof Context) {
            return (Context) candidate;
        }
        return Context.newContext(candidate);
    }

    /**
     * Merge a child template into the writer.
     *
     * @param context The scope object.
     * @param writer The writer.
     * @throws IOException If a resource cannot be loaded.
     */
    protected abstract void merge(final Context context, Writer writer) throws IOException;

    @Override
    public final String toString() {
        return filename + ":" + line + ":" + column;
    }

    /**
     * Set the file's name.
     *
     * @param filename The file's name.
     * @return This template.
     */
    public BaseTemplate filename(final String filename) {
        this.filename = filename;
        return this;
    }

    @Override
    public String filename() {
        return filename;
    }

    @Override
    public int[] position() {
        return new int[] { line, column };
    }

    /**
     * Set the template position.
     *
     * @param line The line.
     * @param column The column.
     * @return This template.
     */
    public BaseTemplate position(final int line, final int column) {
        this.line = line;
        this.column = column;
        return this;
    }

    @Override
    public <T, S extends TypeSafeTemplate<T>> S as(final Class<S> rootType) {
        notNull(rootType, "The rootType can't be null.");
        isTrue(rootType.isInterface(), "Not an interface: %s", rootType.getName());
        @SuppressWarnings("unchecked")
        S template = (S) newTypeSafeTemplate(rootType, this);
        return template;
    }

    @Override
    public <T> TypeSafeTemplate<T> as() {
        @SuppressWarnings("unchecked")
        TypeSafeTemplate<T> template = (TypeSafeTemplate<T>) newTypeSafeTemplate(TypeSafeTemplate.class, this);
        return template;
    }

    /**
     * Creates a new {@link TypeSafeTemplate}.
     *
     * @param rootType The target type.
     * @param template The target template.
     * @return A new {@link TypeSafeTemplate}.
     */
    private static Object newTypeSafeTemplate(final Class<?> rootType, final Template template) {
        return Proxy.newProxyInstance(template.getClass().getClassLoader(), new Class[] { rootType },
                new InvocationHandler() {
                    private Map<String, Object> attributes = new HashMap<String, Object>();

                    @Override
                    public Object invoke(final Object proxy, final Method method, final Object[] args)
                            throws IOException {
                        String methodName = method.getName();
                        if ("apply".equals(methodName)) {
                            Context context = Context.newBuilder(args[0]).combine(attributes).build();
                            attributes.clear();
                            if (args.length == 2) {
                                template.apply(context, (Writer) args[1]);
                                return null;
                            }
                            return template.apply(context);
                        }

                        if (Modifier.isPublic(method.getModifiers()) && methodName.startsWith("set")) {
                            String attrName = StringUtils.uncapitalize(methodName.substring("set".length()));
                            if (args != null && args.length == 1 && attrName.length() > 0) {
                                attributes.put(attrName, args[0]);
                                if (TypeSafeTemplate.class.isAssignableFrom(method.getReturnType())) {
                                    return proxy;
                                }
                                return null;
                            }
                        }
                        String message = String.format(
                                "No handler method for: '%s(%s)', expected method signature is: 'setXxx(value)'",
                                methodName, args == null ? "" : join(args, ", "));
                        throw new UnsupportedOperationException(message);
                    }
                });
    }

    @Override
    public List<String> collect(final TagType... tagType) {
        isTrue(tagType.length > 0, "At least one tag type is required.");
        Set<String> tagNames = new LinkedHashSet<String>();
        for (TagType tt : tagType) {
            collect(tagNames, tt);
        }
        return new ArrayList<String>(tagNames);
    }

    /**
     * Child classes might want to check if they apply to the tagtype and append them self to the
     * result list.
     *
     * @param result The result list.
     * @param tagType The matching tagtype.
     */
    protected void collect(final Collection<String> result, final TagType tagType) {
    }

    @Override
    public List<String> collectReferenceParameters() {
        Set<String> paramNames = new LinkedHashSet<String>();
        collectReferenceParameters(paramNames);
        return new ArrayList<String>(paramNames);
    }

    /**
     * @param result The result list to add new parameters to.
     */
    protected void collectReferenceParameters(final Collection<String> result) {
    }

    @Override
    public String toJavaScript() {
        synchronized (jsLock) {
            if (javaScript == null) {
                javaScript = JSEngine.RHINO.toJavaScript(this);
            }
            return javaScript;
        }
    }

}