PageRenderTime 26ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/plugin/src/main/java/com/atlassian/confluence/extra/masterdetail/services/DetailsMacroBodyHandlerLegacy.java

https://bitbucket.org/atlassian/confluence-masterdetail-plugin
Java | 279 lines | 197 code | 50 blank | 32 comment | 36 complexity | 76883c800878cbde4028f6fa0b9e404e MD5 | raw file
  1. package com.atlassian.confluence.extra.masterdetail.services;
  2. import com.atlassian.confluence.content.render.xhtml.Namespace;
  3. import com.atlassian.confluence.content.render.xhtml.XhtmlConstants;
  4. import com.atlassian.confluence.extra.masterdetail.DetailsSummaryMacro;
  5. import com.atlassian.confluence.extra.masterdetail.analytics.DetailsSummaryMacroMetricsEvent;
  6. import com.atlassian.confluence.plugins.pageproperties.api.model.PageProperty;
  7. import com.atlassian.confluence.xhtml.api.MacroDefinition;
  8. import com.google.common.collect.ImmutableList;
  9. import com.google.common.collect.ImmutableMap;
  10. import com.google.common.collect.Lists;
  11. import com.google.common.collect.Maps;
  12. import org.apache.commons.lang3.StringUtils;
  13. import org.dom4j.Document;
  14. import org.dom4j.DocumentException;
  15. import org.dom4j.Element;
  16. import org.dom4j.Node;
  17. import org.dom4j.XPath;
  18. import org.dom4j.io.SAXReader;
  19. import org.dom4j.tree.DefaultElement;
  20. import org.slf4j.Logger;
  21. import org.slf4j.LoggerFactory;
  22. import org.xml.sax.SAXException;
  23. import javax.xml.parsers.ParserConfigurationException;
  24. import java.io.ByteArrayInputStream;
  25. import java.io.IOException;
  26. import java.io.StringWriter;
  27. import java.io.UnsupportedEncodingException;
  28. import java.io.Writer;
  29. import java.util.HashMap;
  30. import java.util.Iterator;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.concurrent.ConcurrentHashMap;
  34. import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
  35. /**
  36. * DetailsMacroBodyHandler with SAXReader parsing.
  37. *
  38. * @since 5.3.0
  39. */
  40. public class DetailsMacroBodyHandlerLegacy implements DetailsMacroBodyHandler {
  41. private static final Logger LOG = LoggerFactory.getLogger(DetailsMacroBodyHandlerFastParse.class);
  42. public static final String CHARSET_UTF8 = "UTF-8";
  43. private final Map<String, ImmutableList<ImmutableMap<String, PageProperty>>> detailsById;
  44. private static final String XHTML_NAMESPACE_PREFIX = "xhtml";
  45. private static final String XPATH_TBODY = "//" + XHTML_NAMESPACE_PREFIX + ":tbody";
  46. private static final Map<String, String> NAMESPACE_MAP;
  47. static {
  48. NAMESPACE_MAP = new ConcurrentHashMap<String, String>(XhtmlConstants.STORAGE_NAMESPACES.size());
  49. for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
  50. NAMESPACE_MAP.put(namespace.getPrefix() != null ? namespace.getPrefix() : XHTML_NAMESPACE_PREFIX, namespace.getUri());
  51. }
  52. }
  53. private final DetailsSummaryMacroMetricsEvent.Builder metrics;
  54. DetailsMacroBodyHandlerLegacy(final DetailsSummaryMacroMetricsEvent.Builder metrics) {
  55. this.metrics = metrics;
  56. this.detailsById = Maps.newHashMap();
  57. }
  58. public void handle(MacroDefinition macroDefinition) {
  59. if (!"details".equals(macroDefinition.getName()))
  60. return;
  61. String bodyText = macroDefinition.getBodyText();
  62. String detailsId = StringUtils.trim(macroDefinition.getParameter(DetailsSummaryMacro.PARAM_ID));
  63. if (detailsId == null)
  64. detailsId = "";
  65. if (StringUtils.isBlank(bodyText)) {
  66. addToDetails(detailsId, ImmutableMap.of());
  67. return;
  68. }
  69. try {
  70. metrics.detailsExtractionStart();
  71. ImmutableMap<String, PageProperty> extractedDetails = extractDetails(bodyText);
  72. metrics.detailsExtractionFinish(extractedDetails.size());
  73. addToDetails(detailsId, extractedDetails);
  74. } catch (Exception e) {
  75. LOG.error(String.format("Unable to parse detailsById in detailsById macro\n%s", bodyText), e);
  76. }
  77. }
  78. /**
  79. * Get the detailsById extracted from the content. It is a mapping of ids to a map of heading/values.
  80. * The default without an id mapping is keyed with the empty string. Note null is returned if no details
  81. * for the given id are found.
  82. */
  83. public List<? extends Map<String, PageProperty>> getDetails(String detailsId) {
  84. return detailsById.get(detailsId);
  85. }
  86. /**
  87. * @return an ImmutableMap keyed by detail ID to a map of details, where details is a map keyed by heading text to
  88. * ExtractedDetail objects
  89. */
  90. public ImmutableMap<String, ImmutableList<ImmutableMap<String, PageProperty>>> getDetails() {
  91. return ImmutableMap.copyOf(detailsById);
  92. }
  93. private void addToDetails(String id, ImmutableMap<String, PageProperty> details) {
  94. List<ImmutableMap<String, PageProperty>> newDetails;
  95. List<ImmutableMap<String, PageProperty>> currentDetails = detailsById.get(id);
  96. if (currentDetails == null) {
  97. newDetails = Lists.newArrayList();
  98. } else {
  99. newDetails = Lists.newArrayList(currentDetails);
  100. }
  101. // Note that this means different keys in different PP macros won't be combined into a single map - that
  102. // happens later.
  103. newDetails.add(details);
  104. detailsById.put(id, ImmutableList.copyOf(newDetails));
  105. }
  106. private ImmutableMap<String, PageProperty> extractDetails(String macroBodyXhtml)
  107. throws IOException, DocumentException, ParserConfigurationException, SAXException {
  108. final Document macroBodyDoc = getMacroBodyDocument(macroBodyXhtml);
  109. final XPath xpath = macroBodyDoc.createXPath(XPATH_TBODY);
  110. xpath.setNamespaceURIs(NAMESPACE_MAP);
  111. final ImmutableMap<String, PageProperty> emptyMap = ImmutableMap.of();
  112. final Element tableElement = (Element) xpath.selectSingleNode(macroBodyDoc);
  113. if (tableElement == null)
  114. return emptyMap;
  115. @SuppressWarnings("unchecked") final List<Element> rowElements = tableElement.elements("tr");
  116. if (rowElements == null)
  117. return emptyMap;
  118. return loadDetailPairsFromTableRows(rowElements);
  119. }
  120. private ImmutableMap<String, PageProperty> loadDetailPairsFromTableRows(List<Element> rowElements) throws IOException {
  121. // The table might be in horizontal or vertical orientation, and with or without table-header (th) cells.
  122. boolean firstRowIsThs = false;
  123. List<Element> keyElements = Lists.newArrayList();
  124. List<Element> valueElements = Lists.newArrayList();
  125. for (Element rowElem : rowElements) {
  126. @SuppressWarnings("unchecked") final List<Element> tds = rowElem.elements("td");
  127. @SuppressWarnings("unchecked") final List<Element> ths = rowElem.elements("th");
  128. if (!tds.isEmpty()) {
  129. if (firstRowIsThs) {
  130. // The tds in this row are the values corresponding to the keys in the previous header row.
  131. // Copy them in and then break - we aren't interested in any subsequent rows.
  132. valueElements = Lists.newArrayList(tds);
  133. break;
  134. }
  135. if (!ths.isEmpty()) {
  136. // New table markup with header as first column.
  137. keyElements.add(ths.get(0));
  138. valueElements.add(tds.get(0));
  139. } else {
  140. // Old markup with tds only - keys on left, values on right.
  141. keyElements.add(tds.get(0));
  142. valueElements.add(tds.size() > 1 ? tds.get(1) : null);
  143. }
  144. } else if (!ths.isEmpty()) {
  145. // New table markup with header as first row - keyElements *are* th elements in this row
  146. keyElements = Lists.newArrayList(ths);
  147. firstRowIsThs = true;
  148. }
  149. }
  150. final Map<String, PageProperty> results = new HashMap<String, PageProperty>();
  151. // Now that we know the key and value element lists we can match them up and create String pairs.
  152. // Note that the lists might contain nulls, so getInnerHtml needs to be sm4rt.
  153. for (int i = 0; i < keyElements.size(); i++) {
  154. final Element keyElement = keyElements.get(i);
  155. final Element valueElement = (valueElements.size() > i) ? valueElements.get(i) : null;
  156. String key = getKeyText(keyElement);
  157. if (!results.containsKey(key)) {
  158. results.put(key, new PageProperty(getInnerHtml(valueElement), getInnerHtml(keyElement)));
  159. }
  160. }
  161. return ImmutableMap.copyOf(results);
  162. }
  163. private String getKeyText(Element element) throws IOException {
  164. if (element == null)
  165. return "";
  166. String key;
  167. if (element.isTextOnly()) {
  168. key = element.getText();
  169. } else {
  170. Writer stringWriter = new StringWriter();
  171. @SuppressWarnings("unchecked")
  172. Iterator<Node> it = element.nodeIterator();
  173. while (it.hasNext()) {
  174. Node node = it.next();
  175. if (node instanceof DefaultElement) {
  176. Element defaultElement = (DefaultElement) node;
  177. stringWriter.append(defaultElement.getStringValue());
  178. } else
  179. stringWriter.append(node.getText());
  180. }
  181. key = stringWriter.toString();
  182. }
  183. return StringUtils.remove(escapeHtml4(key), "&nbsp;");
  184. }
  185. private String getInnerHtml(Element element) throws IOException {
  186. if (element == null)
  187. return "";
  188. /* Why not just element.write(stringWriter), you may be wondering? Because...
  189. a. You get also the element tags (like "<th>Element Text</th>") and...
  190. b. Doesn't work for some elements
  191. NOTE: with the current SAXParser, it is impossible to remove the namespaces from the output
  192. of node.write - decided to leave it for now.
  193. */
  194. if (element.isTextOnly()) {
  195. return escapeHtml4(element.getText());
  196. } else {
  197. Writer stringWriter = new StringWriter();
  198. @SuppressWarnings("unchecked")
  199. Iterator<Node> it = element.nodeIterator();
  200. while (it.hasNext()) {
  201. Node node = it.next();
  202. node.write(stringWriter);
  203. }
  204. return stringWriter.toString();
  205. }
  206. }
  207. private org.dom4j.Document getMacroBodyDocument(final String macroBodyXhtml)
  208. throws DocumentException, UnsupportedEncodingException, ParserConfigurationException, SAXException {
  209. StringBuilder builder = new StringBuilder();
  210. // Add internal DTD declaration to allow support for entities in attribute vales.
  211. builder.append("<!DOCTYPE xml>").append("<xml");
  212. for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
  213. builder.append(" xmlns");
  214. if (!namespace.isDefaultNamespace())
  215. builder.append(":").append(namespace.getPrefix());
  216. builder.append("=\"").append(namespace.getUri()).append("\"");
  217. if (namespace.isDefaultNamespace()) {
  218. builder.append(" xmlns:xhtml=\"").append(namespace.getUri()).append("\"");
  219. }
  220. }
  221. builder.append(">")
  222. .append(macroBodyXhtml)
  223. .append("</xml>");
  224. final ByteArrayInputStream is = new ByteArrayInputStream(builder.toString().getBytes(CHARSET_UTF8));
  225. final SAXReader saxReader = new SAXReader(false);
  226. return saxReader.read(is);
  227. }
  228. }