/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/product/ConfluenceProductHandler.java
Java | 365 lines | 295 code | 47 blank | 23 comment | 21 complexity | 2a562a192227f4c7308d3959feff3a8b MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
- package com.atlassian.maven.plugins.amps.product;
- import com.atlassian.maven.plugins.amps.MavenContext;
- import com.atlassian.maven.plugins.amps.MavenGoals;
- import com.atlassian.maven.plugins.amps.Node;
- import com.atlassian.maven.plugins.amps.Product;
- import com.atlassian.maven.plugins.amps.ProductArtifact;
- import com.atlassian.maven.plugins.amps.XmlOverride;
- import com.atlassian.maven.plugins.amps.product.manager.WebAppManager;
- import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
- import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
- import com.google.common.annotations.VisibleForTesting;
- import com.google.common.collect.ImmutableMap;
- import com.google.common.collect.ImmutableSet;
- import org.apache.maven.artifact.Artifact;
- import org.apache.maven.artifact.resolver.ArtifactResolver;
- import org.apache.maven.artifact.versioning.ComparableVersion;
- import org.apache.maven.plugin.MojoExecutionException;
- import org.apache.maven.repository.RepositorySystem;
- import javax.annotation.Nonnull;
- import java.io.File;
- import java.io.IOException;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Set;
- import java.util.TreeMap;
- import static com.atlassian.maven.plugins.amps.util.NetworkUtils.getLoopbackInterface;
- import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
- import static java.lang.Boolean.getBoolean;
- import static java.lang.Character.getNumericValue;
- import static java.lang.String.format;
- import static java.util.Collections.emptyList;
- import static java.util.Collections.singletonList;
- import static java.util.Optional.empty;
- import static org.apache.commons.io.FileUtils.deleteDirectory;
- import static org.apache.commons.lang3.StringUtils.isBlank;
- import static org.apache.commons.lang3.StringUtils.isNotBlank;
- import static org.apache.maven.artifact.Artifact.LATEST_VERSION;
- import static org.apache.maven.artifact.Artifact.RELEASE_VERSION;
- public class ConfluenceProductHandler extends AbstractWebappProductHandler {
- /**
- * In addition to the --add-opens required by Tomcat and Felix, Confluence needs extra ones to start up correctly under Java 17
- */
- private static final Set<JavaModulePackage> EXTRA_ADD_OPENS = ImmutableSet.of(
- // Required to allow XStream to persist AtomicXyz classes in Bandana
- new JavaModulePackage("java.base", "java.util.concurrent.atomic"),
- // Required to allow XStream to persist java.util.Properties in Bandana (e.g. for storing mail server details)
- new JavaModulePackage("java.base", "java.util"));
- private static final Set<JavaModulePackage> EXTRA_ADD_EXPORTS = ImmutableSet.of(
- // Required by com.atlassian.velocity.VelocityHelper
- new JavaModulePackage("java.base", "sun.security.action"),
- // Required by DaisyDiff via com.atlassian.confluence.diff.DaisyHtmlDiffer
- new JavaModulePackage("java.xml", "com.sun.org.apache.xml.internal.utils"));
- // system property to override deploying the synchrony-proxy webapp
- public static final String REQUIRE_SYNCHRONY_PROXY = "require.synchrony.proxy";
- public static final String SYNCHRONY_PROXY_VERSION = "synchrony.proxy.version";
- @VisibleForTesting
- static final String HAZELCAST_LISTEN_PORT = "confluence.cluster.hazelcast.listenPort";
- @VisibleForTesting
- static final String SYNCHRONY_PORT = "synchrony.port";
- private static final ProductArtifact CONFLUENCE_ARTIFACT = new ProductArtifact(
- "com.atlassian.confluence", "confluence-webapp", "RELEASE");
- private static final ProductArtifact CONFLUENCE_TEST_RESOURCES_ARTIFACT = new ProductArtifact(
- "com.atlassian.confluence.plugins", "confluence-plugin-test-resources");
- private static final ProductArtifact SYNCHRONY_PROXY = new ProductArtifact(
- "com.atlassian.synchrony", "synchrony-proxy", "RELEASE", "war");
- private static final TreeMap<ComparableVersion, String> SYNCHRONY_PROXY_VERSIONS = new TreeMap<>();
- static {
- SYNCHRONY_PROXY_VERSIONS.put(new ComparableVersion("6.4.10000"), "1.0.17");
- SYNCHRONY_PROXY_VERSIONS.put(new ComparableVersion("10000"), "RELEASE");
- }
- public ConfluenceProductHandler(final MavenContext context, final MavenGoals goals,
- final RepositorySystem repositorySystem, final ArtifactResolver artifactResolver,
- final WebAppManager webAppManager) {
- super(context, goals, new ConfluencePluginProvider(), repositorySystem, artifactResolver, webAppManager);
- }
- @Nonnull
- @Override
- public String getId() {
- return "confluence";
- }
- @Override
- protected boolean isStaticPlugin() {
- // assume all Confluence plugins should be installed as bundled plugins -- a pretty good assumption
- return false;
- }
- @Override
- protected void fixJvmArgs(final Product product) {
- product.setJvmArgs(JvmArgsFix.empty()
- .with("-Xmx", "4g")
- .with("-Xms", "1g")
- .withAddOpens(ADD_OPENS_FOR_TOMCAT)
- .withAddOpens(ADD_OPENS_FOR_FELIX)
- .withAddOpens(EXTRA_ADD_OPENS)
- .withAddExports(EXTRA_ADD_EXPORTS)
- .apply(product.getJvmArgs()));
- }
- @Nonnull
- @Override
- public ProductArtifact getArtifact() {
- return CONFLUENCE_ARTIFACT;
- }
- @Nonnull
- @Override
- public Optional<ProductArtifact> getTestResourcesArtifact() {
- return Optional.of(CONFLUENCE_TEST_RESOURCES_ARTIFACT);
- }
- @Override
- public int getDefaultHttpPort() {
- return 1990;
- }
- @Override
- public int getDefaultHttpsPort() {
- return 8441;
- }
- @Override
- @Nonnull
- protected Map<String, String> getProductSpecificSystemProperties(final Product product, final int nodeIndex) {
- final ImmutableMap.Builder<String, String> systemProperties = ImmutableMap.builder();
- systemProperties.put("cargo.servlet.uriencoding", "UTF-8");
- if (product.isMultiNode()) {
- setHazelcastListenPort(product);
- systemProperties.put("confluence.allow.loopback.cluster", "true"); // because all nodes are on localhost
- systemProperties.put("confluence.cluster.node.name", product.getInstanceId() + "-" + nodeIndex);
- setUpSynchrony(product, nodeIndex);
- }
- final String homeDirectory = getHomeDirectories(product).get(nodeIndex).getPath();
- systemProperties.put("confluence.home", homeDirectory);
- return systemProperties.build();
- }
- private void setUpSynchrony(final Product product, final int nodeIndex) {
- final Node node = product.getNodes().get(nodeIndex);
- if (nodeIndex == 0) {
- // Node 0 runs a Confluence-managed Synchrony instance
- // Allow for this port to be user-configured
- node.defaultSystemProperty(SYNCHRONY_PORT, () -> String.valueOf(pickFreePort(0)));
- } else {
- // Other nodes use the first node's instance (thanks to Ganesh Gautam for this resource-saving technique)
- node.setSystemProperty("synchrony.proxy.enabled", "false");
- final String nodeZeroSynchronyPort = product.getNodes().get(0).getSystemProperties().get(SYNCHRONY_PORT);
- if (isBlank(nodeZeroSynchronyPort)) {
- throw new IllegalStateException(
- format("First node's Synchrony port is blank: '%s'", nodeZeroSynchronyPort));
- }
- final String nodeZeroSynchronyUrl = format("http://localhost:%s/synchrony/v1", nodeZeroSynchronyPort);
- node.setSystemProperty("synchrony.service.url", nodeZeroSynchronyUrl);
- }
- }
- @Override
- protected boolean useBackdoorToInstallLicense() {
- // No need: the customiseInstance method applies the license before product startup
- return false;
- }
- private void setHazelcastListenPort(final Product product) {
- // Allow for the user to configure this at the global or product level, otherwise pick a free port
- product.defaultSystemProperty(HAZELCAST_LISTEN_PORT, () -> String.valueOf(pickFreePort(0)));
- }
- @Override
- @Nonnull
- protected Optional<File> getUserInstalledPluginsDirectory(final Product product, final File webappDir, File homeDir) {
- // indicates plugins should be bundled
- return empty();
- }
- @Override
- @Nonnull
- protected List<ProductArtifact> getExtraProductDeployables(final Product product) {
- return shouldDeploySynchronyProxy(product) ? singletonList(SYNCHRONY_PROXY) : emptyList();
- }
- private boolean shouldDeploySynchronyProxy(final Product product) {
- // Not ideal to use an AMPS sys prop to toggle the Synchrony proxy; affects all AMPS-run Confluence instances.
- // It would make more sense for this to be configured at the mojo and/or <product> level (breaking change).
- boolean synchronyProxyRequired = true;
- if (isNotBlank(System.getProperty(REQUIRE_SYNCHRONY_PROXY))) {
- synchronyProxyRequired = getBoolean(REQUIRE_SYNCHRONY_PROXY);
- }
- return synchronyProxyRequired && getNumericValue(product.getVersion().charAt(0)) >= 6; // won't work for 10.x
- }
- @Override
- protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir)
- throws MojoExecutionException {
- product.setCargoXmlOverrides(serverXmlConfluenceOverride());
- if (shouldDeploySynchronyProxy(product)) {
- resolveSynchronyProxyVersion(product);
- installSynchronyProxy(product);
- }
- if (product.isMultiNode()) {
- configureCluster(homeDir, product);
- }
- if (product.hasUserConfiguredLicense()) {
- // Changing the license after plugin system start is too late in some cases (e.g. DC mode)
- final String license = product.getUserConfiguredLicense().orElseThrow(IllegalStateException::new);
- configureLicense(homeDir, license);
- }
- }
- private void configureLicense(final File homeDir, final String license) throws MojoExecutionException {
- final Optional<File> confluenceConfigFile = getConfluenceConfigFile(homeDir);
- if (confluenceConfigFile.isPresent()) {
- // Throws exception => no lambda
- new ConfluenceLicenseConfigurer().configure(confluenceConfigFile.get(), license);
- }
- }
- private void configureCluster(final File homeDir, final Product product) throws MojoExecutionException {
- // Can't do this via getReplacements() alone, because it requires adding/removing XML elements
- final Optional<File> confluenceConfigFile = getConfluenceConfigFile(homeDir);
- if (confluenceConfigFile.isPresent()) {
- // Throws exception => no lambda
- final File sharedHome = getSharedHome(product);
- new ConfluenceClusterConfigurer().configure(confluenceConfigFile.get(), sharedHome, getLoopbackInterface());
- }
- }
- private File getSharedHome(final Product product) {
- if (isNotBlank(product.getSharedHome())) {
- return new File(product.getSharedHome());
- }
- // Otherwise, use the shared-home found in the default Confluence home ZIP
- return new File(getHomeDirectories(product).get(0), "shared-home");
- }
- private void resolveSynchronyProxyVersion(final Product product) throws MojoExecutionException {
- log.debug("Resolving synchrony proxy version for Confluence " + product.getVersion());
- final String synchronyProxyVersionProperty = System.getProperty(SYNCHRONY_PROXY_VERSION);
- if (isNotBlank(synchronyProxyVersionProperty)) {
- log.debug("Synchrony proxy version is already set in system variable (to "
- + synchronyProxyVersionProperty + ")");
- SYNCHRONY_PROXY.setVersion(synchronyProxyVersionProperty);
- } else {
- log.debug("Synchrony proxy version is not set. Attempting to set corresponding version");
- final Map.Entry<ComparableVersion, String> synchronyProxyVersion =
- SYNCHRONY_PROXY_VERSIONS.ceilingEntry(new ComparableVersion(product.getVersion()));
- if (synchronyProxyVersion != null) {
- SYNCHRONY_PROXY.setVersion(synchronyProxyVersion.getValue());
- log.debug("Synchrony proxy version is set to " + synchronyProxyVersion.getValue());
- }
- }
- // check for latest stable version if version not specified
- if (RELEASE_VERSION.equals(SYNCHRONY_PROXY.getVersion()) ||
- LATEST_VERSION.equals(SYNCHRONY_PROXY.getVersion())) {
- log.debug("determining latest stable synchrony-proxy version...");
- Artifact warArtifact = repositorySystem.createProjectArtifact(SYNCHRONY_PROXY.getGroupId(),
- SYNCHRONY_PROXY.getArtifactId(), SYNCHRONY_PROXY.getVersion());
- String stableVersion = product.getArtifactRetriever().getLatestStableVersion(warArtifact);
- log.debug("using latest stable synchrony-proxy version: " + stableVersion);
- SYNCHRONY_PROXY.setVersion(stableVersion);
- }
- }
- private void installSynchronyProxy(final Product product) throws MojoExecutionException {
- final File confInstall = getBaseDirectory(product);
- final File war = goals.copyWebappWar(
- SYNCHRONY_PROXY, new File(confInstall, "synchrony-proxy"), "synchrony-proxy");
- SYNCHRONY_PROXY.setPath(war.getPath());
- }
- private Collection<XmlOverride> serverXmlConfluenceOverride() {
- return singletonList(new XmlOverride(
- "conf/server.xml", "//Connector", "maxThreads", "48"));
- }
- @Override
- @Nonnull
- protected File getBundledPluginPath(final Product product, final File productDir) {
- final String bundleDirPath = "WEB-INF/atlassian-bundled-plugins";
- final File bundleDir = new File(productDir, bundleDirPath);
- if (bundleDir.exists() && bundleDir.isDirectory()) {
- return bundleDir;
- } else {
- return new File(productDir,
- "WEB-INF/classes/com/atlassian/confluence/setup/atlassian-bundled-plugins.zip");
- }
- }
- @Nonnull
- @Override
- protected List<Replacement> getReplacements(@Nonnull final Product product, final int nodeIndex) {
- final List<Replacement> replacements = super.getReplacements(product, nodeIndex);
- final File homeDirectory = getHomeDirectories(product).get(nodeIndex);
- replacements.add(new Replacement("@project-dir@", homeDirectory.getParent()));
- replacements.add(new Replacement("/confluence-home/", "/home/", false));
- final String contextPath = product.getContextPath().replaceAll("^/|/$", "");
- final String baseUrlElement = format("<baseUrl>%s://%s:%d/%s</baseUrl>",
- product.getProtocol(), product.getServer(), product.getWebPortForNode(nodeIndex), contextPath);
- replacements.add(new Replacement(
- "<baseUrl>http://localhost:1990/confluence</baseUrl>", baseUrlElement, false));
- replacements.add(new Replacement(
- "<baseUrl>http://localhost:8080</baseUrl>", baseUrlElement, false));
- return replacements;
- }
- @Nonnull
- @Override
- protected List<File> getConfigFiles(@Nonnull final Product product, @Nonnull final File homeDirectory) {
- final List<File> configFiles = super.getConfigFiles(product, homeDirectory);
- configFiles.add(new File(new File(homeDirectory, "database"), "confluencedb.script"));
- configFiles.add(new File(new File(homeDirectory, "database"), "confluencedb.log"));
- getConfluenceConfigFile(homeDirectory).ifPresent(configFiles::add);
- return configFiles;
- }
- private static Optional<File> getConfluenceConfigFile(@Nonnull final File homeDirectory) {
- return Optional.of(new File(homeDirectory, "confluence.cfg.xml"))
- .filter(File::isFile);
- }
- private static class ConfluencePluginProvider extends AbstractPluginProvider {
- @Override
- protected Collection<ProductArtifact> getSalArtifacts(final String salVersion) {
- return Arrays.asList(
- new ProductArtifact("com.atlassian.sal", "sal-api", salVersion),
- new ProductArtifact("com.atlassian.sal", "sal-confluence-plugin", salVersion));
- }
- @Override
- protected Collection<ProductArtifact> getPdkInstallArtifacts(final String pdkInstallVersion) {
- return emptyList();
- }
- }
- @Override
- protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
- throws MojoExecutionException, IOException {
- super.cleanupProductHomeForZip(product, snapshotDir);
- deleteDirectory(new File(snapshotDir, "plugins-osgi-cache"));
- deleteDirectory(new File(snapshotDir, "plugins-temp"));
- deleteDirectory(new File(snapshotDir, "temp"));
- }
- }