All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.marklogic.appdeployer.command.databases.DeployOtherDatabasesCommand Maven / Gradle / Ivy

Go to download

Java client for the MarkLogic REST Management API and for deploying applications to MarkLogic

The newest version!
/*
 * Copyright (c) 2023 MarkLogic Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.marklogic.appdeployer.command.databases;

import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.marklogic.appdeployer.AppConfig;
import com.marklogic.appdeployer.ConfigDir;
import com.marklogic.appdeployer.command.AbstractUndoableCommand;
import com.marklogic.appdeployer.command.CommandContext;
import com.marklogic.appdeployer.command.ResourceFilenameFilter;
import com.marklogic.appdeployer.command.SortOrderConstants;
import com.marklogic.appdeployer.command.forests.DeployForestsCommand;
import com.marklogic.mgmt.PayloadParser;
import com.marklogic.mgmt.api.API;
import com.marklogic.mgmt.api.configuration.Configuration;
import com.marklogic.mgmt.api.configuration.Configurations;
import com.marklogic.mgmt.api.database.Database;
import com.marklogic.mgmt.api.database.DatabaseSorter;
import com.marklogic.mgmt.api.forest.Forest;
import com.marklogic.mgmt.mapper.DefaultResourceMapper;
import com.marklogic.mgmt.mapper.ResourceMapper;
import com.marklogic.mgmt.resource.databases.DatabaseManager;
import com.marklogic.mgmt.util.ObjectMapperFactory;
import com.marklogic.rest.util.JsonNodeUtil;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.*;

/**
 * As of release 3.14.0, this now handles all databases, not just "databases other than the default content database".
 * Its name will be changed to DeployDatabasesCommand in a future release, which likely will also be when
 * DeployContentDatabaseCommand is deleted (probably 4.0).
 */
public class DeployOtherDatabasesCommand extends AbstractUndoableCommand {

	// Each of these is copied to the instances of DeployDatabaseCommand that are created
	private Integer forestsPerHost;
	private boolean checkForCustomForests = true;
	private String forestFilename;
	private boolean createForestsOnEachHost = true;

	/**
	 * Defines database names that, by default, this command will never undeploy.
	 */
	private Set defaultDatabasesToNotUndeploy = new HashSet<>();

	private DeployDatabaseCommandFactory deployDatabaseCommandFactory = new DefaultDeployDatabaseCommandFactory();

	private PayloadParser payloadParser = new PayloadParser();

	public DeployOtherDatabasesCommand() {
		setExecuteSortOrder(SortOrderConstants.DEPLOY_OTHER_DATABASES);
		setUndoSortOrder(SortOrderConstants.DELETE_OTHER_DATABASES);
		setExecuteSortOrder(SortOrderConstants.DEPLOY_OTHER_DATABASES);
		setUndoSortOrder(SortOrderConstants.DELETE_OTHER_DATABASES);
		initializeDefaultDatabasesToNotUndeploy();
		setResourceClassType(Database.class);
	}

	public DeployOtherDatabasesCommand(int forestsPerHost) {
		this();
		setForestsPerHost(forestsPerHost);
	}

	protected void initializeDefaultDatabasesToNotUndeploy() {
		defaultDatabasesToNotUndeploy = new HashSet<>();
		defaultDatabasesToNotUndeploy.add("App-Services");
		defaultDatabasesToNotUndeploy.add("Documents");
		defaultDatabasesToNotUndeploy.add("Extensions");
		defaultDatabasesToNotUndeploy.add("Fab");
		defaultDatabasesToNotUndeploy.add("Last-Login");
		defaultDatabasesToNotUndeploy.add("Meters");
		defaultDatabasesToNotUndeploy.add("Modules");
		defaultDatabasesToNotUndeploy.add("Schemas");
		defaultDatabasesToNotUndeploy.add("Security");
		defaultDatabasesToNotUndeploy.add("Triggers");
	}

	/**
	 * Deploys each of the databases found via buildDatabasePlans.
	 *
	 * @param context
	 */
	@Override
	public void execute(CommandContext context) {
		List databasePlans = buildDatabasePlans(context);

		if (context.getAppConfig().isSortOtherDatabaseByDependencies()) {
			databasePlans = sortDatabasePlans(databasePlans);
		} else {
			logger.info("Not sorting databases by dependencies, will sort them by their filenames instead");
		}

		if (context.getAppConfig().getCmaConfig().isDeployDatabases()) {
			deployDatabasesAndForestsViaCma(context, databasePlans);
		} else {
			// Otherwise, create each database one at a time, which also handles sub-databases
			databasePlans.forEach(databasePlan -> {
				databasePlan.getDeployDatabaseCommand().execute(context);
			});

			// Either create forests in one bulk CMA request, or via a command per database
			if (context.getAppConfig().getCmaConfig().isDeployForests()) {
				deployAllForestsInSingleCmaRequest(context, databasePlans);
			} else {
				databasePlans.forEach(databasePlan -> {
					DeployForestsCommand dfc = databasePlan.getDeployDatabaseCommand().getDeployForestsCommand();
					if (dfc != null) {
						dfc.execute(context);
					}
				});
			}
		}
	}

