/*
* 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
*
* 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 org.apache.ivy.plugins.parser.xml;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.ivy.Ivy;
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.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;
/**
* 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 {
private static final int MAX_HEADER_LENGTH = 10000;
//CheckStyle:StaticVariableName| OFF
//LINE_SEPARATOR is actually a constant, but we have to modify it for the tests
public static String LINE_SEPARATOR = System.getProperty("line.separator");
//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
*/
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 STD_ATTS = Arrays.asList(new String[] {"organisation",
"module", "branch", "revision", "status", "publication", "namespace"});
/** elements that may appear inside ivy-module, in expected order */
private static final List MODULE_ELEMENTS = Arrays.asList(new String[] {
"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 INFO_ELEMENTS = Arrays.asList(new String[] {"extends",
"ivyauthor", "license", "repository", "description"});
private final ParserSettings settings;
private final PrintWriter out;
private final Map resolvedRevisions;
private final Map 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 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.EMPTY_LIST;
}
}
// 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;
// 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 StringBuffer currentIndent = new StringBuffer();
private ArrayList 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 context = new Stack();
private Stack buffers = new Stack();
private Stack 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);
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)) {
startExtends(attributes);
} else if ("ivy-module/dependencies/dependency".equals(path)) {
startElementInDependency(attributes);
} else if ("dependencies".equals(qName)) {
startDependencies(attributes);
} else if ("ivy-module/configurations/conf".equals(path)) {
startElementInConfigurationsConf(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()));
((ExtendedBuffer) confAttributeBuffers.peek()).setDefaultPrint(false);
String confName = substitute(settings, attributes.getValue("name"));
if (!confs.contains(confName)) {
((ExtendedBuffer) confAttributeBuffers.peek()).setPrint(true);
((ExtendedBuffer) 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.length() > 0)));
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, confs);
if (newConf.length() > 0) {
write(" " + attributes.getQName(i) + "=\"" + newConf + "\"");
((ExtendedBuffer) buffers.peek()).setPrint(true);
}
} else {
write(" " + attributes.getQName(i) + "=\""
+ 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, confs);
if (newConf.length() > 0) {
write(" " + attributes.getQName(i) + "=\"" + newConf + "\"");
((ExtendedBuffer) buffers.peek()).setPrint(true);
}
} else {
write(" " + attributes.getQName(i) + "=\""
+ 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;
if ("revision".equals(name)) {
//replace inline revision with resolved parent revision
ModuleDescriptor merged = options.getMergedDescriptor();
if (merged != null) {
ExtendsDescriptor[] parents = merged.getInheritedDescriptors();
for (int j = 0; value == null && j < parents.length; ++j) {
ModuleRevisionId resolvedId = parents[j].getResolvedParentRevisionId();
if (parentId.equals(resolvedId.getModuleId())) {
value = resolvedId.getRevision();
}
}
}
if (value == null) {
value = substitute(settings, attributes.getValue(i));
}
} else if ("organisation".equals(name)) {
value = org;
} else if ("module".equals(name)) {
value = module;
} else {
value = substitute(settings, attributes.getValue(i));
}
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)) {
((ExtendedBuffer) buffers.peek()).setPrint(true);
String extend = substitute(settings, attributes.getValue("extends"));
if (extend != null) {
for (StringTokenizer tok = new StringTokenizer(extend, ", "); tok
.hasMoreTokens();) {
String current = tok.nextToken();
if (confs.contains(current)) {
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 startDependencies(Attributes attributes) {
// copy
write("<dependencies");
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("defaultconfmapping".equals(attName)) {
String newMapping = removeConfigurationsFromMapping(substitute(settings,
attributes.getValue("defaultconfmapping")), confs);
if (newMapping.length() > 0) {
write(" " + attributes.getQName(i) + "=\"" + newMapping + "\"");
}
} else {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
// add default conf mapping if needed
if (defaultConfMapping != null
&& attributes.getValue("defaultconfmapping") == null) {
String newMapping = removeConfigurationsFromMapping(defaultConfMapping, confs);
if (newMapping.length() > 0) {
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")), confs);
if (newDefaultConf.length() > 0) {
write(" " + attributes.getQName(i) + "=\"" + newDefaultConf + "\"");
}
} else {
write(" " + attributes.getQName(i) + "=\""
+ substitute(settings, attributes.getValue(i)) + "\"");
}
}
}
private void startElementInDependency(Attributes attributes) {
ExtendedBuffer buffer = new ExtendedBuffer(getContext());
buffers.push(buffer);
confAttributeBuffers.push(buffer);
buffer.setDefaultPrint(attributes.getValue("conf") == null
|| attributes.getValue("conf").trim().length() == 0);
write("<dependency");
String org = substitute(settings, attributes.getValue("org"));
org = org == null ? organisation : org;
String module = substitute(settings, attributes.getValue("name"));
String branch = substitute(settings, attributes.getValue("branch"));
String branchConstraint = substitute(settings, attributes.getValue("branchConstraint"));
branchConstraint = branchConstraint == null ? branch : branchConstraint;
// 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 (Iterator iter = resolvedRevisions.keySet().iterator(); iter.hasNext();) {
ModuleRevisionId mrid = (ModuleRevisionId) iter.next();
if (mrid.getModuleId().equals(mid)) {
branch = mrid.getBranch();
break;
}
}
}
String revision = substitute(settings, attributes.getValue("rev"));
String revisionConstraint = substitute(settings, attributes.getValue("revConstraint"));
Map 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 = (String) resolvedBranches.get(systemMrid);
for (int i = 0; i < attributes.getLength(); i++) {
String attName = attributes.getQName(i);
if ("rev".equals(attName)) {
String rev = (String) resolvedRevisions.get(systemMrid);
if (rev != null) {
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() + "\"");
}
} else {
write(" rev=\"" + systemMrid.getRevision() + "\"");
}
} else if ("revConstraint".equals(attName)) {
write(" revConstraint=\"" + revisionConstraint + "\"");
} else if ("org".equals(attName)) {
write(" org=\"" + systemMrid.getOrganisation() + "\"");
} else if ("name".equals(attName)) {
write(" name=\"" + systemMrid.getName() + "\"");
} else if ("branch".equals(attName)) {
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
}
} else if ("branchConstraint".equals(attName)) {
write(" branchConstraint=\"" + branchConstraint + "\"");
} else if ("conf".equals(attName)) {
String oldMapping = substitute(settings, attributes.getValue("conf"));
if (oldMapping.length() > 0) {
String newMapping = removeConfigurationsFromMapping(oldMapping, confs);
if (newMapping.length() > 0) {
write(" conf=\"" + newMapping + "\"");
((ExtendedBuffer) buffers.peek()).setPrint(true);
}
}
} else {
write(" " + attName + "=\""
+ substitute(settings, attributes.getValue(attName)) + "\"");
}
}
if(attributes.getIndex("branch") == -1)
{
if (newBranch != null) {
// erase an existing branch attribute if its new value is blank
if(!newBranch.trim().equals(""))
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) {
url = settings.getRelativeUrlResolver().getURL(relativePathCtx,
settings.substitute(attributes.getValue("file")),
settings.substitute(attributes.getValue("url")));
} else {
//TODO : settings can be null, but I don't why.
//Check if the next 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();
}
}
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("defaultconfmapping"));
if (defaultconf != null) {
defaultConfMapping = defaultconf;
}
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 (StringTokenizer tok = new StringTokenizer(extend, ", ");
tok.hasMoreTokens();) {
String current = tok.nextToken();
if (confs.contains(current)) {
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,
new String[] {"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=\"" + Ivy.DATE_FORMAT.format(pubdate) + "\"");
} else if (attributes.getValue("publication") != null) {
write(" publication=\""
+ substitute(settings, attributes.getValue("publication")) + "\"");
}
if (namespace != null) {
write(" namespace=\"" + namespace + "\"");
}
for (Iterator extras = extraAttributes.entrySet().iterator(); extras.hasNext();) {
Map.Entry extra = (Map.Entry) extras.next();
write(" " + extra.getKey() + "=\"" + extra.getValue() + "\"");
}
}
private void write(String content) {
getWriter().print(content);
}
private PrintWriter getWriter() {
return buffers.isEmpty() ? out : ((ExtendedBuffer) buffers.peek()).getWriter();
}
private String getContext() {
StringBuffer buf = new StringBuffer();
for (Iterator iter = context.iterator(); iter.hasNext();) {
String ctx = (String) iter.next();
buf.append(ctx).append("/");
}
if (buf.length() > 0) {
buf.setLength(buf.length() - 1);
}
return buf.toString();
}
private String substitute(ParserSettings ivy, String value) {
String result = ivy == null ? value : ivy.substitute(value);
return XMLHelper.escape(result);
}
private String removeConfigurationsFromMapping(String mapping, List confsToRemove) {
StringBuffer newMapping = new StringBuffer();
String mappingSep = "";
for (StringTokenizer tokenizer = new StringTokenizer(mapping, ";"); tokenizer
.hasMoreTokens();) {
String current = tokenizer.nextToken();
String[] ops = current.split("->");
String[] lhs = ops[0].split(",");
List confsToWrite = new ArrayList();
for (int j = 0; j < lhs.length; j++) {
if (!confs.contains(lhs[j].trim())) {
confsToWrite.add(lhs[j]);
}
}
if (!confsToWrite.isEmpty()) {
newMapping.append(mappingSep);
String sep = "";
for (Iterator it = confsToWrite.iterator(); it.hasNext();) {
newMapping.append(sep);
newMapping.append(it.next());
sep = ",";
}
if (ops.length == 2) {
newMapping.append("->");
newMapping.append(ops[1]);
}
mappingSep = ";";
}
}
return newMapping.toString();
}
private String removeConfigurationsFromList(String list, List confsToRemove) {
StringBuffer newList = new StringBuffer();
String listSep = "";
for (StringTokenizer tokenizer = new StringTokenizer(list, ","); tokenizer
.hasMoreTokens();) {
String current = tokenizer.nextToken();
if (!confsToRemove.contains(current.trim())) {
newList.append(listSep);
newList.append(current);
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(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 = (String) 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 (String) 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 <extends> 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 inheritedItems = collateInheritedItems(merged, items);
boolean hasItems = !inheritedItems.isEmpty();
if (hasItems && includeContainer) {
if (currentIndent.length() == 0) {
out.print(getIndent());
}
out.print("<" + itemName + ">");
context.push(itemName);
justOpen = null;
}
for (Iterator parents = inheritedItems.entrySet().iterator(); parents.hasNext();) {
Map.Entry entry = (Map.Entry) parents.next();
ModuleRevisionId parent = (ModuleRevisionId) entry.getKey();
List list = (List) entry.getValue();
if (justOpen != null) {
out.println(">");
justOpen = null; //helps endElement() decide how to write close tags
}
writeInheritanceComment(itemName, parent);
for (int c = 0; c < list.size(); ++c) {
InheritableItem item = (InheritableItem) list.get(c);
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>*/ collateInheritedItems(ModuleDescriptor merged,
InheritableItem[] items) {
LinkedHashMap/*<ModuleRevisionId,List>*/ inheritedItems = new LinkedHashMap();
for (int i = 0; i < items.length; ++i) {
ModuleRevisionId source = items[i].getSourceModule();
//ignore items that are defined directly in the child descriptor
if (source != null
&& !source.getModuleId().equals(merged.getModuleRevisionId().getModuleId())) {
List accum = (List) inheritedItems.get(source);
if (accum == null) {
accum = new ArrayList();
inheritedItems.put(source, accum);
}
accum.add(items[i]);
}
}
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 (description != null) {
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();
if ("ivy-module/info".equals(path)) {
//guarantee that inherited description has been written before
//info element closes.
writeInheritedDescription(merged);
} else if ("ivy-module/configurations".equals(path)) {
//write inherited configurations after all child configurations
writeInheritedConfigurations(merged);
} else if ("ivy-module/dependencies".equals(path)) {
//write inherited dependencies after all child dependencies
writeInheritedDependencies(merged);
} else if ("ivy-module".equals(path)) {
//write any remaining inherited data before we close the
//descriptor.
flushAllMergedElements();
}
}
if (qName.equals(justOpen)) {
write("/>");
} else {
write("</" + qName + ">");
}
if (!buffers.isEmpty()) {
ExtendedBuffer buffer = (ExtendedBuffer) buffers.peek();
if (buffer.getContext().equals(path)) {
buffers.pop();
if (buffer.isPrint()) {
write(buffer.toString());
}
}
}
if (!confAttributeBuffers.isEmpty()) {
ExtendedBuffer buffer = (ExtendedBuffer) 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 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;
}
if (!inHeader) {
StringBuffer comment = new StringBuffer();
comment.append(ch, start, length);
write("<!--");
write(comment.toString());
write("-->");
}
}
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, "UTF-8"));
final BufferedInputStream in = new BufferedInputStream(inStream);
in.mark(MAX_HEADER_LENGTH); // assume the header is never larger than 10000 bytes.
copyHeader(in, out);
in.reset(); // reposition the stream at the beginning
try {
UpdaterHandler updaterHandler = new UpdaterHandler(inStreamCtx, out, options);
InputSource inSrc = new InputSource(in);
if (inStreamCtx != null) {
inSrc.setSystemId(inStreamCtx.toExternalForm());
}
XMLHelper.parse(inSrc, null, updaterHandler, updaterHandler);
} catch (ParserConfigurationException e) {
IllegalStateException ise = new IllegalStateException(
"impossible to update Ivy files: parser problem");
ise.initCause(e);
throw ise;
}
}
/**
* Copy xml header from src url ivy file to given printwriter In fact, copies everything before
* <ivy-module to out, except if <ivy-module is not found, in which case nothing is copied. The
* prolog <?xml version="..." encoding="...."?> is also replaced by <?xml version="1.0"
* encoding="UTF-8"?> if it was present.
*
* @param in
* @param out
* @throws IOException
*/
private static void copyHeader(InputStream in, PrintWriter out) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(in));
String line = r.readLine();
if (line != null && line.startsWith("<?xml ")) {
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
line = line.substring(line.indexOf(">") + 1, line.length());
}
for (; line != null; line = r.readLine()) {
int index = line.indexOf("<ivy-module");
if (index == -1) {
out.write(line);
out.write(LINE_SEPARATOR);
} else {
out.write(line.substring(0, index));
break;
}
}
// r.close();
}
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.booleanValue();
}
void setPrint(boolean print) {
this.print = Boolean.valueOf(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 static 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}
*/
public 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);
}
}
}