com.medallia.spider.api.StRenderer.java Source code

Java tutorial

Introduction

Here is the source code for com.medallia.spider.api.StRenderer.java

Source

/*
 * This file is part of the Spider Web Framework.
 * 
 * The Spider Web Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * The Spider Web Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with the Spider Web Framework.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.medallia.spider.api;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateErrorListener;
import org.antlr.stringtemplate.StringTemplateGroup;
import org.antlr.stringtemplate.StringTemplateWriter;
import org.antlr.stringtemplate.language.ASTExpr;
import org.apache.commons.lang.StringEscapeUtils;

import com.medallia.spider.MethodInvoker;
import com.medallia.spider.MethodInvoker.LifecycleHandlerSet;
import com.medallia.spider.api.StRenderable.Input;
import com.medallia.spider.api.StRenderable.Output;
import com.medallia.spider.api.StRenderable.PostAction;
import com.medallia.spider.api.StRenderable.StTemplatePostAction;
import com.medallia.spider.api.StRenderable.V;
import com.medallia.spider.sttools.StTool;
import com.medallia.tiny.CollUtils;
import com.medallia.tiny.Empty;
import com.medallia.tiny.Implement;
import com.medallia.tiny.ObjectProvider;
import com.medallia.tiny.string.HtmlString;
import com.medallia.tiny.string.JsString;
import com.medallia.tiny.string.StringTemplateBuilder.SimpleAttributeRenderer;

/**
 * Class that handles the rendering of an instance of {@link StRenderable},
 * which is given in the constructor. The
 * {@link #actionAndRender(ObjectProvider, Map)} method is called to do the
 * rendering.
 * <p>
 * 
 * The PostAction returned from this method should be handled by the caller. One
 * exception is {@link StTemplatePostAction} which is replaced with a
 * {@link StRenderPostAction} by rendering the template. The caller can check
 * whether the returned object is an instance of that interface and if so
 * retrieve the rendered content.
 * 
 */
public abstract class StRenderer {
    private final StringTemplateFactory stringTemplateFactory;
    private final StRenderable renderable;

    /**
     * @param stringTemplateFactory object returned from {@link #makeStringTemplateFactory(StringTemplateErrorListener, StToolProvider)}
     * @param debugMode true if debug mode is on; the template source is then re-read from disk every time
     */
    public StRenderer(StringTemplateFactory stringTemplateFactory, StRenderable renderable) {
        this.stringTemplateFactory = stringTemplateFactory;
        this.renderable = renderable;
    }

    /** @return the default target */
    protected PostAction defaultPostAction() {
        return stRenderPostAction(getTemplateNameFromClass(renderable.getClassForTemplateName()));
    }

    /** PostAction that holds the result of the rendering of the template source */
    public interface StRenderPostAction extends PostAction {
        /** @return the rendered content */
        String getStContent();
    }

    /** Call {@link #render(String)} and wrap the return value in a {@link StRenderPostAction} */
    protected StRenderPostAction stRenderPostAction(String templateName) {
        final String stContent = render(templateName);
        return new StRenderPostAction() {
            public String getStContent() {
                return stContent;
            }
        };
    }

    /**
     * Call the action method of the {@link StRenderable}, render the template if applicable and return the result.
     * 
     * @param injector dependency injector with the objects available for injection
     * @param inputParams the request parameters
     * @return result of the action and render
     * @throws MissingAttributesException if the template referenced any attributes not set by the action method
     */
    public PostAction actionAndRender(ObjectProvider injector, LifecycleHandlerSet hs,
            Map<String, String[]> inputParams) throws MissingAttributesException {
        PostAction pa = invokeAction(injector, hs, inputParams);
        return pa == null ? defaultPostAction() : render(pa);
    }

    private PostAction render(PostAction pa) {
        if (pa instanceof StTemplatePostAction) {
            return stRenderPostAction(((StTemplatePostAction) pa).templateName());
        } else {
            return pa;
        }
    }

    /** map from the class to the action method of that class; stored for performance reasons */
    private static final ConcurrentMap<Class<?>, Method> ACTION_METHOD_MAP = Empty.concurrentMap();

