org.craftercms.deployer.impl.TargetServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.craftercms.deployer.impl.TargetServiceImpl.java

Source

/*
 * Copyright (C) 2007-2017 Crafter Software Corporation.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.craftercms.deployer.impl;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Template;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.configuration2.CombinedConfiguration;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.tree.OverrideCombiner;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.AbstractFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.craftercms.commons.spring.ApacheCommonsConfiguration2PropertySource;
import org.craftercms.commons.validation.ValidationException;
import org.craftercms.commons.validation.ValidationResult;
import org.craftercms.deployer.api.DeploymentPipeline;
import org.craftercms.deployer.api.Target;
import org.craftercms.deployer.api.TargetService;
import org.craftercms.deployer.api.exceptions.DeployerConfigurationException;
import org.craftercms.deployer.api.exceptions.DeployerException;
import org.craftercms.deployer.api.exceptions.TargetAlreadyExistsException;
import org.craftercms.deployer.api.exceptions.TargetNotFoundException;
import org.craftercms.deployer.api.exceptions.TargetServiceException;
import org.craftercms.deployer.utils.ConfigUtils;
import org.craftercms.deployer.utils.handlebars.MissingValueHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import org.xml.sax.InputSource;

import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_DEPLOYMENT_PIPELINE_CONFIG_KEY;
import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_ENV_CONFIG_KEY;
import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_ID_CONFIG_KEY;
import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_SCHEDULED_DEPLOYMENT_CRON_CONFIG_KEY;
import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_SCHEDULED_DEPLOYMENT_ENABLED_CONFIG_KEY;
import static org.craftercms.deployer.impl.DeploymentConstants.TARGET_SITE_NAME_CONFIG_KEY;

/**
 * Default implementation of {@link TargetService}.
 *
 * @author avasquez
 */
@Component("targetService")
public class TargetServiceImpl implements TargetService, ApplicationListener<ApplicationReadyEvent> {

    private static final Logger logger = LoggerFactory.getLogger(TargetServiceImpl.class);

    public static final String YAML_FILE_EXTENSION = "yaml";
    public static final String APPLICATION_CONTEXT_FILENAME_FORMAT = "%s-context.xml";
    public static final String CONFIG_PROPERTY_SOURCE_NAME = "targetConfig";

    public static final String TARGET_ENV_MODEL_KEY = "env";
    public static final String TARGET_SITE_NAME_MODEL_KEY = "site_name";
    public static final String TARGET_ID_MODEL_KEY = "target_id";

    protected File targetConfigFolder;
    protected Resource baseTargetYamlConfigResource;
    protected Resource baseTargetYamlConfigOverrideResource;
    protected Resource baseTargetContextResource;
    protected Resource baseTargetContextOverrideResource;
    protected String defaultTargetConfigTemplateName;
    protected Handlebars targetConfigTemplateEngine;
    protected ApplicationContext mainApplicationContext;
    protected DeploymentPipelineFactory deploymentPipelineFactory;
    protected TaskScheduler taskScheduler;
    protected ProcessedCommitsStore processedCommitsStore;
    protected Set<Target> loadedTargets;

    public TargetServiceImpl(@Value("${deployer.main.targets.config.folderPath}") File targetConfigFolder,
            @Value("${deployer.main.targets.config.baseYaml.location}") Resource baseTargetYamlConfigResource,
            @Value("${deployer.main.targets.config.baseYaml.overrideLocation}") Resource baseTargetYamlConfigOverrideResource,
            @Value("${deployer.main.targets.config.baseContext.location}") Resource baseTargetContextResource,
            @Value("${deployer.main.targets.config.baseContext.overrideLocation}") Resource baseTargetContextOverrideResource,
            @Value("${deployer.main.targets.config.templates.default}") String defaultTargetConfigTemplateName,
            @Autowired Handlebars targetConfigTemplateEngine, @Autowired ApplicationContext mainApplicationContext,
            @Autowired DeploymentPipelineFactory deploymentPipelineFactory, @Autowired TaskScheduler taskScheduler,
            @Autowired ProcessedCommitsStore processedCommitsStore) throws IOException {
        this.targetConfigFolder = targetConfigFolder;
        this.baseTargetYamlConfigResource = baseTargetYamlConfigResource;
        this.baseTargetYamlConfigOverrideResource = baseTargetYamlConfigOverrideResource;
        this.baseTargetContextResource = baseTargetContextResource;
        this.baseTargetContextOverrideResource = baseTargetContextOverrideResource;
        this.defaultTargetConfigTemplateName = defaultTargetConfigTemplateName;
        this.targetConfigTemplateEngine = targetConfigTemplateEngine;
        this.mainApplicationContext = mainApplicationContext;
        this.deploymentPipelineFactory = deploymentPipelineFactory;
        this.taskScheduler = taskScheduler;
        this.processedCommitsStore = processedCommitsStore;
        this.loadedTargets = new HashSet<>();
    }

