PageRenderTime 97ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/atlassian/amps
Java | 1026 lines | 665 code | 111 blank | 250 comment | 78 complexity | 5914b55f1e66f5fd9b80aa458c7100e4 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.license.LicenseInstaller;
  8. import com.atlassian.maven.plugins.amps.util.ArtifactResolutionException;
  9. import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
  10. import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
  11. import com.atlassian.maven.plugins.amps.util.ProjectUtils;
  12. import com.atlassian.maven.plugins.amps.util.ZipUtils;
  13. import com.google.common.annotations.VisibleForTesting;
  14. import com.google.common.collect.ImmutableMap;
  15. import com.google.common.collect.ImmutableSet;
  16. import org.apache.maven.artifact.Artifact;
  17. import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
  18. import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
  19. import org.apache.maven.artifact.resolver.ArtifactResolver;
  20. import org.apache.maven.plugin.MojoExecutionException;
  21. import org.apache.maven.plugin.logging.Log;
  22. import org.apache.maven.project.MavenProject;
  23. import org.apache.maven.repository.RepositorySystem;
  24. import org.twdata.maven.mojoexecutor.MojoExecutor;
  25. import javax.annotation.Nonnull;
  26. import javax.annotation.Nullable;
  27. import java.io.File;
  28. import java.io.IOException;
  29. import java.io.UnsupportedEncodingException;
  30. import java.net.InetAddress;
  31. import java.net.UnknownHostException;
  32. import java.nio.charset.StandardCharsets;
  33. import java.util.ArrayList;
  34. import java.util.Collection;
  35. import java.util.HashMap;
  36. import java.util.HashSet;
  37. import java.util.Iterator;
  38. import java.util.List;
  39. import java.util.Map;
  40. import java.util.Optional;
  41. import java.util.Properties;
  42. import java.util.Set;
  43. import java.util.regex.Pattern;
  44. import static com.atlassian.maven.plugins.amps.product.AmpsDefaults.DEFAULT_CONTAINER;
  45. import static com.atlassian.maven.plugins.amps.product.JavaModulePackage.fromJavaBaseModule;
  46. import static com.atlassian.maven.plugins.amps.product.JavaModulePackage.fromJavaRmiModule;
  47. import static com.atlassian.maven.plugins.amps.product.ProductHandlerFactory.JIRA;
  48. import static com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement.onlyWhenUnzipping;
  49. import static com.atlassian.maven.plugins.amps.util.ConfigFileUtils.replace;
  50. import static com.atlassian.maven.plugins.amps.util.FileUtils.copyDirectory;
  51. import static com.atlassian.maven.plugins.amps.util.FileUtils.doesFileNameMatchArtifact;
  52. import static com.atlassian.maven.plugins.amps.util.FileUtils.makeDirectories;
  53. import static com.atlassian.maven.plugins.amps.util.FileUtils.makeDirectory;
  54. import static com.atlassian.maven.plugins.amps.util.ProjectUtils.createDirectory;
  55. import static com.atlassian.maven.plugins.amps.util.ProjectUtils.firstNotNull;
  56. import static com.atlassian.maven.plugins.amps.util.ZipUtils.unzip;
  57. import static com.atlassian.maven.plugins.amps.util.ZipUtils.zipChildren;
  58. import static java.lang.String.join;
  59. import static java.net.URLEncoder.encode;
  60. import static java.nio.file.Files.delete;
  61. import static java.util.Arrays.stream;
  62. import static java.util.Collections.emptyList;
  63. import static java.util.Collections.singletonList;
  64. import static java.util.Collections.sort;
  65. import static java.util.Objects.requireNonNull;
  66. import static java.util.Optional.empty;
  67. import static java.util.stream.Collectors.joining;
  68. import static org.apache.commons.io.FileUtils.copyFile;
  69. import static org.apache.commons.io.FileUtils.deleteDirectory;
  70. import static org.apache.commons.io.FileUtils.deleteQuietly;
  71. import static org.apache.commons.io.FileUtils.iterateFiles;
  72. import static org.apache.commons.io.FileUtils.listFiles;
  73. import static org.apache.commons.io.FileUtils.moveDirectory;
  74. import static org.apache.commons.lang3.StringUtils.isBlank;
  75. import static org.apache.commons.lang3.StringUtils.isNotBlank;
  76. import static org.apache.maven.artifact.Artifact.LATEST_VERSION;
  77. import static org.apache.maven.artifact.Artifact.RELEASE_VERSION;
  78. /**
  79. * Convenient superclass for {@link ProductHandler} implementations.
  80. *
  81. * Incorporates code previously located in its deleted superclass {@code AmpsProductHandler}.
  82. */
  83. public abstract class AbstractProductHandler implements ProductHandler {
  84. private static final Map<String, Map<String, GroupArtifactPair>> APPLICATION_KEYS =
  85. ImmutableMap.of(
  86. JIRA, ImmutableMap.of(
  87. "jira-software", new GroupArtifactPair("com.atlassian.jira", "jira-software-application"),
  88. "jira-servicedesk", new GroupArtifactPair("com.atlassian.servicedesk", "jira-servicedesk-application")));
  89. private static final ProductArtifact LICENSE_BACKDOOR_PLUGIN =
  90. new ProductArtifact("com.atlassian.platform", "license-backdoor-plugin", "1.0.2");
  91. private static final String TOMCAT_LIB_DIR = "WEB-INF/lib";
  92. protected static final Set<JavaModulePackage> ADD_OPENS_FOR_TOMCAT = ImmutableSet.of(
  93. fromJavaBaseModule("java.lang"),
  94. fromJavaBaseModule("java.io"),
  95. fromJavaRmiModule("sun.rmi.transport"));
  96. protected static final Set<JavaModulePackage> ADD_OPENS_FOR_FELIX = ImmutableSet.of(
  97. fromJavaBaseModule("java.lang"),
  98. fromJavaBaseModule("java.net"),
  99. fromJavaBaseModule("sun.net.www.protocol.jar"),
  100. fromJavaBaseModule("sun.net.www.protocol.file"),
  101. fromJavaBaseModule("sun.net.www.protocol.http"),
  102. fromJavaBaseModule("sun.net.www.protocol.https"));
  103. /**
  104. * Encodes a String for a Properties file. Escapes the ':' and '=' characters.
  105. *
  106. * @param raw the raw properties string to encode
  107. * @return the properties-encoded version of the input string
  108. */
  109. @VisibleForTesting
  110. static String propertiesEncode(final String raw) {
  111. if (raw == null) {
  112. return null;
  113. }
  114. // AS: this looks like a bug to me - check the unit test I wrote for this method.
  115. // However it only affects some replacements that allegedly apply only to Fecru,
  116. // and there's no sign of the replaced strings in the Fecru project, so in reality
  117. // it might not be affecting anything. Such escaping would only be necessary in a
  118. // property key anyway (not in a property value).
  119. return raw
  120. .replaceAll(":", "\\:")
  121. .replaceAll("=", "\\=");
  122. }
  123. protected final Log log;
  124. protected final MavenContext context;
  125. protected final MavenGoals goals;
  126. protected final MavenProject project;
  127. protected final RepositorySystem repositorySystem;
  128. private final ApplicationMapper applicationMapper;
  129. private final ArtifactResolver artifactResolver;
  130. private final PluginProvider pluginProvider;
  131. protected AbstractProductHandler(final MavenContext context, final MavenGoals goals,
  132. final PluginProvider pluginProvider, final RepositorySystem repositorySystem,
  133. final ArtifactResolver artifactResolver) {
  134. this.applicationMapper = new ApplicationMapper(APPLICATION_KEYS);
  135. this.artifactResolver = requireNonNull(artifactResolver);
  136. this.context = requireNonNull(context);
  137. this.goals = requireNonNull(goals);
  138. this.log = requireNonNull(context.getLog());
  139. this.pluginProvider = requireNonNull(pluginProvider);
  140. this.project = requireNonNull(context.getProject());
  141. this.repositorySystem = requireNonNull(repositorySystem);
  142. }
  143. /**
  144. * Copies and creates a zip file of the previous run's home directory minus any installed plugins.
  145. *
  146. * @param homeDirectory The path to the previous run's home directory.
  147. * @param targetZip The path to the final zip file.
  148. * @param product The product
  149. * @since 3.1
  150. */
  151. public void createHomeZip(
  152. @Nonnull final File homeDirectory, @Nonnull final File targetZip, @Nonnull final Product product)
  153. throws MojoExecutionException {
  154. if (!homeDirectory.exists()) {
  155. final String homePath = homeDirectory.getAbsolutePath();
  156. context.getLog().info("home directory doesn't exist, skipping. [" + homePath + "]");
  157. return;
  158. }
  159. try {
  160. // The zip has /someRootFolder/{productId}-home/
  161. final File appDir = getBaseDirectory(product);
  162. final File tmpDir = new File(appDir, "tmp-resources");
  163. final File homeSnapshot = new File(tmpDir, "generated-home");
  164. final String entryBase = "generated-resources/" + product.getId() + "-home";
  165. if (homeSnapshot.exists()) {
  166. deleteDirectory(homeSnapshot);
  167. }
  168. makeDirectories(homeSnapshot);
  169. copyDirectory(homeDirectory, homeSnapshot, true);
  170. cleanupProductHomeForZip(product, homeSnapshot);
  171. ZipUtils.zipDir(targetZip, homeSnapshot, entryBase);
  172. } catch (IOException e) {
  173. throw new IllegalStateException("Error zipping home directory", e);
  174. }
  175. }
  176. /**
  177. * Prepares the home directory for snapshotting. This implementation:
  178. * <ul>
  179. * <li>removes all unnecessary files</li>
  180. * <li>performs product-specific clean-up</li>
  181. * <li>reverses the replacements made earlier in various files, as per {@link #getReplacements(Product, int)}</li>
  182. * <ul>
  183. *
  184. * @param product the product
  185. * @param snapshotDir an image of the home which will be zipped. This is not the working home, so you're free to
  186. * remove files and parametrise them.
  187. * @throws MojoExecutionException if there's a build error
  188. * @throws IOException if there's an I/O error
  189. */
  190. protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
  191. throws MojoExecutionException, IOException {
  192. try {
  193. // we want to get rid of the plugins folders.
  194. // Not used by: fisheye, confluence - Used by: crowd, bamboo, jira
  195. deleteDirectory(new File(snapshotDir, "plugins"));
  196. // Not used by: fisheye, jira - Used by: confluence, crowd, bamboo
  197. deleteDirectory(new File(snapshotDir, "bundled-plugins"));
  198. // Delete the test resources ZIP file, i.e. the homeZip that was used when we started AMPS
  199. getTestResourcesArtifact().ifPresent(testResourcesArtifact -> {
  200. String originalHomeZip = testResourcesArtifact.getArtifactId() + ".zip";
  201. deleteQuietly(new File(snapshotDir, originalHomeZip));
  202. });
  203. undoReplacements(product, snapshotDir);
  204. } catch (IOException ioe) {
  205. throw new MojoExecutionException("Could not delete home/plugins/ and /home/bundled-plugins/", ioe);
  206. }
  207. }
  208. private void undoReplacements(final Product product, final File snapshotDir) throws MojoExecutionException {
  209. final List<Replacement> replacements = getReplacements(product, 0);
  210. sort(replacements);
  211. final List<File> files = getConfigFiles(product, snapshotDir);
  212. replace(files, replacements, true);
  213. }
  214. /**
  215. * Extracts the given product and its home, prepares both and starts the product.
  216. *
  217. * @param product the product to start
  218. * @return the node(s) running the product
  219. */
  220. @Nonnull
  221. @Override
  222. public final List<Node> start(@Nonnull final Product product) throws MojoExecutionException {
  223. final List<File> homeDirs = extractAndProcessHomeDirectories(product);
  224. final File extractedApp = extractApplication(product);
  225. final File finalApp = addArtifactsAndOverrides(product, homeDirs, extractedApp);
  226. addOverridesFromProductPom(product);
  227. // Ask for the system properties (from the ProductHandler and from the pom.xml)
  228. final List<Map<String, String>> systemProperties = mergeSystemProperties(product);
  229. final List<Node> nodes = startProduct(product, finalApp, systemProperties);
  230. if (useBackdoorToInstallLicense()) {
  231. new LicenseInstaller(log).installLicense(product, nodes.get(0).getWebPort());
  232. }
  233. return nodes;
  234. }
  235. /**
  236. * Indicates whether this product handler uses the license backdoor plugin to install any user-configured license.
  237. * This implementation returns {@code true}. Subclasses should return {@code false} if they install the license
  238. * some other way, for example by editing the product's configuration files.
  239. *
  240. * @return see description
  241. */
  242. protected boolean useBackdoorToInstallLicense() {
  243. return true;
  244. }
  245. /**
  246. * Overrides the product context with properties inherited from the product POM. This implementation does nothing.
  247. *
  248. * @param product the product
  249. * @throws MojoExecutionException thrown during creating effective POM
  250. */
  251. protected void addOverridesFromProductPom(Product product) throws MojoExecutionException {
  252. }
  253. @Override
  254. @Nonnull
  255. public String getDefaultContainerId() {
  256. return DEFAULT_CONTAINER;
  257. }
  258. @Override
  259. @Nonnull
  260. public String getDefaultContainerId(@Nonnull final Product product) throws MojoExecutionException {
  261. return ProductContainerVersionMapper.containerForProductVersion(getId(), resolveVersion(product));
  262. }
  263. private String resolveVersion(final Product product) throws MojoExecutionException {
  264. String version = product.getVersion();
  265. if (isBlank(version)) {
  266. version = RELEASE_VERSION;
  267. }
  268. if (RELEASE_VERSION.equals(version) || LATEST_VERSION.equals(version)) {
  269. ProductArtifact productArtifact = getArtifact();
  270. Artifact warArtifact = repositorySystem.createProjectArtifact(
  271. productArtifact.getGroupId(), productArtifact.getArtifactId(), version);
  272. version = product.getArtifactRetriever().getLatestStableVersion(warArtifact);
  273. }
  274. return version;
  275. }
  276. @Override
  277. @Nonnull
  278. public String getDefaultContextPath() {
  279. return "/" + getId();
  280. }
  281. @Override
  282. @Nonnull
  283. public List<File> getHomeDirectories(@Nonnull final Product product) {
  284. final List<Node> nodes = product.getNodes();
  285. if (isBlank(product.getDataHome())) {
  286. // No dataHome configured => use the default home location(s)
  287. final List<File> homeDirectories = new ArrayList<>();
  288. for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
  289. // For backward-compatibility with single-node operation, use no suffix when only one node
  290. final String directorySuffix = nodeIndex == 0 ? "" : "-" + nodeIndex;
  291. homeDirectories.add(new File(getBaseDirectory(product), "home" + directorySuffix));
  292. }
  293. return homeDirectories;
  294. }
  295. switch (nodes.size()) {
  296. case 0:
  297. throw new IllegalStateException("Must be at least one node");
  298. case 1:
  299. // Single node => use the configured product-level home directory
  300. return singletonList(new File(product.getDataHome()));
  301. default:
  302. throw new UnsupportedOperationException(
  303. "You cannot specify a custom <dataHome> directory when multiple nodes are being started");
  304. }
  305. }
  306. private List<File> extractAndProcessHomeDirectories(final Product product) throws MojoExecutionException {
  307. final List<File> homeDirectories = getHomeDirectories(product);
  308. final List<Node> nodes = product.getNodes();
  309. // Check whether the user specified the home directory
  310. if (nodes.size() == 1 && isNotBlank(product.getDataHome())) {
  311. // We're in single-node mode; use the one provided home as-is
  312. return homeDirectories;
  313. }
  314. final File productHomeData = getProductHomeData(product);
  315. if (productHomeData != null) {
  316. for (int i = 0; i < nodes.size(); i++) {
  317. final File homeDirectory = homeDirectories.get(i); // should be the same size as node list
  318. extractAndProcessHomeDirectory(product, productHomeData, homeDirectory, i);
  319. }
  320. }
  321. return homeDirectories;
  322. }
  323. private void extractAndProcessHomeDirectory(
  324. final Product product, final File productHomeData, final File homeDirectory, final int nodeIndex)
  325. throws MojoExecutionException {
  326. if (!homeDirectory.exists()) {
  327. extractProductHomeData(productHomeData, homeDirectory, product);
  328. makeDirectory(homeDirectory);
  329. processHomeDirectory(product, nodeIndex, homeDirectory);
  330. }
  331. overrideAndPatchHomeDir(homeDirectory, product);
  332. }
  333. @Nullable
  334. private File getProductHomeData(final Product product) throws MojoExecutionException {
  335. final String dataPath = product.getDataPath();
  336. if (isNotBlank(dataPath)) {
  337. // Use custom data path (to a ZIP or directory)
  338. final File dataPathAsFile = new File(dataPath);
  339. if (dataPathAsFile.exists()) {
  340. return dataPathAsFile;
  341. }
  342. throw new MojoExecutionException("Unable to use custom test resources set by <dataPath>. '" +
  343. dataPathAsFile.getAbsolutePath() + "' does not exist");
  344. }
  345. File productHomeZip = null;
  346. // No custom data path, so we use the default
  347. final Optional<ProductArtifact> maybeTestResourcesArtifact = getTestResourcesArtifact();
  348. if (maybeTestResourcesArtifact.isPresent()) {
  349. final ProductArtifact testResourcesArtifact = maybeTestResourcesArtifact.get();
  350. // Make sure we have the latest if needed
  351. if (isBlank(product.getDataVersion()) || RELEASE_VERSION.equals(product.getDataVersion()) ||
  352. LATEST_VERSION.equals(product.getDataVersion())) {
  353. setLatestDataVersion(product, testResourcesArtifact);
  354. }
  355. final ProductArtifact testResources = new ProductArtifact(
  356. testResourcesArtifact.getGroupId(), testResourcesArtifact.getArtifactId(), product.getDataVersion());
  357. productHomeZip = goals.copyZip(
  358. getBaseDirectory(product), testResources, testResources.getArtifactId() + ".zip");
  359. }
  360. return productHomeZip;
  361. }
  362. private void setLatestDataVersion(final Product product, final ProductArtifact testResourcesArtifact)
  363. throws MojoExecutionException {
  364. log.info("determining latest stable data version...");
  365. final Artifact dataArtifact = repositorySystem.createProjectArtifact(
  366. testResourcesArtifact.getGroupId(), testResourcesArtifact.getArtifactId(), product.getDataVersion());
  367. final String stableVersion = product.getArtifactRetriever().getLatestStableVersion(dataArtifact);
  368. log.info("using latest stable data version: " + stableVersion);
  369. testResourcesArtifact.setVersion(stableVersion);
  370. product.setDataVersion(stableVersion);
  371. }
  372. private void extractProductHomeData(final File productHomeData, final File homeDir, final Product product)
  373. throws MojoExecutionException {
  374. final File tmpDir = new File(getBaseDirectory(product), "tmp-resources");
  375. makeDirectory(tmpDir);
  376. try {
  377. if (productHomeData.isFile()) {
  378. final File tmp = new File(getBaseDirectory(product), product.getId() + "-home");
  379. unzip(productHomeData, tmpDir.getPath());
  380. final File rootDir = getRootDir(tmpDir, product);
  381. copyDirectory(rootDir, getBaseDirectory(product), true);
  382. moveDirectory(tmp, homeDir);
  383. } else if (productHomeData.isDirectory()) {
  384. copyDirectory(productHomeData, homeDir, true);
  385. }
  386. } catch (final IOException ex) {
  387. throw new MojoExecutionException("Unable to copy home directory", ex);
  388. }
  389. }
  390. private void overrideAndPatchHomeDir(final File homeDir, final Product product) throws MojoExecutionException {
  391. File srcDir = null;
  392. String overridesPath = product.getDataOverridesPath();
  393. if (isNotBlank(overridesPath)) {
  394. srcDir = new File(overridesPath);
  395. if (!srcDir.isDirectory()) {
  396. srcDir = new File(project.getBasedir(), overridesPath);
  397. }
  398. }
  399. if (srcDir == null || !srcDir.isDirectory()) {
  400. srcDir = new File(project.getBasedir(), "src/test/resources/" + product.getInstanceId() + "-home");
  401. }
  402. try {
  403. if (srcDir.exists() && homeDir.exists()) {
  404. copyDirectory(srcDir, homeDir, false);
  405. }
  406. } catch (IOException e) {
  407. throw new MojoExecutionException("Unable to override files using " + srcDir.getAbsolutePath(), e);
  408. }
  409. }
  410. /**
  411. * Returns the root directory of the test data for the given product. This implementation expects the given
  412. * directory to contain a single sub-directory, and returns it as the root directory.
  413. *
  414. * @param tmpDir the directory in which to look for the root directory
  415. * @param product the product being run
  416. * @return the test data root directory
  417. * @throws MojoExecutionException if there's not exactly one entry in the given directory
  418. * @throws IOException if an I/O error occurs
  419. */
  420. @Nonnull
  421. protected File getRootDir(final File tmpDir, final Product product) throws MojoExecutionException, IOException {
  422. final File[] topLevelFiles = tmpDir.listFiles();
  423. if (topLevelFiles == null) {
  424. throw new MojoExecutionException("Could not read files in " + tmpDir);
  425. }
  426. switch (topLevelFiles.length) {
  427. case 0:
  428. throw new MojoExecutionException("No files in " + tmpDir);
  429. case 1:
  430. return topLevelFiles[0]; // happy path
  431. default:
  432. final String filenames = stream(topLevelFiles)
  433. .map(File::getName)
  434. .collect(joining(", "));
  435. throw new MojoExecutionException(
  436. "Expected a single top-level directory in test resources, but found: " + filenames);
  437. }
  438. }
  439. /**
  440. * Takes 'app' (the file of the application - either .war or the exploded directory),
  441. * adds the artifacts, then returns the 'app'.
  442. *
  443. * @param product the product
  444. * @param homeDirs the product's home directory(s)
  445. * @param productFile the product directory or artifact
  446. * @return if {@code app} was a dir, returns it; if {@code app} was a WAR, returns that
  447. * @throws MojoExecutionException if something goes wrong
  448. */
  449. private File addArtifactsAndOverrides(final Product product, final List<File> homeDirs, final File productFile)
  450. throws MojoExecutionException {
  451. try {
  452. final File productDir;
  453. if (productFile.isFile()) {
  454. productDir = new File(getBaseDirectory(product), "webapp");
  455. if (!productDir.exists()) {
  456. unzip(productFile, productDir.getAbsolutePath());
  457. }
  458. } else {
  459. productDir = productFile;
  460. }
  461. for (final File homeDir : homeDirs) {
  462. addArtifacts(product, homeDir, productDir);
  463. overrideWarFiles(product, homeDir, productDir);
  464. }
  465. if (productFile.isFile()) {
  466. final File warFile = new File(productFile.getParentFile(), getId() + ".war");
  467. zipChildren(warFile, productDir);
  468. return warFile;
  469. } else {
  470. return productDir;
  471. }
  472. } catch (final Exception e) {
  473. throw new MojoExecutionException(e.getMessage(), e);
  474. }
  475. }
  476. private void overrideWarFiles(final Product product, final File homeDir, final File appDir)
  477. throws MojoExecutionException {
  478. try {
  479. addOverrides(appDir, product);
  480. customiseInstance(product, homeDir, appDir);
  481. fixJvmArgs(product);
  482. } catch (IOException e) {
  483. throw new MojoExecutionException(
  484. "Unable to override WAR files using src/test/resources/" + product.getInstanceId() + "-app", e);
  485. }
  486. }
  487. /**
  488. * Each product handler can add specific operations on the application's home and war.
  489. * By default no operation is performed in this hook.
  490. *
  491. * @param product the product
  492. * @param homeDir the home directory
  493. * @param explodedWarDir the directory containing the product's exploded WAR
  494. * @throws MojoExecutionException if customisation fails
  495. */
  496. protected void customiseInstance(Product product, File homeDir, File explodedWarDir) throws MojoExecutionException {
  497. // No operation by default
  498. }
  499. /**
  500. * Fix jvmArgs, providing necessary defaults.
  501. *
  502. * @param product product for which to fix jvmArgs
  503. */
  504. protected void fixJvmArgs(final Product product) {
  505. final String jvmArgs = JvmArgsFix.defaults()
  506. .apply(product.getJvmArgs());
  507. product.setJvmArgs(jvmArgs);
  508. }
  509. private void addArtifacts(final Product product, final File homeDir, final File appDir)
  510. throws IOException, MojoExecutionException {
  511. File pluginsDir = getUserInstalledPluginsDirectory(product, appDir, homeDir).orElse(null);
  512. File bundledPluginsDir = new File(getBaseDirectory(product), "bundled-plugins");
  513. makeDirectory(bundledPluginsDir);
  514. // add bundled plugins
  515. final File bundledPluginsFile = getBundledPluginPath(product, appDir);
  516. if (bundledPluginsFile.exists()) {
  517. if (bundledPluginsFile.isDirectory()) {
  518. bundledPluginsDir = bundledPluginsFile;
  519. } else {
  520. unzip(bundledPluginsFile, bundledPluginsDir.getPath());
  521. }
  522. }
  523. if (isStaticPlugin()) {
  524. if (!supportsStaticPlugins()) {
  525. throw new MojoExecutionException("According to your atlassian-plugin.xml file, this plugin is not " +
  526. "atlassian-plugins version 2. This app currently only supports atlassian-plugins " +
  527. "version 2.");
  528. }
  529. pluginsDir = new File(appDir, "WEB-INF/lib");
  530. }
  531. if (pluginsDir == null) {
  532. pluginsDir = bundledPluginsDir;
  533. }
  534. createDirectory(pluginsDir);
  535. // add this plugin itself, if enabled
  536. if (Boolean.TRUE.equals(product.isInstallPlugin())) {
  537. addThisPluginToDirectory(pluginsDir);
  538. addTestPluginToDirectory(pluginsDir);
  539. }
  540. // add plugins2 plugins if necessary
  541. if (!isStaticPlugin()) {
  542. addArtifactsToDirectory(pluginProvider.provide(product), pluginsDir);
  543. }
  544. // add lib artifacts
  545. addArtifactsToDirectory(product.getLibArtifacts(), new File(appDir, getLibArtifactTargetDir()));
  546. // add plugins provided by applications
  547. final List<ProductArtifact> applications = applicationMapper.provideApplications(product);
  548. extractApplicationPlugins(applications, pluginsDir);
  549. final List<ProductArtifact> plugins = new ArrayList<>();
  550. plugins.addAll(product.getBundledArtifacts());
  551. plugins.addAll(getAdditionalPlugins(product));
  552. if (product.hasUserConfiguredLicense() && useBackdoorToInstallLicense()) {
  553. // Install the license backdoor, so that we can replace the default license with the user-provided one
  554. plugins.add(LICENSE_BACKDOOR_PLUGIN);
  555. }
  556. addArtifactsToDirectory(plugins, bundledPluginsDir);
  557. final String[] bundledPlugins = bundledPluginsDir.list();
  558. if (bundledPlugins != null && bundledPlugins.length > 0 && !bundledPluginsFile.isDirectory()) {
  559. zipChildren(bundledPluginsFile, bundledPluginsDir);
  560. }
  561. if (product.getLog4jProperties() != null) {
  562. final Optional<String> log4jPropertiesPath = getLog4jPropertiesPath();
  563. if (log4jPropertiesPath.isPresent()) { // no lambda because exception thrown
  564. copyFile(product.getLog4jProperties(), new File(appDir, log4jPropertiesPath.get()));
  565. }
  566. }
  567. }
  568. /**
  569. * Hook for subclasses to read/modify the contents of the home directory before starting the product.
  570. *
  571. * This implementation makes all the replacements specified by {@link #getReplacements} to the configuration files
  572. * specified by {@link #getConfigFiles(Product, File)}.
  573. *
  574. * @param product the product being started
  575. * @param nodeIndex the zero-based index of the node being started
  576. * @param homeDir the node's home directory
  577. */
  578. protected void processHomeDirectory(final Product product, final int nodeIndex, final File homeDir)
  579. throws MojoExecutionException {
  580. replace(getConfigFiles(product, homeDir), getReplacements(product, nodeIndex), false);
  581. }
  582. /**
  583. * List the configuration files. Used when doing a snapshot to reopen on another
  584. * machine, with different port, context path, path, instanceId
  585. * <p/>
  586. * Files returned by this method are guaranteed to be reversed when creating the home zip.
  587. *
  588. * @param product the product
  589. * @param snapshotDir A snapshot equivalent to the home in most cases. It is a copy of the folder returned by
  590. * {@link #getSnapshotDirectories(Product)}
  591. * @return a mutable list of files
  592. */
  593. @Nonnull
  594. protected List<File> getConfigFiles(@Nonnull final Product product, @Nonnull final File snapshotDir) {
  595. return new ArrayList<>();
  596. }
  597. @Nonnull
  598. @Override
  599. public File getBaseDirectory(@Nonnull final Product ctx) {
  600. return ProjectUtils.createDirectory(new File(project.getBuild().getDirectory(), ctx.getInstanceId()));
  601. }
  602. /**
  603. * Lists parameters which must be replaced in the configuration files of the home directory.
  604. * <p/>
  605. * Replacements returned by this method are guaranteed to be reversed when creating the home zip.
  606. *
  607. * @param product the product
  608. * @param nodeIndex the zero-based node index
  609. * @return a mutable list of replacements
  610. */
  611. @Nonnull
  612. protected List<Replacement> getReplacements(@Nonnull final Product product, final int nodeIndex) {
  613. // Standard replacements:
  614. final List<Replacement> replacements = new ArrayList<>();
  615. final String buildDirectory = project.getBuild().getDirectory();
  616. final String baseDirectory = getBaseDirectory(product).getAbsolutePath();
  617. final Optional<File> homeDirectory = getOnlyHomeDirectory(product);
  618. replacements.add(new Replacement("%PROJECT_BUILD_DIR%", buildDirectory));
  619. replacements.add(new Replacement("%PRODUCT_BASE_DIR%", baseDirectory));
  620. homeDirectory.ifPresent(homeDir ->
  621. replacements.add(new Replacement("%PRODUCT_HOME_DIR%", homeDir.getAbsolutePath())));
  622. // These replacements are especially for Fecru, but there's no reason not to find them in other config files
  623. replacements.add(replaceDirectory("%PROJECT_BUILD_DIR_URL_ENCODED%", buildDirectory));
  624. replacements.add(replaceDirectory("%PRODUCT_BASE_DIR_URL_ENCODED%", baseDirectory));
  625. homeDirectory.ifPresent(file ->
  626. replacements.add(replaceDirectory("%PRODUCT_HOME_DIR_URL_ENCODED%", file.getAbsolutePath())));
  627. replacements.add(onlyWhenUnzipping("localhost", product.getServer()));
  628. try {
  629. final String localHostName = InetAddress.getLocalHost().getHostName();
  630. replacements.add(new Replacement("%LOCAL_HOST_NAME%", localHostName));
  631. } catch (UnknownHostException e) {
  632. // If we can't get the local computer's hostname, it's probable no product could,
  633. // so we don't need to search-replace the value.
  634. }
  635. return replacements;
  636. }
  637. private static Replacement replaceDirectory(final String placeholder, final String directory) {
  638. final String encoding = StandardCharsets.UTF_8.name();
  639. try {
  640. return new Replacement(placeholder, encode(propertiesEncode(directory), encoding));
  641. } catch (UnsupportedEncodingException e) {
  642. throw new IllegalStateException(encoding + " should be supported on any JVM", e);
  643. }
  644. }
  645. private Optional<File> getOnlyHomeDirectory(final Product product) {
  646. final List<File> homeDirectories = getHomeDirectories(product);
  647. if (homeDirectories.size() == 1) {
  648. return Optional.of(homeDirectories.get(0));
  649. }
  650. return empty();
  651. }
  652. @Nonnull
  653. @Override
  654. public final List<File> getSnapshotDirectories(@Nonnull Product product) {
  655. return getHomeDirectories(product);
  656. }
  657. /**
  658. * Extracts (or copies) the program files for the given product.
  659. *
  660. * @param product the product for which to extract the program files
  661. * @return the extracted/copied file or directory
  662. * @throws MojoExecutionException if execution fails
  663. */
  664. @Nonnull
  665. protected abstract File extractApplication(Product product) throws MojoExecutionException;
  666. /**
  667. * Starts the given product.
  668. *
  669. * @param product the product to start
  670. * @param productFile the product's own root directory or artifact
  671. * @param systemProperties any system properties to be passed to the product (one map per node)
  672. * @return the node(s) on which the product is running; a non-empty list
  673. * @throws MojoExecutionException if the operation fails
  674. */
  675. @Nonnull
  676. protected abstract List<Node> startProduct(
  677. Product product, File productFile, List<Map<String, String>> systemProperties)
  678. throws MojoExecutionException;
  679. /**
  680. * Indicates whether this product supports static plugins.
  681. *
  682. * @return see description
  683. */
  684. protected abstract boolean supportsStaticPlugins();
  685. /**
  686. * Returns the bundled plugin path for the given product.
  687. *
  688. * @param product the product
  689. * @param productDir the directory in which the product is installed
  690. * @return see description
  691. */
  692. @Nonnull
  693. protected abstract File getBundledPluginPath(Product product, File productDir);
  694. /**
  695. * Returns the directory in which user-installed plugins should be placed.
  696. *
  697. * @param product the product
  698. * @param webappDir the product's webapp directory
  699. * @param homeDir the home directory
  700. * @return empty for no such directory
  701. */
  702. @Nonnull
  703. protected abstract Optional<File> getUserInstalledPluginsDirectory(Product product, File webappDir, File homeDir);
  704. /**
  705. * Hook for product handlers to specify additional plugins to be loaded when the product starts. This implementation
  706. * returns an empty list.
  707. *
  708. * @param product the product to receive the plugins
  709. * @return any additional plugins
  710. * @throws MojoExecutionException if execution fails
  711. */
  712. @Nonnull
  713. protected List<ProductArtifact> getAdditionalPlugins(final Product product) throws MojoExecutionException {
  714. return emptyList();
  715. }
  716. /**
  717. * Returns the path to the product's {@code log4j.properties} file.
  718. *
  719. * @return empty if there isn't one
  720. */
  721. @Nonnull
  722. protected Optional<String> getLog4jPropertiesPath() {
  723. return empty();
  724. }
  725. /**
  726. * Indicates whether the current Maven project is a static plugin.
  727. *
  728. * @return see description
  729. * @throws IOException if there is an I/O error
  730. */
  731. protected boolean isStaticPlugin() throws IOException {
  732. final File atlassianPluginXml = new File(project.getBasedir(), "src/main/resources/atlassian-plugin.xml");
  733. if (atlassianPluginXml.exists()) {
  734. final String text = org.apache.commons.io.FileUtils.readFileToString(atlassianPluginXml, StandardCharsets.UTF_8);
  735. return !text.contains("pluginsVersion=\"2\"") && !text.contains("plugins-version=\"2\"");
  736. } else {
  737. // probably an osgi bundle
  738. return false;
  739. }
  740. }
  741. private void addThisPluginToDirectory(final File targetDir) throws IOException {
  742. final File thisPlugin = getPluginFile();
  743. if (thisPlugin.exists()) {
  744. // remove any existing version
  745. final Iterator<File> files = iterateFiles(targetDir, null, false);
  746. while (files.hasNext()) {
  747. final File file = files.next();
  748. if (doesFileNameMatchArtifact(file.getName(), project.getArtifactId())) {
  749. delete(file.toPath());
  750. }
  751. }
  752. // add the plugin jar to the directory
  753. copyFile(thisPlugin, new File(targetDir, thisPlugin.getName()));
  754. } else {
  755. log.info("No plugin in the current project - " + thisPlugin.getAbsolutePath());
  756. }
  757. }
  758. private void addTestPluginToDirectory(final File targetDir) throws IOException {
  759. final File testPluginFile = getTestPluginFile();
  760. if (testPluginFile.exists()) {
  761. // add the test plugin jar to the directory
  762. copyFile(testPluginFile, new File(targetDir, testPluginFile.getName()));
  763. }
  764. }
  765. private File getPluginFile() {
  766. return new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".jar");
  767. }
  768. private File getTestPluginFile() {
  769. return new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + "-tests.jar");
  770. }
  771. private void addArtifactsToDirectory(final List<ProductArtifact> artifacts, final File pluginsDir)
  772. throws MojoExecutionException {
  773. // copy the all the plugins we want in the webapp, first removing plugins from the webapp that we want to update
  774. if (!artifacts.isEmpty() && pluginsDir.isDirectory()) {
  775. listFiles(pluginsDir, null, false).stream()
  776. .filter(File::isFile)
  777. .filter(file -> fileMatchesAnyArtifact(file, artifacts))
  778. .forEach(org.apache.commons.io.FileUtils::deleteQuietly);
  779. goals.copyPlugins(pluginsDir, artifacts);
  780. }
  781. }
  782. private boolean fileMatchesAnyArtifact(final File file, final Collection<ProductArtifact> artifacts) {
  783. return artifacts.stream()
  784. .map(ProductArtifact::getArtifactId)
  785. .anyMatch(artifactId -> doesFileNameMatchArtifact(file.getName(), artifactId));
  786. }
  787. private void extractApplicationPlugins(final List<ProductArtifact> applications, final File bundledPluginsDir)
  788. throws IOException {
  789. for (final ProductArtifact application : applications) {
  790. final File artifact = resolveApplicationArtifact(application).getFile();
  791. log.info("Extracting " + artifact.getAbsolutePath() + " into " + bundledPluginsDir.getAbsolutePath());
  792. unzip(artifact, bundledPluginsDir.getAbsolutePath(), 0, true, Pattern.compile(".*\\.jar"));
  793. log.debug("Extracted.");
  794. }
  795. }
  796. private Artifact resolveApplicationArtifact(final ProductArtifact application) {
  797. final Artifact artifact = repositorySystem.createArtifact(application.getGroupId(), application.getArtifactId(),
  798. application.getVersion(), "compile", "obr");
  799. final ArtifactResolutionResult resolutionResult = resolve(artifact);
  800. if (resolutionResult.isSuccess()) {
  801. return artifact;
  802. }
  803. throw new ArtifactResolutionException(resolutionResult);
  804. }
  805. private ArtifactResolutionResult resolve(final Artifact artifact) {
  806. final ArtifactResolutionRequest artifactResolutionRequest = new ArtifactResolutionRequest();
  807. artifactResolutionRequest.setArtifact(artifact);
  808. final MojoExecutor.ExecutionEnvironment executionEnvironment = context.getExecutionEnvironment();
  809. artifactResolutionRequest.setLocalRepository(executionEnvironment.getMavenSession().getLocalRepository());
  810. artifactResolutionRequest.setRemoteRepositories(
  811. executionEnvironment.getMavenProject().getRemoteArtifactRepositories());
  812. return artifactResolver.resolve(artifactResolutionRequest);
  813. }
  814. private void addOverrides(final File appDir, final Product product) throws IOException {
  815. final File srcDir = new File(project.getBasedir(), "src/test/resources/" + product.getInstanceId() + "-app");
  816. if (srcDir.exists() && appDir.exists()) {
  817. copyDirectory(srcDir, appDir, true);
  818. }
  819. }
  820. /**
  821. * Merges the properties: pom.xml overrides {@code AbstractProductHandlerMojo#setDefaultValues}, which overrides the
  822. * Product Handler.
  823. *
  824. * @param product the Product
  825. * @return the list of complete system property maps (one map per product node)
  826. */
  827. @Nonnull
  828. protected final List<Map<String, String>> mergeSystemProperties(final Product product) {
  829. final List<Map<String, String>> mergedPropertyMaps = new ArrayList<>();
  830. final int nodeCount = product.getNodes().size();
  831. for (int i = 0; i < nodeCount; i++) {
  832. mergedPropertyMaps.add(mergeSystemProperties(product, i));
  833. }
  834. return mergedPropertyMaps;
  835. }
  836. private Map<String, String> mergeSystemProperties(final Product product, final int nodeIndex) {
  837. // Start from the base properties
  838. final Map<String, String> properties = new HashMap<>(getSystemProperties(product, nodeIndex));
  839. // Set the JARs to be skipped when scanning for TLDs and web fragments (read from context.xml by Tomcat)
  840. properties.put("jarsToSkip", getJarsToSkipWhenScanningForTldsAndWebFragments());
  841. // Enter the System Property Variables from product/node context, overwriting duplicates
  842. properties.putAll(product.getSystemPropertiesForNode(nodeIndex));
  843. // Overwrite the default system properties with user input arguments
  844. final Properties userProperties = context.getExecutionEnvironment().getMavenSession().getUserProperties();
  845. userProperties.forEach((key, value) -> properties.put((String) key, (String) value));
  846. return properties;
  847. }
  848. private String getJarsToSkipWhenScanningForTldsAndWebFragments() {
  849. final Set<String> jarsToSkip = new HashSet<>();
  850. jarsToSkip.add("${tomcat.util.scan.StandardJarScanFilter.jarsToSkip}"); // the Tomcat default
  851. jarsToSkip.addAll(getExtraJarsToSkipWhenScanningForTldsAndWebFragments());
  852. return join(",", jarsToSkip);
  853. }
  854. /**
  855. * Products should override this method in order to avoid scanning JARs known not to contain TLDs or web fragments.
  856. * This implementation returns an empty, immutable list.
  857. *
  858. * @return a list of any extra JAR name patterns to skip, e.g. "foo*.jar"
  859. */
  860. @Nonnull
  861. protected Collection<String> getExtraJarsToSkipWhenScanningForTldsAndWebFragments() {
  862. return emptyList();
  863. }
  864. /**
  865. * Returns any system properties that this Product Handler wants to pass to the given node.
  866. *
  867. * @param product the product being started
  868. * @param nodeIndex the zero-based index of the node being started
  869. * @return a list of property maps, one map per node
  870. */
  871. @Nonnull
  872. protected abstract Map<String, String> getSystemProperties(Product product, int nodeIndex);
  873. /**
  874. * Returns the directory into which JARs listed in {@code <libArtifacts>} are copied.
  875. *
  876. * This implementation returns {@value #TOMCAT_LIB_DIR}.
  877. *
  878. * @return the directory where lib artifacts should be written
  879. */
  880. @Nonnull
  881. protected String getLibArtifactTargetDir() {
  882. return TOMCAT_LIB_DIR;
  883. }
  884. /**
  885. * Sets the versions of the arguments to the latest stable version of the product.
  886. *
  887. * @param product the product whose version to set
  888. * @param productArtifact the artifact whose version to set
  889. * @throws MojoExecutionException if execution fails
  890. */
  891. protected final void setLatestStableVersion(final Product product, final ProductArtifact productArtifact)
  892. throws MojoExecutionException {
  893. log.info("determining latest stable product version...");
  894. final Artifact warArtifact = repositorySystem.createProjectArtifact(
  895. productArtifact.getGroupId(), productArtifact.getArtifactId(), productArtifact.getVersion());
  896. final String stableVersion = product.getArtifactRetriever().getLatestStableVersion(warArtifact);
  897. log.info("using latest stable product version: " + stableVersion);
  898. productArtifact.setVersion(stableVersion);
  899. product.setVersion(stableVersion);
  900. }
  901. /**
  902. * Returns the artifact for the given product.
  903. *
  904. * @param product the product for which to create the artifact
  905. * @return a new artifact
  906. */
  907. @Nonnull
  908. protected final ProductArtifact getArtifact(final Product product) {
  909. final ProductArtifact defaultArtifact = getArtifact();
  910. return new ProductArtifact(
  911. firstNotNull(product.getGroupId(), defaultArtifact.getGroupId()),
  912. firstNotNull(product.getArtifactId(), defaultArtifact.getArtifactId()),
  913. firstNotNull(product.getVersion(), defaultArtifact.getVersion()));
  914. }
  915. }