blob: d2af9b5ba7784715fe1d12f73022c2253ce1b81f [file] [log] [blame]
// Copyright 2014 The Bazel Authors. 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.devtools.build.lib.query2.query.output;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashFunction;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.DependencyFilter;
import com.google.devtools.build.lib.packages.EnvironmentGroup;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.LabelPrinter;
import com.google.devtools.build.lib.packages.License;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.PackageGroup;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.query2.common.CommonQueryOptions;
import com.google.devtools.build.lib.query2.compat.FakeLoadTarget;
import com.google.devtools.build.lib.query2.engine.OutputFormatterCallback;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
import com.google.devtools.build.lib.query2.engine.SynchronizedDelegatingOutputFormatterCallback;
import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback;
import com.google.devtools.build.lib.query2.query.aspectresolvers.AspectResolver;
import java.io.OutputStream;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* An output formatter that prints the result as XML.
*/
class XmlOutputFormatter extends AbstractUnorderedFormatter {
private AspectResolver aspectResolver;
private DependencyFilter dependencyFilter;
private boolean packageGroupIncludesDoubleSlash;
private boolean relativeLocations;
private boolean displaySourceFileLocation;
private QueryOptions queryOptions;
@Override
public String getName() {
return "xml";
}
@Override
public ThreadSafeOutputFormatterCallback<Target> createStreamCallback(
OutputStream out, QueryOptions options, QueryEnvironment<?> env) {
return new SynchronizedDelegatingOutputFormatterCallback<>(
createPostFactoStreamCallback(out, options, env.getLabelPrinter()));
}
@Override
public void setOptions(
CommonQueryOptions options, AspectResolver aspectResolver, HashFunction hashFunction) {
super.setOptions(options, aspectResolver, hashFunction);
this.aspectResolver = aspectResolver;
this.dependencyFilter = FormatUtils.getDependencyFilter(options);
this.packageGroupIncludesDoubleSlash = options.incompatiblePackageGroupIncludesDoubleSlash;
this.relativeLocations = options.relativeLocations;
this.displaySourceFileLocation = options.displaySourceFileLocation;
Preconditions.checkArgument(options instanceof QueryOptions);
this.queryOptions = (QueryOptions) options;
}
@Override
public OutputFormatterCallback<Target> createPostFactoStreamCallback(
OutputStream out, QueryOptions options, LabelPrinter labelPrinter) {
return new OutputFormatterCallback<Target>() {
private Document doc;
private Element queryElem;
@Override
public void start() {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
doc = factory.newDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
// This shouldn't be possible: all the configuration is hard-coded.
throw new IllegalStateException("XML output failed", e);
}
doc.setXmlVersion("1.1");
queryElem = doc.createElement("query");
queryElem.setAttribute("version", "2");
doc.appendChild(queryElem);
}
@Override
public void processOutput(Iterable<Target> partialResult) throws InterruptedException {
for (Target target : partialResult) {
queryElem.appendChild(createTargetElement(doc, target, labelPrinter));
}
}
@Override
public void close(boolean failFast) {
if (!failFast) {
try {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(doc), new StreamResult(out));
} catch (TransformerFactoryConfigurationError | TransformerException e) {
// This shouldn't be possible: all the configuration is hard-coded.
throw new IllegalStateException("XML output failed", e);
}
}
}
};
}
/**
* Creates and returns a new DOM tree for the specified build target.
*
* <p>XML structure: - element tag is &lt;source-file>, &lt;generated-file> or &lt;rule
* class="cc_library">, following the terminology of {@link Target#getTargetKind()}. - 'name'
* attribute is target's label. - 'location' attribute is consistent with output of --output
* location. - rule attributes are represented in the DOM structure.
*/
private Element createTargetElement(Document doc, Target target, LabelPrinter labelPrinter)
throws InterruptedException {
Element elem;
if (target instanceof Rule) {
Rule rule = (Rule) target;
elem = doc.createElement("rule");
elem.setAttribute("class", rule.getRuleClass());
for (Attribute attr : rule.getAttributes()) {
AttributeValueSource attributeValueSource =
AttributeValueSource.forRuleAndAttribute(rule, attr);
if (attributeValueSource == AttributeValueSource.RULE
|| queryOptions.xmlShowDefaultValues) {
// TODO(b/162524370): mayTreatMultipleAsNone should be true for types that drop multiple
// values.
Iterable<Object> values =
PossibleAttributeValues.forRuleAndAttribute(
rule, attr, /* mayTreatMultipleAsNone= */ false);
Element attrElem = createValueElement(doc, attr.getType(), values, labelPrinter);
attrElem.setAttribute("name", attr.getName());
elem.appendChild(attrElem);
}
}
// Include explicit elements for all direct inputs and outputs of a rule;
// this goes beyond what is available from the attributes above, since it
// may also (depending on options) include implicit outputs,
// host-configuration outputs, and default values.
for (Label label : rule.getSortedLabels(dependencyFilter)) {
Element inputElem = doc.createElement("rule-input");
inputElem.setAttribute("name", labelPrinter.toString(label));
elem.appendChild(inputElem);
}
for (Label label :
aspectResolver.computeAspectDependencies(target, dependencyFilter).values()) {
Element inputElem = doc.createElement("rule-input");
inputElem.setAttribute("name", labelPrinter.toString(label));
elem.appendChild(inputElem);
}
for (OutputFile outputFile : rule.getOutputFiles()) {
Element outputElem = doc.createElement("rule-output");
outputElem.setAttribute("name", labelPrinter.toString(outputFile.getLabel()));
elem.appendChild(outputElem);
}
for (String feature : rule.getPackage().getFeatures()) {
Element outputElem = doc.createElement("rule-default-setting");
outputElem.setAttribute("name", feature);
elem.appendChild(outputElem);
}
} else if (target instanceof PackageGroup) {
PackageGroup packageGroup = (PackageGroup) target;
elem = doc.createElement("package-group");
elem.setAttribute("name", packageGroup.getName());
Element includes =
createValueElement(doc, BuildType.LABEL_LIST, packageGroup.getIncludes(), labelPrinter);
includes.setAttribute("name", "includes");
elem.appendChild(includes);
Element packages =
createValueElement(
doc,
Type.STRING_LIST,
packageGroup.getContainedPackages(packageGroupIncludesDoubleSlash),
labelPrinter);
packages.setAttribute("name", "packages");
elem.appendChild(packages);
} else if (target instanceof OutputFile) {
OutputFile outputFile = (OutputFile) target;
elem = doc.createElement("generated-file");
elem.setAttribute(
"generating-rule", labelPrinter.toString(outputFile.getGeneratingRule().getLabel()));
} else if (target instanceof InputFile) {
elem = doc.createElement("source-file");
InputFile inputFile = (InputFile) target;
if (inputFile.getName().equals("BUILD")) {
addStarlarkFilesToElement(doc, elem, inputFile, labelPrinter);
addFeaturesToElement(doc, elem, inputFile);
elem.setAttribute("package_contains_errors",
String.valueOf(inputFile.getPackage().containsErrors()));
}
addPackageGroupsToElement(doc, elem, inputFile, labelPrinter);
} else if (target instanceof EnvironmentGroup) {
EnvironmentGroup envGroup = (EnvironmentGroup) target;
elem = doc.createElement("environment-group");
elem.setAttribute("name", envGroup.getName());
Element environments =
createValueElement(doc, BuildType.LABEL_LIST, envGroup.getEnvironments(), labelPrinter);
environments.setAttribute("name", "environments");
elem.appendChild(environments);
Element defaults =
createValueElement(doc, BuildType.LABEL_LIST, envGroup.getDefaults(), labelPrinter);
defaults.setAttribute("name", "defaults");
elem.appendChild(defaults);
} else if (target instanceof FakeLoadTarget) {
elem = doc.createElement("source-file");
} else {
throw new IllegalArgumentException(target.toString());
}
elem.setAttribute("name", labelPrinter.toString(target.getLabel()));
String location = FormatUtils.getLocation(target, relativeLocations, displaySourceFileLocation);
if (!queryOptions.xmlLineNumbers) {
int firstColon = location.indexOf(':');
if (firstColon != -1) {
location = location.substring(0, firstColon);
}
}
elem.setAttribute("location", location);
return elem;
}
private static void addPackageGroupsToElement(
Document doc, Element parent, Target target, LabelPrinter labelPrinter) {
for (Label visibilityDependency : target.getVisibility().getDependencyLabels()) {
Element elem = doc.createElement("package-group");
elem.setAttribute("name", labelPrinter.toString(visibilityDependency));
parent.appendChild(elem);
}
for (Label visibilityDeclaration : target.getVisibility().getDeclaredLabels()) {
Element elem = doc.createElement("visibility-label");
elem.setAttribute("name", labelPrinter.toString(visibilityDeclaration));
parent.appendChild(elem);
}
}
private static void addFeaturesToElement(Document doc, Element parent, InputFile inputFile) {
for (String feature : inputFile.getPackage().getFeatures()) {
Element elem = doc.createElement("feature");
elem.setAttribute("name", feature);
parent.appendChild(elem);
}
}
private void addStarlarkFilesToElement(
Document doc, Element parent, InputFile inputFile, LabelPrinter labelPrinter)
throws InterruptedException {
Iterable<Label> dependencies =
aspectResolver.computeBuildFileDependencies(inputFile.getPackage());
for (Label starlarkFileDep : dependencies) {
Element elem = doc.createElement("load");
elem.setAttribute("name", labelPrinter.toString(starlarkFileDep));
parent.appendChild(elem);
}
}
/**
* Creates and returns a new DOM tree for the specified attribute values. For non-configurable
* attributes, this is a single value. For configurable attributes, this contains one value for
* each configuration. (Only toplevel values are named attributes; list elements are unnamed.)
*
* <p>In the case of configurable attributes, multi-value attributes (e.g. lists) merge all
* configured lists into an aggregate flattened list. Single-value attributes simply refrain to
* set a value and annotate the DOM element as configurable.
*
* <p>(The ungainly qualified class name is required to avoid ambiguity with
* OutputFormatter.OutputType.)
*/
private static Element createValueElement(
Document doc, Type<?> type, Iterable<Object> values, LabelPrinter labelPrinter) {
// "Import static" with method scope:
Type<?> LABEL_LIST = BuildType.LABEL_LIST;
Type<?> LICENSE = BuildType.LICENSE;
Type<?> STRING_LIST = Type.STRING_LIST;
final Element elem;
final boolean hasMultipleValues = Iterables.size(values) > 1;
Type<?> elemType = type.getListElementType();
if (elemType != null) { // it's a list (includes "distribs")
elem = doc.createElement("list");
for (Object value : values) {
for (Object elemValue : (Collection<?>) value) {
elem.appendChild(createValueElement(doc, elemType, elemValue, labelPrinter));
}
}
} else if (type instanceof Type.DictType) {
Set<Object> visitedValues = new HashSet<>();
elem = doc.createElement("dict");
Type.DictType<?, ?> dictType = (Type.DictType<?, ?>) type;
for (Object value : values) {
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
if (visitedValues.add(entry.getKey())) {
Element pairElem = doc.createElement("pair");
elem.appendChild(pairElem);
pairElem.appendChild(
createValueElement(doc, dictType.getKeyType(), entry.getKey(), labelPrinter));
pairElem.appendChild(
createValueElement(doc, dictType.getValueType(), entry.getValue(), labelPrinter));
}
}
}
} else if (type == LICENSE) {
elem = createSingleValueElement(doc, "license", hasMultipleValues);
if (!hasMultipleValues) {
License license = (License) Iterables.getOnlyElement(values);
Element exceptions =
createValueElement(doc, LABEL_LIST, license.getExceptions(), labelPrinter);
exceptions.setAttribute("name", "exceptions");
elem.appendChild(exceptions);
Element licenseTypes =
createValueElement(doc, STRING_LIST, license.getLicenseTypes(), labelPrinter);
licenseTypes.setAttribute("name", "license-types");
elem.appendChild(licenseTypes);
}
} else { // INTEGER STRING LABEL DISTRIBUTION OUTPUT
elem = createSingleValueElement(doc, type.toString(), hasMultipleValues);
if (!hasMultipleValues && !Iterables.isEmpty(values)) {
Object value = Iterables.getOnlyElement(values);
// Values such as those of attribute "linkstamp" may be null.
if (value != null) {
try {
if (value instanceof Label) {
elem.setAttribute("value", labelPrinter.toString((Label) value));
} else {
elem.setAttribute("value", value.toString());
}
} catch (DOMException e) {
elem.setAttribute("value", "[[[ERROR: could not be encoded as XML]]]");
}
}
}
}
return elem;
}
private static Element createValueElement(
Document doc, Type<?> type, Object value, LabelPrinter labelPrinter) {
return createValueElement(doc, type, ImmutableList.of(value), labelPrinter);
}
/**
* Creates the given DOM element, adding <code>configurable="yes"</code> if it represents a
* configurable single-value attribute (configurable list attributes simply have their lists
* merged into an aggregate flat list).
*/
private static Element createSingleValueElement(Document doc, String name, boolean configurable) {
Element elem = doc.createElement(name);
if (configurable) {
elem.setAttribute("configurable", "yes");
}
return elem;
}
}