/** * Copyright 2010 Bing Ran<bing_ran@hotmail.com> * * 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 cn.bran.japid.template; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; import cn.bran.japid.MyTuple2; import cn.bran.japid.tags.Each; import cn.bran.japid.tags.Each.BreakLoop; import cn.bran.japid.tags.Each.ContinueLoop; import cn.bran.japid.util.StringUtils; import cn.bran.japid.classmeta.MimeTypeEnum; import cn.bran.japid.compiler.NamedArg; import cn.bran.japid.compiler.NamedArgRuntime; import cn.bran.japid.util.HTMLUtils; import cn.bran.japid.util.JapidFlags; import cn.bran.japid.util.WebUtils; /** * a java based template suing StringBuilder as the content buffer, no play * dependency. * * @author bran * */ public abstract class JapidTemplateBaseWithoutPlay implements Serializable { public String sourceTemplate = ""; private StringBuilder out; private Map<String, String> headers;// = new TreeMap<String, String>(); // directive for tracing templates navigation private Boolean traceFile = null; private String contentType = ""; private Boolean stopwatch = null; long startTime = System.nanoTime(); // nano-second when starting rendering protected long renderingTime = -1; // in microsecond // the template that calls this as a tag protected JapidTemplateBaseWithoutPlay caller; // <marker, time consumption> public List<MyTuple2<String, Long>> timeLogs; private void init() { if (headers == null) { headers = new TreeMap<String, String>(); // headers = new HashMap<String, String>(); headers.put("Content-Type", "text/html; charset=utf-8"); } if (timeLogs == null) { timeLogs = new LinkedList<MyTuple2<String, Long>>(); } if (out == null) out = new StringBuilder(4000); } public void setOut(StringBuilder out) { this.out = out; } protected StringBuilder getOut() { return out; } // public JapidTemplateBase() { // // }; protected void putHeader(String k, String v) { headers.put(k, v); } protected Map<String, String> getHeaders() { return this.headers; } public JapidTemplateBaseWithoutPlay(StringBuilder out2) { this.out = out2; init(); } public JapidTemplateBaseWithoutPlay(JapidTemplateBaseWithoutPlay caller) { if (caller != null) { out = caller.getOut(); } this.caller = caller; this.timeLogs = caller.timeLogs; this.headers = caller.getHeaders(); this.stopwatch = caller.stopwatch; init(); } // don't use it since it will lead to new instance of stringencoder // Charset UTF8 = Charset.forName("UTF-8"); final protected void p(String s) { if (s != null && !s.isEmpty()) out.append(s); // writeString(s); } final protected void pln(String s) { if (s != null && !s.isEmpty()) out.append(s); // writeString(s); out.append('\n'); } /** * @param s * @throws IOException * @throws UnsupportedEncodingException */ final private void writeString(String s) { // ByteBuffer bb = StringUtils.encodeUTF8(s); // out.write(bb.array(), 0, bb.position()); // ok my code is slower in large trunk of data if (s != null && !s.isEmpty()) out.append(s); } // final protected void pln(byte[] ba) { // try { // out.write(ba); // out.write('\n'); // } catch (IOException e) { // throw new RuntimeException(e); // } // } final protected void p(Object s) { if (s != null) { writeString(s.toString()); // out.append(s); } } final protected void pln(Object s) { if (s != null) writeString(s.toString()); pln(); } final protected void pln() { out.append('\n'); } /** * The template pattern to implement the template/layout relationship. * Clients call a template's render(), which store params in fields and * calls in super class's layout, which does the whole page layout and calls * back child's doLayout to get the child content. */ protected void layout() { doLayout(); } protected abstract void doLayout(); static protected byte[] getBytes(String src) { if (src == null || src.length() == 0) return new byte[] {}; try { return src.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } @Override public String toString() { return this.out.toString(); } /** * reflect this object for a method of the name * * @param methodName * @return */ protected String get(String methodName, String defaultVal) { try { Method method = this.getClass().getMethod(methodName, (Class[]) null); String invoke = (String) method.invoke(this, (Object[]) null); return invoke; } catch (Exception e) { return defaultVal; } } /** * reflect this object for a method of the name * * @param methodName * @return */ protected String get(String methodName) { try { Method method = this.getClass().getMethod(methodName, (Class[]) null); String invoke = (String) method.invoke(this, (Object[]) null); return invoke; } catch (Exception e) { throw new RuntimeException(e); } } protected boolean asBoolean(Object o) { return WebUtils.asBoolean(o); } /** * escape the string representation of the object to make it HTML safe. * * @param o * @return */ public static String escape(Object o) { if (o == null) return null; return HTMLUtils.htmlEscape(o.toString()); } /** * @param currentClass */ public static Method getRenderMethod(Class<? extends JapidTemplateBaseWithoutPlay> currentClass) { java.lang.reflect.Method[] methods = currentClass.getDeclaredMethods(); Method r = null; for (java.lang.reflect.Method m : methods) { if (m.getName().equals("render")) { Class<?>[] parameterTypes = m.getParameterTypes(); int paramLength = parameterTypes.length; if (paramLength == 1) { Class<?> t = parameterTypes[0]; if (t != NamedArgRuntime[].class) { if (r == null) r = m; } } else { boolean hasNamedArg = false; for (Class<?> c : parameterTypes) { if (c == NamedArgRuntime.class || c == NamedArgRuntime[].class) { hasNamedArg = true; break; } } if (!hasNamedArg) { // a candidate. choose the one with longer param list if (r == null) r = m; else if (paramLength > r.getParameterTypes().length) r = m; } } } } if (r != null) return r; else throw new RuntimeException("no render method found for the template: " + currentClass.getCanonicalName()); } /* * based on https://github.com/branaway/Japid/issues/12 This static mapping * will be later user in method renderModel to construct an proper Object[] * array which is needed to invoke the method render(Object... args) over * reflection. */ public java.lang.reflect.Method renderMethodInstance; public boolean hasDoBody = false; protected void setHasDoBody() { hasDoBody = true; } protected void setRenderMethod(Method renderMethod) { // System.out.println("-> setrender name: " + renderMethod); renderMethodInstance = renderMethod; } public String[] argNamesInstance = null; protected void setArgNames(String[] argNames) { // System.out.println("-> set args names: " + argNames); this.argNamesInstance = argNames; } public String[] argTypesInstance = null; protected void setArgTypes(String[] argTypes) { // System.out.println("-> set args names: " + argNames); this.argTypesInstance = argTypes; } public Object[] argDefaultsInstance = null; private MimeTypeEnum mimeType; private Boolean traceFileExit = null; public static boolean globalTraceFile = false; public static Boolean globalTraceFileHtml = null; public static Boolean globalTraceFileJson = null; protected void setArgDefaults(Object[] argDefaults) { // System.out.println("-> set args names: " + argNames); this.argDefaultsInstance = argDefaults; } // public cn.bran.japid.template.RenderResult // renderModel(cn.bran.japid.template.JapidModelMap model) { // // a static utils method of JapidModelMap to build up an Object[] array. // Nulls are used where the args are omitted. // Object[] args = model.buildArgs(argNamesInstance); // try { // return (cn.bran.japid.template.RenderResult ) // renderMethodInstance.invoke(this, args); // } catch (IllegalArgumentException e) { // throw new RuntimeException(e); // } catch (IllegalAccessException e) { // throw new RuntimeException(e); // } catch (InvocationTargetException e) { // Throwable t = e.getTargetException(); // throw new RuntimeException(t); // } // } // protected static NamedArgRuntime named(String name, Object val) { return new NamedArgRuntime(name, val); } public cn.bran.japid.template.RenderResult render(NamedArgRuntime... named) { Object[] args = null; if (hasDoBody) // called without the callback block args = buildArgs(named, null); else args = buildArgs(named); return runRenderer(args); } /** * @param args * @return */ protected cn.bran.japid.template.RenderResult runRenderer(Object[] args) { try { return (cn.bran.japid.template.RenderResult) renderMethodInstance.invoke(this, args); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { Throwable t = e.getTargetException(); throw new RuntimeException(t); } } /** * build * * @param argNames * @param namedArgs * @return */ public Object[] buildArgs(NamedArgRuntime[] namedArgs) { Map<String, Object> map = new HashMap<String, Object>(); for (NamedArgRuntime na : namedArgs) { map.put(na.name, na.val); } Object[] ret = new Object[argNamesInstance.length]; for (int i = 0; i < argNamesInstance.length; i++) { String name = argNamesInstance[i]; if (map.containsKey(name)) { ret[i] = map.remove(name); } else { // any default set? Object defa = this.argDefaultsInstance[i]; if (defa != null) ret[i] = defa; else { // set default value for primitives and Strings, or null for // complex object String type = argTypesInstance[i]; Object defaultVal = getDefaultValForType(type); ret[i] = defaultVal; } } } if (map.size() > 0) { Set<String> keys = map.keySet(); String sep = ", "; String ks = "[" + StringUtils.join(keys, sep) + "]"; String vs = "[" + StringUtils.join(argNamesInstance, sep) + "]"; throw new RuntimeException("One or more argument names are not valid: " + ks + ". Valid argument names are: " + vs); } return ret; } protected Object[] buildArgs(NamedArgRuntime[] named, Object body) { Object[] obsNoBody = buildArgs(named); int len = obsNoBody.length; Object[] ret = new Object[len + 1]; System.arraycopy(obsNoBody, 0, ret, 0, len); ret[len] = body; return ret; } private static Object getDefaultValForType(String type) { if (type.equals("String")) return ""; else if (/* type.equals("Boolean") || */type.equals("boolean")) return false; else if (type.equals("char") /* || type.equals("Character") */) return (char) 0; else if (type.equals("byte") /* || type.equals("Byte") */) return (byte) 0; else if (type.equals("short") /* || type.equals("Short") */) return (short) 0; else if (type.equals("int") /* || type.equals("Integer") */) return 0; else if (type.equals("float") /* || type.equals("Float") */) return 0f; else if (type.equals("long") /* || type.equals("Long") */) return 0L; else if (type.equals("double") /* || type.equals("Double") */) return 0d; return null; } protected void handleException(RuntimeException e) { throw e; } protected void setSourceTemplate(String st) { this.sourceTemplate = st; } /** * templates call this method to insert the current template name in a * mine-type sensitive comment. It does not respect the `tracefile * directive. It's useful to mark template files that generate xml/xhtml * that requires doctype tag in the first line of the output. It's a * convenient substitute of the `tracefile directive in such cases. * * @author Bing Ran (bing.ran@hotmail.com) */ protected void traceFile() { this.traceFileExit = true; p(makeBeginBorder(this.sourceTemplate)); } /** * @author Bing Ran (bing.ran@hotmail.com) * @return */ protected String makeBeginBorder(String viewSource) { if (StringUtils.isEmpty(contentType)) return null; String formatter = getContentCommentFormatter(contentType); if (formatter == null) return ""; return String.format(formatter, "enter: \"" + viewSource + "\""); } /** * @author Bing Ran (bing.ran@hotmail.com) * @return */ protected String makeEndBorder(String viewSource) { if (StringUtils.isEmpty(contentType)) return null; String formatter = getContentCommentFormatter(contentType); if (formatter == null) return ""; String content = "exit: \"" + viewSource + "\""; if (shouldRecordTime()) { // add time consumption to the endline for debugging purpose content += ". Duration/μs: " + renderingTime; } return String.format(formatter, content); } /** * determine if the current template should mark the entrance and the exit * in the output * * @author Bing Ran (bing.ran@hotmail.com) * @return */ private boolean shouldTraceFile() { if (traceFile != null) return traceFile; else if (this.mimeType == MimeTypeEnum.xml || this.mimeType == MimeTypeEnum.html) if (globalTraceFileHtml != null) return globalTraceFileHtml; else return globalTraceFile; else if (this.mimeType == MimeTypeEnum.js || this.mimeType == MimeTypeEnum.json) if (globalTraceFileJson != null) return globalTraceFileJson; else return globalTraceFile; return false; } protected void beginDoLayout(String viewSource) { if (shouldTraceFile()) p(makeBeginBorder(viewSource)); // if (shouldRecordTime()) { // startTime = System.nanoTime(); // } } /** * @author Bing Ran (bing.ran@gmail.com) * @return */ private boolean shouldRecordTime() { // if (caller != null && caller.shouldRecordTime()) // return true; // else return isStopwatch(); } protected void endDoLayout(String viewSource) { if (shouldRecordTime()) { calcDuration(); logTime(sourceTemplate, renderingTime); } if (shouldTraceFile()) p(makeEndBorder(viewSource)); else if (traceFileExit != null && traceFileExit) p(makeEndBorder(viewSource)); } private void calcDuration() { long duration = System.nanoTime() - startTime; renderingTime = duration / 1000; // JapidFlags._log("Time consumed to render \"" + sourceTemplate + "\": " + renderingTime + " μs"); } public static String getContentCommentFormatter(String contentTypeString) { if (contentTypeString.contains("xml") || contentTypeString.contains("html")) return "<!-- %s -->"; if (contentTypeString.contains("json") || contentTypeString.contains("javascript") || contentTypeString.contains("css")) return "/* %s */"; return null; } /** * @return the contentType */ public String getContentType() { return contentType; } /** * @param contentType * the contentType to set */ public void setContentType(String contentType) { this.contentType = contentType; if (contentType.contains("xml")) this.mimeType = MimeTypeEnum.xml; else if (contentType.contains("html")) this.mimeType = MimeTypeEnum.html; else if (contentType.contains("javascript")) this.mimeType = MimeTypeEnum.js; else if (contentType.contains("json")) this.mimeType = MimeTypeEnum.json; else if (contentType.contains("css")) this.mimeType = MimeTypeEnum.css; } /** * @return the traceFile */ public Boolean getTraceFile() { return traceFile; } /** * @param traceFile * the traceFile to set */ public void setTraceFile(Boolean traceFile) { this.traceFile = traceFile; } /** * @deprecated the Each tag is deprecated in favor of using native loop */ protected void breakLoop() { throw new BreakLoop(); } /** * @deprecated the Each tag is deprecated in favor of using native loop */ protected void continueLoop() { throw new ContinueLoop(); } /** * @author Bing Ran (bing.ran@gmail.com) * @param strings * @return */ protected static int getCollectionSize(Object col) { if (col instanceof Collection) { return ((Collection) col).size(); } if (col.getClass().isArray()) { return Array.getLength(col); } if (col instanceof Iterable || col instanceof Iterator) { return -1; } return -1; } public boolean isStopwatch() { return stopwatch != null ? stopwatch : false; } public void setStopwatchOn() { this.stopwatch = true; } // XXX reconsider this later. consider layout with param case carefully. // protected void startRendering() { // try { // layout(); // } catch (RuntimeException __e) { // handleException(__e); // } // } protected cn.bran.japid.template.RenderResult getRenderResult() { return new cn.bran.japid.template.RenderResult(getHeaders(), getOut(), renderingTime); } /** * For debugging purpose. Can be called by ` * @author Bing Ran (bing.ran@gmail.com) * @param marker */ protected void logDuration(String marker) { if (shouldRecordTime()) { long endtime = System.nanoTime(); long duration = endtime - startTime; long t = duration / 1000; logTime(marker, t); // JapidFlags._log("Time consumed up to \"" + marker + "\" in \"" + // sourceTemplate + "\": " + t + " μs"); } } /** * @author Bing Ran (bing.ran@gmail.com) * @param marker * @param t */ private void logTime(String marker, long t) { // if (caller != null) { // caller.logTime(marker, t); // } else { timeLogs.add(new MyTuple2(marker, t)); // } } }