/* * 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. */ import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.Console; import java.io.IOException; import java.io.StringReader; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Sonatype nexus artifact staging/deployment script. This could be made * nicer, but this keeps it to JDK classes only. * *
The implementation is based on the REST API documentation of
* nexus-staging-plugin
* and on anecdotal evidence and reverse-engineered information from around
* the web... Weird that such a crucial piece of infrastructure has such obscure
* documentation.
*/
public class StageArtifacts {
private static final String DEFAULT_NEXUS_URI = "https://repository.apache.org";
private static class Params {
URI nexusUri = URI.create(DEFAULT_NEXUS_URI);
String userName;
char[] userPass;
Path mavenDir;
String description;
private static char[] envVar(String envVar) {
var value = System.getenv(envVar);
return value == null ? null : value.toCharArray();
}
static void requiresArgument(String[] args, int at) {
if (at + 1 >= args.length) {
throw new RuntimeException("Option '" + args[at]
+ "' requires an argument, pass --help for help.");
}
}
static Params parse(String[] args) {
try {
var params = new Params();
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "-n":
case "--nexus":
requiresArgument(args, i);
params.nexusUri = URI.create(args[++i]);
break;
case "-u":
case "--user":
requiresArgument(args, i);
params.userName = args[++i];
break;
case "-p":
case "--password":
requiresArgument(args, i);
params.userPass = args[++i].toCharArray();
break;
case "--description":
requiresArgument(args, i);
params.description = args[++i];
break;
case "-h":
case "--help":
System.out.println("java " + StageArtifacts.class.getName() + " [options] path-to-maven-artifacts");
System.out.println(" -u, --user User name for authentication.");
System.out.println(" better: ASF_USERNAME env. var.");
System.out.println(" -p, --password Password for authentication.");
System.out.println(" better: ASF_PASSWORD env. var.");
System.out.println(" -n, --nexus URL to Apache Nexus (optional).");
System.out.println(" --description Staging repo description (optional).");
System.out.println("");
System.out.println(" path Path to maven artifact directory.");
System.out.println("");
System.out.println(" Password can be omitted for console prompt-input.");
System.exit(0);
default:
if (params.mavenDir != null) {
throw new RuntimeException("Exactly one maven artifact directory should be provided.");
}
params.mavenDir = Paths.get(args[i]);
break;
}
}
if (params.userName == null) {
var v = envVar("ASF_USERNAME");
if (v != null) {
params.userName = new String(v);
}
}
Objects.requireNonNull(params.userName, "User name is required for authentication.");
if (params.userPass == null) {
params.userPass = envVar("ASF_PASSWORD");
if (params.userPass == null) {
Console console = System.console();
if (console != null) {
System.out.println("Enter password for " + params.userName + ":");
params.userPass = console.readPassword();
} else {
throw new RuntimeException("No console, can't prompt for password.");
}
}
}
Objects.requireNonNull(params.userPass, "User password is required for authentication.");
if (params.mavenDir == null || !Files.isDirectory(params.mavenDir)) {
throw new RuntimeException("Maven artifact directory is required and must exist.");
}
return params;
} catch (IndexOutOfBoundsException e) {
throw new RuntimeException("Required argument missing (pass --help for help)?");
}
}
}
private static class NexusApi {
private final HttpClient client;
private final URI nexusUri;
public NexusApi(Params params) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(params.userName, params.userPass);
}
};
this.client = HttpClient.newBuilder()
.authenticator(authenticator)
.build();
this.nexusUri = params.nexusUri;
}
public String requestProfileId(PomInfo pomInfo) throws IOException {
String result = sendGet("/service/local/staging/profile_evaluate", Map.of(
"g", pomInfo.groupId,
"a", pomInfo.artifactId,
"v", pomInfo.version,
"t", "maven2"
));
return XmlElement.parse(result)
.onlychild("stagingProfiles")
.onlychild("data")
.onlychild("stagingProfile")
.onlychild("id")
.text();
}
public String createStagingRepository(String profileId, String description) throws IOException {
String result = sendPost("/service/local/staging/profiles/" + URLEncoder.encode(profileId, StandardCharsets.UTF_8) + "/start",
"application/xml",
HttpURLConnection.HTTP_CREATED,
("