/* * Copyright 2011 cruxframework.org. * * 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 org.cruxframework.crux.core.declarativeui.template; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import org.cruxframework.crux.core.client.screen.DeviceAdaptive.Device; import org.cruxframework.crux.core.client.utils.StringUtils; import org.cruxframework.crux.core.declarativeui.CruxXmlPreProcessor; import org.cruxframework.crux.core.declarativeui.XPathUtils; import org.cruxframework.crux.core.rebind.crossdevice.Devices; import org.cruxframework.crux.core.utils.RegexpPatterns; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * @author Thiago da Rosa de Bustamante * */ public class TemplatesPreProcessor implements CruxXmlPreProcessor { private XPathExpression findTemplatesExpression; private XPathExpression findScreensExpression; private XPathExpression findViewsExpression; private XPathExpression findBodyExpression; private XPathExpression findCrossBrowserExpression; private XPathExpression templateAttributesExpression; private TemplateLoader templateProvider; private TemplateParser templateParser; /** * */ public TemplatesPreProcessor(TemplateLoader templateProvider) { this.templateProvider = templateProvider; this.templateParser = new TemplateParser(); XPath findPath = XPathUtils.getCruxPagesXPath(); XPath htmlPath = XPathUtils.getHtmlXPath(); try { findTemplatesExpression = findPath.compile(".//*[contains(namespace-uri(), 'http://www.cruxframework.org/templates/')]"); findScreensExpression = findPath.compile("//c:screen"); findViewsExpression = findPath.compile("//v:view"); findBodyExpression = htmlPath.compile("//h:body"); findCrossBrowserExpression = findPath.compile("//c:crossDevice"); templateAttributesExpression = findPath.compile("//@*[contains(., 'X{')] | //text()[contains(., 'X{')]"); } catch (XPathExpressionException e) { throw new TemplateException("Error initializing templates pre-processor.", e); } } /** * * @param doc * @return */ public Document preprocess(Document doc, String device) { if (doc == null) { return null; } Set<String> controllers = new HashSet<String>(); Set<String> resources = new HashSet<String>(); Set<String> dataSources = new HashSet<String>(); Set<String> formatters = new HashSet<String>(); Document result = preprocess(doc, device, controllers, resources, dataSources, formatters); updateScreenProperties(doc, controllers, resources, dataSources, formatters); return result; } /** * * @param doc * @param controllers * @param resources * @param dataSources * @param formatters * @param serializables */ private void updateScreenProperties(Document doc, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters) { try { NodeList nodes = (NodeList)findScreensExpression.evaluate(doc, XPathConstants.NODESET); Element screen = null; if (nodes.getLength() > 0) { screen = (Element)nodes.item(0); } else { nodes = (NodeList)findViewsExpression.evaluate(doc, XPathConstants.NODESET); if (nodes.getLength() > 0) { screen = (Element)nodes.item(0); } else { screen = doc.createElementNS("http://www.cruxframework.org/crux", "c:screen"); NodeList bodyNodes = (NodeList)findBodyExpression.evaluate(doc, XPathConstants.NODESET); if (bodyNodes.getLength() > 0) { Element body = (Element)bodyNodes.item(0); body.appendChild(screen); } } } extractScreenPropertiesFromElement(screen, controllers, resources, dataSources, formatters); updateScreenProperty(screen, controllers, "useController"); updateScreenProperty(screen, resources, "useResource"); updateScreenProperty(screen, dataSources, "useDataSource"); updateScreenProperty(screen, formatters, "useFormatter"); } catch (XPathExpressionException e) { throw new TemplateException("Error pre-processing templates.", e); } } /** * * @param screen * @param properties * @param property */ private void updateScreenProperty(Element screen, Set<String> properties, String property) { StringBuilder str = new StringBuilder(); boolean first = true; for (String propValue : properties) { if (!first) { str.append(","); } str.append(propValue); first = false; } if (str.length() > 0) { screen.setAttribute(property, str.toString()); } } /** * * @param doc * @return */ private Document preprocess(Document doc, String device, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters) { Element documentElement = doc.getDocumentElement(); preprocessCrossBrowserTags(documentElement, device, controllers, dataSources, formatters); preprocess(documentElement, device, controllers, resources, dataSources, formatters, false); return doc; } /** * * @param doc * @return */ private void preprocess(Element root, String device, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters, boolean allowInnerSections) { try { NodeList nodes = (NodeList)findTemplatesExpression.evaluate(root, XPathConstants.NODESET); for (int i = 0; i < nodes.getLength(); i++) { Element element = (Element)nodes.item(i); if (isAttached(element) && (allowInnerSections || !isAnInnerSection(element))) { preprocessTemplate(element, device, controllers, resources, dataSources, formatters); } } } catch (XPathExpressionException e) { throw new TemplateException("Error pre-processing templates.", e); } } /** * * @param documentElement * @param device * @param controllers * @param dataSources * @param formatters */ private void preprocessCrossBrowserTags(Element documentElement, String device, Set<String> controllers, Set<String> dataSources, Set<String> formatters) { try { NodeList childNodes = (NodeList)findCrossBrowserExpression.evaluate(documentElement, XPathConstants.NODESET); List<Element> elements = new ArrayList<Element>(); for (int i = 0; i < childNodes.getLength(); i++) { Element element = (Element)childNodes.item(i); if (isAttached(element)) { elements.add(element); } } for (Element element: elements) { preprocessCrossBrowserTag(element, device, controllers, dataSources, formatters); } } catch (XPathExpressionException e) { throw new TemplateException("Error pre-processing templates.", e); } } /** * * @param element * @return */ private boolean isAttached(Node element) { boolean attached = false; while (element.getParentNode() != null) { if (element.equals(element.getOwnerDocument().getDocumentElement())) { attached = true; break; } element = element.getParentNode(); } return attached; } /** * * @param doc * @param controllers * @param dataSources * @param formatters * @param element */ private void preprocessTemplate(Element element, String device, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters) { Document doc = element.getOwnerDocument(); String library = element.getNamespaceURI(); library = library.substring(library.lastIndexOf('/')+1); Document template = templateProvider.getTemplate(library, element.getLocalName()); if (template == null) { throw new TemplateException("Template not found. Library: ["+library+"]. Template: ["+element.getLocalName()+"]."); } template = preprocess(template, device, controllers, resources, dataSources, formatters); updateTemplateAttributes(element, template); updateTemplateChildren(element, device, template, controllers, resources, dataSources, formatters); Element templateElement = (Element) doc.importNode(template.getDocumentElement(), true); extractScreenPropertiesFromElement(templateElement, controllers, resources, dataSources, formatters); replaceByChildren(element, templateElement); } /** * * @param element * @param device * @param controllers * @param dataSources * @param formatters */ private void preprocessCrossBrowserTag(Element element, String device, Set<String> controllers, Set<String> dataSources, Set<String> formatters) { List<Element> replacements = getCrossBrowserReplacements(element, device); if (replacements.size() == 0) { throw new TemplateException("No condition associated with device ["+device+"]"); } NodeList nodes = element.getChildNodes(); Node parentNode = element.getParentNode(); for (int i=0; i< nodes.getLength(); i++) { Node node = nodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element)node; if (!child.getLocalName().equals("conditions") || !child.getNamespaceURI().equals("http://www.cruxframework.org/crux")) { updateCrossBrowserAttributes(child, replacements); element.removeChild(child); parentNode.insertBefore(child, element); } } } parentNode.removeChild(element); } /** * * @param crossBrowserElement * @param device */ private List<Element> getCrossBrowserReplacements(Element crossBrowserElement, String device) { List<Element> result = new ArrayList<Element>(); NodeList nodes = crossBrowserElement.getElementsByTagNameNS("http://www.cruxframework.org/crux", "conditions"); Device[] supportedDevices = Devices.getDevicesForDevice(device); for (Device supportedDevice : supportedDevices) { for (int i=0; i< nodes.getLength(); i++) { Element child = (Element)nodes.item(i); NodeList conditions = child.getElementsByTagNameNS("http://www.cruxframework.org/crux", "condition"); for (int j=0; j< conditions.getLength(); j++) { Element condition = (Element)conditions.item(j); String userAgentValue = condition.getAttribute("when"); Device templateDevice = Device.valueOf(userAgentValue); if (templateDevice.equals(supportedDevice)) { NodeList replacements = condition.getElementsByTagNameNS("http://www.cruxframework.org/crux", "parameter"); for (int k=0; k< replacements.getLength(); k++) { Element replacement = (Element)replacements.item(k); result.add(replacement); } return result; } } } } return result; } /** * * @param child * @param replacements */ private void updateCrossBrowserAttributes(Element child, List<Element> replacements) { try { NodeList nodes = (NodeList)templateAttributesExpression.evaluate(child, XPathConstants.NODESET); for (int i = 0; i < nodes.getLength(); i++) { Node attribute = nodes.item(i); String attrValue = attribute.getNodeValue(); if (attrValue.contains("X{")) { applyCrossBrowserReplacement(attribute, replacements); } } } catch (XPathExpressionException e) { throw new TemplateException("Error pre-processing templates.", e); } } /** * * @param attribute * @param replacements */ private void applyCrossBrowserReplacement(Node attribute, List<Element> replacements) { String attrValue = attribute.getNodeValue(); Map<String, String> replace = new HashMap<String, String>(); int indexStarParam = attrValue.indexOf("X{"); while (indexStarParam >= 0) { attrValue = attrValue.substring(indexStarParam+2); int indexEndParam = attrValue.indexOf("}"); if (indexEndParam >= 0) { String param = attrValue.substring(0, indexEndParam); String paramValue = getCrossBrowserReplacement(param, replacements); replace.put(param, paramValue); attrValue = attrValue.substring(indexEndParam+1); indexStarParam = attrValue.indexOf("X{"); } else { break; } } Set<String> parameters = replace.keySet(); if (parameters.size() > 0) { attrValue = attribute.getNodeValue(); for (String key : parameters) { attrValue = attrValue.replace("X{"+key+"}", replace.get(key)); } attribute.setNodeValue(attrValue); } } /** * * @param name * @param replacements */ private String getCrossBrowserReplacement(String name, List<Element> replacements) { for (Element replacement : replacements) { if (replacement.getLocalName().equals("parameter")) { String nameAttribute = replacement.getAttribute("name"); if (!StringUtils.isEmpty(nameAttribute) && nameAttribute.equals(name)) { return replacement.getAttribute("value"); } } } throw new TemplateException("CrossDevice parameter ["+name+"] not found."); } /** * * @param template * @param controllers * @param dataSources * @param formatters */ private void extractScreenPropertiesFromElement(Element template, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters) { extractScreenPropertyFromTemplate(controllers, template.getAttribute("useController")); extractScreenPropertyFromTemplate(resources, template.getAttribute("useResource")); extractScreenPropertyFromTemplate(dataSources, template.getAttribute("useDataSource")); extractScreenPropertyFromTemplate(formatters, template.getAttribute("useFormatter")); } /** * * @param properties * @param property */ private void extractScreenPropertyFromTemplate(Set<String> properties, String property) { if (property != null) { String[] strs = RegexpPatterns.REGEXP_COMMA.split(property); for (String str : strs) { if (!StringUtils.isEmpty(str) && !str.contains("#{")) { properties.add(str); } } } } /** * * @param replacementElement * @param templateElement */ private void replaceByChildren(Node replacementElement, Node templateElement) { Node parentNode = replacementElement.getParentNode(); Node refNode = replacementElement; List<Node> children = getChildren(templateElement); for (int i=children.size()-1; i>=0; i--) { Node node = children.get(i); refNode = parentNode.insertBefore(node, refNode); } parentNode.removeChild(replacementElement); } /** * * @param element * @return */ private List<Node> getChildren(Node element) { List<Node> children = new ArrayList<Node>(); Node child = element.getFirstChild(); while (child != null) { children.add(child); child = child.getNextSibling(); } return children; } /** * * @param elementFromElement * @return */ private boolean isAnInnerSection(Element element) { String namespace = element.getNamespaceURI(); if (namespace != null) { Element documentElement = element.getOwnerDocument().getDocumentElement(); element = (Element) element.getParentNode(); while (element != null && !element.equals(documentElement)) { if (namespace.equals(element.getNamespaceURI())) { return true; } element = (Element) element.getParentNode(); } } return false; } /** * * @param element * @param template */ private void updateTemplateAttributes(Element element, Document template) { Set<Node> parameters = templateParser.getParametersNodes(template); for (Node attributeNode : parameters) { Set<String> attributes = new HashSet<String>(); String nodeValue = attributeNode.getNodeValue(); templateParser.extractParameterNames(nodeValue, attributes); for (String attribute: attributes) { String value = element.getAttribute(attribute); nodeValue = nodeValue.replace("#{"+attribute+"}", value); } attributeNode.setNodeValue(nodeValue); } } /** * * @param element * @param device * @param template * @param controllers * @param resources * @param dataSources * @param formatters */ private void updateTemplateChildren(Element element, String device, Document template, Set<String> controllers, Set<String> resources, Set<String> dataSources, Set<String> formatters) { Map<String, Node> sections = templateParser.getSectionElements(template); List<Node> children = getChildren(element); for (int i=children.size()-1; i>=0; i--) { Node section = children.get(i); if (section.getNodeType() == Node.ELEMENT_NODE) { String sectionName = section.getLocalName(); Node templateNode = sections.get(sectionName); preprocess((Element)section, device, controllers, resources, dataSources, formatters, true); section = template.importNode(section, true); replaceByChildren(templateNode, section); } } } }