blob: 1b5160e9f67c3f910be58026ccfdec5959625659 [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
*
* 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.netbeans.nbbuild;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* Increments specification versions of all specified modules,
* in the trunk or in a branch, in a regulated manner.
* @author Jesse Glick
*/
public final class RefreshDependencyVersions extends Task {
private File nbroot;
private String codenameBase;
private String release;
private String specification;
private boolean dryRun = false;
private final Set<Dep> injectDeps = new HashSet<>();
public RefreshDependencyVersions() {}
public void setNbroot(File f) {
nbroot = f;
}
public void setModule(String codenameBase) {
this.codenameBase = codenameBase;
}
public void setRelease(String release) {
this.release = release == null || release.length() == 0 ? null : release;
}
public void setSpecification(String specification) {
this.specification = specification == null || specification.length() == 0 ? null : specification;
}
public void setDryRun(boolean b) {
dryRun = b;
}
public Dep createInject() {
Dep dep = new Dep();
injectDeps.add(dep);
return dep;
}
public @Override void execute() throws BuildException {
if (nbroot == null || codenameBase == null) {
throw new BuildException("Missing params 'nbroot' or 'modules'", getLocation());
}
log("RefreshDependencyVersions parameters: nbroot = '" + nbroot.getAbsolutePath() + "'"
+ ", module = " + codenameBase + ", dryrun = " + dryRun + ", inject = " + injectDeps, Project.MSG_VERBOSE);
validateInjectedDependencies(injectDeps);
Map<String,Object> properties = getProject().getProperties();
ModuleListParser listParser;
try {
listParser = new ModuleListParser(properties, ModuleType.NB_ORG, getProject());
} catch (IOException ioe) {
throw new BuildException("Can't read module list!", ioe, getLocation());
}
Set<ModuleListParser.Entry> allModules = listParser.findAll();
Dep sourceModuleVersion = null;
// find the versions of the source module
for(ModuleListParser.Entry moduleEntry: allModules) {
String path = moduleEntry.getNetbeansOrgPath();
File projectDir = new File(nbroot, path.replace('/', File.separatorChar));
if (!projectDir.isDirectory()) {
log("No such directory " + projectDir + "; skipping", Project.MSG_WARN);
continue;
}
Dep mv = loadDepFrom(projectDir);
if (mv == null) {
log("Not a module: " + projectDir + ", skipping!", Project.MSG_WARN);
continue;
}
log("Loading module versions: " + mv.getCodenameBase()
+ ", release = " + mv.getRelease()
+ ", spec = " + mv.getSpecification(),
Project.MSG_VERBOSE);
if (codenameBase.equals(mv.getCodenameBase())) {
if (mv.getRelease() == null) {
throw new BuildException("Unknown release version of '" + codenameBase + "'.", getLocation());
}
if (mv.getSpecification() == null) {
throw new BuildException("Unknown specification version of '" + codenameBase + "'.", getLocation());
}
if (release != null && compare(mv.getRelease(), release) < 0) {
throw new BuildException("Release version of '" + codenameBase + "' is " + mv.getRelease() + ". Requested to update to " + release, getLocation());
}
if (specification != null && compare(mv.getSpecification(), specification) < 0) {
throw new BuildException("Specification version of '" + codenameBase + "' is " + mv.getSpecification() + ". Requested to update to " + specification, getLocation());
}
sourceModuleVersion = mv;
break;
}
}
if (sourceModuleVersion == null) {
throw new BuildException("Can't find '" + codenameBase + "' module!", getLocation());
}
// update all modules that depend on the source module version older or equal to release & specification,
// but not update the source module itself and not the modules we are injecting
NBPRJ:
for(ModuleListParser.Entry moduleEntry: allModules) {
String path = moduleEntry.getNetbeansOrgPath();
File projectFile = new File(new File(nbroot, path.replace('/', File.separatorChar)), "nbproject" + File.separatorChar + "project.xml");
if (!projectFile.exists()) {
log("Non-existent project.xml: " + path + ", skipping!", Project.MSG_WARN);
}
Document nbprj = null;
try {
try (InputStream is = new FileInputStream(projectFile)) {
nbprj = XMLUtil.parse(new InputSource(is), false, true, null, null);
}
} catch (Exception ioe) {
throw new BuildException("Can't parse " + projectFile, ioe, getLocation());
}
// check that nbprj is not the source module or one of the injects
Node cnb = findChild(findChild(findChild(nbprj.getDocumentElement(), "configuration"), "data"), "code-name-base");
if (cnb.getTextContent().equals(codenameBase)) {
log("Won't touch dependencies of the source module (" + codenameBase + ")", Project.MSG_VERBOSE);
continue;
}
for(Dep inject : injectDeps) {
if (cnb.getTextContent().equals(inject.getCodenameBase())) {
log("Won't touch dependencies of the injected module (" + inject.getCodenameBase() + ")", Project.MSG_VERBOSE);
continue NBPRJ;
}
}
boolean updated = false;
StringBuilder refreshMsg = new StringBuilder();
StringBuilder injectMsg = new StringBuilder();
Node sourceDep = findDependencyFor(nbprj, codenameBase);
if (sourceDep != null) {
boolean compareSpecification = true;
// check release versions
if (release != null) {
Node sourceRelease = findChild(findChild(sourceDep, "run-dependency"), "release-version");
if (sourceRelease != null && sourceRelease.getTextContent().length() > 0)
{
int c = compare(sourceRelease.getTextContent(), release);
if (c > 0) {
// sourceDep is newer than release, skip
continue;
} else if (c < 0) {
// sourceDep is older then release, ignore specification and updateVersions
compareSpecification = false;
}
}
}
// check specification versions
if (compareSpecification && specification != null) {
Node sourceSpecification = findChild(findChild(sourceDep, "run-dependency"), "specification-version");
if (sourceSpecification != null && sourceSpecification.getTextContent().length() > 0) {
if (compare(sourceSpecification.getTextContent(), specification) >= 0) {
// sourceDep is newer or equal than specification, skip
continue;
}
} else {
Node sourceImplementation = findChild(findChild(sourceDep, "run-dependency"), "implementation-version");
if (sourceImplementation != null) {
// sourceDep is implementation dependency, skip
continue;
}
}
}
updated |= updateVersions(sourceDep, sourceModuleVersion, false, refreshMsg);
for(Dep inject : injectDeps) {
if (injectMsg.length() > 0) {
injectMsg.append(", ");
}
Node injectedDep = findDependencyFor(nbprj, inject.getCodenameBase());
if (injectedDep != null) {
updated |= updateVersions(injectedDep, inject, true, injectMsg);
} else {
addDependency(sourceDep.getParentNode(), inject, injectMsg);
updated = true;
}
}
}
if (updated) {
try {
if (!dryRun) {
try (OutputStream os = new FileOutputStream(projectFile)) {
XMLUtil.write(nbprj, os);
}
} else {
if (!projectFile.canWrite()) {
log("Would need to update " + projectFile + ", but it's readonly!", Project.MSG_ERR);
}
}
} catch (IOException ioe) {
throw new BuildException("Can't write " + projectFile, ioe, getLocation());
}
}
if (refreshMsg.length() > 0) {
log("Dependencies refreshed in " + projectFile.getAbsolutePath() + ": " + refreshMsg.toString(), Project.MSG_INFO);
}
if (injectMsg.length() > 0) {
log("Dependencies injected to " + projectFile.getAbsolutePath() + ": " + injectMsg.toString(), Project.MSG_INFO);
}
}
}
private static Node findChild(Node node, String childName) {
NodeList kids = node.getChildNodes();
for(int i = 0; i < kids.getLength(); i++) {
if (kids.item(i).getNodeName().equals(childName)) {
return kids.item(i);
}
}
return null;
}
private static Node findDependencyFor(Document nbprj, String codenameBase) {
NodeList allDeps = nbprj.getElementsByTagName("dependency");
for(int i = 0; i < allDeps.getLength(); i++) {
Node dep = allDeps.item(i);
Node cnb = findChild(dep, "code-name-base");
if (cnb.getTextContent().equals(codenameBase)) {
return dep;
}
}
return null;
}
private static boolean updateVersions(Node dep, Dep sourceDep, boolean injecting, StringBuilder log) {
StringBuilder s = new StringBuilder();
Node runDep = findChild(dep, "run-dependency");
if (injecting) {
Node buildPrerequisite = findChild(dep, "build-prerequisite");
if (buildPrerequisite == null) {
buildPrerequisite = dep.getOwnerDocument().createElement("build-prerequisite");
dep.appendChild(buildPrerequisite);
s.append(" adding <build-prerequisite/>");
}
Node compileDep = findChild(dep, "compile-dependency");
if (compileDep == null) {
compileDep = dep.getOwnerDocument().createElement("compile-dependency");
dep.appendChild(compileDep);
s.append(" adding <compile-dependency/>");
}
if (runDep == null) {
runDep = dep.getOwnerDocument().createElement("run-dependency");
dep.appendChild(runDep);
s.append(" adding <run-dependency/>");
}
}
if (runDep != null) {
// update the release version
Node releaseVersion = findChild(runDep, "release-version");
if (releaseVersion != null && sourceDep.getRelease() != null) {
String nue = checkReleaseVersion(sourceDep.getRelease());
if (!releaseVersion.getTextContent().equals(nue)) {
releaseVersion.setTextContent(nue);
s.append(" release = ").append(nue);
}
}
// update the specification version
Node specVersion = findChild(runDep, "specification-version");
if (!injecting) {
assert sourceDep.getSpecification() != null : "Need specification version when refreshing dependencies"; //NOI18N
if (specVersion == null) {
Node implVersion = findChild(runDep, "implementation-version");
if (implVersion == null) {
// umm, no spec version and not an impl. dependency
specVersion = dep.getOwnerDocument().createElement("specification-version");
specVersion.setTextContent("");
runDep.appendChild(specVersion);
}
}
if (specVersion != null) {
String nue = checkSpecificationVersion(sourceDep.getSpecification());
if (!specVersion.getTextContent().equals(nue)) {
specVersion.setTextContent(nue);
s.append(" spec = ").append(nue);
}
}
} else {
Node implVersion = findChild(runDep, "implementation-version");
if (sourceDep.isImplementation()) {
if (specVersion != null) {
runDep.removeChild(specVersion);
s.append(" removing spec = " + specVersion.getTextContent());
}
if (implVersion == null) {
implVersion = dep.getOwnerDocument().createElement("implementation-version");
runDep.appendChild(implVersion);
s.append(" adding impl");
}
} else {
if (sourceDep.getSpecification() != null) {
String nue = checkSpecificationVersion(sourceDep.getSpecification());
if (specVersion == null) {
specVersion = dep.getOwnerDocument().createElement("specification-version");
specVersion.setTextContent(nue);
runDep.appendChild(specVersion);
s.append(" adding spec = ").append(nue);
} else if (!specVersion.getTextContent().equals(nue)) {
specVersion.setTextContent(nue);
s.append(" updating spec = ").append(nue);
}
}
if (implVersion != null) {
runDep.removeChild(implVersion);
s.append(" removing impl");
}
}
}
}
log.append(sourceDep.getCodenameBase()).append(':');
if (s.length() > 0) {
log.append(s);
return true;
} else {
log.append(" up-to-date");
return false;
}
}
private void addDependency(Node moduleDependencies, Dep inject, StringBuilder log) {
Document nbprj = moduleDependencies.getOwnerDocument();
Element nueDep = nbprj.createElement("dependency");
moduleDependencies.appendChild(nueDep);
Element nueCnb = nbprj.createElement("code-name-base");
nueCnb.setTextContent(inject.getCodenameBase());
nueDep.appendChild(nueCnb);
log.append(inject.getCodenameBase());
Element nueBuildPrerequisite = nbprj.createElement("build-prerequisite");
nueDep.appendChild(nueBuildPrerequisite);
Element nueCompileDep = nbprj.createElement("compile-dependency");
nueDep.appendChild(nueCompileDep);
Element nueRunDep = nbprj.createElement("run-dependency");
nueDep.appendChild(nueRunDep);
Element nueRelease = nbprj.createElement("release-version");
String nue = checkReleaseVersion(inject.getRelease());
nueRelease.setTextContent(nue);
nueRunDep.appendChild(nueRelease);
log.append('/').append(nue);
if (inject.isImplementation()) {
nueRunDep.appendChild(nbprj.createElement("implementation-version"));
log.append(" = impl");
} else {
Element nueSpec = nbprj.createElement("specification-version");
nue = checkSpecificationVersion(inject.getSpecification());
nueSpec.setTextContent(nue);
nueRunDep.appendChild(nueSpec);
log.append(" >= " + nue);
}
}
private static String[] gulp(File file, String enc) throws IOException {
try (InputStream is = new FileInputStream(file)) {
BufferedReader r = new BufferedReader(new InputStreamReader(is, enc));
List<String> l = new ArrayList<>();
String line;
while ((line = r.readLine()) != null) {
l.add(line);
}
return l.toArray(new String[l.size()]);
}
}
// private static void spit(File file, String enc, String[] lines) throws IOException {
// OutputStream os = new FileOutputStream(file);
// try {
// PrintWriter w = new PrintWriter(new OutputStreamWriter(os, enc));
// for (String line : lines) {
// w.println(line);
// }
// w.flush();
// } finally {
// os.close();
// }
// }
private void validateInjectedDependencies(Set<Dep> deps) throws BuildException {
for(Dep dep : deps) {
if (dep.getCodenameBase() == null || dep.getCodenameBase().length() == 0) {
throw new BuildException("The 'codenamebase' attribute of injected dependency can't be empty.", getLocation());
}
if (dep.getRelease() == null || dep.getRelease().length() == 0) {
throw new BuildException("The 'release' attribute of injected dependency can't be empty.", getLocation());
}
if (!dep.isImplementation()) {
if (dep.getSpecification() == null || dep.getSpecification().length() == 0) {
throw new BuildException("The 'specification' attribute of injected dependency can't be empty. Or implementation='true' must be specified.", getLocation());
}
}
}
}
private static String checkReleaseVersion(String v) {
if (v.equals("0")) {
return "0-1";
} else {
return v;
}
}
private static String checkSpecificationVersion(String v) {
if (v.endsWith(".0") && v.indexOf('.') != v.length() - 2) {
// ends up with '.0' and it's not just 'x.0', but eg. x.x.0
v = v.substring(0, v.length() - 2);
}
return v;
}
private Dep loadDepFrom(File projectDir) throws BuildException {
Dep dep = new Dep();
try {
File pp = new File(projectDir, "nbproject" + File.separatorChar + "project.properties");
if (pp.isFile()) {
String[] lines = gulp(pp, "ISO-8859-1");
for (int i = 0; i < lines.length; i++) {
Matcher m1 = Pattern.compile("(spec\\.version\\.base=)(.+)").matcher(lines[i]);
if (m1.matches()) {
dep.setSpecification(m1.group(2));
}
}
} else {
if (!new File(projectDir, "nbproject" + File.separatorChar + "project.xml").isFile()) {
log("No such file " + pp + "; unprojectized module?", Project.MSG_VERBOSE);
return null;
}
}
File mf = new File(projectDir, "manifest.mf");
if (mf.isFile()) {
String[] lines = gulp(mf, "UTF-8");
for (int i = 0; i < lines.length; i++) {
Matcher m1 = Pattern.compile("(OpenIDE-Module: )(.+)").matcher(lines[i]);
if (m1.matches()) {
String fullName = m1.group(2);
int idx = fullName.lastIndexOf('/');
if (idx != -1) {
dep.setCodenameBase(fullName.substring(0, idx));
dep.setRelease(fullName.substring(idx + 1));
} else {
dep.setCodenameBase(fullName);
dep.setCodenameBase("0");
}
}
m1 = Pattern.compile("(OpenIDE-Module-Specification-Version: )(.+)").matcher(lines[i]);
if (m1.matches()) {
dep.setSpecification(m1.group(2));
}
}
return dep;
} else {
log("No such file " + mf + "; not a real module?", Project.MSG_VERBOSE);
return null;
}
} catch (IOException e) {
throw new BuildException("While processing project files in " + projectDir + ": " + e, e, getLocation());
}
}
private static int compare(String v1, String v2) {
String [] v1arr = v1.split("\\.");
String [] v2arr = v2.split("\\.");
int len = Math.min(v1arr.length, v2arr.length);
for (int i = 0; i < len; i++) {
int v1int = str2ver(v1arr[i]);
int v2int = str2ver(v2arr[i]);
int result = v1int - v2int;
if (result != 0) {
return result;
}
}
// 1.1 is older then 1.1.1
return v1arr.length - v2arr.length;
}
private static int str2ver(String s) {
int i = s.indexOf('-');
if (i == -1) {
return Integer.parseInt(s);
} else {
return Integer.parseInt(s.substring(0, i));
}
}
// private class ModuleVersion {
// private String codebaseName;
// private String release;
// private String specification;
// private String implementation;
//
// public ModuleVersion() {
// }
//
// public String getCodebaseName() {
// return codebaseName;
// }
//
// public String getImplementation() {
// return implementation;
// }
//
// public String getRelease() {
// return release;
// }
//
// public String getSpecification() {
// return specification;
// }
//
// } // End of ModuleVersion class
public static final class Dep {
private String codenameBase;
private String release;
private String specification;
private boolean implementation;
public Dep() {
}
public String getCodenameBase() {
return codenameBase;
}
public void setCodenameBase(String codenameBase) {
this.codenameBase = codenameBase == null || codenameBase.length() == 0 ? null : codenameBase;
}
public boolean isImplementation() {
return implementation;
}
public void setImplementation(boolean implementation) {
if (implementation && specification != null) {
throw new IllegalArgumentException("Can't use implementation dependency when specification version is set.");
}
this.implementation = implementation;
}
public String getRelease() {
return release;
}
public void setRelease(String release) {
this.release = release == null || release.length() == 0 ? null : release;
}
public String getSpecification() {
return specification;
}
public void setSpecification(String specification) {
if (implementation && specification != null && specification.length() > 0) {
throw new IllegalArgumentException("Can't specification version is set when using implementation dependency.");
}
this.specification = specification == null || specification.length() == 0 ? null : specification;
}
public @Override boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Dep other = (Dep) obj;
if (this.codenameBase != other.codenameBase && (this.codenameBase == null || !this.codenameBase.equals(other.codenameBase))) {
return false;
}
if (this.release != other.release && (this.release == null || !this.release.equals(other.release))) {
return false;
}
if (this.specification != other.specification && (this.specification == null || !this.specification.equals(other.specification))) {
return false;
}
if (this.implementation != other.implementation) {
return false;
}
return true;
}
public @Override int hashCode() {
int hash = 7;
hash = 79 * hash + (this.codenameBase != null ? this.codenameBase.hashCode() : 0);
hash = 79 * hash + (this.release != null ? this.release.hashCode() : 0);
hash = 79 * hash + (this.specification != null ? this.specification.hashCode() : 0);
hash = 79 * hash + (this.implementation ? 1 : 0);
return hash;
}
public @Override String toString() {
if (specification != null) {
return codenameBase + "/" + release + " >= " + specification;
} else {
return codenameBase + "/" + release + " = impl";
}
}
} // End of Dep class
}