| // 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.output; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| 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.EnvironmentGroup; |
| import com.google.devtools.build.lib.packages.FilesetEntry; |
| import com.google.devtools.build.lib.packages.InputFile; |
| 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.query2.CommonQueryOptions; |
| import com.google.devtools.build.lib.query2.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.output.OutputFormatter.AbstractUnorderedFormatter; |
| import com.google.devtools.build.lib.syntax.Type; |
| import java.io.IOException; |
| 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 { |
| |
| // AbstractUnorderedFormatter also has an options field it's of type CommonQueryOptions, a |
| // superclass of QueryOptions. Store this here to ensure correct type is passed to this class. |
| 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)); |
| } |
| |
| @Override |
| public void setOptions(CommonQueryOptions options, AspectResolver aspectResolver) { |
| super.setOptions(options, aspectResolver); |
| |
| Preconditions.checkArgument(options instanceof QueryOptions); |
| this.queryOptions = (QueryOptions) options; |
| } |
| |
| @Override |
| public OutputFormatterCallback<Target> createPostFactoStreamCallback( |
| final OutputStream out, final QueryOptions options) { |
| 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 IOException, InterruptedException { |
| for (Target target : partialResult) { |
| queryElem.appendChild(createTargetElement(doc, target)); |
| } |
| } |
| |
| @Override |
| public void close(boolean failFast) throws IOException { |
| 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. |
| * |
| * XML structure: |
| * - element tag is <source-file>, <generated-file> or <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. |
| * @throws InterruptedException |
| */ |
| private Element createTargetElement(Document doc, Target target) |
| 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()) { |
| PossibleAttributeValues values = getPossibleAttributeValues(rule, attr); |
| if (values.source == AttributeValueSource.RULE || queryOptions.xmlShowDefaultValues) { |
| Element attrElem = createValueElement(doc, attr.getType(), values); |
| 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.getLabels(dependencyFilter)) { |
| Element inputElem = doc.createElement("rule-input"); |
| inputElem.setAttribute("name", label.toString()); |
| elem.appendChild(inputElem); |
| } |
| for (Label label : |
| aspectResolver.computeAspectDependencies(target, dependencyFilter).values()) { |
| Element inputElem = doc.createElement("rule-input"); |
| inputElem.setAttribute("name", label.toString()); |
| elem.appendChild(inputElem); |
| } |
| for (OutputFile outputFile: rule.getOutputFiles()) { |
| Element outputElem = doc.createElement("rule-output"); |
| outputElem.setAttribute("name", outputFile.getLabel().toString()); |
| 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()); |
| includes.setAttribute("name", "includes"); |
| elem.appendChild(includes); |
| Element packages = |
| createValueElement(doc, Type.STRING_LIST, packageGroup.getContainedPackages()); |
| 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", |
| outputFile.getGeneratingRule().getLabel().toString()); |
| } else if (target instanceof InputFile) { |
| elem = doc.createElement("source-file"); |
| InputFile inputFile = (InputFile) target; |
| if (inputFile.getName().equals("BUILD")) { |
| addSkylarkFilesToElement(doc, elem, inputFile); |
| addFeaturesToElement(doc, elem, inputFile); |
| elem.setAttribute("package_contains_errors", |
| String.valueOf(inputFile.getPackage().containsErrors())); |
| } |
| |
| addPackageGroupsToElement(doc, elem, inputFile); |
| } 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()); |
| environments.setAttribute("name", "environments"); |
| elem.appendChild(environments); |
| Element defaults = createValueElement(doc, |
| BuildType.LABEL_LIST, |
| envGroup.getDefaults()); |
| 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", target.getLabel().toString()); |
| String location = getLocation(target, options.relativeLocations); |
| if (!queryOptions.xmlLineNumbers) { |
| int firstColon = location.indexOf(':'); |
| if (firstColon != -1) { |
| location = location.substring(0, firstColon); |
| } |
| } |
| |
| elem.setAttribute("location", location); |
| return elem; |
| } |
| |
| private void addPackageGroupsToElement(Document doc, Element parent, Target target) { |
| for (Label visibilityDependency : target.getVisibility().getDependencyLabels()) { |
| Element elem = doc.createElement("package-group"); |
| elem.setAttribute("name", visibilityDependency.toString()); |
| parent.appendChild(elem); |
| } |
| |
| for (Label visibilityDeclaration : target.getVisibility().getDeclaredLabels()) { |
| Element elem = doc.createElement("visibility-label"); |
| elem.setAttribute("name", visibilityDeclaration.toString()); |
| parent.appendChild(elem); |
| } |
| } |
| |
| private 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 addSkylarkFilesToElement(Document doc, Element parent, InputFile inputFile) |
| throws InterruptedException { |
| Iterable<Label> dependencies = |
| aspectResolver.computeBuildFileDependencies(inputFile.getPackage()); |
| |
| for (Label skylarkFileDep : dependencies) { |
| Element elem = doc.createElement("load"); |
| elem.setAttribute("name", skylarkFileDep.toString()); |
| 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) { |
| // "Import static" with method scope: |
| Type<?> FILESET_ENTRY = BuildType.FILESET_ENTRY; |
| 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)); |
| } |
| } |
| } 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())); |
| pairElem.appendChild(createValueElement(doc, |
| dictType.getValueType(), entry.getValue())); |
| } |
| } |
| } |
| } 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()); |
| exceptions.setAttribute("name", "exceptions"); |
| elem.appendChild(exceptions); |
| |
| Element licenseTypes = createValueElement(doc, STRING_LIST, license.getLicenseTypes()); |
| licenseTypes.setAttribute("name", "license-types"); |
| elem.appendChild(licenseTypes); |
| } |
| } else if (type == FILESET_ENTRY) { |
| // Fileset entries: not configurable. |
| FilesetEntry filesetEntry = (FilesetEntry) Iterables.getOnlyElement(values); |
| elem = doc.createElement("fileset-entry"); |
| elem.setAttribute("srcdir", filesetEntry.getSrcLabel().toString()); |
| elem.setAttribute("destdir", filesetEntry.getDestDir().toString()); |
| elem.setAttribute("symlinks", filesetEntry.getSymlinkBehavior().toString()); |
| elem.setAttribute("strip_prefix", filesetEntry.getStripPrefix()); |
| |
| if (filesetEntry.getExcludes() != null) { |
| Element excludes = |
| createValueElement(doc, LABEL_LIST, filesetEntry.getExcludes()); |
| excludes.setAttribute("name", "excludes"); |
| elem.appendChild(excludes); |
| } |
| if (filesetEntry.getFiles() != null) { |
| Element files = createValueElement(doc, LABEL_LIST, filesetEntry.getFiles()); |
| files.setAttribute("name", "files"); |
| elem.appendChild(files); |
| } |
| } 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 { |
| 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) { |
| return createValueElement(doc, type, ImmutableList.of(value)); |
| } |
| |
| /** |
| * 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; |
| } |
| } |