XmlContentBuilder.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2015 wcm.io
* %%
* 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.
* #L%
*/
package io.wcm.tooling.commons.contentpackagebuilder;
import static io.wcm.tooling.commons.contentpackagebuilder.XmlNamespaces.NS_JCR;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.util.ISO9075;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import io.wcm.tooling.commons.contentpackagebuilder.element.ContentElement;
/**
* Builds CMS content packages.
*/
final class XmlContentBuilder {
private final DocumentBuilder documentBuilder;
private final Map<String, String> xmlNamespaces;
private final ValueConverter valueConverter = new ValueConverter();
static final String PN_PRIMARY_TYPE = "jcr:primaryType";
static final String NT_PAGE = "cq:Page";
static final String NT_PAGE_CONTENT = "cq:PageContent";
static final String NT_UNSTRUCTURED = "nt:unstructured";
static final String NT_FILE = "nt:file";
static final String NT_RESOURCE = "nt:resource";
XmlContentBuilder(Map<String, String> xmlNamespaces) {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
try {
this.documentBuilder = documentBuilderFactory.newDocumentBuilder();
this.documentBuilder.setEntityResolver(new PropertiesEntityResolver());
}
catch (ParserConfigurationException ex) {
throw new IllegalStateException("Failed to set up XML document builder: " + ex.getMessage(), ex);
}
this.xmlNamespaces = xmlNamespaces;
}
/**
* Build XML for cq:Page.
* @param content Hierarchy of content elements.
* @return cq:Page JCR XML
*/
public Document buildPage(ContentElement content) {
Document doc = documentBuilder.newDocument();
Element jcrRoot = createJcrRoot(doc, NT_PAGE);
Element jcrContent = createJcrContent(doc, jcrRoot, NT_PAGE_CONTENT);
exportPayload(doc, jcrContent, content);
return doc;
}
/**
* Build XML for cq:Page.
* @param content Content with page properties and nested nodes
* @return cq:Page JCR XML
*/
public Document buildPage(Map<String, Object> content) {
Document doc = documentBuilder.newDocument();
Element jcrRoot = createJcrRoot(doc, NT_PAGE);
Element jcrContent = createJcrContent(doc, jcrRoot, NT_PAGE_CONTENT);
exportPayload(doc, jcrContent, content);
return doc;
}
/**
* Build XML for any JCR content.
* @param content Hierarchy of content elements.
* @return JCR XML
*/
public Document buildContent(ContentElement content) {
Document doc = documentBuilder.newDocument();
String primaryType = StringUtils.defaultString((String)content.getProperties().get(PN_PRIMARY_TYPE), NT_UNSTRUCTURED);
Element jcrRoot = createJcrRoot(doc, primaryType);
exportPayload(doc, jcrRoot, content);
return doc;
}
/**
* Build XML for any JCR content.
* @param content Content with properties and nested nodes
* @return JCR XML
*/
public Document buildContent(Map<String, Object> content) {
Document doc = documentBuilder.newDocument();
String primaryType = StringUtils.defaultString((String)content.get(PN_PRIMARY_TYPE), NT_UNSTRUCTURED);
Element jcrRoot = createJcrRoot(doc, primaryType);
exportPayload(doc, jcrRoot, content);
return doc;
}
/**
* Build XML for nt:file
* @param mimeType Mime type
* @param encoding Encoding
* @return nt:file XML
*/
public Document buildNtFile(String mimeType, String encoding) {
Document doc = documentBuilder.newDocument();
Element jcrRoot = createJcrRoot(doc, NT_FILE);
Element jcrContent = createJcrContent(doc, jcrRoot, NT_RESOURCE);
if (StringUtils.isNotEmpty(mimeType)) {
setAttributeNamespaceAware(jcrContent, "jcr:mimeType", mimeType);
}
if (StringUtils.isNotEmpty(encoding)) {
setAttributeNamespaceAware(jcrContent, "jcr:encoding", encoding);
}
return doc;
}
/**
* Build filter XML for package metadata files.
* @param filters Filters
* @return Filter XML
*/
public Document buildFilter(List<PackageFilter> filters) {
Document doc = documentBuilder.newDocument();
Element workspaceFilterElement = doc.createElement("workspaceFilter");
workspaceFilterElement.setAttribute("version", "1.0");
doc.appendChild(workspaceFilterElement);
for (PackageFilter filter : filters) {
Element filterElement = doc.createElement("filter");
filterElement.setAttribute("root", filter.getRootPath());
workspaceFilterElement.appendChild(filterElement);
for (PackageFilterRule rule : filter.getRules()) {
Element ruleElement = doc.createElement(rule.isInclude() ? "include" : "exclude");
ruleElement.setAttribute("pattern", rule.getPattern());
filterElement.appendChild(ruleElement);
}
}
return doc;
}
private Element createJcrRoot(Document doc, String primaryType) {
Element jcrRoot = doc.createElementNS(NS_JCR, "jcr:root");
for (Map.Entry<String, String> namespace : xmlNamespaces.entrySet()) {
jcrRoot.setAttribute("xmlns:" + namespace.getKey(), namespace.getValue());
}
setAttributeNamespaceAware(jcrRoot, PN_PRIMARY_TYPE, primaryType);
doc.appendChild(jcrRoot);
return jcrRoot;
}
private Element createJcrContent(Document doc, Element jcrRoot, String primaryType) {
Element jcrContent = doc.createElementNS(NS_JCR, "jcr:content");
setAttributeNamespaceAware(jcrContent, PN_PRIMARY_TYPE, primaryType);
jcrRoot.appendChild(jcrContent);
return jcrContent;
}
private void exportPayload(Document doc, Element element, ContentElement content) {
for (Map.Entry<String, Object> entry : content.getProperties().entrySet()) {
Object value = entry.getValue();
if (value == null) {
continue;
}
if (!hasAttributeNamespaceAware(element, entry.getKey())) {
String stringValue = valueConverter.toString(entry.getKey(), value);
setAttributeNamespaceAware(element, entry.getKey(), stringValue);
}
}
for (Map.Entry<String, ContentElement> entry : content.getChildren().entrySet()) {
ContentElement child = entry.getValue();
Element subElement = doc.createElement(validateAndEncodeName(entry.getKey()));
if (!hasAttributeNamespaceAware(subElement, PN_PRIMARY_TYPE) && !child.getProperties().containsKey(PN_PRIMARY_TYPE)) {
setAttributeNamespaceAware(subElement, PN_PRIMARY_TYPE, NT_UNSTRUCTURED);
}
element.appendChild(subElement);
exportPayload(doc, subElement, child);
}
}
@SuppressWarnings("unchecked")
private void exportPayload(Document doc, Element element, Map<String, Object> content) {
for (Map.Entry<String,Object> entry : content.entrySet()) {
Object value = entry.getValue();
if (value == null) {
continue;
}
if (value instanceof Map) {
Map<String, Object> childMap = (Map<String, Object>)value;
Element subElement = doc.createElement(validateAndEncodeName(entry.getKey()));
if (!hasAttributeNamespaceAware(subElement, PN_PRIMARY_TYPE) && !childMap.containsKey(PN_PRIMARY_TYPE)) {
setAttributeNamespaceAware(subElement, PN_PRIMARY_TYPE, NT_UNSTRUCTURED);
}
element.appendChild(subElement);
exportPayload(doc, subElement, childMap);
}
else if (!hasAttributeNamespaceAware(element, entry.getKey())) {
String stringValue = valueConverter.toString(entry.getKey(), value);
setAttributeNamespaceAware(element, entry.getKey(), stringValue);
}
}
}
private void setAttributeNamespaceAware(Element element, String key, String value) {
String namespace = getNamespace(key);
if (namespace == null) {
element.setAttribute(validateAndEncodeName(key), value);
}
else {
element.setAttributeNS(namespace, validateAndEncodeName(key), value);
}
}
private boolean hasAttributeNamespaceAware(Element element, String key) {
String namespace = getNamespace(key);
if (namespace == null) {
return element.hasAttribute(key);
}
else {
return element.hasAttributeNS(namespace, key);
}
}
private String getNamespace(String key) {
if (!StringUtils.contains(key, ":")) {
return null;
}
String nsPrefix = StringUtils.substringBefore(key, ":");
return xmlNamespaces.get(nsPrefix);
}
private String validateAndEncodeName(String name) {
if (!NameUtil.isValidName(name)) {
throw new IllegalArgumentException("Illegal name (not following JCR standards): " + name);
}
return ISO9075.encode(name);
}
}