RepositoryHttpsUpgradeStrategy.java
/*
* 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.maven.cling.invoker.mvnup.goals;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORY;
/**
* Strategy for upgrading HTTP repository URLs to HTTPS in POM files.
*
* <p>Since Maven 3.8.1+, HTTP repositories are blocked by the {@code maven-default-http-blocker}
* mirror, so any {@code http://} repository URL will cause build failures. This strategy
* automatically converts {@code http://} URLs to {@code https://}, using canonical HTTPS URLs
* for well-known repositories.
*
* <p>The strategy handles:
* <ul>
* <li>{@code <repositories>/<repository>} URLs</li>
* <li>{@code <pluginRepositories>/<pluginRepository>} URLs</li>
* <li>{@code <distributionManagement>/<repository>} and {@code <snapshotRepository>} URLs</li>
* <li>Profile-scoped repositories in all of the above sections</li>
* </ul>
*/
@Named
@Singleton
@Priority(15)
public class RepositoryHttpsUpgradeStrategy extends AbstractUpgradeStrategy {
private static final String HTTP_SCHEME = "http://";
private static final String DISTRIBUTION_MANAGEMENT = "distributionManagement";
private static final String SNAPSHOT_REPOSITORY = "snapshotRepository";
/**
* Well-known repository URL mappings from HTTP to their canonical HTTPS equivalents.
* Uses a LinkedHashMap to preserve insertion order for predictable iteration.
*/
private static final Map<String, String> WELL_KNOWN_URL_MAPPINGS = createWellKnownMappings();
private static Map<String, String> createWellKnownMappings() {
Map<String, String> mappings = new LinkedHashMap<>();
// Apache repository URLs
mappings.put("http://repository.apache.org/snapshots", "https://repository.apache.org/snapshots");
mappings.put("http://repository.apache.org/releases", "https://repository.apache.org/releases");
// Maven Central variants - all map to the canonical HTTPS URL
mappings.put("http://repo1.maven.org/maven2", "https://repo.maven.apache.org/maven2");
mappings.put("http://repo.maven.apache.org/maven2", "https://repo.maven.apache.org/maven2");
mappings.put("http://central.maven.org/maven2", "https://repo.maven.apache.org/maven2");
return mappings;
}
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
return isOptionEnabled(options, options.model(), true);
}
@Override
public String getDescription() {
return "Upgrading HTTP repository URLs to HTTPS";
}
@Override
protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking for HTTP repository URLs)");
context.indent();
try {
boolean hasUpgrades = upgradeRepositoryUrls(pomDocument, context);
if (hasUpgrades) {
modifiedPoms.add(pomPath);
context.success("HTTP repository URLs upgraded to HTTPS");
} else {
context.success("No HTTP repository URLs found");
}
} catch (Exception e) {
context.failure("Failed to upgrade repository URLs: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Upgrades all HTTP repository URLs in the POM document to HTTPS.
*
* @param pomDocument the POM document to process
* @param context the upgrade context for logging
* @return true if any URLs were upgraded
*/
private boolean upgradeRepositoryUrls(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
boolean hasUpgrades = false;
// Process root-level repositories and pluginRepositories
hasUpgrades |= upgradeRepositoriesSection(root, REPOSITORIES, REPOSITORY, "repositories", context);
hasUpgrades |=
upgradeRepositoriesSection(root, PLUGIN_REPOSITORIES, PLUGIN_REPOSITORY, "pluginRepositories", context);
// Process distributionManagement
hasUpgrades |= upgradeDistributionManagement(root, "distributionManagement", context);
// Process profile-scoped repositories
hasUpgrades |= upgradeProfileRepositories(root, context);
return hasUpgrades;
}
/**
* Upgrades repository URLs in a repositories or pluginRepositories section.
*/
private boolean upgradeRepositoriesSection(
Element parent, String containerName, String elementType, String sectionLabel, UpgradeContext context) {
Element container = parent.childElement(containerName).orElse(null);
if (container == null) {
return false;
}
return container
.childElements(elementType)
.map(repo -> upgradeUrlElement(repo, sectionLabel, context))
.reduce(false, Boolean::logicalOr);
}
/**
* Upgrades repository and snapshotRepository URLs in the distributionManagement section.
*/
private boolean upgradeDistributionManagement(Element parent, String sectionLabel, UpgradeContext context) {
Element distMgmt = parent.childElement(DISTRIBUTION_MANAGEMENT).orElse(null);
if (distMgmt == null) {
return false;
}
boolean hasUpgrades = false;
// Process <distributionManagement>/<repository>
Element repository = distMgmt.childElement(REPOSITORY).orElse(null);
if (repository != null) {
hasUpgrades |= upgradeUrlElement(repository, sectionLabel + "/repository", context);
}
// Process <distributionManagement>/<snapshotRepository>
Element snapshotRepository = distMgmt.childElement(SNAPSHOT_REPOSITORY).orElse(null);
if (snapshotRepository != null) {
hasUpgrades |= upgradeUrlElement(snapshotRepository, sectionLabel + "/snapshotRepository", context);
}
return hasUpgrades;
}
/**
* Upgrades repository URLs in profile-scoped sections.
*/
private boolean upgradeProfileRepositories(Element root, UpgradeContext context) {
Element profiles = root.childElement(PROFILES).orElse(null);
if (profiles == null) {
return false;
}
return profiles.childElements(PROFILE)
.map(profile -> {
boolean upgraded = false;
upgraded |= upgradeRepositoriesSection(
profile, REPOSITORIES, REPOSITORY, "profile/repositories", context);
upgraded |= upgradeRepositoriesSection(
profile, PLUGIN_REPOSITORIES, PLUGIN_REPOSITORY, "profile/pluginRepositories", context);
upgraded |= upgradeDistributionManagement(profile, "profile/distributionManagement", context);
return upgraded;
})
.reduce(false, Boolean::logicalOr);
}
/**
* Upgrades the URL element of a repository from HTTP to HTTPS.
*
* @param repositoryElement the repository element containing a url child
* @param sectionLabel the section label for logging
* @param context the upgrade context for logging
* @return true if the URL was upgraded
*/
private boolean upgradeUrlElement(Element repositoryElement, String sectionLabel, UpgradeContext context) {
Element urlElement = repositoryElement.childElement("url").orElse(null);
if (urlElement == null) {
return false;
}
String url = urlElement.textContent();
if (url == null) {
return false;
}
String trimmedUrl = url.trim();
if (!trimmedUrl.startsWith(HTTP_SCHEME)) {
return false;
}
String repositoryId = repositoryElement.childText("id");
String idLabel = repositoryId != null ? repositoryId : "(no id)";
// Try well-known URL mappings first
String upgradedUrl = mapWellKnownUrl(trimmedUrl);
if (upgradedUrl == null) {
// Generic http -> https conversion
upgradedUrl = "https://" + trimmedUrl.substring(HTTP_SCHEME.length());
}
// Preserve original whitespace around the URL
String newUrl = url.replace(trimmedUrl, upgradedUrl);
Editor editor = new Editor(repositoryElement.document());
editor.setTextContent(urlElement, newUrl);
context.detail("Upgraded repository URL from " + trimmedUrl + " to " + upgradedUrl + " for " + idLabel + " in "
+ sectionLabel);
return true;
}
/**
* Maps a well-known HTTP URL to its canonical HTTPS equivalent.
*
* @param httpUrl the HTTP URL to map
* @return the canonical HTTPS URL, or null if no well-known mapping exists
*/
static String mapWellKnownUrl(String httpUrl) {
// Normalize: remove trailing slash for comparison
String normalized = httpUrl.endsWith("/") ? httpUrl.substring(0, httpUrl.length() - 1) : httpUrl;
for (Map.Entry<String, String> mapping : WELL_KNOWN_URL_MAPPINGS.entrySet()) {
if (normalized.equals(mapping.getKey()) || normalized.startsWith(mapping.getKey() + "/")) {
// Replace the matched prefix with the canonical HTTPS URL,
// preserving any trailing path segments
String remainder = normalized.substring(mapping.getKey().length());
String result = mapping.getValue() + remainder;
// Restore trailing slash if original had one
if (httpUrl.endsWith("/") && !result.endsWith("/")) {
result += "/";
}
return result;
}
}
return null;
}
}