/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/product/JiraProductHandler.java
Java | 498 lines | 395 code | 56 blank | 47 comment | 35 complexity | 59c86350cd84f2b78f3b5e55913c76bd 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.DataSource;
- import com.atlassian.maven.plugins.amps.MavenContext;
- import com.atlassian.maven.plugins.amps.MavenGoals;
- 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.database.DatabaseType;
- import com.atlassian.maven.plugins.amps.database.DatabaseTypeFactory;
- import com.atlassian.maven.plugins.amps.product.common.ValidationException;
- import com.atlassian.maven.plugins.amps.product.common.XMLDocumentHandler;
- import com.atlassian.maven.plugins.amps.product.common.XMLDocumentProcessor;
- import com.atlassian.maven.plugins.amps.product.jira.config.DatabaseTypeUpdaterTransformer;
- import com.atlassian.maven.plugins.amps.product.jira.config.DbConfigValidator;
- import com.atlassian.maven.plugins.amps.product.jira.config.SchemeUpdaterTransformer;
- 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.ImmutableList;
- import com.google.common.collect.ImmutableMap;
- import org.apache.commons.io.IOUtils;
- import org.apache.commons.lang3.StringUtils;
- 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.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Properties;
- import java.util.Set;
- import static com.atlassian.maven.plugins.amps.util.ConfigFileUtils.RegexReplacement;
- import static com.atlassian.maven.plugins.amps.util.FileUtils.fixWindowsSlashes;
- import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
- import static com.atlassian.maven.plugins.amps.util.PropertyUtils.storeProperties;
- import static com.atlassian.maven.plugins.amps.util.VersionUtils.getVersion;
- import static java.lang.String.format;
- import static java.nio.charset.StandardCharsets.UTF_8;
- import static java.nio.file.Files.createDirectories;
- import static java.util.Arrays.asList;
- import static java.util.Collections.unmodifiableList;
- import static java.util.Objects.requireNonNull;
- import static java.util.stream.Collectors.toSet;
- import static org.apache.commons.io.FileUtils.deleteQuietly;
- import static org.apache.commons.io.FileUtils.writeStringToFile;
- import static org.apache.commons.lang3.StringUtils.isBlank;
- public class JiraProductHandler extends AbstractWebappProductHandler {
- @VisibleForTesting
- static final String INSTALLED_PLUGINS_DIR = "installed-plugins";
- @VisibleForTesting
- static final String PLUGINS_DIR = "plugins";
- @VisibleForTesting
- static final String BUNDLED_PLUGINS_UNZIPPED = "WEB-INF/atlassian-bundled-plugins";
- @VisibleForTesting
- static final String BUNDLED_PLUGINS_FROM_4_1 = "WEB-INF/classes/atlassian-bundled-plugins.zip";
- @VisibleForTesting
- static final String BUNDLED_PLUGINS_UPTO_4_0 = "WEB-INF/classes/com/atlassian/jira/plugin/atlassian-bundled-plugins.zip";
- @VisibleForTesting
- static final String FILENAME_DBCONFIG = "dbconfig.xml";
- private static final String JIRADS_PROPERTIES_FILE = "JiraDS.properties";
- private static final String JIRA_HOME_PLACEHOLDER = "${jirahome}";
- 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}'";
- public JiraProductHandler(
- final MavenContext context, final MavenGoals goals, final RepositorySystem repositorySystem,
- final ArtifactResolver artifactResolver, final WebAppManager webAppManager) {
- super(context, goals, new JiraPluginProvider(), repositorySystem, artifactResolver, webAppManager);
- }
- // -------------------- Simple value-returning methods ---------------------
- @Override
- @Nonnull
- public String getId() {
- return "jira";
- }
- @Override
- @Nonnull
- public String getDefaultContainerId() {
- // However note that recent versions of Jira specify their own Tomcat container version, see
- // AbstractWebappProductHandler.addOverridesFromProductPom for how this works.
- return "tomcat7x";
- }
- @Override
- public int getDefaultHttpPort() {
- return 2990;
- }
- @Override
- public int getDefaultHttpsPort() {
- return 8442;
- }
- @Nonnull
- @Override
- public ProductArtifact getArtifact() {
- return new ProductArtifact("com.atlassian.jira", "atlassian-jira-webapp", "RELEASE");
- }
- @Nonnull
- @Override
- public Optional<ProductArtifact> getTestResourcesArtifact() {
- return Optional.of(new ProductArtifact("com.atlassian.jira.plugins", "jira-plugin-test-resources"));
- }
- @Nonnull
- @Override
- protected Collection<String> getExtraJarsToSkipWhenScanningForTldsAndWebFragments() {
- // Fixes AMPS-1429 by skipping these JARs
- return ImmutableList.of("jotm*.jar", "xapool*.jar");
- }
- // --------- Non-trivial logic starts here, generally in the order called by the superclass ---------
- @Override
- protected void processHomeDirectory(final Product product, final int nodeIndex, final File homeDir)
- throws MojoExecutionException {
- super.processHomeDirectory(product, nodeIndex, homeDir);
- if (product.isMultiNode()) {
- if (nodeIndex == 0) {
- // Only for node 0 because we only need to do it once per cluster
- setUpSharedHome(product, homeDir.getParent());
- }
- createClusterPropertiesFile(product, nodeIndex, homeDir);
- }
- createDbConfigXmlIfNone(homeDir);
- updateDbConfigXmlFromDataSource(product, homeDir);
- }
- @Nonnull
- @Override
- protected List<File> getConfigFiles(@Nonnull final Product product, @Nonnull final File homeDir) {
- final List<File> configFiles = super.getConfigFiles(product, homeDir);
- configFiles.add(new File(homeDir, "database.log"));
- configFiles.add(new File(homeDir, "database.script"));
- configFiles.add(new File(homeDir, FILENAME_DBCONFIG));
- return configFiles;
- }
- @Nonnull
- @Override
- protected List<Replacement> getReplacements(@Nonnull final Product product, final int nodeIndex) {
- String contextPath = product.getContextPath();
- if (!contextPath.startsWith("/")) {
- contextPath = "/" + contextPath;
- }
- // This is similar to BaseUrlUtils.getBaseUrl, except that it respects the product's protocol
- final String baseUrl = product.getProtocol() + "://" + product.getServer() + ":" +
- product.getWebPortForNode(nodeIndex) + contextPath;
- final List<Replacement> replacements = super.getReplacements(product, nodeIndex);
- // We don't re-wrap snapshots with these values:
- replacements.add(0, new Replacement("http://localhost:8080", baseUrl, false));
- replacements.add(new Replacement("@project-dir@", getProjectDir(product), false));
- replacements.add(new Replacement("/jira-home/", "/home/", false));
- replacements.add(new Replacement("@base-url@", baseUrl, false));
- replacements.add(new RegexReplacement(SERVER_ID_PATTERN, "''"));
- return replacements;
- }
- @Nonnull
- @Override
- protected Optional<File> getUserInstalledPluginsDirectory(
- final Product product, final File webappDir, final File homeDir) {
- final File pluginHomeDirectory = getPluginHomeDirectory(product, homeDir);
- return Optional.of(new File(new File(pluginHomeDirectory, PLUGINS_DIR), INSTALLED_PLUGINS_DIR));
- }
- @Nonnull
- @Override
- protected File getBundledPluginPath(final Product product, final File productDir) {
- // the zip became a directory in 6.3, so if the directory exists and is a directory, use it,
- // otherwise fallback to the old behaviour.
- final File bundleDir = new File(productDir, BUNDLED_PLUGINS_UNZIPPED);
- if (bundleDir.exists() && bundleDir.isDirectory()) {
- return bundleDir;
- } else {
- // this location used from 4.1 onwards (inclusive), until replaced by unzipped dir.
- String bundledPluginPluginsPath = BUNDLED_PLUGINS_FROM_4_1;
- String[] version = product.getVersion().split("-", 2)[0].split("\\.");
- try {
- long major = Long.parseLong(version[0]);
- long minor = (version.length > 1) ? Long.parseLong(version[1]) : 0;
- if (major < 4 || major == 4 && minor == 0) {
- bundledPluginPluginsPath = BUNDLED_PLUGINS_UPTO_4_0;
- }
- } catch (NumberFormatException e) {
- log.debug(format("Unable to parse Jira version '%s', assuming Jira 4.1 or newer.", product.getVersion()), e);
- }
- return new File(productDir, bundledPluginPluginsPath);
- }
- }
- @Override
- protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir) {
- // Jira 7.12.x has new tomcat version which requires additional characters to be whitelisted
- if (new ComparableVersion(product.getVersion()).compareTo(new ComparableVersion("7.12.0")) >= 0) {
- product.setCargoXmlOverrides(serverXmlJiraOverride());
- }
- }
- @Override
- protected void fixJvmArgs(final Product product) {
- final JvmArgsFix argsFix = JvmArgsFix.empty()
- .withAddOpens(ADD_OPENS_FOR_TOMCAT)
- .withAddOpens(ADD_OPENS_FOR_FELIX);
- final ComparableVersion productVersion = new ComparableVersion(product.getVersion());
- // Jira 8 raises memory requirements, to account for increased memory usage by Lucene
- if (productVersion.compareTo(new ComparableVersion("8.0.0-ALPHA")) >= 0) {
- product.setJvmArgs(argsFix
- .with("-Xmx", "2g")
- .with("-Xms", "1g")
- .apply(product.getJvmArgs()));
- }
- // 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.
- else if (productVersion.compareTo(new ComparableVersion("7.7.0-ALPHA")) >= 0) {
- product.setJvmArgs(argsFix
- .with("-Xmx", "768m")
- .with("-Xms", "384m")
- .apply(product.getJvmArgs()));
- } else {
- super.fixJvmArgs(product);
- }
- }
- @Override
- protected DataSource getDefaultDataSource(final Product product) {
- return getDataSourceFromJiraDSFile(product).orElse(getHsqlDataSource(product));
- }
- @Override
- @Nonnull
- protected Map<String, String> getProductSpecificSystemProperties(final Product product, final int nodeIndex) {
- final ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
- final String homeDirectory = fixWindowsSlashes(getHomeDirectories(product).get(nodeIndex).getPath());
- properties.put("jira.home", homeDirectory);
- properties.put("cargo.servlet.uriencoding", "UTF-8");
- if (product.isAwaitFullInitialization()) {
- properties.put("com.atlassian.jira.startup.LauncherContextListener.SYNCHRONOUS", "true");
- }
- return properties.build();
- }
- @Override
- @Nonnull
- protected List<ProductArtifact> getExtraContainerDependencies() {
- return asList(
- new ProductArtifact("hsqldb", "hsqldb", "1.8.0.5"),
- new ProductArtifact("javax.transaction", "jta", "1.0.1B"),
- new ProductArtifact("ots-jts", "ots-jts", "1.0"),
- // for data source and transaction manager providers
- new ProductArtifact("jotm", "jotm", "1.4.3"),
- new ProductArtifact("jotm", "jotm-jrmp_stubs", "1.4.3"),
- new ProductArtifact("jotm", "jotm-iiop_stubs", "1.4.3"),
- new ProductArtifact("jotm", "jonas_timer", "1.4.3"),
- new ProductArtifact("jotm", "objectweb-datasource", "1.4.3"),
- new ProductArtifact("carol", "carol", "1.5.2"),
- new ProductArtifact("carol", "carol-properties", "1.0"),
- new ProductArtifact("xapool", "xapool", "1.3.1"),
- new ProductArtifact("commons-logging", "commons-logging", "1.1.1")
- );
- }
- @Override
- protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
- throws MojoExecutionException, IOException {
- super.cleanupProductHomeForZip(product, snapshotDir);
- deleteQuietly(new File(snapshotDir, "log/atlassian-jira.log"));
- }
- // ------------ Mostly private helper methods --------------
- private void updateDbConfigXmlFromDataSource(final Product product, final File homeDir)
- throws MojoExecutionException {
- if (product.getDataSources().size() == 1) {
- final DataSource ds = product.getDataSources().get(0);
- final DatabaseType dbType = new DatabaseTypeFactory(log).getDatabaseType(ds).orElseThrow(
- () -> new MojoExecutionException("Could not find database type for " + ds));
- updateDbConfigXml(homeDir, dbType, ds.getSchema());
- } else if (product.getDataSources().size() > 1) {
- throw new MojoExecutionException("Jira does not support multiple data sources");
- }
- }
- private static void setUpSharedHome(final Product product, final String parent) throws MojoExecutionException {
- final File sharedHome;
- if (isBlank(product.getSharedHome())) {
- sharedHome = new File(parent, "shared-home");
- product.setSharedHome(sharedHome.getAbsolutePath());
- } else {
- sharedHome = new File(product.getSharedHome());
- if (sharedHome.isFile()) {
- final String error =
- format("The specified shared home '%s' is a file, not a directory", product.getSharedHome());
- throw new MojoExecutionException(error);
- }
- }
- try {
- createDirectories(sharedHome.toPath());
- } catch (IOException e) {
- throw new MojoExecutionException("Could not create shared home " + sharedHome, e);
- }
- }
- // See https://confluence.atlassian.com/jirakb/how-to-move-the-shared-home-folder-in-jira-data-center-1044112948.html
- private static void createClusterPropertiesFile(final Product product, final int nodeIndex, final File homeDir)
- throws MojoExecutionException {
- final File clusterPropertiesFile = new File(homeDir, "cluster.properties");
- if (!clusterPropertiesFile.isFile()) {
- // The home ZIP didn't contain a cluster.properties file (unsurprisingly); generate it
- final Properties clusterProperties = new Properties();
- // This ID must be unique across the cluster
- clusterProperties.setProperty("jira.node.id", "node" + nodeIndex);
- // The location of the shared home directory for all Jira nodes
- clusterProperties.setProperty("jira.shared.home", requireNonNull(product.getSharedHome()));
- // As noted in the DC install docs, these Ehcache properties need to be set when nodes are on the same host
- clusterProperties.setProperty("ehcache.listener.port", String.valueOf(pickFreePort(0)));
- clusterProperties.setProperty("ehcache.object.port", String.valueOf(pickFreePort(0)));
- storeProperties(clusterProperties, clusterPropertiesFile, "Created by AMPS " + getVersion());
- }
- }
- // only needed for older versions of Jira; 7.0 onwards will have JiraDS.properties
- @VisibleForTesting
- static void createDbConfigXmlIfNone(final File homeDir) throws MojoExecutionException {
- final File dbConfigXml = new File(homeDir, FILENAME_DBCONFIG);
- if (dbConfigXml.exists()) {
- return;
- }
- try (final InputStream templateIn =
- JiraProductHandler.class.getResourceAsStream("jira-dbconfig-template.xml")) {
- if (templateIn == null) {
- throw new MojoExecutionException("Missing internal resource: jira-dbconfig-template.xml");
- }
- final String template = IOUtils.toString(templateIn, UTF_8);
- final File dbFile = getHsqlDatabaseFile(homeDir);
- final String jdbcUrl = "jdbc:hsqldb:file:" + dbFile.toURI().getPath();
- final String result = template.replace("@jdbc-url@", jdbcUrl);
- writeStringToFile(dbConfigXml, result, UTF_8);
- } catch (final IOException ioe) {
- throw new MojoExecutionException("Unable to create config file: " + FILENAME_DBCONFIG, ioe);
- }
- }
- // only needed for older versions of Jira; 7.0 onwards will have JiraDS.properties
- private static File getHsqlDatabaseFile(final File homeDirectory) {
- return new File(homeDirectory, "database");
- }
- private static File getPluginHomeDirectory(final Product product, final File homeDir) {
- return Optional.ofNullable(product.getSharedHome())
- .filter(StringUtils::isNotBlank)
- .map(File::new)
- .orElse(homeDir);
- }
- private static Collection<XmlOverride> serverXmlJiraOverride() {
- return unmodifiableList(asList(
- new XmlOverride("conf/server.xml",
- "//Connector", "relaxedPathChars", "[]|"),
- new XmlOverride("conf/server.xml",
- "//Connector", "relaxedQueryChars", "[]|{}^\\`\"<>")
- ));
- }
- private String getFirstJiraHome(final Product product) {
- return fixWindowsSlashes(getHomeDirectories(product).get(0).getAbsolutePath());
- }
- private Optional<DataSource> getDataSourceFromJiraDSFile(final Product jira) {
- final String jiraHome = getFirstJiraHome(jira); // all nodes should have the same config
- final File dsPropsFile = new File(jiraHome, JIRADS_PROPERTIES_FILE);
- if (dsPropsFile.isFile()) {
- final DataSource dataSource = new DataSource();
- try (final FileInputStream inputStream = new FileInputStream(dsPropsFile)) {
- final Properties dsProps = new Properties();
- dsProps.load(inputStream);
- dataSource.setJndi(dsProps.getProperty("jndi"));
- dataSource.setUrl(dsProps.getProperty("url").replace(JIRA_HOME_PLACEHOLDER, jiraHome));
- dataSource.setDriver(dsProps.getProperty("driver-class"));
- dataSource.setUsername(dsProps.getProperty("username"));
- dataSource.setPassword(dsProps.getProperty("password"));
- return Optional.of(dataSource);
- } catch (IOException e) {
- log.warn("failed to read " + dsPropsFile.getAbsolutePath(), e);
- }
- }
- return Optional.empty();
- }
- private DataSource getHsqlDataSource(final Product jira) {
- final DataSource dataSource = new DataSource();
- dataSource.setJndi("jdbc/JiraDS");
- dataSource.setUrl(format("jdbc:hsqldb:%s/database", getFirstJiraHome(jira))); // only one node anyway, if H2
- dataSource.setDriver("org.hsqldb.jdbcDriver");
- dataSource.setUsername("sa");
- dataSource.setPassword("");
- return dataSource;
- }
- /**
- * Update Jira dbconfig.xml in case user provide their own database connection configuration in pom
- * Jira database type was detected by uri/url prefix and database driver
- * Jira database type defines database-type and schema or schema-less for specific Jira database
- * Please refer documentation url: http://www.atlassian.com/software/jira/docs/latest/databases/index.html
- * example:
- * <pre>
- * {@code
- * <dataSource>
- * <jndi>${dataSource.jndi}</jndi>
- * <url>${dataSource.url}</url>
- * <driver>${dataSource.driver}</driver>
- * <username>${dataSource.user}</username>
- * <password>${dataSource.password}</password>
- * <schema>${dataSource.schema}</schema>
- * </dataSource>
- * }
- * </pre>
- *
- * @param homeDir the application's home directory
- * @param dbType the database type in use
- * @param schema the schema to use
- * @throws MojoExecutionException if {@code dbconfig.xml} can't be updated
- */
- @VisibleForTesting
- public static void updateDbConfigXml(final File homeDir, final DatabaseType dbType, final String schema)
- throws MojoExecutionException {
- final File dbConfigXml = new File(homeDir, FILENAME_DBCONFIG);
- if (!dbConfigXml.exists() || dbType == null) {
- return;
- }
- try {
- new XMLDocumentProcessor(new XMLDocumentHandler(dbConfigXml))
- .load()
- .validate(new DbConfigValidator())
- .transform(new DatabaseTypeUpdaterTransformer(dbType))
- .transform(new SchemeUpdaterTransformer(dbType, schema))
- .saveIfModified();
- } catch (ValidationException e) {
- throw new MojoExecutionException("Validation of dbconfig.xml file failed", e);
- }
- }
- private String getProjectDir(final Product product) {
- final Set<String> projectDirs = getHomeDirectories(product).stream()
- .map(File::getParent)
- .collect(toSet());
- if (projectDirs.size() == 1) {
- return projectDirs.iterator().next();
- }
- throw new IllegalStateException("Expected a single project directory, but found " + projectDirs);
- }
- private static class JiraPluginProvider extends AbstractPluginProvider {
- @Override
- protected Collection<ProductArtifact> getSalArtifacts(final String salVersion) {
- return asList(
- new ProductArtifact("com.atlassian.sal", "sal-api", salVersion),
- new ProductArtifact("com.atlassian.sal", "sal-jira-plugin", salVersion));
- }
- @Override
- protected Collection<ProductArtifact> getPdkInstallArtifacts(final String pdkInstallVersion) {
- final List<ProductArtifact> plugins = new ArrayList<>(super.getPdkInstallArtifacts(pdkInstallVersion));
- plugins.add(new ProductArtifact("commons-fileupload", "commons-fileupload", "1.2.1"));
- return plugins;
- }
- }
- }