    /** @return the action method of the given class; throws AssertionError if no such method exists */
    private static Method findActionMethod(Class<?> clazz) {
        Method am = ACTION_METHOD_MAP.get(clazz);
        if (am != null)
            return am;

        for (Method m : CollUtils.concat(Arrays.asList(clazz.getMethods()),
                Arrays.asList(clazz.getDeclaredMethods()))) {
            if (m.getName().equals("action")) {
                int modifiers = m.getModifiers();
                if (Modifier.isPrivate(modifiers) || Modifier.isStatic(modifiers))
                    continue;
                m.setAccessible(true);
                ACTION_METHOD_MAP.put(clazz, m);
                return m;
            }
        }
        throw new AssertionError("No action method found in " + clazz);
    }

    private static final ConcurrentMap<Class<?>, Class<?>> INPUT_ANNOTATION_MAP = Empty.concurrentMap();
    private static final ConcurrentMap<Class<?>, Class<?>> OUTPUT_ANNOTATION_MAP = Empty.concurrentMap();

    /** @return the interface declared within the given class which is also annotated with the given annotation */
    private static <X extends Annotation> Class<X> findInterfaceWithAnnotation(Map<Class<?>, Class<?>> methodMap,
            Class<?> clazz, Class<? extends Annotation> annotation) {
        Class<?> annotatedInterface = methodMap.get(clazz);
        if (annotatedInterface == null) {
            for (Class<?> c : CollUtils.concat(Arrays.asList(clazz.getClasses()),
                    Arrays.asList(clazz.getDeclaredClasses()))) {
                Annotation i = c.getAnnotation(annotation);
                if (i != null) {
                    annotatedInterface = c;
                    methodMap.put(clazz, annotatedInterface);
                    break;
                }
            }
        }
        @SuppressWarnings("unchecked")
        Class<X> x = (Class<X>) annotatedInterface;
        return x;
    }

    /**
     * Call the action method of the {@link StRenderable} and return the post action that needs to be rendered
     * 
     * @param injector dependency injector with the objects available for injection
     * @param hs handler for the life cycle of the injected objects
     * @param inputParams the request parameters
     * @return result of the action and render
     */
    public PostAction invokeAction(ObjectProvider injector, LifecycleHandlerSet hs,
            Map<String, String[]> inputParams) {
        DynamicInputImpl dynamicInput = new DynamicInputImpl(inputParams, inputArgParsers);
        injector = injector.copyWith(dynamicInput).errorOnUnknownType();

        Method am = findActionMethod(renderable.getClass());
        Class<Input> inputInterface = findInterfaceWithAnnotation(INPUT_ANNOTATION_MAP, renderable.getClass(),
                Input.class);
        if (inputInterface != null) {
            injector.register(createInput(inputInterface, dynamicInput));
        }

        return (PostAction) new MethodInvoker(injector, hs).invoke(am, renderable);
    }

    /** object that can parse a request parameter argument into a proper type */
    public interface InputArgParser<X> {
        /** @return the parsed object */
        X parse(String str);
    }

    private final Map<Class<?>, InputArgParser<?>> inputArgParsers = Empty.hashMap();