    @PostConstruct
    public void init() throws DeployerException {
        if (!targetConfigFolder.exists()) {
            logger.info("Target config folder " + targetConfigFolder + " doesn't exist. Creating it");

            try {
                FileUtils.forceMkdir(targetConfigFolder);
            } catch (IOException e) {
                throw new DeployerException("Failed to create target config folder at " + targetConfigFolder);
            }
        }
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // Load all existing targets on startup
        try {
            resolveTargets();
        } catch (DeployerException e) {
            logger.error("Error while loading targets on startup", e);
        }
    }

    @PreDestroy
    public void destroy() {
        logger.info("Closing all targets...");

        if (CollectionUtils.isNotEmpty(loadedTargets)) {
            loadedTargets.forEach(Target::close);
        }
    }

    @Override
    public synchronized List<Target> resolveTargets() throws TargetServiceException {
        Collection<File> configFiles = getTargetConfigFiles();
        List<Target> targets = new ArrayList<>();

        if (CollectionUtils.isNotEmpty(configFiles)) {
            closeTargetsWithNoConfigFile(configFiles);

            for (File file : configFiles) {
                Target target = resolveTargetFromConfigFile(file);
                targets.add(target);
            }
        }

        return targets;
    }

    @Override
    public List<Target> getAllTargets() throws TargetServiceException {
        return new ArrayList<>(loadedTargets);
    }

    @Override
    public synchronized Target getTarget(String env, String siteName) throws TargetNotFoundException {
        String id = TargetImpl.getId(env, siteName);
        Target target = findLoadedTargetById(id);

        if (target != null) {
            return target;
        } else {
            throw new TargetNotFoundException(id);
        }
    }

    @Override
    public synchronized Target createTarget(String env, String siteName, boolean replace, String templateName,
            Map<String, Object> templateParams) throws TargetAlreadyExistsException, TargetServiceException {
        String id = TargetImpl.getId(env, siteName);
        File configFile = new File(targetConfigFolder, id + "." + YAML_FILE_EXTENSION);

        if (!replace && configFile.exists()) {
            throw new TargetAlreadyExistsException(id);
        } else {
            createConfigFromTemplate(env, siteName, id, templateName, templateParams, configFile);
        }

        return resolveTargetFromConfigFile(configFile);
    }

    @Override
    public synchronized void deleteTarget(String env, String siteName)
            throws TargetNotFoundException, TargetServiceException {
        Target target = getTarget(env, siteName);
        String id = target.getId();

        target.close();

        logger.info("Removing loaded target '{}'", id);

        loadedTargets.remove(target);

        try {
            processedCommitsStore.delete(id);
        } catch (DeployerException e) {
            throw new TargetServiceException(
                    "Error while deleting processed commit from store for target '" + id + "'", e);
        }

        File configFile = target.getConfigurationFile();
        if (configFile.exists()) {
            logger.info("Deleting target configuration file at {}", configFile);

            FileUtils.deleteQuietly(configFile);
        }

        File contextFile = new File(targetConfigFolder,
                String.format(APPLICATION_CONTEXT_FILENAME_FORMAT, configFile.getName()));
        if (contextFile.exists()) {
            logger.info("Deleting target context file at {}", contextFile);

            FileUtils.deleteQuietly(contextFile);
        }
    }

    protected Collection<File> getTargetConfigFiles() throws TargetServiceException {
        if (targetConfigFolder.exists()) {
            Collection<File> yamlFiles = FileUtils.listFiles(targetConfigFolder, new CustomConfigFileFilter(),
                    null);

            if (CollectionUtils.isEmpty(yamlFiles)) {
                logger.warn("No YAML config files found under {}", targetConfigFolder.getAbsolutePath());
            }

            return yamlFiles;
        } else {
            logger.warn("Config folder {} doesn't exist. Trying to create it...",
                    targetConfigFolder.getAbsolutePath());

            try {
                FileUtils.forceMkdir(targetConfigFolder);
            } catch (IOException e) {
                throw new TargetServiceException("Unable to create config folder " + targetConfigFolder, e);
            }

            return Collections.emptyList();
        }
    }

