blob: 83440c4ac1d79e3c92d9c3ddcfb24ac0c2131b00 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* https://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 org.apache.ivy.plugins.parser.xml;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ExtendsDescriptor;
import org.apache.ivy.core.module.descriptor.InheritableItem;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.plugins.namespace.NameSpaceHelper;
import org.apache.ivy.plugins.namespace.Namespace;
import org.apache.ivy.plugins.parser.ParserSettings;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.repository.file.FileResource;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.util.Checks;
import org.apache.ivy.util.DateUtil;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.XMLHelper;
import org.apache.ivy.util.extendable.ExtendableItemHelper;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;
import static org.apache.ivy.util.StringUtils.isNullOrEmpty;
import static org.apache.ivy.util.StringUtils.joinArray;
import static org.apache.ivy.util.StringUtils.splitToArray;
/**
* Used to update ivy files. Uses ivy file as source and not ModuleDescriptor to preserve as much as
* possible the original syntax
*/
public final class XmlModuleDescriptorUpdater {
// CheckStyle:StaticVariableName| OFF
// LINE_SEPARATOR is actually a constant, but we have to modify it for the tests
public static String LINE_SEPARATOR = System.lineSeparator();
// CheckStyle:StaticVariableName| ON
private XmlModuleDescriptorUpdater() {
}
/**
* used to copy a module descriptor xml file (also known as ivy file) and update the revisions
* of its dependencies, its status and revision
*
* @param srcURL
* the url of the source module descriptor file
* @param destFile
* The file to which the updated module descriptor should be output
* @param options
* UpdateOptions
* @throws IOException if something goes wrong
* @throws SAXException if something goes wrong
*/
public static void update(URL srcURL, File destFile, UpdateOptions options) throws IOException,
SAXException {
if (destFile.getParentFile() != null) {
destFile.getParentFile().mkdirs();
}
OutputStream destStream = new FileOutputStream(destFile);
try {
update(srcURL, destStream, options);
} finally {
try {
destStream.close();
} catch (IOException e) {
Message.warn("failed to close a stream : " + e.toString());
}
}
}
public static void update(URL srcURL, OutputStream destFile, UpdateOptions options)
throws IOException, SAXException {
InputStream in = srcURL.openStream();
try {
update(srcURL, in, destFile, options);
} finally {
try {
in.close();
} catch (IOException e) {
Message.warn("failed to close a stream : " + e.toString());
}
try {
destFile.close();
} catch (IOException e) {
Message.warn("failed to close a stream : " + e.toString());
}
}
}
public static void update(InputStream in, Resource res, File destFile, UpdateOptions options)
throws IOException, SAXException {
if (destFile.getParentFile() != null) {
destFile.getParentFile().mkdirs();
}
OutputStream fos = new FileOutputStream(destFile);
try {
// TODO: use resource as input stream context?
URL inputStreamContext = null;
if (res instanceof URLResource) {
inputStreamContext = ((URLResource) res).getURL();
} else if (res instanceof FileResource) {
inputStreamContext = ((FileResource) res).getFile().toURI().toURL();
}
update(inputStreamContext, in, fos, options);
} finally {
try {
in.close();
} catch (IOException e) {
Message.warn("failed to close a stream : " + e.toString());
}
try {
fos.close();
} catch (IOException e) {
Message.warn("failed to close a stream : " + e.toString());
}
}
}
private static class UpdaterHandler extends DefaultHandler implements LexicalHandler {
/** standard attributes of ivy-module/info */
private static final Collection<String> STD_ATTS = Arrays.asList("organisation",
"module", "branch", "revision", "status", "publication", "namespace");
/** elements that may appear inside ivy-module, in expected order */
private static final List<String> MODULE_ELEMENTS = Arrays.asList("info",
"configurations", "publications", "dependencies", "conflicts");
/** element position of "configurations" inside "ivy-module" */
private static final int CONFIGURATIONS_POSITION = MODULE_ELEMENTS
.indexOf("configurations");
/** element position of "dependencies" inside "ivy-module" */
private static final int DEPENDENCIES_POSITION = MODULE_ELEMENTS.indexOf("dependencies");
/** elements that may appear inside of ivy-module/info */
private static final Collection<String> INFO_ELEMENTS = Arrays.asList("extends",
"ivyauthor", "license", "repository", "description");
private final ParserSettings settings;
private final PrintWriter out;
private final Map<ModuleRevisionId, String> resolvedRevisions;
private final Map<ModuleRevisionId, String> resolvedBranches;
private final String status;
private final String revision;
private final Date pubdate;
private final Namespace ns;
private final boolean replaceInclude;
private final boolean generateRevConstraint;
private boolean inHeader = true;
private final List<String> confs;
private final URL relativePathCtx;
private final UpdateOptions options;
public UpdaterHandler(URL relativePathCtx, PrintWriter out, final UpdateOptions options) {
this.options = options;
this.settings = options.getSettings();
this.out = out;
this.resolvedRevisions = options.getResolvedRevisions();
this.resolvedBranches = options.getResolvedBranches();
this.status = options.getStatus();
this.revision = options.getRevision();
this.pubdate = options.getPubdate();
this.ns = options.getNamespace();
this.replaceInclude = options.isReplaceInclude();
this.generateRevConstraint = options.isGenerateRevConstraint();
this.relativePathCtx = relativePathCtx;
if (options.getConfsToExclude() != null) {
this.confs = Arrays.asList(options.getConfsToExclude());
} else {
this.confs = Collections.emptyList();
}
}
// never print *ln* cause \n is found in copied characters stream
// nor do we need do handle indentation, original one is maintained except for attributes
private String organisation = null;
// defaultConf of imported configurations, if any
private String defaultConf = null;
// defaultConfMapping of imported configurations, if any
private String defaultConfMapping = null;
// confMappingOverride of imported configurations, if any
private Boolean confMappingOverride = null;
// used to know if the last open tag was empty, to adjust termination
// with /> instead of ></qName>
private String justOpen = null;
// track the size of the left indent, so that inserted elements are formatted
// like nearby elements.
// true when we're reading indent whitespace
private boolean indenting;
private StringBuilder currentIndent = new StringBuilder();
private List<String> indentLevels = new ArrayList<>(); // ArrayList<String>
// true if an ivy-module/info/description element has been found in the published descriptor
private boolean hasDescription = false;
// true if merged configurations have been written
private boolean mergedConfigurations = false;
// true if merged deps have been written
private boolean mergedDependencies = false;
// the new value of the defaultconf attribute on the publications tag
private String newDefaultConf = null;
private Stack<String> context = new Stack<>();
private Stack<ExtendedBuffer> buffers = new Stack<>();
private Stack<ExtendedBuffer> confAttributeBuffers = new Stack<>();
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
inHeader = false;
endIndent();
if (justOpen != null) {
write(">");
}
flushMergedElementsBefore(qName);
// according to ivy.xsd, all <dependency> elements must occur before
// the <exclude>, <override> or <conflict> elements
if (options.isMerge()
&& ("exclude".equals(localName) || "override".equals(localName) || "conflict"
.equals(localName)) && "ivy-module/dependencies".equals(getContext())) {
ModuleDescriptor merged = options.getMergedDescriptor();
writeInheritedDependencies(merged);
out.println();
out.print(getIndent());
}
context.push(qName);
String path = getContext();
if ("info".equals(qName)) {
infoStarted(attributes);
} else if (replaceInclude && "include".equals(qName)
&& context.contains("configurations")) {
// TODO, in the case of !replaceInclude, we should still replace the relative path
// by an absolute path.
includeStarted(attributes);
} else if ("ivy-module/info/extends".equals(path)) {
if (options.isMerge()) {
ModuleDescriptor mergedDescriptor = options.getMergedDescriptor();
for (ExtendsDescriptor inheritedDescriptor : mergedDescriptor.getInheritedDescriptors()) {
ModuleDescriptor rprid = inheritedDescriptor.getParentMd();
if (rprid instanceof DefaultModuleDescriptor) {
DefaultModuleDescriptor defaultModuleDescriptor = (DefaultModuleDescriptor) rprid;
if (defaultModuleDescriptor.getDefaultConf() != null) {
defaultConf = defaultModuleDescriptor.getDefaultConf();
}
if (defaultModuleDescriptor.getDefaultConfMapping() != null) {
defaultConfMapping = defaultModuleDescriptor.getDefaultConfMapping();
}
if (defaultModuleDescriptor.isMappingOverride()) {
confMappingOverride = Boolean.TRUE;
}
}
}
}
startExtends(attributes);
} else if ("ivy-module/dependencies/dependency".equals(path)) {
startElementInDependency(attributes);
} else if ("ivy-module/configurations/conf".equals(path)) {
startElementInConfigurationsConf(qName, attributes);
} else if ("dependencies".equals(qName) || "configurations".equals(qName)) {
startElementWithConfAttributes(qName, attributes);
} else if ("ivy-module/publications/artifact/conf".equals(path)
|| "ivy-module/dependencies/dependency/conf".equals(path)
|| "ivy-module/dependencies/dependency/artifact/conf".equals(path)) {
buffers.push(new ExtendedBuffer(getContext()));
confAttributeBuffers.peek().setDefaultPrint(false);
String confName = substitute(settings, attributes.getValue("name"));
if (!confs.contains(confName)) {
confAttributeBuffers.peek().setPrint(true);
buffers.peek().setPrint(true);
write("<" + qName);
for (int i = 0; i < attributes.getLength(); i++) {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
} else if ("ivy-module/publications/artifact".equals(path)) {
ExtendedBuffer buffer = new ExtendedBuffer(getContext());
buffers.push(buffer);
confAttributeBuffers.push(buffer);
write("<" + qName);
buffer.setDefaultPrint(attributes.getValue("conf") == null
&& (newDefaultConf == null || !newDefaultConf.isEmpty()));
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("conf".equals(attName)) {
String confName = substitute(settings, attributes.getValue("conf"));
String newConf = removeConfigurationsFromList(confName);
if (!newConf.isEmpty()) {
write(" " + attName + "=\"" + newConf + "\"");
buffers.peek().setPrint(true);
}
} else {
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
} else if ("ivy-module/dependencies/dependency/artifact".equals(path)) {
ExtendedBuffer buffer = new ExtendedBuffer(getContext());
buffers.push(buffer);
confAttributeBuffers.push(buffer);
write("<" + qName);
buffer.setDefaultPrint(attributes.getValue("conf") == null);
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("conf".equals(attName)) {
String confName = substitute(settings, attributes.getValue("conf"));
String newConf = removeConfigurationsFromList(confName);
if (!newConf.isEmpty()) {
write(" " + attName + "=\"" + newConf + "\"");
buffers.peek().setPrint(true);
}
} else {
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
} else if ("ivy-module/publications".equals(path)) {
startPublications(attributes);
} else {
if (options.isMerge() && path.startsWith("ivy-module/info")) {
ModuleDescriptor merged = options.getMergedDescriptor();
if (path.equals("ivy-module/info/description")) {
// if the descriptor already contains a description, don't bother printing
// the merged version.
hasDescription = true;
} else if (!INFO_ELEMENTS.contains(qName)) {
// according to the XSD, we should write description after all of the other
// standard <info> elements but before any extended elements.
writeInheritedDescription(merged);
}
}
// copy
write("<" + qName);
for (int i = 0; i < attributes.getLength(); i++) {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
justOpen = qName;
// indent.append("\t");
}
private void startExtends(Attributes attributes) {
// in merge mode, comment out extends element
if (options.isMerge()) {
write("<!-- ");
}
write("<extends");
String org = substitute(settings, attributes.getValue("organisation"));
String module = substitute(settings, attributes.getValue("module"));
ModuleId parentId = new ModuleId(org, module);
for (int i = 0; i < attributes.getLength(); i++) {
String name = attributes.getQName(i);
String value = null;
switch (name) {
case "organisation":
value = org;
break;
case "module":
value = module;
break;
case "revision":
// replace inline revision with resolved parent revision
ModuleDescriptor merged = options.getMergedDescriptor();
if (merged != null) {
for (ExtendsDescriptor parent : merged.getInheritedDescriptors()) {
ModuleRevisionId resolvedId = parent.getResolvedParentRevisionId();
if (parentId.equals(resolvedId.getModuleId())) {
value = resolvedId.getRevision();
if (value != null) {
break;
}
}
}
}
if (value == null) {
value = substitute(settings, attributes.getValue(i));
}
break;
default:
value = substitute(settings, attributes.getValue(i));
break;
}
write(" " + name + "=\"" + value + "\"");
}
}
private void startElementInConfigurationsConf(String qName, Attributes attributes) {
buffers.push(new ExtendedBuffer(getContext()));
String confName = substitute(settings, attributes.getValue("name"));
if (!confs.contains(confName)) {
buffers.peek().setPrint(true);
String extend = substitute(settings, attributes.getValue("extends"));
if (extend != null) {
for (String tok : splitToArray(extend)) {
if (confs.contains(tok)) {
throw new IllegalArgumentException(
"Cannot exclude a configuration which is extended.");
}
}
}
write("<" + qName);
for (int i = 0; i < attributes.getLength(); i++) {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
}
private void startElementWithConfAttributes(String qName, Attributes attributes) {
// copy
write("<" + qName);
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("defaultconf".equals(attName) || "defaultconfmapping".equals(attName)) {
String newMapping = removeConfigurationsFromMapping(
substitute(settings, attributes.getValue(attName)));
if (!newMapping.isEmpty()) {
write(" " + attName + "=\"" + newMapping + "\"");
}
} else {
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
// add default conf if needed
if (defaultConf != null && attributes.getValue("defaultconf") == null) {
String newConf = removeConfigurationsFromMapping(defaultConf);
if (!newConf.isEmpty()) {
write(" defaultconf=\"" + newConf + "\"");
}
}
// add default conf mapping if needed
if (defaultConfMapping != null && attributes.getValue("defaultconfmapping") == null) {
String newMapping = removeConfigurationsFromMapping(defaultConfMapping);
if (!newMapping.isEmpty()) {
write(" defaultconfmapping=\"" + newMapping + "\"");
}
}
// add confmappingoverride if needed
if (confMappingOverride != null && attributes.getValue("confmappingoverride") == null) {
write(" confmappingoverride=\"" + confMappingOverride.toString() + "\"");
}
}
private void startPublications(Attributes attributes) {
write("<publications");
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("defaultconf".equals(attName)) {
newDefaultConf = removeConfigurationsFromList(
substitute(settings, attributes.getValue("defaultconf")));
if (!newDefaultConf.isEmpty()) {
write(" " + attName + "=\"" + newDefaultConf + "\"");
}
} else {
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
}
private void startElementInDependency(Attributes attributes) {
ExtendedBuffer buffer = new ExtendedBuffer(getContext());
buffers.push(buffer);
confAttributeBuffers.push(buffer);
buffer.setDefaultPrint(isNullOrEmpty(attributes.getValue("conf")));
write("<dependency");
String org = substitute(settings, attributes.getValue("org"));
if (org == null) {
org = organisation;
}
String module = substitute(settings, attributes.getValue("name"));
String branch = substitute(settings, attributes.getValue("branch"));
String branchConstraint = substitute(settings, attributes.getValue("branchConstraint"));
if (branchConstraint == null) {
branchConstraint = branch;
}
// look for the branch used in resolved revisions
if (branch == null) {
ModuleId mid = ModuleId.newInstance(org, module);
if (ns != null) {
mid = NameSpaceHelper.transform(mid, ns.getToSystemTransformer());
}
for (ModuleRevisionId mrid : resolvedRevisions.keySet()) {
if (mrid.getModuleId().equals(mid)) {
branch = mrid.getBranch();
break;
}
}
}
String revision = substitute(settings, attributes.getValue("rev"));
String revisionConstraint = substitute(settings, attributes.getValue("revConstraint"));
Map<String, String> extraAttributes = ExtendableItemHelper.getExtraAttributes(settings, attributes,
XmlModuleDescriptorParser.DEPENDENCY_REGULAR_ATTRIBUTES);
ModuleRevisionId localMrid = ModuleRevisionId.newInstance(org, module, branch,
revision, extraAttributes);
ModuleRevisionId systemMrid = (ns == null) ? localMrid : ns.getToSystemTransformer()
.transform(localMrid);
String newBranch = resolvedBranches.get(systemMrid);
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
switch (attName) {
case "org":
write(" org=\"" + systemMrid.getOrganisation() + "\"");
break;
case "name":
write(" name=\"" + systemMrid.getName() + "\"");
break;
case "rev":
String rev = resolvedRevisions.get(systemMrid);
if (rev == null) {
write(" rev=\"" + systemMrid.getRevision() + "\"");
} else {
write(" rev=\"" + rev + "\"");
if (attributes.getIndex("branchConstraint") == -1
&& branchConstraint != null) {
write(" branchConstraint=\"" + branchConstraint + "\"");
}
if (generateRevConstraint && attributes.getIndex("revConstraint") == -1
&& !rev.equals(systemMrid.getRevision())) {
write(" revConstraint=\"" + systemMrid.getRevision() + "\"");
}
}
break;
case "revConstraint":
write(" revConstraint=\"" + revisionConstraint + "\"");
break;
case "branch":
if (newBranch != null) {
write(" branch=\"" + newBranch + "\"");
} else if (!resolvedBranches.containsKey(systemMrid)) {
write(" branch=\"" + systemMrid.getBranch() + "\"");
} else {
// if resolvedBranches contains the systemMrid, but the new branch is null,
// the branch attribute will be removed altogether
}
break;
case "branchConstraint":
write(" branchConstraint=\"" + branchConstraint + "\"");
break;
case "conf":
String oldMapping = substitute(settings, attributes.getValue("conf"));
if (!oldMapping.isEmpty()) {
String newMapping = removeConfigurationsFromMapping(oldMapping);
if (!newMapping.isEmpty()) {
write(" conf=\"" + newMapping + "\"");
buffers.peek().setPrint(true);
}
}
break;
default:
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(attName)) + "\"");
break;
}
}
if (attributes.getIndex("branch") == -1) {
// erase an existing branch attribute if its new value is blank
if (!isNullOrEmpty(newBranch)) {
write(" branch=\"" + newBranch + "\"");
} else if (options.isUpdateBranch() && systemMrid.getBranch() != null) {
// this dependency is on a specific branch, we set it explicitly in the updated
// file
write(" branch=\"" + systemMrid.getBranch() + "\"");
}
}
}
private void includeStarted(Attributes attributes) throws SAXException {
final ExtendedBuffer buffer = new ExtendedBuffer(getContext());
buffers.push(buffer);
try {
URL url;
if (settings == null) {
// TODO : settings can be null, but I don't why.
// Check if the following code is correct in that case
String fileName = attributes.getValue("file");
if (fileName == null) {
String urlStr = attributes.getValue("url");
url = new URL(urlStr);
} else {
url = Checks.checkAbsolute(fileName, "settings.include").toURI().toURL();
}
} else {
url = settings.getRelativeUrlResolver().getURL(relativePathCtx,
settings.substitute(attributes.getValue("file")),
settings.substitute(attributes.getValue("url")));
}
XMLHelper.parse(url, null, new DefaultHandler() {
private boolean insideConfigurations = false;
private boolean doIndent = false;
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
if ("configurations".equals(qName)) {
insideConfigurations = true;
String defaultconf = substitute(settings,
attributes.getValue("defaultconf"));
if (defaultconf != null) {
defaultConf = defaultconf;
}
String defaultMapping = substitute(settings,
attributes.getValue("defaultconfmapping"));
if (defaultMapping != null) {
defaultConfMapping = defaultMapping;
}
String mappingOverride = substitute(settings,
attributes.getValue("confmappingoverride"));
if (mappingOverride != null) {
confMappingOverride = Boolean.valueOf(mappingOverride);
}
} else if ("conf".equals(qName) && insideConfigurations) {
String confName = substitute(settings, attributes.getValue("name"));
if (!confs.contains(confName)) {
buffer.setPrint(true);
if (doIndent) {
write("/>\n\t\t");
}
String extend = substitute(settings, attributes.getValue("extends"));
if (extend != null) {
for (String tok : splitToArray(extend)) {
if (confs.contains(tok)) {
throw new IllegalArgumentException(
"Cannot exclude a configuration which is extended.");
}
}
}
write("<" + qName);
for (int i = 0; i < attributes.getLength(); i++) {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
doIndent = true;
}
}
}
public void endElement(String uri, String localName, String name)
throws SAXException {
if ("configurations".equals(name)) {
insideConfigurations = false;
}
}
});
} catch (Exception e) {
Message.warn("exception occurred while importing configurations: " + e.getMessage());
throw new SAXException(e);
}
}
private void infoStarted(Attributes attributes) {
String module = substitute(settings, attributes.getValue("module"));
String rev = null;
String branch = null;
String status = null;
String namespace = null;
Map<String, String> extraAttributes = null;
if (options.isMerge()) {
// get attributes from merged descriptor, ignoring raw XML
ModuleDescriptor merged = options.getMergedDescriptor();
ModuleRevisionId mergedMrid = merged.getModuleRevisionId();
organisation = mergedMrid.getOrganisation();
branch = mergedMrid.getBranch();
rev = mergedMrid.getRevision();
status = merged.getStatus();
// TODO: should namespace be added to ModuleDescriptor interface, so we don't
// have to do this kind of check?
if (merged instanceof DefaultModuleDescriptor) {
Namespace ns = ((DefaultModuleDescriptor) merged).getNamespace();
if (ns != null) {
namespace = ns.getName();
}
}
if (namespace == null) {
namespace = attributes.getValue("namespace");
}
extraAttributes = merged.getQualifiedExtraAttributes();
} else {
// get attributes from raw XML, performing property substitution
organisation = substitute(settings, attributes.getValue("organisation"));
rev = substitute(settings, attributes.getValue("revision"));
branch = substitute(settings, attributes.getValue("branch"));
status = substitute(settings, attributes.getValue("status"));
namespace = substitute(settings, attributes.getValue("namespace"));
extraAttributes = new LinkedHashMap<>(attributes.getLength());
for (int i = 0; i < attributes.getLength(); i++) {
String qname = attributes.getQName(i);
if (!STD_ATTS.contains(qname)) {
extraAttributes.put(qname, substitute(settings, attributes.getValue(i)));
}
}
}
// apply override values provided in options
if (revision != null) {
rev = revision;
}
if (options.getBranch() != null) {
branch = options.getBranch();
}
if (this.status != null) {
status = this.status;
}
// if necessary translate mrid using optional namespace argument
ModuleRevisionId localMid = ModuleRevisionId.newInstance(organisation, module, branch,
rev, ExtendableItemHelper.getExtraAttributes(settings, attributes,
Arrays.asList("organisation", "module", "revision", "status",
"publication", "namespace")));
ModuleRevisionId systemMid = (ns == null) ? localMid : ns.getToSystemTransformer()
.transform(localMid);
write("<info");
if (organisation != null) {
write(" organisation=\"" + XMLHelper.escape(systemMid.getOrganisation()) + "\"");
}
write(" module=\"" + XMLHelper.escape(systemMid.getName()) + "\"");
if (branch != null) {
write(" branch=\"" + XMLHelper.escape(systemMid.getBranch()) + "\"");
}
if (systemMid.getRevision() != null) {
write(" revision=\"" + XMLHelper.escape(systemMid.getRevision()) + "\"");
}
write(" status=\"" + XMLHelper.escape(status) + "\"");
if (pubdate != null) {
write(" publication=\"" + DateUtil.format(pubdate) + "\"");
} else if (attributes.getValue("publication") != null) {
write(" publication=\"" + substitute(settings, attributes.getValue("publication"))
+ "\"");
}
if (namespace != null) {
write(" namespace=\"" + namespace + "\"");
}
for (Map.Entry<String, String> extra : extraAttributes.entrySet()) {
write(" " + extra.getKey() + "=\"" + extra.getValue() + "\"");
}
}
private void write(String content) {
getWriter().print(content);
}
private PrintWriter getWriter() {
return buffers.isEmpty() ? out : buffers.peek().getWriter();
}
private String getContext() {
return joinArray(context.toArray(new String[context.size()]), "/");
}
private String substitute(ParserSettings ivy, String value) {
String result = (ivy == null) ? value : ivy.substitute(value);
return XMLHelper.escape(result);
}
private String removeConfigurationsFromMapping(String mapping) {
StringBuilder newMapping = new StringBuilder();
String mappingSep = "";
for (String groups : mapping.trim().split("\\s*;\\s*")) {
String[] ops = groups.split("->");
List<String> confsToWrite = new ArrayList<>();
for (String lh : splitToArray(ops[0])) {
if (!confs.contains(lh)) {
confsToWrite.add(lh);
}
}
if (!confsToWrite.isEmpty()) {
newMapping.append(mappingSep);
String sep = "";
String listSep = groups.contains(", ") ? ", " : ",";
for (String confToWrite : confsToWrite) {
newMapping.append(sep).append(confToWrite);
sep = listSep;
}
if (ops.length == 2) {
newMapping.append("->").append(joinArray(splitToArray(ops[1]), sep));
}
mappingSep = ";";
}
}
return newMapping.toString();
}
private String removeConfigurationsFromList(String list) {
StringBuilder newList = new StringBuilder();
String sep = "";
String listSep = list.contains(", ") ? ", " : ",";
for (String current : splitToArray(list)) {
if (!confs.contains(current)) {
newList.append(sep).append(current);
sep = listSep;
}
}
return newList.toString();
}
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
characters(ch, start, length);
}
public void characters(char[] ch, int start, int length) throws SAXException {
if (justOpen != null) {
write(">");
justOpen = null;
}
write(XMLHelper.escape(String.valueOf(ch, start, length)));
// examine characters for current indent level, keeping in mind
// that our indent might be split across multiple calls to characters()
for (int i = start, end = start + length; i < end; ++i) {
char c = ch[i];
if (c == '\r' || c == '\n') {
// newline resets the indent level
currentIndent.setLength(0);
indenting = true;
} else if (indenting) {
// indent continues until first non-whitespace character
if (Character.isWhitespace(c)) {
currentIndent.append(c);
} else {
endIndent();
}
}
}
}
/** record the current indent level for future elements that appear at the same depth */
private void endIndent() {
if (indenting) {
// record the indent at this level. if we insert any elements at
// this level, we'll use the same indent.
setIndent(context.size() - 1, currentIndent.toString());
indenting = false;
}
}
/**
* Set the indent for the given depth. Indents less than the provided depth will be
* calculated automatically, if they have not already been defined.
*/
private void setIndent(int level, String indent) {
fillIndents(level);
indentLevels.set(level, indent);
}
/**
* Guarantee that indent levels have been calculated up to and including the given depth
* (starting at 0).
*/
private void fillIndents(int level) {
if (indentLevels.isEmpty()) {
// add a default single-level indent until we see indents in the document
indentLevels.add(" ");
}
String oneLevel = indentLevels.get(0);
for (int fill = indentLevels.size(); fill <= level; ++fill) {
indentLevels.add(indentLevels.get(fill - 1) + oneLevel);
}
}
/**
* get the whitespace that should precede new elements at the current depth in the document
*/
private String getIndent() {
int level = context.size() - 1;
fillIndents(level);
return indentLevels.get(level);
}
/**
* Write XML elements that do not appear in the source descriptor, but have been copied in
* from a parent module descriptor via &lt;extends&gt; declaration.
*
* @param merged
* child descriptor containing the merged data
* @param items
* the list of inherited items to print
* @param printer
* a printer that knows how to write the given type of item
* @param itemName
* the name of the container element, e.g. "configurations"
* @param includeContainer
* if true, include an enclosing element named <code>itemName</code>. Otherwise
* just write the inherited items inline, with a comment indicating where they
* came from.
*/
private void writeInheritedItems(ModuleDescriptor merged, InheritableItem[] items,
ItemPrinter printer, String itemName, boolean includeContainer) {
// first categorize inherited items by their source module, so that
// we can add some useful comments
PrintWriter out = getWriter();
Map<ModuleRevisionId, List<InheritableItem>> inheritedItems = collateInheritedItems(
merged, items);
boolean hasItems = !inheritedItems.isEmpty();
if (hasItems && includeContainer) {
if (currentIndent.length() == 0) {
out.print(getIndent());
}
String newConf = (defaultConf == null) ? ""
: removeConfigurationsFromMapping(defaultConf);
String newMapping = (defaultConfMapping == null) ? ""
: removeConfigurationsFromMapping(defaultConfMapping);
out.print(String.format("<%s%s%s%s>", itemName,
newConf.isEmpty() ? "" : " defaultconf=\"" + newConf + "\"",
newMapping.isEmpty() ? "" : " defaultconfmapping=\"" + newMapping + "\"",
(confMappingOverride != null) ? " confmappingoverride=\"" + confMappingOverride + "\"" : ""));
context.push(itemName);
justOpen = null;
}
for (Map.Entry<ModuleRevisionId, List<InheritableItem>> entry : inheritedItems
.entrySet()) {
if (justOpen != null) {
out.println(">");
justOpen = null; // helps endElement() decide how to write close tags
}
writeInheritanceComment(itemName, entry.getKey());
for (InheritableItem item : entry.getValue()) {
out.print(getIndent());
printer.print(merged, item, out);
}
}
if (hasItems) {
if (includeContainer) {
context.pop();
out.println(getIndent() + "</" + itemName + ">");
out.println();
}
// restore the prior indent
out.print(currentIndent);
}
}
private void writeInheritanceComment(String itemDescription, Object parentInfo) {
PrintWriter out = getWriter();
out.println();
out.println(getIndent() + "<!-- " + itemDescription + " inherited from " + parentInfo
+ " -->");
}
/**
* Collect the given list of inherited descriptor items into lists keyed by parent Id. Thus
* all of the items inherited from parent A can be written together, then all of the items
* from parent B, and so on.
*
* @param merged
* the merged child descriptor
* @param items
* the inherited items to collate
* @return maps parent ModuleRevisionId to a List of InheritedItems imported from that
* parent
*/
private Map<ModuleRevisionId, List<InheritableItem>> collateInheritedItems(
ModuleDescriptor merged, InheritableItem[] items) {
Map<ModuleRevisionId, List<InheritableItem>> inheritedItems = new LinkedHashMap<>();
for (InheritableItem item : items) {
ModuleRevisionId source = item.getSourceModule();
// ignore items that are defined directly in the child descriptor
if (source != null
&& !source.getModuleId().equals(merged.getModuleRevisionId().getModuleId())) {
List<InheritableItem> accum = inheritedItems.get(source);
if (accum == null) {
accum = new ArrayList<>();
inheritedItems.put(source, accum);
}
accum.add(item);
}
}
return inheritedItems;
}
/**
* If no info/description element has yet been written, write the description inherited from
* the parent descriptor, if any. Calling this method more than once has no affect.
*/
private void writeInheritedDescription(ModuleDescriptor merged) {
if (!hasDescription) {
hasDescription = true;
String description = merged.getDescription();
if (!isNullOrEmpty(description)) {
PrintWriter writer = getWriter();
if (justOpen != null) {
writer.println(">");
}
writeInheritanceComment("description", "parent");
writer.println(getIndent() + "<description>" + XMLHelper.escape(description)
+ "</description>");
// restore the indent that existed before we wrote the extra elements
writer.print(currentIndent);
justOpen = null;
}
}
}
private void writeInheritedConfigurations(ModuleDescriptor merged) {
if (!mergedConfigurations) {
mergedConfigurations = true;
writeInheritedItems(merged, merged.getConfigurations(),
ConfigurationPrinter.INSTANCE, "configurations", false);
}
}
private void writeInheritedDependencies(ModuleDescriptor merged) {
if (!mergedDependencies) {
mergedDependencies = true;
writeInheritedItems(merged, merged.getDependencies(), DependencyPrinter.INSTANCE,
"dependencies", false);
}
}
/**
* <p>
* If publishing in merge mode, guarantee that any merged elements appearing before
* <code>moduleElement</code> have been written. This method should be called <i>before</i>
* we write the start tag of <code>moduleElement</code>. This covers cases where merged
* elements like "configurations" and "dependencies" appear in the parent descriptor, but
* are completely missing in the child descriptor.
* </p>
*
* <p>
* For example, if "moduleElement" is "dependencies", guarantees that "configurations" has
* been written. If <code>moduleElement</code> is <code>null</code>, then all missing merged
* elements will be flushed.
* </p>
*
* @param moduleElement
* a descriptor element name, for example "configurations" or "info"
*/
private void flushMergedElementsBefore(String moduleElement) {
if (options.isMerge() && context.size() == 1 && "ivy-module".equals(context.peek())
&& !(mergedConfigurations && mergedDependencies)) {
// calculate the position of the element in ivy-module
int position = (moduleElement == null) ? MODULE_ELEMENTS.size()
: MODULE_ELEMENTS.indexOf(moduleElement);
ModuleDescriptor merged = options.getMergedDescriptor();
// see if we should write <configurations>
if (!mergedConfigurations && position > CONFIGURATIONS_POSITION
&& merged.getConfigurations().length > 0) {
mergedConfigurations = true;
writeInheritedItems(merged, merged.getConfigurations(),
ConfigurationPrinter.INSTANCE, "configurations", true);
}
// see if we should write <dependencies>
if (!mergedDependencies && position > DEPENDENCIES_POSITION
&& merged.getDependencies().length > 0) {
mergedDependencies = true;
writeInheritedItems(merged, merged.getDependencies(),
DependencyPrinter.INSTANCE, "dependencies", true);
}
}
}
private void flushAllMergedElements() {
flushMergedElementsBefore(null);
}
public void endElement(String uri, String localName, String qName) throws SAXException {
String path = getContext();
if (options.isMerge()) {
ModuleDescriptor merged = options.getMergedDescriptor();
switch (path) {
case "ivy-module/info":
// guarantee that inherited description has been written before
// info element closes.
writeInheritedDescription(merged);
break;
case "ivy-module/configurations":
// write inherited configurations after all child configurations
writeInheritedConfigurations(merged);
break;
case "ivy-module/dependencies":
// write inherited dependencies after all child dependencies
writeInheritedDependencies(merged);
break;
case "ivy-module":
// write any remaining inherited data before we close the
// descriptor.
flushAllMergedElements();
break;
}
}
if (qName.equals(justOpen)) {
write("/>");
} else {
write("</" + qName + ">");
}
if (!buffers.isEmpty()) {
ExtendedBuffer buffer = buffers.peek();
if (buffer.getContext().equals(path)) {
buffers.pop();
if (buffer.isPrint()) {
write(buffer.toString());
}
}
}
if (!confAttributeBuffers.isEmpty()) {
ExtendedBuffer buffer = confAttributeBuffers.peek();
if (buffer.getContext().equals(path)) {
confAttributeBuffers.pop();
}
}
// <extends> element is commented out when in merge mode.
if (options.isMerge() && "ivy-module/info/extends".equals(path)) {
write(" -->");
}
justOpen = null;
context.pop();
}
public void endDocument() throws SAXException {
out.print(LINE_SEPARATOR);
out.flush();
out.close();
}
public void processingInstruction(String target, String data) throws SAXException {
write("<?");
write(target);
write(" ");
write(data);
write("?>");
write(LINE_SEPARATOR);
}
public void warning(SAXParseException e) throws SAXException {
throw e;
}
public void error(SAXParseException e) throws SAXException {
throw e;
}
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
public void endCDATA() throws SAXException {
}
public void endDTD() throws SAXException {
}
public void startCDATA() throws SAXException {
}
public void comment(char[] ch, int start, int length) throws SAXException {
if (justOpen != null) {
write(">");
justOpen = null;
}
write("<!--");
write(String.valueOf(ch, start, length));
write("-->");
if (inHeader) {
write(LINE_SEPARATOR);
}
}
public void endEntity(String name) throws SAXException {
}
public void startEntity(String name) throws SAXException {
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
}
}
public static void update(URL inStreamCtx, InputStream inStream, OutputStream outStream,
final UpdateOptions options) throws IOException, SAXException {
final PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8));
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.write(LINE_SEPARATOR);
try {
UpdaterHandler updaterHandler = new UpdaterHandler(inStreamCtx, out, options);
InputSource inSrc = new InputSource(new BufferedInputStream(inStream));
if (inStreamCtx != null) {
inSrc.setSystemId(inStreamCtx.toExternalForm());
}
XMLHelper.parse(inSrc, null, updaterHandler, updaterHandler);
} catch (ParserConfigurationException e) {
throw new IllegalStateException("impossible to update Ivy files: parser problem", e);
}
}
private static class ExtendedBuffer {
private String context = null;
private Boolean print = null;
private boolean defaultPrint = false;
private StringWriter buffer = new StringWriter();
private PrintWriter writer = new PrintWriter(buffer);
ExtendedBuffer(String context) {
this.context = context;
}
boolean isPrint() {
if (print == null) {
return defaultPrint;
}
return print;
}
void setPrint(boolean print) {
this.print = print;
}
void setDefaultPrint(boolean print) {
this.defaultPrint = print;
}
PrintWriter getWriter() {
return writer;
}
String getContext() {
return context;
}
public String toString() {
writer.flush();
return buffer.toString();
}
}
/**
* Prints a descriptor item's XML representation
*/
protected interface ItemPrinter {
/**
* Print an XML representation of <code>item</code> to <code>out</code>.
*
* @param parent
* the module descriptor containing <code>item</code>
* @param item
* subcomponent of the descriptor, for example a {@link DependencyDescriptor} or
* {@link Configuration}
* @param out PrintWriter
*/
void print(ModuleDescriptor parent, Object item, PrintWriter out);
}
protected static class DependencyPrinter implements ItemPrinter {
public static final DependencyPrinter INSTANCE = new DependencyPrinter();
public void print(ModuleDescriptor parent, Object item, PrintWriter out) {
XmlModuleDescriptorWriter.printDependency(parent, (DependencyDescriptor) item, out);
}
}
protected static class ConfigurationPrinter implements ItemPrinter {
public static final ConfigurationPrinter INSTANCE = new ConfigurationPrinter();
public void print(ModuleDescriptor parent, Object item, PrintWriter out) {
XmlModuleDescriptorWriter.printConfiguration((Configuration) item, out);
}
}
}