/* * Copyright (C) 2011 The Android Open Source Project * * 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. */ package com.android.manifmerger; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.manifmerger.IMergerLog.FileAndLine; import com.android.manifmerger.IMergerLog.Severity; import com.android.utils.ILogger; import com.android.utils.XmlUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import java.io.File; import java.io.FileNotFoundException; import java.io.Reader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** * A few XML handling utilities. */ class MergerXmlUtils { private static final String DATA_ORIGIN_FILE = "manif.merger.file"; //$NON-NLS-1$ private static final String DATA_FILE_NAME = "manif.merger.filename"; //$NON-NLS-1$ private static final String DATA_LINE_NUMBER = "manif.merger.line#"; //$NON-NLS-1$ /** * Parses the given XML file as a DOM document. * The parser does not validate the DTD nor any kind of schema. * It is namespace aware. * <p/> * This adds a user tag with the original {@link File} to the returned document. * You can retrieve this file later by using {@link #extractXmlFilename(Node)}. * * @param xmlFile The XML {@link File} to parse. Must not be null. * @param log An {@link ILogger} for reporting errors. Must not be null. * @param merger The {@link ManifestMerger} this document is intended for * @return A new DOM {@link Document}, or null. */ @Nullable static Document parseDocument(@NonNull final File xmlFile, @NonNull final IMergerLog log, @NonNull ManifestMerger merger) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Reader reader = XmlUtils.getUtfReader(xmlFile); InputSource is = new InputSource(reader); factory.setNamespaceAware(true); factory.setValidating(false); DocumentBuilder builder = factory.newDocumentBuilder(); // We don't want the default handler which prints errors to stderr. builder.setErrorHandler(new ErrorHandler() { @Override public void warning(SAXParseException e) { log.error(Severity.WARNING, new FileAndLine(xmlFile.getAbsolutePath(), 0), "Warning when parsing: %1$s", e.toString()); } @Override public void fatalError(SAXParseException e) { log.error(Severity.ERROR, new FileAndLine(xmlFile.getAbsolutePath(), 0), "Fatal error when parsing: %1$s", xmlFile.getName(), e.toString()); } @Override public void error(SAXParseException e) { log.error(Severity.ERROR, new FileAndLine(xmlFile.getAbsolutePath(), 0), "Error when parsing: %1$s", e.toString()); } }); Document doc = builder.parse(is); doc.setUserData(DATA_ORIGIN_FILE, xmlFile, null /*handler*/); findLineNumbers(doc, 1); if (merger.isInsertSourceMarkers()) { setSource(doc, xmlFile); } return doc; } catch (FileNotFoundException e) { log.error(Severity.ERROR, new FileAndLine(xmlFile.getAbsolutePath(), 0), "XML file not found"); } catch (Exception e) { log.error(Severity.ERROR, new FileAndLine(xmlFile.getAbsolutePath(), 0), "Failed to parse XML file: %1$s", e.toString()); } return null; } /** * Parses the given XML string as a DOM document. * The parser does not validate the DTD nor any kind of schema. * It is namespace aware. * * @param xml The XML string to parse. Must not be null. * @param log An {@link ILogger} for reporting errors. Must not be null. * @return A new DOM {@link Document}, or null. */ @VisibleForTesting @Nullable static Document parseDocument(@NonNull String xml, @NonNull IMergerLog log, @NonNull FileAndLine errorContext) { try { Document doc = XmlUtils.parseDocument(xml, true); findLineNumbers(doc, 1); if (errorContext.getFileName() != null) { setSource(doc, new File(errorContext.getFileName())); } return doc; } catch (Exception e) { log.error(Severity.ERROR, errorContext, "Failed to parse XML string"); } return null; } /** * Decorates the document with the specified file name, which can be * retrieved later by calling {@link #extractLineNumber(Node)}. * <p/> * It also tries to add line number information, with the caveat that the * current implementation is a gross approximation. * <p/> * There is no need to call this after calling one of the {@code parseDocument()} * methods since they already decorated their own document. * * @param doc The document to decorate. * @param fileName The name to retrieve later for that document. */ static void decorateDocument(@NonNull Document doc, @NonNull String fileName) { doc.setUserData(DATA_FILE_NAME, fileName, null /*handler*/); findLineNumbers(doc, 1); } /** * Returns a new {@link FileAndLine} structure that identifies * the base filename & line number from which the XML node was parsed. * <p/> * When the line number is unknown (e.g. if a {@link Document} instance is given) * then line number 0 will be used. * * @param node The node or document where the error occurs. Must not be null. * @return A new non-null {@link FileAndLine} combining the file name and line number. */ @NonNull static FileAndLine xmlFileAndLine(@NonNull Node node) { String name = extractXmlFilename(node); int line = extractLineNumber(node); // 0 in case of error or unknown return new FileAndLine(name, line); } /** * Extracts the origin {@link File} that {@link #parseDocument(File, IMergerLog, * ManifestMerger)} added to the XML document or the string added by * * @param xmlNode Any node from a document returned by {@link #parseDocument(File, IMergerLog, * ManifestMerger)}. * @return The {@link File} object used to create the document or null. */ @Nullable static String extractXmlFilename(@Nullable Node xmlNode) { if (xmlNode != null && xmlNode.getNodeType() != Node.DOCUMENT_NODE) { xmlNode = xmlNode.getOwnerDocument(); } if (xmlNode != null) { Object data = xmlNode.getUserData(DATA_ORIGIN_FILE); if (data instanceof File) { return ((File) data).getPath(); } data = xmlNode.getUserData(DATA_FILE_NAME); if (data instanceof String) { return (String) data; } } return null; } public static void setSource(@NonNull Node node, @NonNull File source) { //noinspection ConstantConditions for (; node != null; node = node.getNextSibling()) { short nodeType = node.getNodeType(); if (nodeType == Node.ELEMENT_NODE || nodeType == Node.COMMENT_NODE || nodeType == Node.DOCUMENT_NODE || nodeType == Node.CDATA_SECTION_NODE) { node.setUserData(DATA_ORIGIN_FILE, source, null); } Node child = node.getFirstChild(); setSource(child, source); } } /** * This is a CRUDE INEXACT HACK to decorate the DOM with some kind of line number * information for elements. It's inexact because by the time we get the DOM we * already have lost all the information about whitespace between attributes. * <p/> * Also we don't even try to deal with \n vs \r vs \r\n insanity. This only counts * the \n occurring in text nodes to determine line advances, which is clearly flawed. * <p/> * However it's good enough for testing, and we'll replace it by a PositionXmlParser * once it's moved into com.android.util. */ private static int findLineNumbers(Node node, int line) { for (; node != null; node = node.getNextSibling()) { node.setUserData(DATA_LINE_NUMBER, Integer.valueOf(line), null /*handler*/); if (node.getNodeType() == Node.TEXT_NODE) { String text = node.getNodeValue(); if (!text.isEmpty()) { for (int pos = 0; (pos = text.indexOf('\n', pos)) != -1; pos++) { ++line; } } } Node child = node.getFirstChild(); if (child != null) { line = findLineNumbers(child, line); } } return line; } /** * Extracts the line number that {@link #findLineNumbers} added to the XML nodes. * * @param xmlNode Any node from a document returned by {@link #parseDocument(File, IMergerLog, * ManifestMerger)}. * @return The line number if found or 0. */ static int extractLineNumber(@Nullable Node xmlNode) { if (xmlNode != null) { Object data = xmlNode.getUserData(DATA_LINE_NUMBER); if (data instanceof Integer) { return ((Integer) data).intValue(); } } return 0; } /** * Outputs the given XML {@link Document} to the file {@code outFile}. * * TODO right now reformats the document. Needs to output as-is, respecting white-space. * * @param doc The document to output. Must not be null. * @param outFile The {@link File} where to write the document. * @param log A log in case of error. * @return True if the file was written, false in case of error. */ static boolean printXmlFile( @NonNull Document doc, @NonNull File outFile, @NonNull IMergerLog log) { // Quick thing based on comments from http://stackoverflow.com/questions/139076 try { Transformer tf = TransformerFactory.newInstance().newTransformer(); tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ "4"); //$NON-NLS-1$ tf.transform(new DOMSource(doc), new StreamResult(outFile)); return true; } catch (TransformerException e) { log.error(Severity.ERROR, new FileAndLine(outFile.getName(), 0), "Failed to write XML file: %1$s", e.toString()); return false; } } /** * Outputs the given XML {@link Document} as a string. * * TODO right now reformats the document. Needs to output as-is, respecting white-space. * * @param doc The document to output. Must not be null. * @param log A log in case of error. * @return A string representation of the XML. Null in case of error. */ static String printXmlString( @NonNull Document doc, @NonNull IMergerLog log) { try { Transformer tf = TransformerFactory.newInstance().newTransformer(); tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ "4"); //$NON-NLS-1$ StringWriter sw = new StringWriter(); tf.transform(new DOMSource(doc), new StreamResult(sw)); return sw.toString(); } catch (TransformerException e) { log.error(Severity.ERROR, new FileAndLine(extractXmlFilename(doc), 0), "Failed to write XML file: %1$s", e.toString()); return null; } } /** * Dumps the structure of the DOM to a simple text string. * * @param node The first node to dump (recursively). Can be null. * @param nextSiblings If true, will also dump the following siblings. * If false, it will just process the given node. * @return A string representation of the Node structure, useful for debugging. */ @NonNull static String dump(@Nullable Node node, boolean nextSiblings) { return dump(node, 0 /*offset*/, nextSiblings, true /*deep*/, null /*keyAttr*/); } /** * Dumps the structure of the DOM to a simple text string. * Each line is terminated with a \n separator. * * @param node The first node to dump. Can be null. * @param offsetIndex The offset to add at the begining of each line. Each offset is * converted into 2 space characters. * @param nextSiblings If true, will also dump the following siblings. * If false, it will just process the given node. * @param deep If true, this will recurse into children. * @param keyAttr An optional attribute *local* name to insert when writing an element. * For example when writing an Activity, it helps to always insert "name" attribute. * @return A string representation of the Node structure, useful for debugging. */ @NonNull static String dump( @Nullable Node node, int offsetIndex, boolean nextSiblings, boolean deep, @Nullable String keyAttr) { StringBuilder sb = new StringBuilder(); String offset = ""; //$NON-NLS-1$ for (int i = 0; i < offsetIndex; i++) { offset += " "; //$NON-NLS-1$ } if (node == null) { sb.append(offset).append("(end reached)\n"); } else { for (; node != null; node = node.getNextSibling()) { String type = null; short t = node.getNodeType(); switch(t) { case Node.ELEMENT_NODE: String attr = ""; if (keyAttr != null) { for (Node a : sortedAttributeList(node.getAttributes())) { if (a != null && keyAttr.equals(a.getLocalName())) { attr = String.format(" %1$s=%2$s", a.getNodeName(), a.getNodeValue()); break; } } } sb.append(String.format("%1$s<%2$s%3$s>\n", offset, node.getNodeName(), attr)); break; case Node.COMMENT_NODE: sb.append(String.format("%1$s<!-- %2$s -->\n", offset, node.getNodeValue())); break; case Node.TEXT_NODE: String txt = node.getNodeValue().trim(); if (txt.isEmpty()) { // Keep this for debugging. TODO make it a flag // to dump whitespace on debugging. Otherwise ignore it. // txt = "[whitespace]"; break; } sb.append(String.format("%1$s%2$s\n", offset, txt)); break; case Node.ATTRIBUTE_NODE: sb.append(String.format("%1$s @%2$s = %3$s\n", offset, node.getNodeName(), node.getNodeValue())); break; case Node.CDATA_SECTION_NODE: type = "cdata"; //$NON-NLS-1$ break; case Node.DOCUMENT_NODE: type = "document"; //$NON-NLS-1$ break; case Node.PROCESSING_INSTRUCTION_NODE: type = "PI"; //$NON-NLS-1$ break; default: type = Integer.toString(t); } if (type != null) { sb.append(String.format("%1$s[%2$s] <%3$s> %4$s\n", offset, type, node.getNodeName(), node.getNodeValue())); } if (deep) { for (Attr attr : sortedAttributeList(node.getAttributes())) { sb.append(String.format("%1$s @%2$s = %3$s\n", offset, attr.getNodeName(), attr.getNodeValue())); } Node child = node.getFirstChild(); if (child != null) { sb.append(dump(child, offsetIndex+1, true, true, keyAttr)); } } if (!nextSiblings) { break; } } } return sb.toString(); } /** * Returns a sorted list of attributes. * The list is never null and does not contain null items. * * @param attrMap A Node map as returned by {@link Node#getAttributes()}. * Can be null, in which case an empty list is returned. * @return A non-null, possible empty, list of all nodes that are actual {@link Attr}, * sorted by increasing attribute name. */ @NonNull static List<Attr> sortedAttributeList(@Nullable NamedNodeMap attrMap) { List<Attr> list = new ArrayList<Attr>(); if (attrMap != null) { for (int i = 0; i < attrMap.getLength(); i++) { Node attr = attrMap.item(i); if (attr instanceof Attr) { list.add((Attr) attr); } } } if (list.size() > 1) { // Sort it by attribute name Collections.sort(list, getAttrComparator()); } return list; } /** * Returns a comparator for {@link Attr}, alphabetically sorted by name. * The "name" attribute is special and always sorted to the front. */ @NonNull static Comparator<? super Attr> getAttrComparator() { return new Comparator<Attr>() { @Override public int compare(Attr a1, Attr a2) { String s1 = a1 == null ? "" : a1.getNodeName(); //$NON-NLS-1$ String s2 = a2 == null ? "" : a2.getNodeName(); //$NON-NLS-1$ boolean name1 = s1.equals("name"); //$NON-NLS-1$ boolean name2 = s2.equals("name"); //$NON-NLS-1$ if (name1 && name2) { return 0; } else if (name1) { return -1; // name is always first } else if (name2) { return 1; // name is always first } else { return s1.compareTo(s2); } } }; } /** * Inject attributes into an existing document. * <p/> * The map keys are "/manifest/elements...|attribute-ns-uri attribute-local-name", * for example "/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion". * (note the space separator between the attribute URI and its local name.) * The elements will be created if they don't exists. Existing attributes will be modified. * The replacement is done on the main document <em>before</em> merging. * The value can be null to remove an existing attribute. * * @param doc The document to modify in-place. * @param attributeMap A map of attributes to inject in the form [pseudo-xpath] => value. * @param log A log in case of error. */ static void injectAttributes( @Nullable Document doc, @Nullable Map<String, String> attributeMap, @NonNull IMergerLog log) { if (doc == null || attributeMap == null || attributeMap.isEmpty()) { return; } // 1=path 2=URI 3=local name final Pattern keyRx = Pattern.compile("^/([^\\|]+)\\|([^ ]*) +(.+)$"); //$NON-NLS-1$ final FileAndLine docInfo = xmlFileAndLine(doc); nextAttribute: for (Entry<String, String> entry : attributeMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (key == null || key.isEmpty()) { continue; } Matcher m = keyRx.matcher(key); if (!m.matches()) { log.error(Severity.WARNING, docInfo, "Invalid injected attribute key: %s", key); continue; } String path = m.group(1); String attrNsUri = m.group(2); String attrName = m.group(3); String[] segment = path.split(Pattern.quote("/")); //$NON-NLS-1$ // Get the path elements. Create them as needed if they don't exist. Node element = doc; nextSegment: for (int i = 0; i < segment.length; i++) { // Find a child with the segment's name String name = segment[i]; for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE && child.getNamespaceURI() == null && child.getNodeName().equals(name)) { // Found it. Continue to the next inner segment. element = child; continue nextSegment; } } // No such element. Create it. if (value == null) { // If value is null, we want to remove, not create and if can't find the // element, then we're done: there's no such attribute to remove. break nextAttribute; } Element child = doc.createElement(name); element = element.insertBefore(child, element.getFirstChild()); } if (element == null) { log.error(Severity.WARNING, docInfo, "Invalid injected attribute path: %s", path); return; } NamedNodeMap attrs = element.getAttributes(); if (attrs != null) { if (attrNsUri != null && attrNsUri.isEmpty()) { attrNsUri = null; } Node attr = attrs.getNamedItemNS(attrNsUri, attrName); if (value == null) { // We want to remove the attribute from the attribute map. if (attr != null) { attrs.removeNamedItemNS(attrNsUri, attrName); } } else { // We want to add or replace the attribute. if (attr == null) { attr = doc.createAttributeNS(attrNsUri, attrName); if (attrNsUri != null) { attr.setPrefix(XmlUtils.lookupNamespacePrefix(element, attrNsUri)); } attrs.setNamedItemNS(attr); } attr.setNodeValue(value); } } } } // ------- /** * Flatten the element to a string. This "pretty prints" the XML tree starting * from the given node and all its children and attributes. * <p/> * The output is designed to be printed using {@link #printXmlDiff}. * * @param node The root node to print. * @param nsPrefix A map that is filled with all the URI=>prefix found. * The internal string only contains the expanded URIs but this is rather verbose * so when printing the diff these will be replaced by the prefixes collected here. * @param prefix A "space" prefix added at the beginning of each line for indentation * purposes. The diff printer later relies on this to find out the structure. */ @NonNull static String printElement( @NonNull Node node, @NonNull Map<String, String> nsPrefix, @NonNull String prefix) { StringBuilder sb = new StringBuilder(); sb.append(prefix).append('<'); String uri = node.getNamespaceURI(); if (uri != null) { sb.append(uri).append(':'); nsPrefix.put(uri, node.getPrefix()); } sb.append(node.getLocalName()); printAttributes(sb, node, nsPrefix, prefix); sb.append(">\n"); //$NON-NLS-1$ printChildren(sb, node.getFirstChild(), true, nsPrefix, prefix + " "); //$NON-NLS-1$ sb.append(prefix).append("</"); //$NON-NLS-1$ if (uri != null) { sb.append(uri).append(':'); } sb.append(node.getLocalName()); sb.append(">\n"); //$NON-NLS-1$ return sb.toString(); } /** * Flatten several children elements to a string. * This is an implementation detail for {@link #printElement(Node, Map, String)}. * <p/> * If {@code nextSiblings} is false, the string conversion takes only the given * child element and stops there. * <p/> * If {@code nextSiblings} is true, the string conversion also takes _all_ the siblings * after the given element. The idea is the caller can call this with the first child * of a parent and get a string showing all the children at the same time. They are * sorted to avoid the ordering issue. */ @NonNull private static StringBuilder printChildren( @NonNull StringBuilder sb, @NonNull Node child, boolean nextSiblings, @NonNull Map<String, String> nsPrefix, @NonNull String prefix) { ArrayList<String> children = new ArrayList<String>(); boolean hasText = false; for (; child != null; child = child.getNextSibling()) { short t = child.getNodeType(); if (nextSiblings && t == Node.TEXT_NODE) { // We don't typically have meaningful text nodes in an Android manifest. // If there are, just dump them as-is into the element representation. // We do trim whitespace and ignore all-whitespace or empty text nodes. String s = child.getNodeValue().trim(); if (!s.isEmpty()) { sb.append(s); hasText = true; } } else if (t == Node.ELEMENT_NODE) { children.add(printElement(child, nsPrefix, prefix)); if (!nextSiblings) { break; } } } if (hasText) { sb.append('\n'); } if (!children.isEmpty()) { Collections.sort(children); for (String s : children) { sb.append(s); } } return sb; } /** * Flatten several attributes to a string using their alphabetical order. * This is an implementation detail for {@link #printElement(Node, Map, String)}. */ @NonNull private static StringBuilder printAttributes( @NonNull StringBuilder sb, @NonNull Node node, @NonNull Map<String, String> nsPrefix, @NonNull String prefix) { ArrayList<String> attrs = new ArrayList<String>(); NamedNodeMap attrMap = node.getAttributes(); if (attrMap != null) { StringBuilder sb2 = new StringBuilder(); for (int i = 0; i < attrMap.getLength(); i++) { Node attr = attrMap.item(i); if (attr instanceof Attr) { sb2.setLength(0); sb2.append('@'); String uri = attr.getNamespaceURI(); if (uri != null) { sb2.append(uri).append(':'); nsPrefix.put(uri, attr.getPrefix()); } sb2.append(attr.getLocalName()); sb2.append("=\"").append(attr.getNodeValue()).append('\"'); //$NON-NLS-1$ attrs.add(sb2.toString()); } } } Collections.sort(attrs); for(String attr : attrs) { sb.append('\n'); sb.append(prefix).append(" ").append(attr); //$NON-NLS-1$ } return sb; } //------------ /** * Computes a quick diff between two strings generated by * {@link #printElement(Node, Map, String)}. * <p/> * This is a <em>not</em> designed to be a full contextual diff. * It just stops at the first difference found, printing up to 3 lines of diff * and backtracking to add prior contextual information to understand the * structure of the element where the first diff line occurred (by printing * each parent found till the root one as well as printing the attribute * named by {@code keyAttr}). * * @param sb The string builder where to output is written. * @param expected The expected XML tree (as generated by {@link #printElement}.) * For best result this would be the "destination" XML we're merging into, * e.g. the main manifest. * @param actual The actual XML tree (as generated by {@link #printElement}.) * For best result this would be the "source" XML we're merging from, * e.g. a library manifest. * @param nsPrefixE The map of URI=>prefix for the expected XML tree. * @param nsPrefixA The map of URI=>prefix for the actual XML tree. * @param keyAttr An optional attribute *full* name (uri:local name) to always * insert when writing the contextual lines before a diff line. * For example when writing an Activity, it helps to always insert * the "name" attribute since that's the key element to help the user * identify which node is being dumped. */ static void printXmlDiff( StringBuilder sb, String expected, String actual, Map<String, String> nsPrefixE, Map<String, String> nsPrefixA, String keyAttr) { String[] aE = expected.split("\n"); String[] aA = actual.split("\n"); int lE = aE.length; int lA = aA.length; int lm = lE < lA ? lA : lE; boolean eofE = false; boolean eofA = false; boolean contextE = true; boolean contextA = true; int numDiff = 0; StringBuilder sE = new StringBuilder(); StringBuilder sA = new StringBuilder(); outerLoop: for (int i = 0, iE = 0, iA = 0; i < lm; i++) { if (iE < lE && iA < lA && aE[iE].equals(aA[iA])) { if (numDiff > 0) { // If we found a difference, stop now. break outerLoop; } iE++; iA++; continue; } else { // Try to print some context for each side based on previous lines's space prefix. if (contextE) { if (iE > 0) { String p = diffGetPrefix(aE[iE]); for (int kE = iE-1; kE >= 0; kE--) { if (!aE[kE].startsWith(p)) { sE.insert(0, '\n').insert(0, diffReplaceNs(aE[kE], nsPrefixE)).insert(0, " "); if (p.isEmpty()) { break; } p = diffGetPrefix(aE[kE]); } else if (aE[kE].contains(keyAttr) || kE == 0) { sE.insert(0, '\n').insert(0, diffReplaceNs(aE[kE], nsPrefixE)).insert(0, " "); } } } contextE = false; } if (iE >= lE) { if (!eofE) { sE.append("--(end reached)\n"); eofE = true; } } else { sE.append("--").append(diffReplaceNs(aE[iE++], nsPrefixE)).append('\n'); } if (contextA) { if (iA > 0) { String p = diffGetPrefix(aA[iA]); for (int kA = iA-1; kA >= 0; kA--) { if (!aA[kA].startsWith(p)) { sA.insert(0, '\n').insert(0, diffReplaceNs(aA[kA], nsPrefixA)).insert(0, " "); p = diffGetPrefix(aA[kA]); if (p.isEmpty()) { break; } } else if (aA[kA].contains(keyAttr) || kA == 0) { sA.insert(0, '\n').insert(0, diffReplaceNs(aA[kA], nsPrefixA)).insert(0, " "); } } } contextA = false; } if (iA >= lA) { if (!eofA) { sA.append("++(end reached)\n"); eofA = true; } } else { sA.append("++").append(diffReplaceNs(aA[iA++], nsPrefixA)).append('\n'); } // Dump up to 3 lines of difference numDiff++; if (numDiff == 3) { break outerLoop; } } } sb.append(sE); sb.append(sA); } /** * Returns all the whitespace at the beginning of a string. * Implementation details for {@link #printXmlDiff} used to find the "parent" * element and include it in the context of the diff. */ private static String diffGetPrefix(String str) { int pos = 0; int len = str.length(); while (pos < len && str.charAt(pos) == ' ') { pos++; } return str.substring(0, pos); } /** * Simplifies a diff line by replacing NS URIs by their prefix. * Implementation details for {@link #printXmlDiff}. */ private static String diffReplaceNs(String str, Map<String, String> nsPrefix) { for (Entry<String, String> entry : nsPrefix.entrySet()) { String uri = entry.getKey(); String prefix = entry.getValue(); if (prefix != null && str.contains(uri)) { str = str.replaceAll(Pattern.quote(uri), Matcher.quoteReplacement(prefix)); } } return str; } /** * Returns the file associated with the given specific node, if any. * Note that this will not search upwards for parent nodes; it returns a * file associated with this specific node, if any. */ @Nullable public static File getFileFor(@NonNull Node node) { return (File) node.getUserData(DATA_ORIGIN_FILE); } /** * Sets the file associated with the given node, if any */ public static void setFileFor(Node node, File file) { node.setUserData(MergerXmlUtils.DATA_ORIGIN_FILE, file, null); } }