PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://bitbucket.org/atlassian/confluence-masterdetail-plugin
Java | 317 lines | 240 code | 52 blank | 25 comment | 52 complexity | 6a7feeb15dbe8673f4d730457b644540 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.slf4j.Logger;
  14. import org.slf4j.LoggerFactory;
  15. import javax.xml.namespace.QName;
  16. import javax.xml.stream.XMLEventReader;
  17. import javax.xml.stream.XMLStreamConstants;
  18. import javax.xml.stream.XMLStreamException;
  19. import javax.xml.stream.events.Characters;
  20. import javax.xml.stream.events.StartElement;
  21. import javax.xml.stream.events.XMLEvent;
  22. import java.io.IOException;
  23. import java.io.Reader;
  24. import java.io.StringReader;
  25. import java.io.StringWriter;
  26. import java.io.UnsupportedEncodingException;
  27. import java.util.HashMap;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. import static com.atlassian.confluence.content.render.xhtml.XhtmlConstants.XHTML_NAMESPACE_URI;
  32. import static org.apache.commons.text.StringEscapeUtils.escapeHtml4;
  33. import static org.apache.commons.text.StringEscapeUtils.unescapeHtml4;
  34. /**
  35. * DetailsMacroBodyHandler with STaX parsing logic, resulting in around a 4x speedup over legacy SAXReader parsing.
  36. *
  37. * @since 5.3.0
  38. */
  39. public class DetailsMacroBodyHandlerFastParse implements DetailsMacroBodyHandler {
  40. private static final Logger LOG = LoggerFactory.getLogger(DetailsMacroBodyHandlerFastParse.class);
  41. public static final String CHARSET_UTF8 = "UTF-8";
  42. private final Map<String, ImmutableList<ImmutableMap<String, PageProperty>>> detailsById;
  43. private static final String XHTML_NAMESPACE_PREFIX = "xhtml";
  44. private static final String XPATH_TBODY = "//" + XHTML_NAMESPACE_PREFIX + ":tbody";
  45. private static final QName TR_QNAME = new QName(XHTML_NAMESPACE_URI, "tr");
  46. private static final QName TD_QNAME = new QName(XHTML_NAMESPACE_URI, "td");
  47. private static final QName TH_QNAME = new QName(XHTML_NAMESPACE_URI, "th");
  48. private static final Map<String, String> NAMESPACE_MAP;
  49. static {
  50. NAMESPACE_MAP = new ConcurrentHashMap<String, String>(XhtmlConstants.STORAGE_NAMESPACES.size());
  51. for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
  52. NAMESPACE_MAP.put(namespace.getPrefix() != null ? namespace.getPrefix() : XHTML_NAMESPACE_PREFIX, namespace.getUri());
  53. }
  54. }
  55. private final DetailsSummaryMacroMetricsEvent.Builder metrics;
  56. @FunctionalInterface
  57. public interface XMLEventReaderSupplier {
  58. XMLEventReader supplyXMLEventReader(Reader xml) throws XMLStreamException;
  59. }
  60. private final XMLEventReaderSupplier xmlEventReaderSupplier;
  61. DetailsMacroBodyHandlerFastParse(final DetailsSummaryMacroMetricsEvent.Builder metrics, XMLEventReaderSupplier xmlEventReaderSupplier) {
  62. this.metrics = metrics;
  63. this.detailsById = Maps.newHashMap();
  64. this.xmlEventReaderSupplier = xmlEventReaderSupplier;
  65. }
  66. public static String readElementBody(XMLEventReader eventReader)
  67. throws XMLStreamException {
  68. StringWriter buf = new StringWriter(1024);
  69. int depth = 0;
  70. while (eventReader.hasNext()) {
  71. XMLEvent xmlEvent = eventReader.peek();
  72. if (xmlEvent.isStartElement()) {
  73. ++depth;
  74. } else if (xmlEvent.isEndElement()) {
  75. --depth;
  76. if (depth < 0)
  77. break;
  78. }
  79. xmlEvent = eventReader.nextEvent();
  80. if (xmlEvent.isCharacters()) {
  81. Characters xmlEventCharacters = xmlEvent.asCharacters();
  82. if (xmlEventCharacters.isCData()) {
  83. buf.append("<![CDATA[").append(xmlEventCharacters.getData()).append("]]>");
  84. } else {
  85. buf.append(escapeHtml4(xmlEventCharacters.getData()));
  86. }
  87. } else {
  88. xmlEvent.writeAsEncodedUnicode(buf);
  89. }
  90. }
  91. return buf.getBuffer().toString();
  92. }
  93. public static String readElementBodyCharacters(XMLEventReader eventReader)
  94. throws XMLStreamException {
  95. StringWriter buf = new StringWriter(1024);
  96. int depth = 0;
  97. while (eventReader.hasNext()) {
  98. XMLEvent xmlEvent = eventReader.peek();
  99. if (xmlEvent.isStartElement()) {
  100. ++depth;
  101. } else if (xmlEvent.isEndElement()) {
  102. --depth;
  103. if (depth < 0)
  104. break;
  105. }
  106. xmlEvent = eventReader.nextEvent();
  107. if (xmlEvent.isCharacters()) {
  108. xmlEvent.asCharacters().writeAsEncodedUnicode(buf);
  109. }
  110. }
  111. String body = buf.getBuffer().toString();
  112. return escapeHtml4(body);
  113. }
  114. public void handle(MacroDefinition macroDefinition) {
  115. if (!"details".equals(macroDefinition.getName()))
  116. return;
  117. String bodyText = macroDefinition.getBodyText();
  118. String detailsId = StringUtils.trim(macroDefinition.getParameter(DetailsSummaryMacro.PARAM_ID));
  119. if (detailsId == null)
  120. detailsId = "";
  121. if (StringUtils.isBlank(bodyText)) {
  122. addToDetails(detailsId, ImmutableMap.of());
  123. return;
  124. }
  125. try {
  126. metrics.detailsExtractionStart();
  127. ImmutableMap<String, PageProperty> extractedDetails = extractDetails(bodyText);
  128. metrics.detailsExtractionFinish(extractedDetails.size());
  129. addToDetails(detailsId, extractedDetails);
  130. } catch (Exception e) {
  131. LOG.error(String.format("Unable to parse detailsById in detailsById macro\n%s", bodyText), e);
  132. }
  133. }
  134. /**
  135. * Get the detailsById extracted from the content. It is a mapping of ids to a map of heading/values.
  136. * The default without an id mapping is keyed with the empty string. Note null is returned if no details
  137. * for the given id are found.
  138. */
  139. public List<? extends Map<String, PageProperty>> getDetails(String detailsId) {
  140. return detailsById.get(detailsId);
  141. }
  142. /**
  143. * @return an ImmutableMap keyed by detail ID to a map of details, where details is a map keyed by heading text to
  144. * ExtractedDetail objects
  145. */
  146. public ImmutableMap<String, ImmutableList<ImmutableMap<String, PageProperty>>> getDetails() {
  147. return ImmutableMap.copyOf(detailsById);
  148. }
  149. private void addToDetails(String id, ImmutableMap<String, PageProperty> details) {
  150. List<ImmutableMap<String, PageProperty>> newDetails;
  151. List<ImmutableMap<String, PageProperty>> currentDetails = detailsById.get(id);
  152. if (currentDetails == null) {
  153. newDetails = Lists.newArrayList();
  154. } else {
  155. newDetails = Lists.newArrayList(currentDetails);
  156. }
  157. // Note that this means different keys in different PP macros won't be combined into a single map - that
  158. // happens later.
  159. newDetails.add(details);
  160. detailsById.put(id, ImmutableList.copyOf(newDetails));
  161. }
  162. private ImmutableMap<String, PageProperty> extractDetails(String macroBodyXhtml)
  163. throws IOException, XMLStreamException {
  164. final XMLEventReader reader = getEventReader(macroBodyXhtml);
  165. List<List<String>> outer = Lists.newArrayList();
  166. List<String> inner = Lists.newArrayList();
  167. int rowIndex = -1;
  168. int columnIndex = -1;
  169. // Any row/column can be a header, but if the first row is a header then consider vertical properties (default is horizontal)
  170. // E.g. [ firstRowIsThs = true ] [ firstRowIsThs = false ] [ firstRowIsThs = false ]
  171. // <th>Key1</th><th>Key2</th> vs. <th>Key1</th><td>Val1</td> vs. <td>Key1</td><td>Val1</td>
  172. // <td>Val1</td><td>Val1</td> <th>Key2</th><td>Val2</td> <td>Key2</td><td>Val2</td>
  173. boolean firstRowIsThs = false;
  174. while (reader.hasNext()) {
  175. XMLEvent event = reader.nextEvent();
  176. switch (event.getEventType()) {
  177. case XMLStreamConstants.START_ELEMENT:
  178. StartElement startElement = event.asStartElement();
  179. if (TR_QNAME.equals(startElement.getName())) {
  180. // New row
  181. rowIndex++;
  182. } else if (TD_QNAME.equals(startElement.getName())) {
  183. if (rowIndex == 0 && firstRowIsThs) {
  184. // We are on the first row, but it is not entirely heading elements
  185. firstRowIsThs = false;
  186. }
  187. columnIndex++;
  188. inner.add(readElementBody(reader));
  189. } else if (TH_QNAME.equals(startElement.getName())) {
  190. columnIndex++;
  191. inner.add(readElementBody(reader));
  192. if (rowIndex == 0 && columnIndex == 0) {
  193. firstRowIsThs = true;
  194. } else if (columnIndex == 0 && firstRowIsThs) {
  195. // We are on the nth row, and we have a header in the first column
  196. firstRowIsThs = false;
  197. }
  198. }
  199. break;
  200. case XMLStreamConstants.END_ELEMENT:
  201. if (TR_QNAME.equals(event.asEndElement().getName())) {
  202. outer.add(inner);
  203. inner = Lists.newArrayList();
  204. columnIndex = -1;
  205. }
  206. break;
  207. default:
  208. break;
  209. }
  210. }
  211. reader.close();
  212. final Map<String, PageProperty> results = new HashMap<>();
  213. if (firstRowIsThs) {
  214. List<String> keys = outer.get(0); // first row are heading/keys
  215. List<String> values = outer.size() > 1 ? outer.get(1) : keys; // second row are values for above keys
  216. for (int i = 0; i < keys.size(); i++) {
  217. String key = keys.get(i);
  218. String value = values.get(i);
  219. String keyText = getKeyText(key);
  220. results.put(keyText, new PageProperty(value, key));
  221. }
  222. } else {
  223. for (List<String> row : outer) {
  224. String key = row.get(0);
  225. String value = (row.size() > 1) ? row.get(1) : key; // If only single column is provided, keys = values
  226. String keyText = getKeyText(key);
  227. if (!results.containsKey(key)) {
  228. results.put(keyText, new PageProperty(value, key));
  229. }
  230. }
  231. }
  232. return ImmutableMap.copyOf(results);
  233. }
  234. private String getKeyText(String keyMarkup) {
  235. final String keyMarkupNoNBSP = StringUtils.remove(keyMarkup, "&nbsp;");
  236. if (!keyMarkup.contains("<")) {
  237. // If key is not an xml element, then it is just characters
  238. return keyMarkupNoNBSP;
  239. }
  240. try {
  241. return readElementBodyCharacters(getEventReader(unescapeHtml4(keyMarkupNoNBSP)));
  242. } catch (Exception e) {
  243. return keyMarkupNoNBSP;
  244. }
  245. }
  246. private XMLEventReader getEventReader(final String macroBodyXhtml) throws XMLStreamException, UnsupportedEncodingException {
  247. StringBuilder builder = new StringBuilder();
  248. // Add internal DTD declaration to allow support for entities in attribute vales.
  249. builder.append("<!DOCTYPE xml>").append("<xml");
  250. for (Namespace namespace : XhtmlConstants.STORAGE_NAMESPACES) {
  251. builder.append(" xmlns");
  252. if (!namespace.isDefaultNamespace())
  253. builder.append(":").append(namespace.getPrefix());
  254. builder.append("=\"").append(namespace.getUri()).append("\"");
  255. if (namespace.isDefaultNamespace()) {
  256. builder.append(" xmlns:xhtml=\"").append(namespace.getUri()).append("\"");
  257. }
  258. }
  259. builder.append(">")
  260. .append(macroBodyXhtml)
  261. .append("</xml>");
  262. StringReader xmlStringReader = new StringReader(builder.toString());
  263. return xmlEventReaderSupplier.supplyXMLEventReader(xmlStringReader);
  264. }
  265. }