/* * Copyright 2012 astamuse company,Ltd. * * 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.astamuse.asta4d.render; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.StringUtils; import org.jsoup.nodes.Attribute; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.select.Elements; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.astamuse.asta4d.Configuration; import com.astamuse.asta4d.Context; import com.astamuse.asta4d.extnode.ExtNodeConstants; import com.astamuse.asta4d.render.concurrent.ConcurrentRenderHelper; import com.astamuse.asta4d.render.concurrent.FutureRendererHolder; import com.astamuse.asta4d.render.transformer.RendererTransformer; import com.astamuse.asta4d.render.transformer.Transformer; import com.astamuse.asta4d.snippet.SnippetInvokeException; import com.astamuse.asta4d.snippet.SnippetInvoker; import com.astamuse.asta4d.snippet.SnippetNotResovlableException; import com.astamuse.asta4d.template.TemplateException; import com.astamuse.asta4d.template.TemplateNotFoundException; import com.astamuse.asta4d.template.TemplateUtil; import com.astamuse.asta4d.util.ElementUtil; import com.astamuse.asta4d.util.SelectorUtil; import com.astamuse.asta4d.util.i18n.I18nMessageHelperTypeAssistant; import com.astamuse.asta4d.util.i18n.LocalizeUtil; /** * * This class is a functions holder which supply the ability of applying rendereres to certain Element. * * @author e-ryu * */ public class RenderUtil { public static final String PSEUDO_ROOT_SELECTOR = ":root"; public static final String TRACE_VAR_TEMPLATE_PATH = "TRACE_VAR_TEMPLATE_PATH#" + RenderUtil.class; public static final String TRACE_VAR_SNIPPET = "TRACE_VAR_SNIPPET#" + RenderUtil.class; private final static Logger logger = LoggerFactory.getLogger(RenderUtil.class); private final static List<String> EXCLUDE_ATTR_NAME_LIST = new ArrayList<>(); static { EXCLUDE_ATTR_NAME_LIST.add(ExtNodeConstants.MSG_NODE_ATTR_KEY); EXCLUDE_ATTR_NAME_LIST.add(ExtNodeConstants.MSG_NODE_ATTR_LOCALE); EXCLUDE_ATTR_NAME_LIST.add(ExtNodeConstants.ATTR_TEMPLATE_PATH); } /** * Find out all the snippet in the passed Document and execute them. The Containing embed tag of the passed Document will be exactly * mixed in here too. <br> * Recursively contained snippets will be executed from outside to inside, thus the inner snippets will not be executed until all of * their outer snippets are finished. Also, the dynamically created snippets and embed tags will comply with this rule too. * * @param doc * the Document to apply snippets * @throws SnippetNotResovlableException * @throws SnippetInvokeException * @throws TemplateException */ public final static void applySnippets(Document doc) throws SnippetNotResovlableException, SnippetInvokeException, TemplateException, TemplateNotFoundException { if (doc == null) { return; } applyClearAction(doc, false); // retrieve ready snippets String selector = SelectorUtil.attr(ExtNodeConstants.SNIPPET_NODE_TAG_SELECTOR, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_READY); List<Element> snippetList = new ArrayList<>(doc.select(selector)); int readySnippetCount = snippetList.size(); int blockedSnippetCount = 0; for (int i = readySnippetCount - 1; i >= 0; i--) { // if parent snippet has not been executed, the current snippet will // not be executed too. if (isBlockedByParentSnippet(doc, snippetList.get(i))) { snippetList.remove(i); blockedSnippetCount++; } } readySnippetCount = readySnippetCount - blockedSnippetCount; String renderDeclaration; Renderer renderer; Context context = Context.getCurrentThreadContext(); Configuration conf = Configuration.getConfiguration(); final SnippetInvoker invoker = conf.getSnippetInvoker(); String refId; String currentTemplatePath; Element renderTarget; for (Element element : snippetList) { if (!conf.isSkipSnippetExecution()) { // for a faked snippet node which is created by template // analyzing process, the render target element should be its // child. if (element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_TYPE).equals(ExtNodeConstants.SNIPPET_NODE_ATTR_TYPE_FAKE)) { renderTarget = element.children().first(); // the hosting element of this faked snippet has been removed by outer a snippet if (renderTarget == null) { element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_FINISHED); continue; } } else { renderTarget = element; } // we have to reset the ref of current snippet at every time to make sure the ref is always unique(duplicated snippet ref // could be created by list rendering) TemplateUtil.resetSnippetRefs(element); context.setCurrentRenderingElement(renderTarget); renderDeclaration = element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_RENDER); refId = element.attr(ExtNodeConstants.ATTR_SNIPPET_REF); currentTemplatePath = element.attr(ExtNodeConstants.ATTR_TEMPLATE_PATH); context.setCurrentRenderingElement(renderTarget); context.setData(TRACE_VAR_TEMPLATE_PATH, currentTemplatePath); try { if (element.hasAttr(ExtNodeConstants.SNIPPET_NODE_ATTR_PARALLEL)) { ConcurrentRenderHelper crHelper = ConcurrentRenderHelper.getInstance(context, doc); final Context newContext = context.clone(); final String declaration = renderDeclaration; crHelper.submitWithContext(newContext, declaration, refId, new Callable<Renderer>() { @Override public Renderer call() throws Exception { return invoker.invoke(declaration); } }); element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_WAITING); } else { renderer = invoker.invoke(renderDeclaration); applySnippetResultToElement(doc, refId, element, renderTarget, renderer); } } catch (SnippetNotResovlableException | SnippetInvokeException e) { throw e; } catch (Exception e) { SnippetInvokeException se = new SnippetInvokeException("Error occured when executing rendering on [" + renderDeclaration + "]:" + e.getMessage(), e); throw se; } context.setData(TRACE_VAR_TEMPLATE_PATH, null); context.setCurrentRenderingElement(null); } else {// if skip snippet element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_FINISHED); } } // load embed nodes which blocking parents has finished List<Element> embedNodeList = doc.select(ExtNodeConstants.EMBED_NODE_TAG_SELECTOR); int embedNodeListCount = embedNodeList.size(); Iterator<Element> embedNodeIterator = embedNodeList.iterator(); Element embed; Element embedContent; while (embedNodeIterator.hasNext()) { embed = embedNodeIterator.next(); if (isBlockedByParentSnippet(doc, embed)) { embedNodeListCount--; continue; } embedContent = TemplateUtil.getEmbedNodeContent(embed); TemplateUtil.mergeBlock(doc, embedContent); embed.before(embedContent); embed.remove(); } if ((readySnippetCount + embedNodeListCount) > 0) { TemplateUtil.regulateElement(null, doc); applySnippets(doc); } else { ConcurrentRenderHelper crHelper = ConcurrentRenderHelper.getInstance(context, doc); String delcaration = null; if (crHelper.hasUnCompletedTask()) { delcaration = null; try { FutureRendererHolder holder = crHelper.take(); delcaration = holder.getRenderDeclaration(); String ref = holder.getSnippetRefId(); String reSelector = SelectorUtil.attr(ExtNodeConstants.SNIPPET_NODE_TAG_SELECTOR, ExtNodeConstants.ATTR_SNIPPET_REF, ref); Element element = doc.select(reSelector).get(0);// must have Element target; if (element.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_TYPE).equals(ExtNodeConstants.SNIPPET_NODE_ATTR_TYPE_FAKE)) { target = element.children().first(); } else { target = element; } applySnippetResultToElement(doc, ref, element, target, holder.getRenderer()); applySnippets(doc); } catch (InterruptedException | ExecutionException e) { throw new SnippetInvokeException("Concurrent snippet invocation failed" + (delcaration == null ? "" : " on [" + delcaration + "]"), e); } } } } private final static void applySnippetResultToElement(Document doc, String snippetRefId, Element snippetElement, Element renderTarget, Renderer renderer) { apply(renderTarget, renderer); if (snippetElement.ownerDocument() == null) { // it means this snippet element is replaced by a // element completely String reSelector = SelectorUtil.attr(ExtNodeConstants.SNIPPET_NODE_TAG_SELECTOR, ExtNodeConstants.ATTR_SNIPPET_REF, snippetRefId); Elements elems = doc.select(reSelector); if (elems.size() > 0) { snippetElement = elems.get(0); } else { snippetElement = null; } } if (snippetElement != null) { snippetElement.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_FINISHED); } } private final static boolean isBlockedByParentSnippet(Document doc, Element elem) { boolean isBlocked; String blockingId = elem.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_BLOCK); if (blockingId.isEmpty()) { // empty block id means there is no parent snippet that need to be // aware. if the original block is from a embed template, it means // that all of the parent snippets have been finished or this // element would not be imported now. isBlocked = false; } else { String parentSelector = SelectorUtil.attr(ExtNodeConstants.SNIPPET_NODE_TAG_SELECTOR, ExtNodeConstants.ATTR_SNIPPET_REF, blockingId); Elements parentSnippetSearch = elem.parents().select(parentSelector); if (parentSnippetSearch.isEmpty()) { isBlocked = false; } else { Element parentSnippet = parentSnippetSearch.first(); if (parentSnippet.attr(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS) .equals(ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_FINISHED)) { isBlocked = false; } else { isBlocked = true; } } } return isBlocked; } /** * Apply given renderer to the given element. * * @param target * applying target element * @param renderer * a renderer for applying */ public final static void apply(Element target, Renderer renderer) { List<Renderer> rendererList = renderer.asUnmodifiableList(); int count = rendererList.size(); if (count == 0) { return; } applyClearAction(target, false); RenderAction renderAction = new RenderAction(); apply(target, rendererList, renderAction, 0, count); } // TODO since this method is called recursively, we need do a test to find // out the threshold of render list size that will cause a // StackOverflowError. private final static void apply(Element target, List<Renderer> rendererList, RenderAction renderAction, int startIndex, int count) { // The renderer list have to be applied recursively because the // transformer will always return a new Element clone. if (startIndex >= count) { return; } final Renderer currentRenderer = rendererList.get(startIndex); RendererType rendererType = currentRenderer.getRendererType(); switch (rendererType) { case GO_THROUGH: apply(target, rendererList, renderAction, startIndex + 1, count); return; /* case DEBUG: currentRenderer.getTransformerList().get(0).invoke(target); apply(target, rendererList, renderAction, startIndex + 1, count); return; */ case RENDER_ACTION: ((RenderActionRenderer) currentRenderer).getStyle().apply(renderAction); apply(target, rendererList, renderAction, startIndex + 1, count); return; default: // do nothing break; } String selector = currentRenderer.getSelector(); List<Transformer<?>> transformerList = currentRenderer.getTransformerList(); List<Element> elemList; if (PSEUDO_ROOT_SELECTOR.equals(selector)) { elemList = new LinkedList<Element>(); elemList.add(target); } else { elemList = new ArrayList<>(target.select(selector)); } if (elemList.isEmpty()) { if (rendererType == RendererType.ELEMENT_NOT_FOUND_HANDLER) { elemList.add(target); transformerList.clear(); transformerList.add(new RendererTransformer(((ElementNotFoundHandler) currentRenderer).alternativeRenderer())); } else if (renderAction.isOutputMissingSelectorWarning()) { String creationInfo = currentRenderer.getCreationSiteInfo(); if (creationInfo == null) { creationInfo = ""; } else { creationInfo = " at [ " + creationInfo + " ]"; } logger.warn( "There is no element found for selector [{}]{}, if it is deserved, try Renderer#disableMissingSelectorWarning() " + "to disable this message and Renderer#enableMissingSelectorWarning could enable this warning again in " + "your renderer chain", selector, creationInfo); apply(target, rendererList, renderAction, startIndex + 1, count); return; } } else { if (rendererType == RendererType.ELEMENT_NOT_FOUND_HANDLER) { apply(target, rendererList, renderAction, startIndex + 1, count); return; } } Element delayedElement = null; Element resultNode; // TODO we suppose that the element is listed as the order from parent // to children, so we reverse it. Perhaps we need a real order process // to ensure the wanted order. Collections.reverse(elemList); boolean renderForRoot; for (Element elem : elemList) { renderForRoot = PSEUDO_ROOT_SELECTOR.equals(selector) || rendererType == RendererType.ELEMENT_NOT_FOUND_HANDLER; if (!renderForRoot) { // faked group node will be not applied by renderers(only when the current selector is not the pseudo :root) if (elem.tagName().equals(ExtNodeConstants.GROUP_NODE_TAG) && ExtNodeConstants.GROUP_NODE_ATTR_TYPE_FAKE.equals(elem.attr(ExtNodeConstants.GROUP_NODE_ATTR_TYPE))) { continue; } } if (elem == target) { delayedElement = elem; continue; } for (Transformer<?> transformer : transformerList) { resultNode = transformer.invoke(elem); elem.before(resultNode); }// for transformer elem.remove(); }// for element // if the root element is one of the process targets, we can not apply // the left renderers to original element because it will be replaced by // a new element even it is not necessary (that is how Transformer // works). if (delayedElement == null) { apply(target, rendererList, renderAction, startIndex + 1, count); } else { if (rendererType == RendererType.ELEMENT_NOT_FOUND_HANDLER && delayedElement instanceof Document) { delayedElement = delayedElement.child(0); } for (Transformer<?> transformer : transformerList) { resultNode = transformer.invoke(delayedElement); delayedElement.before(resultNode); apply(resultNode, rendererList, renderAction, startIndex + 1, count); }// for transformer delayedElement.remove(); } } /** * Clear the redundant elements which are usually created by snippet/renderer applying.If the forFinalClean is true, all the finished * snippet tags will be removed too. * * @param target * @param forFinalClean */ public final static void applyClearAction(Element target, boolean forFinalClean) { String fakeGroup = SelectorUtil.attr(ExtNodeConstants.GROUP_NODE_TAG_SELECTOR, ExtNodeConstants.GROUP_NODE_ATTR_TYPE, ExtNodeConstants.GROUP_NODE_ATTR_TYPE_FAKE); ElementUtil.removeNodesBySelector(target, fakeGroup, true); String clearGroup = SelectorUtil.attr(ExtNodeConstants.GROUP_NODE_TAG_SELECTOR, ExtNodeConstants.ATTR_CLEAR, null); ElementUtil.removeNodesBySelector(target, clearGroup, false); ElementUtil.removeNodesBySelector(target, SelectorUtil.attr(ExtNodeConstants.ATTR_CLEAR_WITH_NS), false); if (forFinalClean) { String removeSnippetSelector = SelectorUtil.attr(ExtNodeConstants.SNIPPET_NODE_TAG_SELECTOR, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS, ExtNodeConstants.SNIPPET_NODE_ATTR_STATUS_FINISHED); // TODO check if there are unfinished snippet left. ElementUtil.removeNodesBySelector(target, removeSnippetSelector, true); ElementUtil.removeNodesBySelector(target, ExtNodeConstants.BLOCK_NODE_TAG_SELECTOR, true); ElementUtil.removeNodesBySelector(target, ExtNodeConstants.GROUP_NODE_TAG_SELECTOR, true); } } // public final static void public final static void applyMessages(Element target) { Context context = Context.getCurrentThreadContext(); List<Element> msgElems = target.select(ExtNodeConstants.MSG_NODE_TAG_SELECTOR); for (final Element msgElem : msgElems) { Attributes attributes = msgElem.attributes(); String key = attributes.get(ExtNodeConstants.MSG_NODE_ATTR_KEY); // List<String> externalizeParamKeys = getExternalizeParamKeys(attributes); Object defaultMsg = new Object() { @Override public String toString() { return ExtNodeConstants.MSG_NODE_ATTRVALUE_HTML_PREFIX + msgElem.html(); } }; Locale locale = LocalizeUtil.getLocale(attributes.get(ExtNodeConstants.MSG_NODE_ATTR_LOCALE)); String currentTemplatePath = attributes.get(ExtNodeConstants.ATTR_TEMPLATE_PATH); if (StringUtils.isEmpty(currentTemplatePath)) { logger.warn("There is a msg tag which does not hold corresponding template file path:{}", msgElem.outerHtml()); } else { context.setData(TRACE_VAR_TEMPLATE_PATH, currentTemplatePath); } final Map<String, Object> paramMap = getMessageParams(attributes, locale, key); String text; switch (I18nMessageHelperTypeAssistant.configuredHelperType()) { case Mapped: text = I18nMessageHelperTypeAssistant.getConfiguredMappedHelper().getMessageWithDefault(locale, key, defaultMsg, paramMap); break; case Ordered: default: // convert map to array List<Object> numberedParamNameList = new ArrayList<>(); for (int index = 0; paramMap.containsKey(ExtNodeConstants.MSG_NODE_ATTR_PARAM_PREFIX + index); index++) { numberedParamNameList.add(paramMap.get(ExtNodeConstants.MSG_NODE_ATTR_PARAM_PREFIX + index)); } text = I18nMessageHelperTypeAssistant.getConfiguredOrderedHelper().getMessageWithDefault(locale, key, defaultMsg, numberedParamNameList.toArray()); } Node node; if (text.startsWith(ExtNodeConstants.MSG_NODE_ATTRVALUE_TEXT_PREFIX)) { node = ElementUtil.text(text.substring(ExtNodeConstants.MSG_NODE_ATTRVALUE_TEXT_PREFIX.length())); } else if (text.startsWith(ExtNodeConstants.MSG_NODE_ATTRVALUE_HTML_PREFIX)) { node = ElementUtil.parseAsSingle(text.substring(ExtNodeConstants.MSG_NODE_ATTRVALUE_HTML_PREFIX.length())); } else { node = ElementUtil.text(text); } msgElem.replaceWith(node); context.setData(TRACE_VAR_TEMPLATE_PATH, null); } } private static Map<String, Object> getMessageParams(final Attributes attributes, final Locale locale, final String key) { List<String> excludeAttrNameList = EXCLUDE_ATTR_NAME_LIST; final Map<String, Object> paramMap = new HashMap<>(); for (Attribute attribute : attributes) { String attrKey = attribute.getKey(); if (excludeAttrNameList.contains(attrKey)) { continue; } String value = attribute.getValue(); final String recursiveKey; if (attrKey.startsWith("@")) { attrKey = attrKey.substring(1); recursiveKey = value; } else if (attrKey.startsWith("#")) { attrKey = attrKey.substring(1); // we treat the # prefixed attribute value as a sub key of current key if (StringUtils.isEmpty(key)) { recursiveKey = value; } else { recursiveKey = key + "." + value; } } else { recursiveKey = null; } if (recursiveKey == null) { paramMap.put(attrKey, value); } else { paramMap.put(attrKey, new Object() { @Override public String toString() { switch (I18nMessageHelperTypeAssistant.configuredHelperType()) { case Mapped: // for the mapped helper, we can pass the parameter map recursively return I18nMessageHelperTypeAssistant.getConfiguredMappedHelper().getMessage(locale, recursiveKey, paramMap); case Ordered: default: return I18nMessageHelperTypeAssistant.getConfiguredOrderedHelper().getMessage(locale, recursiveKey); } } }); } } return paramMap; } }