package milkman.ui.plugin.rest.contenttype; import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.Collection; import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyleSpansBuilder; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import lombok.extern.slf4j.Slf4j; import milkman.ui.plugin.ContentTypePlugin; @Slf4j public class XmlContentType implements ContentTypePlugin { private static final Pattern XML_TAG = Pattern .compile("(?<ELEMENT>(</?\\h*)(\\w+)([^<>]*)(\\h*/?>))" + "|(?<COMMENT><!--[^<>]+-->)"); private static final Pattern ATTRIBUTES = Pattern.compile("(\\w+\\h*)(=)(\\h*\"[^\"]+\")"); private static final int GROUP_OPEN_BRACKET = 2; private static final int GROUP_ELEMENT_NAME = 3; private static final int GROUP_ATTRIBUTES_SECTION = 4; private static final int GROUP_CLOSE_BRACKET = 5; private static final int GROUP_ATTRIBUTE_NAME = 1; private static final int GROUP_EQUAL_SYMBOL = 2; private static final int GROUP_ATTRIBUTE_VALUE = 3; @Override public String getName() { return "XML"; } @Override public String getContentType() { return "/xml"; //matches application/xml and text/xml } @Override public boolean supportFormatting() { return true; } @Override public String formatContent(String text) { return toPrettyString(text, 2); } @Override public StyleSpans<Collection<String>> computeHighlighting(String text) { Matcher matcher = XML_TAG.matcher(text); int lastKwEnd = 0; StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>(); while (matcher.find()) { spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd); if (matcher.group("COMMENT") != null) { spansBuilder.add(Collections.singleton("comment"), matcher.end() - matcher.start()); } else { if (matcher.group("ELEMENT") != null) { String attributesText = matcher.group(GROUP_ATTRIBUTES_SECTION); spansBuilder.add(Collections.singleton("keyword"), matcher.end(GROUP_OPEN_BRACKET) - matcher.start(GROUP_OPEN_BRACKET)); spansBuilder.add(Collections.singleton("keyword"), matcher.end(GROUP_ELEMENT_NAME) - matcher.end(GROUP_OPEN_BRACKET)); if (!attributesText.isEmpty()) { lastKwEnd = 0; Matcher amatcher = ATTRIBUTES.matcher(attributesText); while (amatcher.find()) { spansBuilder.add(Collections.singleton("plain"), amatcher.start() - lastKwEnd); spansBuilder.add(Collections.singleton("value"), amatcher.end(GROUP_ATTRIBUTE_NAME) - amatcher.start(GROUP_ATTRIBUTE_NAME)); spansBuilder.add(Collections.singleton("value"), amatcher.end(GROUP_EQUAL_SYMBOL) - amatcher.end(GROUP_ATTRIBUTE_NAME)); spansBuilder.add(Collections.singleton("value"), amatcher.end(GROUP_ATTRIBUTE_VALUE) - amatcher.end(GROUP_EQUAL_SYMBOL)); lastKwEnd = amatcher.end(); } if (attributesText.length() > lastKwEnd) spansBuilder.add(Collections.emptyList(), attributesText.length() - lastKwEnd); } lastKwEnd = matcher.end(GROUP_ATTRIBUTES_SECTION); spansBuilder.add(Collections.singleton("keyword"), matcher.end(GROUP_CLOSE_BRACKET) - lastKwEnd); } } lastKwEnd = matcher.end(); } spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd); return spansBuilder.create(); } // https://stackoverflow.com/questions/25864316/pretty-print-xml-in-java-8/33541820#33541820 public static String toPrettyString(String xml, int indent) { try { // Turn xml string into a document Document document = DocumentBuilderFactory.newInstance() .newDocumentBuilder() .parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8")))); // Remove whitespaces outside tags document.normalize(); XPath xPath = XPathFactory.newInstance().newXPath(); NodeList nodeList = (NodeList) xPath.evaluate("//text()[normalize-space()='']", document, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); ++i) { Node node = nodeList.item(i); node.getParentNode().removeChild(node); } // Setup pretty print options TransformerFactory transformerFactory = TransformerFactory.newInstance(); transformerFactory.setAttribute("indent-number", indent); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); // Return pretty print xml string StringWriter stringWriter = new StringWriter(); transformer.transform(new DOMSource(document), new StreamResult(stringWriter)); return stringWriter.toString(); } catch (Exception e) { log.warn("Failed to format xml", e); } return xml; } }