    protected void closeTargetsWithNoConfigFile(Collection<File> configFiles) {
        if (CollectionUtils.isNotEmpty(loadedTargets)) {
            loadedTargets.removeIf(target -> {
                File configFile = target.getConfigurationFile();
                if (!configFiles.contains(configFile)) {
                    logger.info("Config file {} doesn't exist anymore for target '{}'. Closing target...",
                            configFile);

                    target.close();

                    return true;
                } else {
                    return false;
                }
            });
        }
    }

    protected Target resolveTargetFromConfigFile(File configFile) throws TargetServiceException {
        String baseName = FilenameUtils.getBaseName(configFile.getName());
        File contextFile = new File(targetConfigFolder,
                String.format(APPLICATION_CONTEXT_FILENAME_FORMAT, baseName));
        Target target = findLoadedTargetByConfigFile(configFile);

        if (target != null) {
            // Check if the YAML config file or the app context file have changed since target load.
            long yamlLastModified = configFile.exists() ? configFile.lastModified() : 0;
            long contextLastModified = contextFile.exists() ? contextFile.lastModified() : 0;
            long targetLoadedDate = target.getLoadDate().toInstant().toEpochMilli();

            // Refresh if the files have been modified.
            if (yamlLastModified >= targetLoadedDate || contextLastModified >= targetLoadedDate) {
                logger.info("Configuration files haven been updated for '{}'. The target will be reloaded.",
                        target.getId());

                target.close();

                loadedTargets.remove(target);

                target = null;
            }
        } else {
            logger.info("No loaded target found for configuration file {}", configFile);
        }

        if (target == null) {
            logger.info("Loading target for configuration file {}", configFile);

            target = createTarget(configFile, contextFile);
            loadedTargets.add(target);
        }

        return target;
    }

    protected Target createTarget(File configFile, File contextFile) throws TargetServiceException {
        try {
            HierarchicalConfiguration config = loadConfiguration(configFile);
            String env = ConfigUtils.getRequiredStringProperty(config, TARGET_ENV_CONFIG_KEY);
            String siteName = ConfigUtils.getRequiredStringProperty(config, TARGET_SITE_NAME_CONFIG_KEY);
            String targetId = TargetImpl.getId(env, siteName);

            config.setProperty(TARGET_ID_CONFIG_KEY, targetId);

            ConfigurableApplicationContext context = loadApplicationContext(config, contextFile);
            DeploymentPipeline deploymentPipeline = deploymentPipelineFactory.getPipeline(config, context,
                    TARGET_DEPLOYMENT_PIPELINE_CONFIG_KEY);
            Target target = new TargetImpl(env, siteName, deploymentPipeline, configFile, config, context);

            scheduleDeployment(target);

            return target;
        } catch (Exception e) {
            throw new TargetServiceException("Failed to create target for configuration file " + configFile, e);
        }
    }

    protected HierarchicalConfiguration loadConfiguration(File configFile) throws DeployerConfigurationException {
        String configFilename = configFile.getPath();

        logger.debug("Loading target YAML config at {}", configFilename);

        HierarchicalConfiguration config = ConfigUtils.loadYamlConfiguration(configFile);

        if (baseTargetYamlConfigResource.exists() || baseTargetYamlConfigOverrideResource.exists()) {
            CombinedConfiguration combinedConfig = new CombinedConfiguration(new OverrideCombiner());

            combinedConfig.addConfiguration(config);

            if (baseTargetYamlConfigOverrideResource.exists()) {
                logger.debug("Loading base target YAML config override at {}",
                        baseTargetYamlConfigOverrideResource);

                combinedConfig
                        .addConfiguration(ConfigUtils.loadYamlConfiguration(baseTargetYamlConfigOverrideResource));
            }
            if (baseTargetYamlConfigResource.exists()) {
                logger.debug("Loading base target YAML config at {}", baseTargetYamlConfigResource);

                combinedConfig.addConfiguration(ConfigUtils.loadYamlConfiguration(baseTargetYamlConfigResource));
            }

            return combinedConfig;
        } else {
            return config;
        }
    }