	/**
	 * Undeploys each of the databases found via buildDatabasePlans.
	 *
	 * @param context
	 */
	@Override
	public void undo(CommandContext context) {
		List databasePlans = buildDatabasePlans(context);

		if (context.getAppConfig().isSortOtherDatabaseByDependencies()) {
			databasePlans = sortDatabasePlans(databasePlans);
			Collections.reverse(databasePlans);
		} else {
			logger.info("Not sorting databases by dependencies, will sort them by their filenames instead");
		}

		databasePlans.forEach(databasePlan -> {
			databasePlan.getDeployDatabaseCommand().undo(context);
		});

		// If no databases were found, may still need to delete the content database in case no file exists for it.
		// That's because the command for creating a REST API server will not delete the content database by default,
		// though it will delete the test database by default
		if (deleteContentDatabaseOnUndo(databasePlans, context.getAppConfig())) {
			DatabaseManager dbMgr = new DeployDatabaseCommand().newDatabaseManageForDeleting(context);
			dbMgr.deleteByName(context.getAppConfig().getContentDatabaseName());
		}
	}

	/**
	 * If no database files are found, may still need to delete the content database in case no file exists for it.
	 * That's because the command for creating a REST API server will not delete the content database by default.
	 *
	 * Per ticket #404, this will now do a check to see if the default content database filename is ignored. If so,
	 * and there are no database files found, then the content database will not be deleted.
	 *
	 * @param databasePlans
	 * @param appConfig
	 * @return
	 */
	protected boolean deleteContentDatabaseOnUndo(List databasePlans, AppConfig appConfig) {
		if (databasePlans == null || databasePlans.isEmpty()) {
			FilenameFilter filter = getResourceFilenameFilter();
			if (filter != null && filter instanceof ResourceFilenameFilter) {
				Set filenamesToIgnore = ((ResourceFilenameFilter) filter).getFilenamesToIgnore();
				if (filenamesToIgnore != null && !filenamesToIgnore.isEmpty() && appConfig.getConfigDirs() != null) {
					for (ConfigDir configDir : appConfig.getConfigDirs()) {
						if (filenamesToIgnore.contains(configDir.getDefaultContentDatabaseFilename())) {
							return false;
						}
					}
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Performs all the work of finding all the database files across all the configuration paths; merging matching
	 * database files together; and then building an instance of DeployDatabaseCommand for each database that needs
	 * to be deployed. This method doesn't make any calls to deploy/undeploy databases though - it just builds up all
	 * the data that is needed to do so.
	 * 

* This is public so that ml-gradle can invoke it when previewing what forests will be created for a database. * * @param context * @return */ public List buildDatabasePlans(CommandContext context) { DatabasePlans databasePlan = new DatabasePlans(); for (ConfigDir configDir : context.getAppConfig().getConfigDirs()) { addDatabaseFiles(context, configDir, databasePlan); } List databasePlans = mergeDatabasePlanFiles(context, databasePlan); buildDeployDatabaseCommands(context, databasePlans); if (logger.isDebugEnabled()) { logger.debug("Logging the files for each database before it's created or updated:"); databasePlans.forEach(plan -> logger.debug(plan + "\n")); } return databasePlans; } /** * @param context * @param configDir * @param databasePlans */ protected void addDatabaseFiles(CommandContext context, ConfigDir configDir, DatabasePlans databasePlans) { final String contentDatabaseFilename = configDir.getDefaultContentDatabaseFilename(); File dir = configDir.getDatabasesDir(); if (dir != null && dir.exists()) { for (File f : listFilesInDirectory(dir)) { String payload = copyFileToString(f, context); String databaseName = payloadParser.getPayloadFieldValue(payload, "database-name", false); if (databaseName != null) { boolean isMainContentDatabase = false; if (databasePlans.getMainContentDatabaseName() == null && f.getName().equals(contentDatabaseFilename)) { databasePlans.setMainContentDatabaseName(databaseName); isMainContentDatabase = true; } if (databasePlans.getDatabasePlanMap().containsKey(databaseName)) { DatabasePlan reference = databasePlans.getDatabasePlanMap().get(databaseName); reference.addFile(f); if (!reference.isMainContentDatabase() && isMainContentDatabase) { reference.setMainContentDatabase(isMainContentDatabase); } } else { databasePlans.getDatabasePlanMap().put(databaseName, new DatabasePlan(databaseName, f, isMainContentDatabase)); } } } } else { logResourceDirectoryNotFound(dir); } } /** * For each DatabasePlan in the DatabasePlan, the files (usually just one) are merged together if needed and * then stored as the payload on the given DatabasePlan. In addition, a check is made to see if a test * database should be created that mirrors the main content database. * * @param context * @param databasePlans * @return */ protected List mergeDatabasePlanFiles(CommandContext context, DatabasePlans databasePlans) { ResourceMapper resourceMapper = new DefaultResourceMapper(new API(context.getManageClient())); ObjectReader objectReader = ObjectMapperFactory.getObjectMapper().readerFor(Database.class); List databasePlanList = new ArrayList<>(); databasePlanList.addAll(databasePlans.getDatabasePlanMap().values()); DatabasePlan testDatabasePlan = null; final String testContentDatabaseName = context.getAppConfig().getTestContentDatabaseName(); for (DatabasePlan reference : databasePlanList) { boolean createTestDatabase = reference.isMainContentDatabase() && context.getAppConfig().isTestPortSet(); if (createTestDatabase) { testDatabasePlan = new DatabasePlan(testContentDatabaseName, reference.getFiles()); } List files = reference.getFiles(); if (files.size() == 1) { String payload = copyFileToString(files.get(0), context); reference.setPayload(payload); reference.setDatabaseForSorting(resourceMapper.readResource(payload, Database.class)); if (createTestDatabase) { String testPayload = payloadTokenReplacer.replaceTokens(copyFileToString(files.get(0)), context.getAppConfig(), true); testDatabasePlan.setPayload(testPayload); Database testDb = resourceMapper.readResource(payload, Database.class); testDb.setDatabaseName(testContentDatabaseName); testDatabasePlan.setDatabaseForSorting(testDb); } } else { List nodes = new ArrayList<>(); files.forEach(file -> { String payload = copyFileToString(file, context); nodes.add(convertPayloadToObjectNode(context, payload)); }); ObjectNode mergedNode = JsonNodeUtil.mergeObjectNodes(nodes.toArray(new ObjectNode[]{})); reference.setMergedObjectNode(mergedNode); try { reference.setDatabaseForSorting(objectReader.readValue(mergedNode)); } catch (IOException e) { throw new RuntimeException("Unable to read ObjectNode into Database class, cause: " + e.getMessage(), e); } if (createTestDatabase) { List testNodes = new ArrayList<>(); files.forEach(file -> { String testPayload = payloadTokenReplacer.replaceTokens(copyFileToString(file), context.getAppConfig(), true); testNodes.add(convertPayloadToObjectNode(context, testPayload)); }); ObjectNode mergedTestNode = JsonNodeUtil.mergeObjectNodes(testNodes.toArray(new ObjectNode[]{})); testDatabasePlan.setMergedObjectNode(mergedTestNode); try { testDatabasePlan.setDatabaseForSorting(objectReader.readValue(mergedTestNode)); } catch (IOException e) { throw new RuntimeException("Unable to read ObjectNode into Database class, cause: " + e.getMessage(), e); } } } } if (testDatabasePlan != null) { databasePlanList.add(testDatabasePlan); testDatabasePlan.setTestContentDatabase(true); } return databasePlanList; } protected List sortDatabasePlans(List databasePlans) { List databases = new ArrayList<>(); Map map = new HashMap<>(); databasePlans.forEach(plan -> { databases.add(plan.getDatabaseForSorting()); map.put(plan.getDatabaseName(), plan); }); String[] sortedNames = new DatabaseSorter().sortDatabasesAndReturnNames(databases); List sortedList = new ArrayList<>(); for (String name : sortedNames) { sortedList.add(map.get(name)); } return sortedList; } /** * For each DatabasePlan, build a DeployDatabaseCommand that can later be executed for the database. * * @param databasePlans */ protected void buildDeployDatabaseCommands(CommandContext context, List databasePlans) { databasePlans.forEach(databasePlan -> { DeployDatabaseCommand command = deployDatabaseCommandFactory.newDeployDatabaseCommand(databasePlan.getLastFile()); command.setCheckForCustomForests(isCheckForCustomForests()); command.setDatabasesToNotUndeploy(this.getDefaultDatabasesToNotUndeploy()); if (databasePlan.isMainContentDatabase() || databasePlan.isTestContentDatabase()) { Integer contentForestsPerHost = context.getAppConfig().getContentForestsPerHost(); if (contentForestsPerHost != null) { command.setForestsPerHost(contentForestsPerHost); } else if (this.forestsPerHost != null) { command.setForestsPerHost(this.forestsPerHost); } else { command.setForestsPerHost(3); // default as defined by /v1/rest-apis } command.setForestFilename("content-forest.json"); } else { if (this.forestsPerHost != null) { command.setForestsPerHost(this.forestsPerHost); } command.setForestFilename(getForestFilename()); } // Set the payload so the command doesn't try to generate it command.setPayload(databasePlan.getPayload()); command.setPostponeForestCreation(context.getAppConfig().getCmaConfig().isDeployForests()); databasePlan.setDeployDatabaseCommand(command); }); } /** * As of 3.15.0, if databases are to be deployed via CMA, then their forests will also be deployed via CMA, * regardless of the setting on the AppConfig instance. *

* Also as of 3.15.0, sub-databases and their forests are never deployed by CMA. Will support this in a future * release. * * @param context * @param databasePlans */ protected void deployDatabasesAndForestsViaCma(CommandContext context, List databasePlans) { Configuration dbConfig = new Configuration(); // Forests must be included in a separate configuration object Configuration forestConfig = new Configuration(); databasePlans.forEach(plan -> { final DeployDatabaseCommand deployDatabaseCommand = plan.getDeployDatabaseCommand(); String payload = deployDatabaseCommand.buildPayloadForSaving(context); dbConfig.addDatabase(convertPayloadToObjectNode(context, payload)); DeployForestsCommand deployForestsCommand = deployDatabaseCommand.buildDeployForestsCommand(plan.getDatabaseName(), context); if (deployForestsCommand != null) { deployForestsCommand.buildForests(context, false).forEach(forest -> forestConfig.addForest(forest.toObjectNode())); } }); new Configurations(dbConfig, forestConfig).submit(context.getManageClient()); // Now account for sub-databases, but not yet (as of 3.15.0) with CMA databasePlans.forEach(plan -> { plan.getDeployDatabaseCommand().deploySubDatabases(plan.getDatabaseName(), context); }); } /** * Each DatabasePlan is expected to have constructed a DeployForestCommand, but not executed it. Each * DeployForestCommand can then be used to build a list of forests. All of those forests can be combined into a * single list and then submitted to CMA, thereby greatly speeding up the creation of the forests. * * @param context * @param databasePlans */ protected void deployAllForestsInSingleCmaRequest(CommandContext context, List databasePlans) { List allForests = new ArrayList<>(); databasePlans.forEach(plan -> { DeployForestsCommand dfc = plan.getDeployDatabaseCommand().getDeployForestsCommand(); if (dfc != null) { allForests.addAll(dfc.buildForests(context, false)); } }); if (!allForests.isEmpty()) { Configuration config = new Configuration(); allForests.forEach(forest -> config.addForest(forest.toObjectNode())); new Configurations(config).submit(context.getManageClient()); } } public void setDeployDatabaseCommandFactory(DeployDatabaseCommandFactory deployDatabaseCommandFactory) { this.deployDatabaseCommandFactory = deployDatabaseCommandFactory; } public Integer getForestsPerHost() { return forestsPerHost; } public void setForestsPerHost(Integer forestsPerHost) { this.forestsPerHost = forestsPerHost; } public boolean isCheckForCustomForests() { return checkForCustomForests; } public void setCheckForCustomForests(boolean checkForCustomForests) { this.checkForCustomForests = checkForCustomForests; } public String getForestFilename() { return forestFilename; } public void setForestFilename(String forestFilename) { this.forestFilename = forestFilename; } public boolean isCreateForestsOnEachHost() { return createForestsOnEachHost; } public void setCreateForestsOnEachHost(boolean createForestsOnEachHost) { this.createForestsOnEachHost = createForestsOnEachHost; } public Set getDefaultDatabasesToNotUndeploy() { return defaultDatabasesToNotUndeploy; } public void setDefaultDatabasesToNotUndeploy(Set defaultDatabasesToNotUndeploy) { this.defaultDatabasesToNotUndeploy = defaultDatabasesToNotUndeploy; } } /** * Defines a set of database plans, including capturing the name of the main content database and whether a test * database mirroring that content database should be created. */ class DatabasePlans { private String mainContentDatabaseName; // Using a LinkedHashMap so that plans are first ordered by filename private Map databasePlanMap = new LinkedHashMap<>(); private DatabasePlan testDatabasePlan; public String getMainContentDatabaseName() { return mainContentDatabaseName; } public void setMainContentDatabaseName(String mainContentDatabaseName) { this.mainContentDatabaseName = mainContentDatabaseName; } public Map getDatabasePlanMap() { return databasePlanMap; } public void setDatabasePlanMap(Map databasePlanMap) { this.databasePlanMap = databasePlanMap; } public DatabasePlan getTestDatabasePlan() { return testDatabasePlan; } public void setTestDatabasePlan(DatabasePlan testDatabasePlan) { this.testDatabasePlan = testDatabasePlan; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy