/plugin/src/main/java/com/atlassian/confluence/extra/masterdetail/services/DetailsMacroBodyHandlerLegacy.java
Java | 279 lines | 197 code | 50 blank | 32 comment | 36 complexity | 76883c800878cbde4028f6fa0b9e404e MD5 | raw file
- package com.atlassian.confluence.extra.masterdetail.services;
- import com.atlassian.confluence.content.render.xhtml.Namespace;
- import com.atlassian.confluence.content.render.xhtml.XhtmlConstants;
- import com.atlassian.confluence.extra.masterdetail.DetailsSummaryMacro;
- import com.atlassian.confluence.extra.masterdetail.analytics.DetailsSummaryMacroMetricsEvent;
- import com.atlassian.confluence.plugins.pageproperties.api.model.PageProperty;
- import com.atlassian.confluence.xhtml.api.MacroDefinition;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.ImmutableMap;
- import com.google.common.collect.Lists;
- import com.google.common.collect.Maps;
- import org.apache.commons.lang3.StringUtils;
- import org.dom4j.Document;
- import org.dom4j.DocumentException;
- import org.dom4j.Element;
- import org.dom4j.Node;
- import org.dom4j.XPath;
- import org.dom4j.io.SAXReader;
- import org.dom4j.tree.DefaultElement;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.xml.sax.SAXException;
- import javax.xml.parsers.ParserConfigurationException;
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.StringWriter;
- import java.io.UnsupportedEncodingException;
- import java.io.Writer;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
- /**
- * DetailsMacroBodyHandler with SAXReader parsing.
- *
- * @since 5.3.0
- */
- public class DetailsMacroBodyHandlerLegacy implements DetailsMacroBodyHandler {
- private static final Logger LOG = LoggerFactory.getLogger(DetailsMacroBodyHandlerFastParse.class);
- public static final String CHARSET_UTF8 = "UTF-8";
- private final Map<String, ImmutableList<ImmutableMap<String, PageProperty>>> detailsById;
- private static final String XHTML_NAMESPACE_PREFIX = "xhtml";
- private static final String XPATH_TBODY = "//" + XHTML_NAMESPACE_PREFIX + ":tbody";
- private static final Map<String, String> NAMESPACE_MAP;
- static {
- NAMESPACE_MAP = new ConcurrentHashMap<String, String>(XhtmlConstants.STORAGE_NAMESPACES.size());
- for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
- NAMESPACE_MAP.put(namespace.getPrefix() != null ? namespace.getPrefix() : XHTML_NAMESPACE_PREFIX, namespace.getUri());
- }
- }
- private final DetailsSummaryMacroMetricsEvent.Builder metrics;
- DetailsMacroBodyHandlerLegacy(final DetailsSummaryMacroMetricsEvent.Builder metrics) {
- this.metrics = metrics;
- this.detailsById = Maps.newHashMap();
- }
- public void handle(MacroDefinition macroDefinition) {
- if (!"details".equals(macroDefinition.getName()))
- return;
- String bodyText = macroDefinition.getBodyText();
- String detailsId = StringUtils.trim(macroDefinition.getParameter(DetailsSummaryMacro.PARAM_ID));
- if (detailsId == null)
- detailsId = "";
- if (StringUtils.isBlank(bodyText)) {
- addToDetails(detailsId, ImmutableMap.of());
- return;
- }
- try {
- metrics.detailsExtractionStart();
- ImmutableMap<String, PageProperty> extractedDetails = extractDetails(bodyText);
- metrics.detailsExtractionFinish(extractedDetails.size());
- addToDetails(detailsId, extractedDetails);
- } catch (Exception e) {
- LOG.error(String.format("Unable to parse detailsById in detailsById macro\n%s", bodyText), e);
- }
- }
- /**
- * Get the detailsById extracted from the content. It is a mapping of ids to a map of heading/values.
- * The default without an id mapping is keyed with the empty string. Note null is returned if no details
- * for the given id are found.
- */
- public List<? extends Map<String, PageProperty>> getDetails(String detailsId) {
- return detailsById.get(detailsId);
- }
- /**
- * @return an ImmutableMap keyed by detail ID to a map of details, where details is a map keyed by heading text to
- * ExtractedDetail objects
- */
- public ImmutableMap<String, ImmutableList<ImmutableMap<String, PageProperty>>> getDetails() {
- return ImmutableMap.copyOf(detailsById);
- }
- private void addToDetails(String id, ImmutableMap<String, PageProperty> details) {
- List<ImmutableMap<String, PageProperty>> newDetails;
- List<ImmutableMap<String, PageProperty>> currentDetails = detailsById.get(id);
- if (currentDetails == null) {
- newDetails = Lists.newArrayList();
- } else {
- newDetails = Lists.newArrayList(currentDetails);
- }
- // Note that this means different keys in different PP macros won't be combined into a single map - that
- // happens later.
- newDetails.add(details);
- detailsById.put(id, ImmutableList.copyOf(newDetails));
- }
- private ImmutableMap<String, PageProperty> extractDetails(String macroBodyXhtml)
- throws IOException, DocumentException, ParserConfigurationException, SAXException {
- final Document macroBodyDoc = getMacroBodyDocument(macroBodyXhtml);
- final XPath xpath = macroBodyDoc.createXPath(XPATH_TBODY);
- xpath.setNamespaceURIs(NAMESPACE_MAP);
- final ImmutableMap<String, PageProperty> emptyMap = ImmutableMap.of();
- final Element tableElement = (Element) xpath.selectSingleNode(macroBodyDoc);
- if (tableElement == null)
- return emptyMap;
- @SuppressWarnings("unchecked") final List<Element> rowElements = tableElement.elements("tr");
- if (rowElements == null)
- return emptyMap;
- return loadDetailPairsFromTableRows(rowElements);
- }
- private ImmutableMap<String, PageProperty> loadDetailPairsFromTableRows(List<Element> rowElements) throws IOException {
- // The table might be in horizontal or vertical orientation, and with or without table-header (th) cells.
- boolean firstRowIsThs = false;
- List<Element> keyElements = Lists.newArrayList();
- List<Element> valueElements = Lists.newArrayList();
- for (Element rowElem : rowElements) {
- @SuppressWarnings("unchecked") final List<Element> tds = rowElem.elements("td");
- @SuppressWarnings("unchecked") final List<Element> ths = rowElem.elements("th");
- if (!tds.isEmpty()) {
- if (firstRowIsThs) {
- // The tds in this row are the values corresponding to the keys in the previous header row.
- // Copy them in and then break - we aren't interested in any subsequent rows.
- valueElements = Lists.newArrayList(tds);
- break;
- }
- if (!ths.isEmpty()) {
- // New table markup with header as first column.
- keyElements.add(ths.get(0));
- valueElements.add(tds.get(0));
- } else {
- // Old markup with tds only - keys on left, values on right.
- keyElements.add(tds.get(0));
- valueElements.add(tds.size() > 1 ? tds.get(1) : null);
- }
- } else if (!ths.isEmpty()) {
- // New table markup with header as first row - keyElements *are* th elements in this row
- keyElements = Lists.newArrayList(ths);
- firstRowIsThs = true;
- }
- }
- final Map<String, PageProperty> results = new HashMap<String, PageProperty>();
- // Now that we know the key and value element lists we can match them up and create String pairs.
- // Note that the lists might contain nulls, so getInnerHtml needs to be sm4rt.
- for (int i = 0; i < keyElements.size(); i++) {
- final Element keyElement = keyElements.get(i);
- final Element valueElement = (valueElements.size() > i) ? valueElements.get(i) : null;
- String key = getKeyText(keyElement);
- if (!results.containsKey(key)) {
- results.put(key, new PageProperty(getInnerHtml(valueElement), getInnerHtml(keyElement)));
- }
- }
- return ImmutableMap.copyOf(results);
- }
- private String getKeyText(Element element) throws IOException {
- if (element == null)
- return "";
- String key;
- if (element.isTextOnly()) {
- key = element.getText();
- } else {
- Writer stringWriter = new StringWriter();
- @SuppressWarnings("unchecked")
- Iterator<Node> it = element.nodeIterator();
- while (it.hasNext()) {
- Node node = it.next();
- if (node instanceof DefaultElement) {
- Element defaultElement = (DefaultElement) node;
- stringWriter.append(defaultElement.getStringValue());
- } else
- stringWriter.append(node.getText());
- }
- key = stringWriter.toString();
- }
- return StringUtils.remove(escapeHtml4(key), " ");
- }
- private String getInnerHtml(Element element) throws IOException {
- if (element == null)
- return "";
- /* Why not just element.write(stringWriter), you may be wondering? Because...
- a. You get also the element tags (like "<th>Element Text</th>") and...
- b. Doesn't work for some elements
- NOTE: with the current SAXParser, it is impossible to remove the namespaces from the output
- of node.write - decided to leave it for now.
- */
- if (element.isTextOnly()) {
- return escapeHtml4(element.getText());
- } else {
- Writer stringWriter = new StringWriter();
- @SuppressWarnings("unchecked")
- Iterator<Node> it = element.nodeIterator();
- while (it.hasNext()) {
- Node node = it.next();
- node.write(stringWriter);
- }
- return stringWriter.toString();
- }
- }
- private org.dom4j.Document getMacroBodyDocument(final String macroBodyXhtml)
- throws DocumentException, UnsupportedEncodingException, ParserConfigurationException, SAXException {
- StringBuilder builder = new StringBuilder();
- // Add internal DTD declaration to allow support for entities in attribute vales.
- builder.append("<!DOCTYPE xml>").append("<xml");
- for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
- builder.append(" xmlns");
- if (!namespace.isDefaultNamespace())
- builder.append(":").append(namespace.getPrefix());
- builder.append("=\"").append(namespace.getUri()).append("\"");
- if (namespace.isDefaultNamespace()) {
- builder.append(" xmlns:xhtml=\"").append(namespace.getUri()).append("\"");
- }
- }
- builder.append(">")
- .append(macroBodyXhtml)
- .append("</xml>");
- final ByteArrayInputStream is = new ByteArrayInputStream(builder.toString().getBytes(CHARSET_UTF8));
- final SAXReader saxReader = new SAXReader(false);
- return saxReader.read(is);
- }
- }