    private Object createInput(Class<?> x, final DynamicInputImpl dynamicInput) {
        return Proxy.newProxyInstance(x.getClassLoader(), new Class<?>[] { x }, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return dynamicInput.getInput(method.getName(), method.getReturnType(), method);
            }
        });
    }

    /** register the given {@link InputArgParser} */
    public <X> void registerArgParser(Class<X> type, InputArgParser<X> parser) {
        inputArgParsers.put(type, parser);
    }

    /** Exception thrown when a referenced StringTemplate attribute is not set */
    public static class MissingAttributesException extends RuntimeException {
        private final List<String> missingAttrs0;

        private MissingAttributesException(List<String> missingAttrs, StringTemplate st) {
            super("Missing attributes: " + missingAttrs + " in:\n" + st.getTemplate());
            this.missingAttrs0 = missingAttrs;
        }

        /** @return list of the referenced attributes that were not set */
        public List<String> getMissingAttributes() {
            return missingAttrs0;
        }
    }

    private String render(String templateName) throws MissingAttributesException {
        StringTemplate st = getStInstance(templateName);
        return render(st);
    }

    /** @return Pattern applied on the class name of the class implementing {@link StRenderable}; the
     * first group of this pattern should match the unique prefix, i.e. the part that maps to the
     * name of the .st file.
     */
    protected abstract Pattern getClassNamePrefixPattern();

    /** @return the template name based on the name of the given class */
    protected String getTemplateNameFromClass(Class<?> c) {
        String tn = c.getName();

        Pattern p = getClassNamePrefixPattern();
        Matcher m = p.matcher(tn);
        if (!m.matches())
            throw new AssertionError(
                    "Default template name expects class name [" + tn + "] to match regex " + p.pattern());

        tn = m.group(1);
        tn = tn.substring(0, 1).toLowerCase() + tn.substring(1);
        return tn;
    }

    /**
     * Holder class for data structures used to detect which attributes are used
     * in the template, but without a value set.
     */
    private static class StMissingAttrs {
        public final List<String> missingAttrs = Empty.list();
        public final Set<String> nullAttrs = Empty.hashSet();
    }

    private static final ThreadLocal<StMissingAttrs> ST_MISSING_ATTRS_TL = new ThreadLocal<StMissingAttrs>();

    /** Object used to look up the path to a named template */
    private interface StTemplatePath {
        /** See {@link StRenderer#findPathForTemplate(Class, String)} */
        String findPathForTemplate(String name);
    }

    /**
     * ThreadLocal used to store a callback to find the path to sub-templates
     * (which can happen in render context if one template includes another).
     */
    private static final ThreadLocal<StTemplatePath> ST_TEMPLATE_PATH_TL = new ThreadLocal<StTemplatePath>();

    private void setStTemplatePathTl() {
        ST_TEMPLATE_PATH_TL.set(new StTemplatePath() {
            @Implement
            public String findPathForTemplate(String name) {
                return StRenderer.this.findPathForTemplate(renderable.getClassForTemplateName(), name);
            }
        });
    }

    private void releaseStTemplatePathTl() {
        ST_TEMPLATE_PATH_TL.remove();
    }

    /** @return the result of rendering the given StringTemplate in the context set up by this class */
    public String render(StringTemplate st) throws MissingAttributesException {
        StMissingAttrs ctx = new StMissingAttrs();

        Class<Output> outputInterface = findInterfaceWithAnnotation(OUTPUT_ANNOTATION_MAP, renderable.getClass(),
                Output.class);
        if (outputInterface != null) {
            for (Field f : outputInterface.getDeclaredFields()) {
                f.setAccessible(true);
                V<?> tag;
                try {
                    tag = (V<?>) f.get(null);
                } catch (Exception e) {
                    throw new RuntimeException("For " + f, e);
                }
                String fname = f.getName().toLowerCase();
                Object obj = renderable.getAttr(tag);
                if (obj != null) {
                    st.setAttribute(fname, obj);
                } else if (renderable.hasAttr(tag)) {
                    ctx.nullAttrs.add(fname);
                }
            }
        }

        ST_MISSING_ATTRS_TL.set(ctx);
        setStTemplatePathTl();
        try {
            String stContent = renderFinal(st);
            if (!ctx.missingAttrs.isEmpty())
                throw new MissingAttributesException(ctx.missingAttrs, st);

            return stContent;
        } finally {
            releaseStTemplatePathTl();
            ST_MISSING_ATTRS_TL.remove();
        }
    }

    /**
     * @return a StringTemplate with the template loaded from the given filename.
     *         This template must be passed to {@link #render(StringTemplate)}
     *         to work correctly.
     */
    protected StringTemplate getStInstance(String templateName) {
        setStTemplatePathTl();
        try {
            return stringTemplateFactory.getStInstance(templateName);
        } finally {
            releaseStTemplatePathTl();
        }
    }

    /** actual perform the rendering of the given StringTemplate by calling {@link StringTemplate#toString()} */
    protected String renderFinal(StringTemplate st) {
        return st.toString();
    }

    /** @return the relative path to the .st files; by default this is a package called "pages" */
    protected String getPageRelativePath() {
        return "pages/";
    }

    /** @return the path to the .st file of the given name, relative to the package of the given class */
    protected String findPathForTemplate(Class<?> c, String name) {
        name = getPageRelativePath() + name;
        String path = name + ".st";
        while (c != null) {
            if (c.getResource(path) != null)
                return c.getPackage().getName().replace('.', '/') + "/" + name;
            c = c.getSuperclass();
        }
        throw new RuntimeException("Cannot find template " + name);
    }

    /**
     * Object that creates {@link StringTemplate} instances; see
     * {@link StRenderer#makeStringTemplateFactory(StringTemplateErrorListener, StToolProvider)}.
     */
    public interface StringTemplateFactory {
        /**
         * @return a StringTemplate with the template loaded from the given
         *         template name. Note that this method should NOT be invoked
         *         directly by client code; instead use
         *         {@link StRenderer#getStInstance(String)}.
         */
        StringTemplate getStInstance(String templateName);

        /** @return a StringTemplate using the given template */
        StringTemplate makeStInstance(String template);

        /** See {@link StringTemplateFactory#setRefreshInterval(int)} */
        void setRefreshInterval(int seconds);
    }

    /** Object that provides instances of {@link StTool} */
    public interface StToolProvider {
        /** @return the StTool for the given name */
        StTool getStTool(String name);
    }

    /**
     * @return a {@link StringTemplateFactory} object, which should be passed to
     *         {@link StRenderer#StRenderer(StringTemplateFactory, StRenderable)}.
     *         This object handles caching of the templates, thus it should be
     *         re-used for best performance.
     */
    public static StringTemplateFactory makeStringTemplateFactory(StringTemplateErrorListener errorListener,
            final StToolProvider stToolProvider) {
        final StringTemplateGroup stGroup = new StringTemplateGroup("StRenderer") {
            @Override
            public String getFileNameFromTemplateName(String name) {
                return super.getFileNameFromTemplateName(ST_TEMPLATE_PATH_TL.get().findPathForTemplate(name));
            }

            @Override
            public StringTemplate getEmbeddedInstanceOf(StringTemplate enclosingInstance, String name)
                    throws IllegalArgumentException {
                final StTool t = stToolProvider.getStTool(name);
                if (t != null)
                    return withEnclosing(enclosingInstance, new StringTemplate(this, name) {
                        @Override
                        public int write(StringTemplateWriter out) throws IOException {
                            // Use ASTExpr to render since the code for using AttributeRenderer is there
                            return new ASTExpr(null, null, null).writeAttribute(this, t.render(this), out);
                        }
                    });
                return super.getEmbeddedInstanceOf(enclosingInstance, name);
            }

            private StringTemplate withEnclosing(StringTemplate enclosingInstance, StringTemplate st) {
                st.setEnclosingInstance(enclosingInstance);
                return st;
            }

            @Override
            public StringTemplate createStringTemplate() {
                return new StringTemplate() {
                    @Override
                    public Object get(StringTemplate self, String attribute) {
                        Object o = super.get(self, attribute);
                        StMissingAttrs ctx;
                        if (self == this && o == null
                                && !(ctx = ST_MISSING_ATTRS_TL.get()).nullAttrs.contains(attribute)) {
                            ctx.missingAttrs.add(attribute);
                        }
                        return o;
                    }
                };
            }
        };
        stGroup.setErrorListener(errorListener);
        registerWebRenderers(stGroup);

        return new StringTemplateFactory() {
            @Implement
            public StringTemplate getStInstance(String templateName) {
                return stGroup.getInstanceOf(templateName);
            }

            @Implement
            public StringTemplate makeStInstance(String template) {
                StringTemplate st = stGroup.createStringTemplate();
                st.setGroup(stGroup);
                st.setTemplate(template);
                return st;
            }

            @Implement
            public void setRefreshInterval(int seconds) {
                stGroup.setRefreshInterval(seconds);
            }
        };
    }

    /** Register renderers (by calling {@link StringTemplateGroup#registerRenderer(Class, Object)}
     * useful for rendering web pages. This includes renderers for:
     * 
     *   o HtmlString
     *   o JsString
     *   
     * All plain String objects are escaped with {@link StringEscapeUtils#escapeHtml(String)}.
     * 
     * @param stGroup the object to register the renderers on
     */
    public static void registerWebRenderers(StringTemplateGroup stGroup) {
        stGroup.registerRenderer(HtmlString.class, HtmlString.ST_RENDERER);
        stGroup.registerRenderer(JsString.class, JsString.ST_RENDERER);
        stGroup.registerRenderer(String.class, new SimpleAttributeRenderer() {
            public String toString(Object o) {
                return StringEscapeUtils.escapeHtml(String.valueOf(o));
            }
        });
    }

}