/* (c) 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.backuprestore; import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; import org.geoserver.backuprestore.utils.BackupUtils; import org.geoserver.catalog.Catalog; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerDataDirectory; import org.geoserver.config.util.XStreamPersister; import org.geoserver.config.util.XStreamPersisterFactory; import org.geoserver.platform.ContextLoadedEvent; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.resource.Resource; import org.geotools.factory.Hints; import org.geotools.filter.text.ecql.ECQL; import org.geotools.util.logging.Logging; import org.opengis.filter.Filter; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.launch.JobExecutionNotRunningException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.NoSuchJobException; import org.springframework.batch.core.launch.NoSuchJobExecutionException; import org.springframework.batch.core.listener.JobExecutionListenerSupport; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import com.thoughtworks.xstream.XStream; /** * Primary controller/facade of the backup and restore subsystem. * * @author Alessio Fabiani, GeoSolutions * */ @SuppressWarnings("rawtypes") public class Backup extends JobExecutionListenerSupport implements DisposableBean, ApplicationContextAware, ApplicationListener { static Logger LOGGER = Logging.getLogger(Backup.class); /* Job Parameters Keys **/ public static final String PARAM_TIME = "time"; public static final String PARAM_JOB_NAME = "job.execution.name"; public static final String PARAM_OUTPUT_FILE_PATH = "output.file.path"; public static final String PARAM_INPUT_FILE_PATH = "input.file.path"; public static final String PARAM_CLEANUP_TEMP = "BK_CLEANUP_TEMP"; public static final String PARAM_DRY_RUN_MODE = "BK_DRY_RUN"; public static final String PARAM_BEST_EFFORT_MODE = "BK_BEST_EFFORT"; /* Jobs Context Keys **/ public static final String BACKUP_JOB_NAME = "backupJob"; public static final String RESTORE_JOB_NAME = "restoreJob"; public static final String RESTORE_CATALOG_KEY = "restore.catalog"; /** catalog */ Catalog catalog; GeoServer geoServer; GeoServerResourceLoader resourceLoader; GeoServerDataDirectory geoServerDataDirectory; XStreamPersisterFactory xpf; JobOperator jobOperator; JobLauncher jobLauncher; JobRepository jobRepository; Job backupJob; Job restoreJob; ConcurrentHashMap<Long, BackupExecutionAdapter> backupExecutions = new ConcurrentHashMap<Long, BackupExecutionAdapter>(); ConcurrentHashMap<Long, RestoreExecutionAdapter> restoreExecutions = new ConcurrentHashMap<Long, RestoreExecutionAdapter>(); Integer totalNumberOfBackupSteps; Integer totalNumberOfRestoreSteps; /** * A static application context */ private static ApplicationContext context; public Backup(Catalog catalog, GeoServerResourceLoader rl) { this.catalog = catalog; this.geoServer = GeoServerExtensions.bean(GeoServer.class); this.resourceLoader = rl; this.geoServerDataDirectory = new GeoServerDataDirectory(rl); this.xpf = GeoServerExtensions.bean(XStreamPersisterFactory.class); } /** * @return the context */ public static ApplicationContext getContext() { return context; } /** * @return the jobOperator */ public JobOperator getJobOperator() { return jobOperator; } /** * @return the jobLauncher */ public JobLauncher getJobLauncher() { return jobLauncher; } /** * @return the Backup job */ public Job getBackupJob() { return backupJob; } /** * @return the Restore job */ public Job getRestoreJob() { return restoreJob; } /** * @return the backupExecutions */ public ConcurrentHashMap<Long, BackupExecutionAdapter> getBackupExecutions() { return backupExecutions; } /** * @return the restoreExecutions */ public ConcurrentHashMap<Long, RestoreExecutionAdapter> getRestoreExecutions() { return restoreExecutions; } @Override public void onApplicationEvent(ApplicationEvent event) { // load the context store here to avoid circular dependency on creation if (event instanceof ContextLoadedEvent) { this.jobOperator = (JobOperator) context.getBean("jobOperator"); this.jobLauncher = (JobLauncher) context.getBean("jobLauncherAsync"); this.jobRepository = (JobRepository) context.getBean("jobRepository"); this.backupJob = (Job) context.getBean(BACKUP_JOB_NAME); this.restoreJob = (Job) context.getBean(RESTORE_JOB_NAME); } } /** * @return */ public Set<Long> getBackupRunningExecutions() { synchronized (jobOperator) { Set<Long> runningExecutions; try { runningExecutions = jobOperator.getRunningExecutions(BACKUP_JOB_NAME); } catch (NoSuchJobException e) { runningExecutions = new HashSet<>(); } return runningExecutions; } } /** * @return */ public Set<Long> getRestoreRunningExecutions() { synchronized (jobOperator) { Set<Long> runningExecutions; try { runningExecutions = jobOperator.getRunningExecutions(RESTORE_JOB_NAME); } catch (NoSuchJobException e) { runningExecutions = new HashSet<>(); } return runningExecutions; } } public Catalog getCatalog() { return catalog; } public GeoServer getGeoServer() { return geoServer; } /** * @return the resourceLoader */ public GeoServerResourceLoader getResourceLoader() { return resourceLoader; } /** * @param resourceLoader the resourceLoader to set */ public void setResourceLoader(GeoServerResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } /** * @return the geoServerDataDirectory */ public GeoServerDataDirectory getGeoServerDataDirectory() { return geoServerDataDirectory; } /** * @param geoServerDataDirectory the geoServerDataDirectory to set */ public void setGeoServerDataDirectory(GeoServerDataDirectory geoServerDataDirectory) { this.geoServerDataDirectory = geoServerDataDirectory; } /** * @return the totalNumberOfBackupSteps */ public Integer getTotalNumberOfBackupSteps() { return totalNumberOfBackupSteps; } /** * @param totalNumberOfBackupSteps the totalNumberOfBackupSteps to set */ public void setTotalNumberOfBackupSteps(Integer totalNumberOfBackupSteps) { this.totalNumberOfBackupSteps = totalNumberOfBackupSteps; } /** * @return the totalNumberOfRestoreSteps */ public Integer getTotalNumberOfRestoreSteps() { return totalNumberOfRestoreSteps; } /** * @param totalNumberOfRestoreSteps the totalNumberOfRestoreSteps to set */ public void setTotalNumberOfRestoreSteps(Integer totalNumberOfRestoreSteps) { this.totalNumberOfRestoreSteps = totalNumberOfRestoreSteps; } @Override public void destroy() throws Exception { // Nothing to do. } @Override public void setApplicationContext(ApplicationContext context) throws BeansException { Backup.context = context; try { AbstractJob backupJob = (AbstractJob) context.getBean(BACKUP_JOB_NAME); if (backupJob != null) { this.setTotalNumberOfBackupSteps(backupJob.getStepNames().size()); } AbstractJob restoreJob = (AbstractJob) context.getBean(BACKUP_JOB_NAME); if (restoreJob != null) { this.setTotalNumberOfRestoreSteps(restoreJob.getStepNames().size()); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not fully configure the Backup Facade!", e); } } protected String getItemName(XStreamPersister xp, Class clazz) { return xp.getClassAliasingMapper().serializedClass(clazz); } /** * @return * @throws IOException * */ public BackupExecutionAdapter runBackupAsync(final Resource archiveFile, final boolean overwrite, final Filter filter, final Hints params) throws IOException { // Check if archiveFile exists if (archiveFile.file().exists()) { if (!overwrite && FileUtils.sizeOf(archiveFile.file()) > 0) { // Unless the user explicitly wants to overwrite the archiveFile, throw an exception whenever it already exists throw new IOException( "The target archive file already exists. Use 'overwrite=TRUE' if you want to overwrite it."); } else { FileUtils.forceDelete(archiveFile.file()); } } else { // Make sure the parent path exists if (!archiveFile.file().getParentFile().exists()) { try { archiveFile.file().getParentFile().mkdirs(); } finally { if (!archiveFile.file().getParentFile().exists()) { throw new IOException("The path to target archive file is unreachable."); } } } } // Initialize ZIP FileUtils.touch(archiveFile.file()); // Write flat files into a temporary folder Resource tmpDir = BackupUtils.geoServerTmpDir(getGeoServerDataDirectory()); // Fill Job Parameters JobParametersBuilder paramsBuilder = new JobParametersBuilder(); if (filter != null) { paramsBuilder.addString("filter", ECQL.toCQL(filter)); } paramsBuilder .addString(PARAM_JOB_NAME, BACKUP_JOB_NAME) .addString(PARAM_OUTPUT_FILE_PATH, BackupUtils.getArchiveURLProtocol(tmpDir) + tmpDir.path()) .addLong(PARAM_TIME, System.currentTimeMillis()); parseParams(params, paramsBuilder); JobParameters jobParameters = paramsBuilder.toJobParameters(); // Send Execution Signal BackupExecutionAdapter backupExecution; try { if (getRestoreRunningExecutions().isEmpty() && getBackupRunningExecutions().isEmpty()) { synchronized (jobOperator) { // Start a new Job JobExecution jobExecution = jobLauncher.run(backupJob, jobParameters); backupExecution = new BackupExecutionAdapter(jobExecution, totalNumberOfBackupSteps); backupExecutions.put(backupExecution.getId(), backupExecution); backupExecution.setArchiveFile(archiveFile); backupExecution.setOverwrite(overwrite); backupExecution.setFilter(filter); backupExecution.getOptions().add("OVERWRITE=" + overwrite); for (Entry jobParam : jobParameters.toProperties().entrySet()) { if (!PARAM_OUTPUT_FILE_PATH.equals(jobParam.getKey()) && !PARAM_INPUT_FILE_PATH.equals(jobParam.getKey()) && !PARAM_TIME.equals(jobParam.getKey())) { backupExecution.getOptions() .add(jobParam.getKey() + "=" + jobParam.getValue()); } } return backupExecution; } } else { throw new IOException( "Could not start a new Backup Job Execution since there are currently Running jobs."); } } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) { throw new IOException("Could not start a new Backup Job Execution: ", e); } finally { } } /** * @return * @return * @throws IOException * */ public RestoreExecutionAdapter runRestoreAsync(final Resource archiveFile, final Filter filter, final Hints params) throws IOException { // Extract archive into a temporary folder Resource tmpDir = BackupUtils.geoServerTmpDir(getGeoServerDataDirectory()); BackupUtils.extractTo(archiveFile, tmpDir); // Fill Job Parameters JobParametersBuilder paramsBuilder = new JobParametersBuilder(); if (filter != null) { paramsBuilder.addString("filter", ECQL.toCQL(filter)); } paramsBuilder .addString(PARAM_JOB_NAME, RESTORE_JOB_NAME) .addString(PARAM_INPUT_FILE_PATH, BackupUtils.getArchiveURLProtocol(tmpDir) + tmpDir.path()) .addLong(PARAM_TIME, System.currentTimeMillis()); parseParams(params, paramsBuilder); JobParameters jobParameters = paramsBuilder.toJobParameters(); RestoreExecutionAdapter restoreExecution; try { if (getRestoreRunningExecutions().isEmpty() && getBackupRunningExecutions().isEmpty()) { synchronized (jobOperator) { // Start a new Job JobExecution jobExecution = jobLauncher.run(restoreJob, jobParameters); restoreExecution = new RestoreExecutionAdapter(jobExecution, totalNumberOfRestoreSteps); restoreExecutions.put(restoreExecution.getId(), restoreExecution); restoreExecution.setArchiveFile(archiveFile); restoreExecution.setFilter(filter); for (Entry jobParam : jobParameters.toProperties().entrySet()) { if (!PARAM_OUTPUT_FILE_PATH.equals(jobParam.getKey()) && !PARAM_INPUT_FILE_PATH.equals(jobParam.getKey()) && !PARAM_TIME.equals(jobParam.getKey())) { restoreExecution.getOptions() .add(jobParam.getKey() + "=" + jobParam.getValue()); } } return restoreExecution; } } else { throw new IOException( "Could not start a new Restore Job Execution since there are currently Running jobs."); } } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) { throw new IOException("Could not start a new Restore Job Execution: ", e); } finally { } } @Override public void afterJob(JobExecution jobExecution) { // Release locks on GeoServer Configuration: try { List<BackupRestoreCallback> callbacks = GeoServerExtensions.extensions(BackupRestoreCallback.class); for (BackupRestoreCallback callback : callbacks) { callback.onEndRequest(); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not unlock GeoServer Catalog Configuration!", e); } } @Override public void beforeJob(JobExecution jobExecution) { // Acquire GeoServer Configuration Lock in READ mode List<BackupRestoreCallback> callbacks = GeoServerExtensions.extensions(BackupRestoreCallback.class); for (BackupRestoreCallback callback : callbacks) { callback.onBeginRequest(jobExecution.getJobParameters().getString(PARAM_JOB_NAME)); } } /** * Stop a running Backup/Restore Execution * * @param executionId * @return * @throws NoSuchJobExecutionException * @throws JobExecutionNotRunningException */ public void stopExecution(Long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException { LOGGER.info("Stopping execution id [" + executionId + "]"); JobExecution jobExecution = null; try { if (this.backupExecutions.get(executionId) != null) { jobExecution = this.backupExecutions.get(executionId).getDelegate(); } else if (this.restoreExecutions.get(executionId) != null) { jobExecution = this.restoreExecutions.get(executionId).getDelegate(); } jobOperator.stop(executionId); } finally { if (jobExecution != null) { final BatchStatus status = jobExecution.getStatus(); if (!status.isGreaterThan(BatchStatus.STARTED)) { jobExecution.setStatus(BatchStatus.STOPPING); jobExecution.setEndTime(new Date()); jobRepository.update(jobExecution); } } // Release locks on GeoServer Configuration: try { List<BackupRestoreCallback> callbacks = GeoServerExtensions.extensions(BackupRestoreCallback.class); for (BackupRestoreCallback callback : callbacks) { callback.onEndRequest(); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not unlock GeoServer Catalog Configuration!", e); } } } /** * Restarts a running Backup/Restore Execution * * @param executionId * @return * @throws JobInstanceAlreadyCompleteException * @throws NoSuchJobExecutionException * @throws NoSuchJobException * @throws JobRestartException * @throws JobParametersInvalidException */ public Long restartExecution(Long executionId) throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, NoSuchJobException, JobRestartException, JobParametersInvalidException { return jobOperator.restart(executionId); } /** * Abort a running Backup/Restore Execution * * @param executionId * @throws NoSuchJobExecutionException * @throws JobExecutionAlreadyRunningException */ public void abandonExecution(Long executionId) throws NoSuchJobExecutionException, JobExecutionAlreadyRunningException { LOGGER.info("Aborting execution id [" + executionId + "]"); JobExecution jobExecution = null; try { if (this.backupExecutions.get(executionId) != null) { jobExecution = this.backupExecutions.get(executionId).getDelegate(); } else if (this.restoreExecutions.get(executionId) != null) { jobExecution = this.restoreExecutions.get(executionId).getDelegate(); } jobOperator.abandon(executionId); } finally { if (jobExecution != null) { jobExecution.setStatus(BatchStatus.ABANDONED); jobExecution.setEndTime(new Date()); jobRepository.update(jobExecution); } // Release locks on GeoServer Configuration: try { List<BackupRestoreCallback> callbacks = GeoServerExtensions.extensions(BackupRestoreCallback.class); for (BackupRestoreCallback callback : callbacks) { callback.onEndRequest(); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not unlock GeoServer Catalog Configuration!", e); } } } /** * @param params * @param paramsBuilder */ private void parseParams(final Hints params, JobParametersBuilder paramsBuilder) { if (params != null) { for (Entry<Object, Object> param : params.entrySet()) { if (param.getKey() instanceof Hints.OptionKey) { final Set<String> key = ((Hints.OptionKey) param.getKey()).getOptions(); for (String k : key) { switch (k) { case PARAM_CLEANUP_TEMP: case PARAM_DRY_RUN_MODE: case PARAM_BEST_EFFORT_MODE: if (paramsBuilder.toJobParameters().getString(k) == null) { paramsBuilder.addString(k, "true"); } } } } } } } public XStreamPersister createXStreamPersisterXML() { return initXStreamPersister(new XStreamPersisterFactory().createXMLPersister()); } public XStreamPersister createXStreamPersisterJSON() { return initXStreamPersister(new XStreamPersisterFactory().createJSONPersister()); } public XStreamPersister initXStreamPersister(XStreamPersister xp) { xp.setCatalog(catalog); // xp.setReferenceByName(true); XStream xs = xp.getXStream(); // ImportContext xs.alias("backup", BackupExecutionAdapter.class); // security xs.allowTypes(new Class[] { BackupExecutionAdapter.class }); xs.allowTypeHierarchy(Resource.class); return xp; } }