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