PageRenderTime 33ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/product/ConfluenceProductHandler.java

https://bitbucket.org/atlassian/amps
Java | 365 lines | 295 code | 47 blank | 23 comment | 21 complexity | 2a562a192227f4c7308d3959feff3a8b MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. package com.atlassian.maven.plugins.amps.product;
  2. import com.atlassian.maven.plugins.amps.MavenContext;
  3. import com.atlassian.maven.plugins.amps.MavenGoals;
  4. import com.atlassian.maven.plugins.amps.Node;
  5. import com.atlassian.maven.plugins.amps.Product;
  6. import com.atlassian.maven.plugins.amps.ProductArtifact;
  7. import com.atlassian.maven.plugins.amps.XmlOverride;
  8. import com.atlassian.maven.plugins.amps.product.manager.WebAppManager;
  9. import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
  10. import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
  11. import com.google.common.annotations.VisibleForTesting;
  12. import com.google.common.collect.ImmutableMap;
  13. import com.google.common.collect.ImmutableSet;
  14. import org.apache.maven.artifact.Artifact;
  15. import org.apache.maven.artifact.resolver.ArtifactResolver;
  16. import org.apache.maven.artifact.versioning.ComparableVersion;
  17. import org.apache.maven.plugin.MojoExecutionException;
  18. import org.apache.maven.repository.RepositorySystem;
  19. import javax.annotation.Nonnull;
  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.util.Arrays;
  23. import java.util.Collection;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Optional;
  27. import java.util.Set;
  28. import java.util.TreeMap;
  29. import static com.atlassian.maven.plugins.amps.util.NetworkUtils.getLoopbackInterface;
  30. import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
  31. import static java.lang.Boolean.getBoolean;
  32. import static java.lang.Character.getNumericValue;
  33. import static java.lang.String.format;
  34. import static java.util.Collections.emptyList;
  35. import static java.util.Collections.singletonList;
  36. import static java.util.Optional.empty;
  37. import static org.apache.commons.io.FileUtils.deleteDirectory;
  38. import static org.apache.commons.lang3.StringUtils.isBlank;
  39. import static org.apache.commons.lang3.StringUtils.isNotBlank;
  40. import static org.apache.maven.artifact.Artifact.LATEST_VERSION;
  41. import static org.apache.maven.artifact.Artifact.RELEASE_VERSION;
  42. public class ConfluenceProductHandler extends AbstractWebappProductHandler {
  43. /**
  44. * In addition to the --add-opens required by Tomcat and Felix, Confluence needs extra ones to start up correctly under Java 17
  45. */
  46. private static final Set<JavaModulePackage> EXTRA_ADD_OPENS = ImmutableSet.of(
  47. // Required to allow XStream to persist AtomicXyz classes in Bandana
  48. new JavaModulePackage("java.base", "java.util.concurrent.atomic"),
  49. // Required to allow XStream to persist java.util.Properties in Bandana (e.g. for storing mail server details)
  50. new JavaModulePackage("java.base", "java.util"));
  51. private static final Set<JavaModulePackage> EXTRA_ADD_EXPORTS = ImmutableSet.of(
  52. // Required by com.atlassian.velocity.VelocityHelper
  53. new JavaModulePackage("java.base", "sun.security.action"),
  54. // Required by DaisyDiff via com.atlassian.confluence.diff.DaisyHtmlDiffer
  55. new JavaModulePackage("java.xml", "com.sun.org.apache.xml.internal.utils"));
  56. // system property to override deploying the synchrony-proxy webapp
  57. public static final String REQUIRE_SYNCHRONY_PROXY = "require.synchrony.proxy";
  58. public static final String SYNCHRONY_PROXY_VERSION = "synchrony.proxy.version";
  59. @VisibleForTesting
  60. static final String HAZELCAST_LISTEN_PORT = "confluence.cluster.hazelcast.listenPort";
  61. @VisibleForTesting
  62. static final String SYNCHRONY_PORT = "synchrony.port";
  63. private static final ProductArtifact CONFLUENCE_ARTIFACT = new ProductArtifact(
  64. "com.atlassian.confluence", "confluence-webapp", "RELEASE");
  65. private static final ProductArtifact CONFLUENCE_TEST_RESOURCES_ARTIFACT = new ProductArtifact(
  66. "com.atlassian.confluence.plugins", "confluence-plugin-test-resources");
  67. private static final ProductArtifact SYNCHRONY_PROXY = new ProductArtifact(
  68. "com.atlassian.synchrony", "synchrony-proxy", "RELEASE", "war");
  69. private static final TreeMap<ComparableVersion, String> SYNCHRONY_PROXY_VERSIONS = new TreeMap<>();
  70. static {
  71. SYNCHRONY_PROXY_VERSIONS.put(new ComparableVersion("6.4.10000"), "1.0.17");
  72. SYNCHRONY_PROXY_VERSIONS.put(new ComparableVersion("10000"), "RELEASE");
  73. }
  74. public ConfluenceProductHandler(final MavenContext context, final MavenGoals goals,
  75. final RepositorySystem repositorySystem, final ArtifactResolver artifactResolver,
  76. final WebAppManager webAppManager) {
  77. super(context, goals, new ConfluencePluginProvider(), repositorySystem, artifactResolver, webAppManager);
  78. }
  79. @Nonnull
  80. @Override
  81. public String getId() {
  82. return "confluence";
  83. }
  84. @Override
  85. protected boolean isStaticPlugin() {
  86. // assume all Confluence plugins should be installed as bundled plugins -- a pretty good assumption
  87. return false;
  88. }
  89. @Override
  90. protected void fixJvmArgs(final Product product) {
  91. product.setJvmArgs(JvmArgsFix.empty()
  92. .with("-Xmx", "4g")
  93. .with("-Xms", "1g")
  94. .withAddOpens(ADD_OPENS_FOR_TOMCAT)
  95. .withAddOpens(ADD_OPENS_FOR_FELIX)
  96. .withAddOpens(EXTRA_ADD_OPENS)
  97. .withAddExports(EXTRA_ADD_EXPORTS)
  98. .apply(product.getJvmArgs()));
  99. }
  100. @Nonnull
  101. @Override
  102. public ProductArtifact getArtifact() {
  103. return CONFLUENCE_ARTIFACT;
  104. }
  105. @Nonnull
  106. @Override
  107. public Optional<ProductArtifact> getTestResourcesArtifact() {
  108. return Optional.of(CONFLUENCE_TEST_RESOURCES_ARTIFACT);
  109. }
  110. @Override
  111. public int getDefaultHttpPort() {
  112. return 1990;
  113. }
  114. @Override
  115. public int getDefaultHttpsPort() {
  116. return 8441;
  117. }
  118. @Override
  119. @Nonnull
  120. protected Map<String, String> getProductSpecificSystemProperties(final Product product, final int nodeIndex) {
  121. final ImmutableMap.Builder<String, String> systemProperties = ImmutableMap.builder();
  122. systemProperties.put("cargo.servlet.uriencoding", "UTF-8");
  123. if (product.isMultiNode()) {
  124. setHazelcastListenPort(product);
  125. systemProperties.put("confluence.allow.loopback.cluster", "true"); // because all nodes are on localhost
  126. systemProperties.put("confluence.cluster.node.name", product.getInstanceId() + "-" + nodeIndex);
  127. setUpSynchrony(product, nodeIndex);
  128. }
  129. final String homeDirectory = getHomeDirectories(product).get(nodeIndex).getPath();
  130. systemProperties.put("confluence.home", homeDirectory);
  131. return systemProperties.build();
  132. }
  133. private void setUpSynchrony(final Product product, final int nodeIndex) {
  134. final Node node = product.getNodes().get(nodeIndex);
  135. if (nodeIndex == 0) {
  136. // Node 0 runs a Confluence-managed Synchrony instance
  137. // Allow for this port to be user-configured
  138. node.defaultSystemProperty(SYNCHRONY_PORT, () -> String.valueOf(pickFreePort(0)));
  139. } else {
  140. // Other nodes use the first node's instance (thanks to Ganesh Gautam for this resource-saving technique)
  141. node.setSystemProperty("synchrony.proxy.enabled", "false");
  142. final String nodeZeroSynchronyPort = product.getNodes().get(0).getSystemProperties().get(SYNCHRONY_PORT);
  143. if (isBlank(nodeZeroSynchronyPort)) {
  144. throw new IllegalStateException(
  145. format("First node's Synchrony port is blank: '%s'", nodeZeroSynchronyPort));
  146. }
  147. final String nodeZeroSynchronyUrl = format("http://localhost:%s/synchrony/v1", nodeZeroSynchronyPort);
  148. node.setSystemProperty("synchrony.service.url", nodeZeroSynchronyUrl);
  149. }
  150. }
  151. @Override
  152. protected boolean useBackdoorToInstallLicense() {
  153. // No need: the customiseInstance method applies the license before product startup
  154. return false;
  155. }
  156. private void setHazelcastListenPort(final Product product) {
  157. // Allow for the user to configure this at the global or product level, otherwise pick a free port
  158. product.defaultSystemProperty(HAZELCAST_LISTEN_PORT, () -> String.valueOf(pickFreePort(0)));
  159. }
  160. @Override
  161. @Nonnull
  162. protected Optional<File> getUserInstalledPluginsDirectory(final Product product, final File webappDir, File homeDir) {
  163. // indicates plugins should be bundled
  164. return empty();
  165. }
  166. @Override
  167. @Nonnull
  168. protected List<ProductArtifact> getExtraProductDeployables(final Product product) {
  169. return shouldDeploySynchronyProxy(product) ? singletonList(SYNCHRONY_PROXY) : emptyList();
  170. }
  171. private boolean shouldDeploySynchronyProxy(final Product product) {
  172. // Not ideal to use an AMPS sys prop to toggle the Synchrony proxy; affects all AMPS-run Confluence instances.
  173. // It would make more sense for this to be configured at the mojo and/or <product> level (breaking change).
  174. boolean synchronyProxyRequired = true;
  175. if (isNotBlank(System.getProperty(REQUIRE_SYNCHRONY_PROXY))) {
  176. synchronyProxyRequired = getBoolean(REQUIRE_SYNCHRONY_PROXY);
  177. }
  178. return synchronyProxyRequired && getNumericValue(product.getVersion().charAt(0)) >= 6; // won't work for 10.x
  179. }
  180. @Override
  181. protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir)
  182. throws MojoExecutionException {
  183. product.setCargoXmlOverrides(serverXmlConfluenceOverride());
  184. if (shouldDeploySynchronyProxy(product)) {
  185. resolveSynchronyProxyVersion(product);
  186. installSynchronyProxy(product);
  187. }
  188. if (product.isMultiNode()) {
  189. configureCluster(homeDir, product);
  190. }
  191. if (product.hasUserConfiguredLicense()) {
  192. // Changing the license after plugin system start is too late in some cases (e.g. DC mode)
  193. final String license = product.getUserConfiguredLicense().orElseThrow(IllegalStateException::new);
  194. configureLicense(homeDir, license);
  195. }
  196. }
  197. private void configureLicense(final File homeDir, final String license) throws MojoExecutionException {
  198. final Optional<File> confluenceConfigFile = getConfluenceConfigFile(homeDir);
  199. if (confluenceConfigFile.isPresent()) {
  200. // Throws exception => no lambda
  201. new ConfluenceLicenseConfigurer().configure(confluenceConfigFile.get(), license);
  202. }
  203. }
  204. private void configureCluster(final File homeDir, final Product product) throws MojoExecutionException {
  205. // Can't do this via getReplacements() alone, because it requires adding/removing XML elements
  206. final Optional<File> confluenceConfigFile = getConfluenceConfigFile(homeDir);
  207. if (confluenceConfigFile.isPresent()) {
  208. // Throws exception => no lambda
  209. final File sharedHome = getSharedHome(product);
  210. new ConfluenceClusterConfigurer().configure(confluenceConfigFile.get(), sharedHome, getLoopbackInterface());
  211. }
  212. }
  213. private File getSharedHome(final Product product) {
  214. if (isNotBlank(product.getSharedHome())) {
  215. return new File(product.getSharedHome());
  216. }
  217. // Otherwise, use the shared-home found in the default Confluence home ZIP
  218. return new File(getHomeDirectories(product).get(0), "shared-home");
  219. }
  220. private void resolveSynchronyProxyVersion(final Product product) throws MojoExecutionException {
  221. log.debug("Resolving synchrony proxy version for Confluence " + product.getVersion());
  222. final String synchronyProxyVersionProperty = System.getProperty(SYNCHRONY_PROXY_VERSION);
  223. if (isNotBlank(synchronyProxyVersionProperty)) {
  224. log.debug("Synchrony proxy version is already set in system variable (to "
  225. + synchronyProxyVersionProperty + ")");
  226. SYNCHRONY_PROXY.setVersion(synchronyProxyVersionProperty);
  227. } else {
  228. log.debug("Synchrony proxy version is not set. Attempting to set corresponding version");
  229. final Map.Entry<ComparableVersion, String> synchronyProxyVersion =
  230. SYNCHRONY_PROXY_VERSIONS.ceilingEntry(new ComparableVersion(product.getVersion()));
  231. if (synchronyProxyVersion != null) {
  232. SYNCHRONY_PROXY.setVersion(synchronyProxyVersion.getValue());
  233. log.debug("Synchrony proxy version is set to " + synchronyProxyVersion.getValue());
  234. }
  235. }
  236. // check for latest stable version if version not specified
  237. if (RELEASE_VERSION.equals(SYNCHRONY_PROXY.getVersion()) ||
  238. LATEST_VERSION.equals(SYNCHRONY_PROXY.getVersion())) {
  239. log.debug("determining latest stable synchrony-proxy version...");
  240. Artifact warArtifact = repositorySystem.createProjectArtifact(SYNCHRONY_PROXY.getGroupId(),
  241. SYNCHRONY_PROXY.getArtifactId(), SYNCHRONY_PROXY.getVersion());
  242. String stableVersion = product.getArtifactRetriever().getLatestStableVersion(warArtifact);
  243. log.debug("using latest stable synchrony-proxy version: " + stableVersion);
  244. SYNCHRONY_PROXY.setVersion(stableVersion);
  245. }
  246. }
  247. private void installSynchronyProxy(final Product product) throws MojoExecutionException {
  248. final File confInstall = getBaseDirectory(product);
  249. final File war = goals.copyWebappWar(
  250. SYNCHRONY_PROXY, new File(confInstall, "synchrony-proxy"), "synchrony-proxy");
  251. SYNCHRONY_PROXY.setPath(war.getPath());
  252. }
  253. private Collection<XmlOverride> serverXmlConfluenceOverride() {
  254. return singletonList(new XmlOverride(
  255. "conf/server.xml", "//Connector", "maxThreads", "48"));
  256. }
  257. @Override
  258. @Nonnull
  259. protected File getBundledPluginPath(final Product product, final File productDir) {
  260. final String bundleDirPath = "WEB-INF/atlassian-bundled-plugins";
  261. final File bundleDir = new File(productDir, bundleDirPath);
  262. if (bundleDir.exists() && bundleDir.isDirectory()) {
  263. return bundleDir;
  264. } else {
  265. return new File(productDir,
  266. "WEB-INF/classes/com/atlassian/confluence/setup/atlassian-bundled-plugins.zip");
  267. }
  268. }
  269. @Nonnull
  270. @Override
  271. protected List<Replacement> getReplacements(@Nonnull final Product product, final int nodeIndex) {
  272. final List<Replacement> replacements = super.getReplacements(product, nodeIndex);
  273. final File homeDirectory = getHomeDirectories(product).get(nodeIndex);
  274. replacements.add(new Replacement("@project-dir@", homeDirectory.getParent()));
  275. replacements.add(new Replacement("/confluence-home/", "/home/", false));
  276. final String contextPath = product.getContextPath().replaceAll("^/|/$", "");
  277. final String baseUrlElement = format("<baseUrl>%s://%s:%d/%s</baseUrl>",
  278. product.getProtocol(), product.getServer(), product.getWebPortForNode(nodeIndex), contextPath);
  279. replacements.add(new Replacement(
  280. "<baseUrl>http://localhost:1990/confluence</baseUrl>", baseUrlElement, false));
  281. replacements.add(new Replacement(
  282. "<baseUrl>http://localhost:8080</baseUrl>", baseUrlElement, false));
  283. return replacements;
  284. }
  285. @Nonnull
  286. @Override
  287. protected List<File> getConfigFiles(@Nonnull final Product product, @Nonnull final File homeDirectory) {
  288. final List<File> configFiles = super.getConfigFiles(product, homeDirectory);
  289. configFiles.add(new File(new File(homeDirectory, "database"), "confluencedb.script"));
  290. configFiles.add(new File(new File(homeDirectory, "database"), "confluencedb.log"));
  291. getConfluenceConfigFile(homeDirectory).ifPresent(configFiles::add);
  292. return configFiles;
  293. }
  294. private static Optional<File> getConfluenceConfigFile(@Nonnull final File homeDirectory) {
  295. return Optional.of(new File(homeDirectory, "confluence.cfg.xml"))
  296. .filter(File::isFile);
  297. }
  298. private static class ConfluencePluginProvider extends AbstractPluginProvider {
  299. @Override
  300. protected Collection<ProductArtifact> getSalArtifacts(final String salVersion) {
  301. return Arrays.asList(
  302. new ProductArtifact("com.atlassian.sal", "sal-api", salVersion),
  303. new ProductArtifact("com.atlassian.sal", "sal-confluence-plugin", salVersion));
  304. }
  305. @Override
  306. protected Collection<ProductArtifact> getPdkInstallArtifacts(final String pdkInstallVersion) {
  307. return emptyList();
  308. }
  309. }
  310. @Override
  311. protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
  312. throws MojoExecutionException, IOException {
  313. super.cleanupProductHomeForZip(product, snapshotDir);
  314. deleteDirectory(new File(snapshotDir, "plugins-osgi-cache"));
  315. deleteDirectory(new File(snapshotDir, "plugins-temp"));
  316. deleteDirectory(new File(snapshotDir, "temp"));
  317. }
  318. }