PageRenderTime 43ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/atlassian/amps
Java | 498 lines | 395 code | 56 blank | 47 comment | 35 complexity | 59c86350cd84f2b78f3b5e55913c76bd 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.DataSource;
  3. import com.atlassian.maven.plugins.amps.MavenContext;
  4. import com.atlassian.maven.plugins.amps.MavenGoals;
  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.database.DatabaseType;
  9. import com.atlassian.maven.plugins.amps.database.DatabaseTypeFactory;
  10. import com.atlassian.maven.plugins.amps.product.common.ValidationException;
  11. import com.atlassian.maven.plugins.amps.product.common.XMLDocumentHandler;
  12. import com.atlassian.maven.plugins.amps.product.common.XMLDocumentProcessor;
  13. import com.atlassian.maven.plugins.amps.product.jira.config.DatabaseTypeUpdaterTransformer;
  14. import com.atlassian.maven.plugins.amps.product.jira.config.DbConfigValidator;
  15. import com.atlassian.maven.plugins.amps.product.jira.config.SchemeUpdaterTransformer;
  16. import com.atlassian.maven.plugins.amps.product.manager.WebAppManager;
  17. import com.atlassian.maven.plugins.amps.util.ConfigFileUtils.Replacement;
  18. import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
  19. import com.google.common.annotations.VisibleForTesting;
  20. import com.google.common.collect.ImmutableList;
  21. import com.google.common.collect.ImmutableMap;
  22. import org.apache.commons.io.IOUtils;
  23. import org.apache.commons.lang3.StringUtils;
  24. import org.apache.maven.artifact.resolver.ArtifactResolver;
  25. import org.apache.maven.artifact.versioning.ComparableVersion;
  26. import org.apache.maven.plugin.MojoExecutionException;
  27. import org.apache.maven.repository.RepositorySystem;
  28. import javax.annotation.Nonnull;
  29. import java.io.File;
  30. import java.io.FileInputStream;
  31. import java.io.IOException;
  32. import java.io.InputStream;
  33. import java.util.ArrayList;
  34. import java.util.Collection;
  35. import java.util.List;
  36. import java.util.Map;
  37. import java.util.Optional;
  38. import java.util.Properties;
  39. import java.util.Set;
  40. import static com.atlassian.maven.plugins.amps.util.ConfigFileUtils.RegexReplacement;
  41. import static com.atlassian.maven.plugins.amps.util.FileUtils.fixWindowsSlashes;
  42. import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
  43. import static com.atlassian.maven.plugins.amps.util.PropertyUtils.storeProperties;
  44. import static com.atlassian.maven.plugins.amps.util.VersionUtils.getVersion;
  45. import static java.lang.String.format;
  46. import static java.nio.charset.StandardCharsets.UTF_8;
  47. import static java.nio.file.Files.createDirectories;
  48. import static java.util.Arrays.asList;
  49. import static java.util.Collections.unmodifiableList;
  50. import static java.util.Objects.requireNonNull;
  51. import static java.util.stream.Collectors.toSet;
  52. import static org.apache.commons.io.FileUtils.deleteQuietly;
  53. import static org.apache.commons.io.FileUtils.writeStringToFile;
  54. import static org.apache.commons.lang3.StringUtils.isBlank;
  55. public class JiraProductHandler extends AbstractWebappProductHandler {
  56. @VisibleForTesting
  57. static final String INSTALLED_PLUGINS_DIR = "installed-plugins";
  58. @VisibleForTesting
  59. static final String PLUGINS_DIR = "plugins";
  60. @VisibleForTesting
  61. static final String BUNDLED_PLUGINS_UNZIPPED = "WEB-INF/atlassian-bundled-plugins";
  62. @VisibleForTesting
  63. static final String BUNDLED_PLUGINS_FROM_4_1 = "WEB-INF/classes/atlassian-bundled-plugins.zip";
  64. @VisibleForTesting
  65. static final String BUNDLED_PLUGINS_UPTO_4_0 = "WEB-INF/classes/com/atlassian/jira/plugin/atlassian-bundled-plugins.zip";
  66. @VisibleForTesting
  67. static final String FILENAME_DBCONFIG = "dbconfig.xml";
  68. private static final String JIRADS_PROPERTIES_FILE = "JiraDS.properties";
  69. private static final String JIRA_HOME_PLACEHOLDER = "${jirahome}";
  70. private static final String SERVER_ID_PATTERN = "'[A-B]{1}[A-Z0-9]{3}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}'";
  71. public JiraProductHandler(
  72. final MavenContext context, final MavenGoals goals, final RepositorySystem repositorySystem,
  73. final ArtifactResolver artifactResolver, final WebAppManager webAppManager) {
  74. super(context, goals, new JiraPluginProvider(), repositorySystem, artifactResolver, webAppManager);
  75. }
  76. // -------------------- Simple value-returning methods ---------------------
  77. @Override
  78. @Nonnull
  79. public String getId() {
  80. return "jira";
  81. }
  82. @Override
  83. @Nonnull
  84. public String getDefaultContainerId() {
  85. // However note that recent versions of Jira specify their own Tomcat container version, see
  86. // AbstractWebappProductHandler.addOverridesFromProductPom for how this works.
  87. return "tomcat7x";
  88. }
  89. @Override
  90. public int getDefaultHttpPort() {
  91. return 2990;
  92. }
  93. @Override
  94. public int getDefaultHttpsPort() {
  95. return 8442;
  96. }
  97. @Nonnull
  98. @Override
  99. public ProductArtifact getArtifact() {
  100. return new ProductArtifact("com.atlassian.jira", "atlassian-jira-webapp", "RELEASE");
  101. }
  102. @Nonnull
  103. @Override
  104. public Optional<ProductArtifact> getTestResourcesArtifact() {
  105. return Optional.of(new ProductArtifact("com.atlassian.jira.plugins", "jira-plugin-test-resources"));
  106. }
  107. @Nonnull
  108. @Override
  109. protected Collection<String> getExtraJarsToSkipWhenScanningForTldsAndWebFragments() {
  110. // Fixes AMPS-1429 by skipping these JARs
  111. return ImmutableList.of("jotm*.jar", "xapool*.jar");
  112. }
  113. // --------- Non-trivial logic starts here, generally in the order called by the superclass ---------
  114. @Override
  115. protected void processHomeDirectory(final Product product, final int nodeIndex, final File homeDir)
  116. throws MojoExecutionException {
  117. super.processHomeDirectory(product, nodeIndex, homeDir);
  118. if (product.isMultiNode()) {
  119. if (nodeIndex == 0) {
  120. // Only for node 0 because we only need to do it once per cluster
  121. setUpSharedHome(product, homeDir.getParent());
  122. }
  123. createClusterPropertiesFile(product, nodeIndex, homeDir);
  124. }
  125. createDbConfigXmlIfNone(homeDir);
  126. updateDbConfigXmlFromDataSource(product, homeDir);
  127. }
  128. @Nonnull
  129. @Override
  130. protected List<File> getConfigFiles(@Nonnull final Product product, @Nonnull final File homeDir) {
  131. final List<File> configFiles = super.getConfigFiles(product, homeDir);
  132. configFiles.add(new File(homeDir, "database.log"));
  133. configFiles.add(new File(homeDir, "database.script"));
  134. configFiles.add(new File(homeDir, FILENAME_DBCONFIG));
  135. return configFiles;
  136. }
  137. @Nonnull
  138. @Override
  139. protected List<Replacement> getReplacements(@Nonnull final Product product, final int nodeIndex) {
  140. String contextPath = product.getContextPath();
  141. if (!contextPath.startsWith("/")) {
  142. contextPath = "/" + contextPath;
  143. }
  144. // This is similar to BaseUrlUtils.getBaseUrl, except that it respects the product's protocol
  145. final String baseUrl = product.getProtocol() + "://" + product.getServer() + ":" +
  146. product.getWebPortForNode(nodeIndex) + contextPath;
  147. final List<Replacement> replacements = super.getReplacements(product, nodeIndex);
  148. // We don't re-wrap snapshots with these values:
  149. replacements.add(0, new Replacement("http://localhost:8080", baseUrl, false));
  150. replacements.add(new Replacement("@project-dir@", getProjectDir(product), false));
  151. replacements.add(new Replacement("/jira-home/", "/home/", false));
  152. replacements.add(new Replacement("@base-url@", baseUrl, false));
  153. replacements.add(new RegexReplacement(SERVER_ID_PATTERN, "''"));
  154. return replacements;
  155. }
  156. @Nonnull
  157. @Override
  158. protected Optional<File> getUserInstalledPluginsDirectory(
  159. final Product product, final File webappDir, final File homeDir) {
  160. final File pluginHomeDirectory = getPluginHomeDirectory(product, homeDir);
  161. return Optional.of(new File(new File(pluginHomeDirectory, PLUGINS_DIR), INSTALLED_PLUGINS_DIR));
  162. }
  163. @Nonnull
  164. @Override
  165. protected File getBundledPluginPath(final Product product, final File productDir) {
  166. // the zip became a directory in 6.3, so if the directory exists and is a directory, use it,
  167. // otherwise fallback to the old behaviour.
  168. final File bundleDir = new File(productDir, BUNDLED_PLUGINS_UNZIPPED);
  169. if (bundleDir.exists() && bundleDir.isDirectory()) {
  170. return bundleDir;
  171. } else {
  172. // this location used from 4.1 onwards (inclusive), until replaced by unzipped dir.
  173. String bundledPluginPluginsPath = BUNDLED_PLUGINS_FROM_4_1;
  174. String[] version = product.getVersion().split("-", 2)[0].split("\\.");
  175. try {
  176. long major = Long.parseLong(version[0]);
  177. long minor = (version.length > 1) ? Long.parseLong(version[1]) : 0;
  178. if (major < 4 || major == 4 && minor == 0) {
  179. bundledPluginPluginsPath = BUNDLED_PLUGINS_UPTO_4_0;
  180. }
  181. } catch (NumberFormatException e) {
  182. log.debug(format("Unable to parse Jira version '%s', assuming Jira 4.1 or newer.", product.getVersion()), e);
  183. }
  184. return new File(productDir, bundledPluginPluginsPath);
  185. }
  186. }
  187. @Override
  188. protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir) {
  189. // Jira 7.12.x has new tomcat version which requires additional characters to be whitelisted
  190. if (new ComparableVersion(product.getVersion()).compareTo(new ComparableVersion("7.12.0")) >= 0) {
  191. product.setCargoXmlOverrides(serverXmlJiraOverride());
  192. }
  193. }
  194. @Override
  195. protected void fixJvmArgs(final Product product) {
  196. final JvmArgsFix argsFix = JvmArgsFix.empty()
  197. .withAddOpens(ADD_OPENS_FOR_TOMCAT)
  198. .withAddOpens(ADD_OPENS_FOR_FELIX);
  199. final ComparableVersion productVersion = new ComparableVersion(product.getVersion());
  200. // Jira 8 raises memory requirements, to account for increased memory usage by Lucene
  201. if (productVersion.compareTo(new ComparableVersion("8.0.0-ALPHA")) >= 0) {
  202. product.setJvmArgs(argsFix
  203. .with("-Xmx", "2g")
  204. .with("-Xms", "1g")
  205. .apply(product.getJvmArgs()));
  206. }
  207. // In Jira 7.7+ we have a HealthCheck that requires min / max memory to be set to a certain minimums or it can block startup.
  208. else if (productVersion.compareTo(new ComparableVersion("7.7.0-ALPHA")) >= 0) {
  209. product.setJvmArgs(argsFix
  210. .with("-Xmx", "768m")
  211. .with("-Xms", "384m")
  212. .apply(product.getJvmArgs()));
  213. } else {
  214. super.fixJvmArgs(product);
  215. }
  216. }
  217. @Override
  218. protected DataSource getDefaultDataSource(final Product product) {
  219. return getDataSourceFromJiraDSFile(product).orElse(getHsqlDataSource(product));
  220. }
  221. @Override
  222. @Nonnull
  223. protected Map<String, String> getProductSpecificSystemProperties(final Product product, final int nodeIndex) {
  224. final ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
  225. final String homeDirectory = fixWindowsSlashes(getHomeDirectories(product).get(nodeIndex).getPath());
  226. properties.put("jira.home", homeDirectory);
  227. properties.put("cargo.servlet.uriencoding", "UTF-8");
  228. if (product.isAwaitFullInitialization()) {
  229. properties.put("com.atlassian.jira.startup.LauncherContextListener.SYNCHRONOUS", "true");
  230. }
  231. return properties.build();
  232. }
  233. @Override
  234. @Nonnull
  235. protected List<ProductArtifact> getExtraContainerDependencies() {
  236. return asList(
  237. new ProductArtifact("hsqldb", "hsqldb", "1.8.0.5"),
  238. new ProductArtifact("javax.transaction", "jta", "1.0.1B"),
  239. new ProductArtifact("ots-jts", "ots-jts", "1.0"),
  240. // for data source and transaction manager providers
  241. new ProductArtifact("jotm", "jotm", "1.4.3"),
  242. new ProductArtifact("jotm", "jotm-jrmp_stubs", "1.4.3"),
  243. new ProductArtifact("jotm", "jotm-iiop_stubs", "1.4.3"),
  244. new ProductArtifact("jotm", "jonas_timer", "1.4.3"),
  245. new ProductArtifact("jotm", "objectweb-datasource", "1.4.3"),
  246. new ProductArtifact("carol", "carol", "1.5.2"),
  247. new ProductArtifact("carol", "carol-properties", "1.0"),
  248. new ProductArtifact("xapool", "xapool", "1.3.1"),
  249. new ProductArtifact("commons-logging", "commons-logging", "1.1.1")
  250. );
  251. }
  252. @Override
  253. protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
  254. throws MojoExecutionException, IOException {
  255. super.cleanupProductHomeForZip(product, snapshotDir);
  256. deleteQuietly(new File(snapshotDir, "log/atlassian-jira.log"));
  257. }
  258. // ------------ Mostly private helper methods --------------
  259. private void updateDbConfigXmlFromDataSource(final Product product, final File homeDir)
  260. throws MojoExecutionException {
  261. if (product.getDataSources().size() == 1) {
  262. final DataSource ds = product.getDataSources().get(0);
  263. final DatabaseType dbType = new DatabaseTypeFactory(log).getDatabaseType(ds).orElseThrow(
  264. () -> new MojoExecutionException("Could not find database type for " + ds));
  265. updateDbConfigXml(homeDir, dbType, ds.getSchema());
  266. } else if (product.getDataSources().size() > 1) {
  267. throw new MojoExecutionException("Jira does not support multiple data sources");
  268. }
  269. }
  270. private static void setUpSharedHome(final Product product, final String parent) throws MojoExecutionException {
  271. final File sharedHome;
  272. if (isBlank(product.getSharedHome())) {
  273. sharedHome = new File(parent, "shared-home");
  274. product.setSharedHome(sharedHome.getAbsolutePath());
  275. } else {
  276. sharedHome = new File(product.getSharedHome());
  277. if (sharedHome.isFile()) {
  278. final String error =
  279. format("The specified shared home '%s' is a file, not a directory", product.getSharedHome());
  280. throw new MojoExecutionException(error);
  281. }
  282. }
  283. try {
  284. createDirectories(sharedHome.toPath());
  285. } catch (IOException e) {
  286. throw new MojoExecutionException("Could not create shared home " + sharedHome, e);
  287. }
  288. }
  289. // See https://confluence.atlassian.com/jirakb/how-to-move-the-shared-home-folder-in-jira-data-center-1044112948.html
  290. private static void createClusterPropertiesFile(final Product product, final int nodeIndex, final File homeDir)
  291. throws MojoExecutionException {
  292. final File clusterPropertiesFile = new File(homeDir, "cluster.properties");
  293. if (!clusterPropertiesFile.isFile()) {
  294. // The home ZIP didn't contain a cluster.properties file (unsurprisingly); generate it
  295. final Properties clusterProperties = new Properties();
  296. // This ID must be unique across the cluster
  297. clusterProperties.setProperty("jira.node.id", "node" + nodeIndex);
  298. // The location of the shared home directory for all Jira nodes
  299. clusterProperties.setProperty("jira.shared.home", requireNonNull(product.getSharedHome()));
  300. // As noted in the DC install docs, these Ehcache properties need to be set when nodes are on the same host
  301. clusterProperties.setProperty("ehcache.listener.port", String.valueOf(pickFreePort(0)));
  302. clusterProperties.setProperty("ehcache.object.port", String.valueOf(pickFreePort(0)));
  303. storeProperties(clusterProperties, clusterPropertiesFile, "Created by AMPS " + getVersion());
  304. }
  305. }
  306. // only needed for older versions of Jira; 7.0 onwards will have JiraDS.properties
  307. @VisibleForTesting
  308. static void createDbConfigXmlIfNone(final File homeDir) throws MojoExecutionException {
  309. final File dbConfigXml = new File(homeDir, FILENAME_DBCONFIG);
  310. if (dbConfigXml.exists()) {
  311. return;
  312. }
  313. try (final InputStream templateIn =
  314. JiraProductHandler.class.getResourceAsStream("jira-dbconfig-template.xml")) {
  315. if (templateIn == null) {
  316. throw new MojoExecutionException("Missing internal resource: jira-dbconfig-template.xml");
  317. }
  318. final String template = IOUtils.toString(templateIn, UTF_8);
  319. final File dbFile = getHsqlDatabaseFile(homeDir);
  320. final String jdbcUrl = "jdbc:hsqldb:file:" + dbFile.toURI().getPath();
  321. final String result = template.replace("@jdbc-url@", jdbcUrl);
  322. writeStringToFile(dbConfigXml, result, UTF_8);
  323. } catch (final IOException ioe) {
  324. throw new MojoExecutionException("Unable to create config file: " + FILENAME_DBCONFIG, ioe);
  325. }
  326. }
  327. // only needed for older versions of Jira; 7.0 onwards will have JiraDS.properties
  328. private static File getHsqlDatabaseFile(final File homeDirectory) {
  329. return new File(homeDirectory, "database");
  330. }
  331. private static File getPluginHomeDirectory(final Product product, final File homeDir) {
  332. return Optional.ofNullable(product.getSharedHome())
  333. .filter(StringUtils::isNotBlank)
  334. .map(File::new)
  335. .orElse(homeDir);
  336. }
  337. private static Collection<XmlOverride> serverXmlJiraOverride() {
  338. return unmodifiableList(asList(
  339. new XmlOverride("conf/server.xml",
  340. "//Connector", "relaxedPathChars", "[]|"),
  341. new XmlOverride("conf/server.xml",
  342. "//Connector", "relaxedQueryChars", "[]|{}^\\`\"<>")
  343. ));
  344. }
  345. private String getFirstJiraHome(final Product product) {
  346. return fixWindowsSlashes(getHomeDirectories(product).get(0).getAbsolutePath());
  347. }
  348. private Optional<DataSource> getDataSourceFromJiraDSFile(final Product jira) {
  349. final String jiraHome = getFirstJiraHome(jira); // all nodes should have the same config
  350. final File dsPropsFile = new File(jiraHome, JIRADS_PROPERTIES_FILE);
  351. if (dsPropsFile.isFile()) {
  352. final DataSource dataSource = new DataSource();
  353. try (final FileInputStream inputStream = new FileInputStream(dsPropsFile)) {
  354. final Properties dsProps = new Properties();
  355. dsProps.load(inputStream);
  356. dataSource.setJndi(dsProps.getProperty("jndi"));
  357. dataSource.setUrl(dsProps.getProperty("url").replace(JIRA_HOME_PLACEHOLDER, jiraHome));
  358. dataSource.setDriver(dsProps.getProperty("driver-class"));
  359. dataSource.setUsername(dsProps.getProperty("username"));
  360. dataSource.setPassword(dsProps.getProperty("password"));
  361. return Optional.of(dataSource);
  362. } catch (IOException e) {
  363. log.warn("failed to read " + dsPropsFile.getAbsolutePath(), e);
  364. }
  365. }
  366. return Optional.empty();
  367. }
  368. private DataSource getHsqlDataSource(final Product jira) {
  369. final DataSource dataSource = new DataSource();
  370. dataSource.setJndi("jdbc/JiraDS");
  371. dataSource.setUrl(format("jdbc:hsqldb:%s/database", getFirstJiraHome(jira))); // only one node anyway, if H2
  372. dataSource.setDriver("org.hsqldb.jdbcDriver");
  373. dataSource.setUsername("sa");
  374. dataSource.setPassword("");
  375. return dataSource;
  376. }
  377. /**
  378. * Update Jira dbconfig.xml in case user provide their own database connection configuration in pom
  379. * Jira database type was detected by uri/url prefix and database driver
  380. * Jira database type defines database-type and schema or schema-less for specific Jira database
  381. * Please refer documentation url: http://www.atlassian.com/software/jira/docs/latest/databases/index.html
  382. * example:
  383. * <pre>
  384. * {@code
  385. * <dataSource>
  386. * <jndi>${dataSource.jndi}</jndi>
  387. * <url>${dataSource.url}</url>
  388. * <driver>${dataSource.driver}</driver>
  389. * <username>${dataSource.user}</username>
  390. * <password>${dataSource.password}</password>
  391. * <schema>${dataSource.schema}</schema>
  392. * </dataSource>
  393. * }
  394. * </pre>
  395. *
  396. * @param homeDir the application's home directory
  397. * @param dbType the database type in use
  398. * @param schema the schema to use
  399. * @throws MojoExecutionException if {@code dbconfig.xml} can't be updated
  400. */
  401. @VisibleForTesting
  402. public static void updateDbConfigXml(final File homeDir, final DatabaseType dbType, final String schema)
  403. throws MojoExecutionException {
  404. final File dbConfigXml = new File(homeDir, FILENAME_DBCONFIG);
  405. if (!dbConfigXml.exists() || dbType == null) {
  406. return;
  407. }
  408. try {
  409. new XMLDocumentProcessor(new XMLDocumentHandler(dbConfigXml))
  410. .load()
  411. .validate(new DbConfigValidator())
  412. .transform(new DatabaseTypeUpdaterTransformer(dbType))
  413. .transform(new SchemeUpdaterTransformer(dbType, schema))
  414. .saveIfModified();
  415. } catch (ValidationException e) {
  416. throw new MojoExecutionException("Validation of dbconfig.xml file failed", e);
  417. }
  418. }
  419. private String getProjectDir(final Product product) {
  420. final Set<String> projectDirs = getHomeDirectories(product).stream()
  421. .map(File::getParent)
  422. .collect(toSet());
  423. if (projectDirs.size() == 1) {
  424. return projectDirs.iterator().next();
  425. }
  426. throw new IllegalStateException("Expected a single project directory, but found " + projectDirs);
  427. }
  428. private static class JiraPluginProvider extends AbstractPluginProvider {
  429. @Override
  430. protected Collection<ProductArtifact> getSalArtifacts(final String salVersion) {
  431. return asList(
  432. new ProductArtifact("com.atlassian.sal", "sal-api", salVersion),
  433. new ProductArtifact("com.atlassian.sal", "sal-jira-plugin", salVersion));
  434. }
  435. @Override
  436. protected Collection<ProductArtifact> getPdkInstallArtifacts(final String pdkInstallVersion) {
  437. final List<ProductArtifact> plugins = new ArrayList<>(super.getPdkInstallArtifacts(pdkInstallVersion));
  438. plugins.add(new ProductArtifact("commons-fileupload", "commons-fileupload", "1.2.1"));
  439. return plugins;
  440. }
  441. }
  442. }