// Copyright 2011, Google Inc. All Rights Reserved.
//
// 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.google.api.ads.common.lib.utils.logging;
import com.google.api.ads.common.lib.conf.AdsApiConfiguration;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.slf4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.EmptyStackException;
import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
/**
* A utility class that pretty prints XML messages.
*/
public final class PrettyPrinter implements PrettyPrinterInterface {
private final Logger libLogger;
private final ImmutableList<String> sensitiveXPathStrings;
private final Supplier<XPath> xpathSupplier;
private final Supplier<Transformer> transformerSupplier;
private final Supplier<DocumentBuilder> documentBuilderSupplier;
/**
* Default constructor used by Guice. Takes {@link Supplier} instances for {@link XPath},
* {@link Transformer}, and {@link DocumentBuilder} because the corresponding factory objects are
* <em>not</em> thread-safe. The Guice modules creating these suppliers should guarantee that the
* {@link Supplier} <em>is</em> thread-safe, however.
*
* @param adsApiConfiguration the API configuration
* @param libLogger the logger to use for errors
* @param xpathSupplier a thread-safe supplier of {@link XPath} objects
* @param transformerSupplier a thread-safe supplier of {@link Transformer} objects
* @param documentBuilderSupplier a thread-safe supplier of {@link DocumentBuilder} objects
*/
@Inject
public PrettyPrinter(
AdsApiConfiguration adsApiConfiguration,
@Named("libLogger") Logger libLogger,
Supplier<XPath> xpathSupplier,
Supplier<Transformer> transformerSupplier,
Supplier<DocumentBuilder> documentBuilderSupplier) {
String[] sensitiveXPathsArray = adsApiConfiguration.getSensitiveXPaths();
this.sensitiveXPathStrings =
sensitiveXPathsArray == null
? ImmutableList.<String>of()
: ImmutableList.<String>copyOf(sensitiveXPathsArray);
this.libLogger = libLogger;
this.xpathSupplier = xpathSupplier;
this.transformerSupplier = transformerSupplier;
this.documentBuilderSupplier = documentBuilderSupplier;
}
/**
* Transforms XML into a pretty-printed format with sensitive strings removed.
* If there is an error initializing formatting the XML message, the formatting
* step will be skipped and the unformatted XML with sensitive strings removed
* will be returned.
*
* @param xml the XML message to be pretty printed
* @return the given message in pretty-printed format
*/
@Override
public String prettyPrint(String xml) {
if (xml == null) {
return xml;
}
Source xmlSource = sanitizeXml(xml);
if (xmlSource == null) {
xmlSource = new StreamSource(new StringReader(xml));
}
String formattedXml = formatXml(xmlSource);
return formattedXml != null ? formattedXml : xml;
}
/**
* Formats/pretty prints the XML source.
*
* @return the formatted XML if formatting succeeded, else null
*/
private String formatXml(@Nullable Source xmlSource) {
Transformer transformer = transformerSupplier.get();
if (xmlSource != null && transformer != null) {
try {
StreamResult result = new StreamResult(new StringWriter());
transformer.transform(xmlSource, result);
return result.getWriter().toString();
} catch (TransformerException e) {
libLogger.warn("Unable to pretty print XML: {}", e);
} catch (NullPointerException e) {
libLogger.warn("Unable to pretty print XML: {}", e);
} catch (ArrayIndexOutOfBoundsException e) {
libLogger.warn("Unable to pretty print XML: {}", e);
} catch (EmptyStackException e) {
libLogger.warn("Unable to pretty print XML: {}", e);
}
}
return null;
}
/**
* Removes any sensitive information (such as an AdWords API developer token) from the provided
* XML.
*
* @return a Source if sanitizing was successful, or null if unable to parse and sanitize the
* provided XML
*/
private Source sanitizeXml(@Nullable String xml) {
if (xml != null && !sensitiveXPathStrings.isEmpty()) {
try {
DocumentBuilder documentBuilder = documentBuilderSupplier.get();
XPath xpath = xpathSupplier.get();
if (documentBuilder != null && xpath != null) {
Document doc = documentBuilder.parse(new InputSource(new StringReader(xml)));
for (String xpathString : sensitiveXPathStrings) {
XPathExpression expr = xpath.compile(xpathString);
Node node = (Node) expr.evaluate(doc, XPathConstants.NODE);
if (node != null && node.getTextContent() != null) {
node.setTextContent("REDACTED");
}
}
return new DOMSource(doc);
}
} catch (SAXException e) {
libLogger.warn("Unable to parse XML: {}", e);
} catch (IOException e) {
libLogger.warn("Unable to parse XML: {}", e);
} catch (XPathExpressionException e) {
libLogger.warn("Unable to parse XML: {}", e);
}
}
return null;
}
}