    protected ConfigurableApplicationContext loadApplicationContext(HierarchicalConfiguration config,
            File contextFile) throws DeployerConfigurationException {
        GenericApplicationContext context = new GenericApplicationContext(mainApplicationContext);

        MutablePropertySources propertySources = context.getEnvironment().getPropertySources();
        propertySources
                .addFirst(new ApacheCommonsConfiguration2PropertySource(CONFIG_PROPERTY_SOURCE_NAME, config));

        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context);
        reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD);

        if (baseTargetContextResource.exists()) {
            logger.debug("Loading base target application context at {}", baseTargetContextResource);

            try {
                reader.loadBeanDefinitions(baseTargetContextResource);
            } catch (Exception e) {
                throw new DeployerConfigurationException(
                        "Failed to load application context at " + baseTargetContextResource, e);
            }
        }
        if (baseTargetContextOverrideResource.exists()) {
            logger.debug("Loading base target application context override at {}",
                    baseTargetContextOverrideResource);

            try {
                reader.loadBeanDefinitions(baseTargetContextOverrideResource);
            } catch (Exception e) {
                throw new DeployerConfigurationException(
                        "Failed to load application context at " + baseTargetContextOverrideResource, e);
            }
        }
        if (contextFile.exists()) {
            logger.debug("Loading target application context at {}", contextFile);

            try (InputStream in = new BufferedInputStream(new FileInputStream(contextFile))) {
                reader.loadBeanDefinitions(new InputSource(in));
            } catch (Exception e) {
                throw new DeployerConfigurationException("Failed to load application context at " + contextFile, e);
            }
        }

        context.refresh();

        return context;
    }

    protected void scheduleDeployment(Target target) throws DeployerConfigurationException {
        boolean enabled = ConfigUtils.getBooleanProperty(target.getConfiguration(),
                TARGET_SCHEDULED_DEPLOYMENT_ENABLED_CONFIG_KEY, true);
        String cron = ConfigUtils.getStringProperty(target.getConfiguration(),
                TARGET_SCHEDULED_DEPLOYMENT_CRON_CONFIG_KEY);

        if (enabled && StringUtils.isNotEmpty(cron)) {
            logger.info("Deployment for target '{}' scheduled with cron {}", target.getId(), cron);

            target.scheduleDeployment(taskScheduler, cron);
        }
    }

    protected void createConfigFromTemplate(String env, String siteName, String targetId, String templateName,
            Map<String, Object> templateParameters, File configFile) throws TargetServiceException {
        if (StringUtils.isEmpty(templateName)) {
            templateName = defaultTargetConfigTemplateName;
        }

        Map<String, Object> templateModel = new HashMap<>();
        templateModel.put(TARGET_ENV_MODEL_KEY, env);
        templateModel.put(TARGET_SITE_NAME_MODEL_KEY, siteName);
        templateModel.put(TARGET_ID_MODEL_KEY, targetId);

        if (MapUtils.isNotEmpty(templateParameters)) {
            templateModel.putAll(templateParameters);
        }

        logger.info("Creating new target YAML configuration at {} using template '{}'", configFile, templateName);

        try (Writer out = new BufferedWriter(new FileWriter(configFile))) {
            processConfigTemplate(templateName, templateModel, out);

            out.flush();
        } catch (IOException e) {
            throw new TargetServiceException("Unable to open writer to YAML configuration file " + configFile, e);
        } catch (TargetServiceException e) {
            FileUtils.deleteQuietly(configFile);

            throw e;
        }
    }

    protected void processConfigTemplate(String templateName, Object templateModel, Writer out)
            throws TargetServiceException {
        MissingValueHelper helper = MissingValueHelper.INSTANCE;

        try {
            Template template = targetConfigTemplateEngine.compile(templateName);
            template.apply(templateModel, out);
        } catch (IOException e) {
            throw new TargetServiceException("Processing of configuration template '" + templateName + "' failed",
                    e);
        }

        ValidationResult result = helper.getValidationResult();

        helper.clearValidationResult();

        if (result != null && result.hasErrors()) {
            throw new TargetServiceException(new ValidationException(result));
        }
    }

    protected Target findLoadedTargetByConfigFile(File configFile) {
        if (CollectionUtils.isNotEmpty(loadedTargets)) {
            return loadedTargets.stream().filter(target -> target.getConfigurationFile().equals(configFile))
                    .findFirst().orElse(null);
        } else {
            return null;
        }
    }

    protected Target findLoadedTargetById(String id) {
        if (CollectionUtils.isNotEmpty(loadedTargets)) {
            return loadedTargets.stream().filter(target -> target.getId().equals(id)).findFirst().orElse(null);
        } else {
            return null;
        }
    }

    protected class CustomConfigFileFilter extends AbstractFileFilter {

        @Override
        public boolean accept(File file) {
            String filename = file.getName();

            return !filename.equals(baseTargetYamlConfigResource.getFilename())
                    && !filename.equals(baseTargetYamlConfigOverrideResource.getFilename())
                    && filename.endsWith(YAML_FILE_EXTENSION);
        }
    }

}