/* (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.tasklet;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.logging.Logger;
import org.geoserver.backuprestore.Backup;
import org.geoserver.backuprestore.BackupRestoreItem;
import org.geoserver.backuprestore.utils.BackupUtils;
import org.geoserver.config.ServiceInfo;
import org.geoserver.config.util.XStreamPersisterFactory;
import org.geoserver.config.util.XStreamServiceLoader;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.platform.resource.ResourceStore;
import org.geoserver.platform.resource.Resources;
import org.geoserver.platform.resource.Resources.AnyFilter;
import org.geoserver.util.Filter;
import org.geotools.util.logging.Logging;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.StoppableTasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.Assert;
/**
* Base Class for Backup and Restore custom Tasklets. <br>
* Exposes some utility methods to correctly marshall/unmarshall items to/from backup folder.
*
* The logic is executed asynchronously using injected {@link #setTaskExecutor(TaskExecutor)} - timeout value is required to be set, so that the batch
* job does not hang forever if the external process hangs.
*
* Tasklet periodically checks for termination status (i.e. {@link #doExecute(StepContribution,ChunkContext,JobExecution)} finished its execution or
* {@link #setTimeout(long)} expired or job was interrupted). The check interval is given by {@link #setTerminationCheckInterval(long)}.
*
* When job interrupt is detected tasklet's execution is terminated immediately by throwing {@link JobInterruptedException}.
*
* {@link #setInterruptOnCancel(boolean)} specifies whether the tasklet should attempt to interrupt the thread that executes the system command if it
* is still running when tasklet exits (abnormally).
*
*
* @author Robert Kasanicky
* @author Will Schipp
* @author Alessio Fabiani, GeoSolutions
*/
@SuppressWarnings("rawtypes")
public abstract class AbstractCatalogBackupRestoreTasklet<T> extends BackupRestoreItem
implements StoppableTasklet, InitializingBean {
protected static Logger LOGGER = Logging.getLogger(AbstractCatalogBackupRestoreTasklet.class);
/*
*
*/
protected static Map<String, Filter<Resource>> resources = new HashMap<String, Filter<Resource>>();
/*
*
*/
static {
resources.put("demo", AnyFilter.INSTANCE);
resources.put("images", AnyFilter.INSTANCE);
resources.put("logs", new Filter<Resource>() {
@Override
public boolean accept(Resource res) {
if (res.name().endsWith(".properties")) {
return true;
}
return false;
}
});
resources.put("palettes", AnyFilter.INSTANCE);
resources.put("plugIns", AnyFilter.INSTANCE);
// NOTE: it would be better to use ad-hoc Visitors in order to scan the
// Style Resources and download only the ones needed.
// This maybe an improvement for a future release/refactoring.
resources.put("styles", new Filter<Resource>() {
@Override
public boolean accept(Resource res) {
if (res.name().toLowerCase().endsWith("sld") || // exclude everything ends with SLD ext (SLD, YSLD, ...)
res.name().toLowerCase().endsWith(".xml")
|| res.name().toLowerCase().endsWith(".css")) // exclude CSS also
{
return false;
}
return true;
}
});
resources.put("user_projections", AnyFilter.INSTANCE);
resources.put("validation", AnyFilter.INSTANCE);
resources.put("www", AnyFilter.INSTANCE);
}
private long timeout = 0;
private long checkInterval = 1000;
private StepExecution execution = null;
private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
private boolean interruptOnCancel = false;
private volatile boolean stopped = false;
public AbstractCatalogBackupRestoreTasklet(Backup backupFacade,
XStreamPersisterFactory xStreamPersisterFactory) {
super(backupFacade, xStreamPersisterFactory);
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
throws Exception {
super.retrieveInterstepData(chunkContext.getStepContext().getStepExecution());
JobExecution jobExecution = chunkContext.getStepContext().getStepExecution()
.getJobExecution();
FutureTask<RepeatStatus> theTask = new FutureTask<RepeatStatus>(
new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return doExecute(contribution, chunkContext, jobExecution);
}
});
long t0 = System.currentTimeMillis();
taskExecutor.execute(theTask);
while (true) {
Thread.sleep(checkInterval);// moved to the end of the logic
JobExecution currentExecution = chunkContext.getStepContext().getStepExecution()
.getJobExecution();
if (currentExecution.isStopping()) {
stopped = true;
}
if (theTask.isDone()) {
return theTask.get();
} else if (System.currentTimeMillis() - t0 > timeout) {
theTask.cancel(interruptOnCancel);
JobInterruptedException exception = new JobInterruptedException(
"Job " + currentExecution
+ " did not finish within the timeout.");
logValidationExceptions((T) null, exception);
return RepeatStatus.FINISHED;
} else if (execution != null && execution.isTerminateOnly()) {
theTask.cancel(interruptOnCancel);
JobInterruptedException exception = new JobInterruptedException(
"Job " + currentExecution
+ " interrupted while executing.");
logValidationExceptions((T) null, exception);
return RepeatStatus.FINISHED;
} else if (stopped) {
theTask.cancel(interruptOnCancel);
contribution.setExitStatus(ExitStatus.STOPPED);
return RepeatStatus.FINISHED;
}
}
}
/**
*
* @param contribution
* @param chunkContext
* @param jobExecution
* @return
* @throws Exception
*/
abstract RepeatStatus doExecute(StepContribution contribution, ChunkContext chunkContext,
JobExecution jobExecution) throws Exception;
/**
* @param resourceStore
* @param baseDir
* @throws Exception
* @throws IOException
*/
public void backupRestoreAdditionalResources(ResourceStore resourceStore, Resource baseDir)
throws Exception {
try {
for (Entry<String, Filter<Resource>> entry : resources.entrySet()) {
Resource resource = resourceStore.get(entry.getKey());
if (resource != null && Resources.exists(resource)) {
List<Resource> resources = Resources.list(resource, entry.getValue(), false);
Resource targetDir = BackupUtils.dir(baseDir, resource.name());
for (Resource res : resources) {
if (res.getType() != Type.DIRECTORY) {
Resources.copy(res.file(), targetDir);
} else {
Resources.copy(res, BackupUtils.dir(targetDir, res.name()));
}
}
}
}
} catch (Exception e) {
logValidationExceptions((T) null, e);
}
}
//
@SuppressWarnings({ "unchecked", "static-access" })
public void doWrite(Object item, Resource directory, String fileName) throws Exception {
try {
if (item instanceof ServiceInfo) {
ServiceInfo service = (ServiceInfo) item;
XStreamServiceLoader loader = findServiceLoader(service);
try {
loader.save(service, backupFacade.getGeoServer(),
BackupUtils.dir(directory, fileName));
} catch (Throwable t) {
throw new RuntimeException(t);
// LOGGER.log(Level.SEVERE, "Error occurred while saving configuration", t);
}
} else {
// unwrap dynamic proxies
OutputStream out = Resources.fromPath(fileName, directory).out();
try {
if (getXp() == null) {
xstream = getxStreamPersisterFactory().createXMLPersister();
setXp(xstream.getXStream());
}
item = xstream.unwrapProxies(item);
getXp().toXML(item, out);
} finally {
out.close();
}
}
} catch (Exception e) {
logValidationExceptions((T) null, e);
}
}
//
@SuppressWarnings({ "unchecked" })
public Object doRead(Resource directory, String fileName) throws Exception {
Object item = null;
try {
InputStream in = Resources.fromPath(fileName, directory).in();
// Try first using the Services Loaders
final List<XStreamServiceLoader> loaders = GeoServerExtensions
.extensions(XStreamServiceLoader.class);
for (XStreamServiceLoader<ServiceInfo> l : loaders) {
try {
if (l.getFilename().equals(fileName)) {
item = l.load(backupFacade.getGeoServer(),
Resources.fromPath(fileName, directory));
if (item != null && item instanceof ServiceInfo) {
return item;
}
}
} catch (Exception e) {
// Just skip and try with another loader
item = null;
}
}
try {
if (item == null) {
try {
if (getXp() == null) {
xstream = getxStreamPersisterFactory().createXMLPersister();
setXp(xstream.getXStream());
}
item = getXp().fromXML(in);
} finally {
in.close();
}
}
} catch (Exception e) {
// Collect warnings
item = null;
if (getCurrentJobExecution() != null) {
getCurrentJobExecution().addWarningExceptions(Arrays.asList(e));
}
}
} catch (Exception e) {
logValidationExceptions((T) null, e);
}
return item;
}
@SuppressWarnings({ "unchecked" })
protected XStreamServiceLoader findServiceLoader(ServiceInfo service) {
XStreamServiceLoader loader = null;
final List<XStreamServiceLoader> loaders = GeoServerExtensions
.extensions(XStreamServiceLoader.class);
for (XStreamServiceLoader<ServiceInfo> l : loaders) {
if (l.getServiceClass().isInstance(service)) {
loader = l;
break;
}
}
if (loader == null) {
throw new IllegalArgumentException("No loader for " + service.getName());
}
return loader;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(backupFacade, "backupFacade must be set");
Assert.notNull(getxStreamPersisterFactory(), "xstream must be set");
Assert.isTrue(timeout > 0, "timeout value must be greater than zero");
Assert.notNull(taskExecutor, "taskExecutor is required");
}
/**
* Timeout in milliseconds.
*
* @param timeout upper limit for how long the execution of the external program is allowed to last.
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
/**
* The time interval how often the tasklet will check for termination status.
*
* @param checkInterval time interval in milliseconds (1 second by default).
*/
public void setTerminationCheckInterval(long checkInterval) {
this.checkInterval = checkInterval;
}
/**
* Sets the task executor that will be used to execute the system command NB! Avoid using a synchronous task executor
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* If <code>true</code> tasklet will attempt to interrupt the thread executing the system command if {@link #setTimeout(long)} has been exceeded
* or user interrupts the job. <code>false</code> by default
*/
public void setInterruptOnCancel(boolean interruptOnCancel) {
this.interruptOnCancel = interruptOnCancel;
}
/**
* Will interrupt the thread executing the system command only if {@link #setInterruptOnCancel(boolean)} has been set to true. Otherwise the
* underlying command will be allowed to finish before the tasklet ends.
*
* @since 3.0
* @see StoppableTasklet#stop()
*/
@Override
public void stop() {
stopped = true;
}
}