Java tutorial
/* * 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)); } }); } }