/* (c) 2017 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.rest; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.backuprestore.AbstractExecutionAdapter; import org.geoserver.backuprestore.Backup; import org.geoserver.backuprestore.BackupExecutionAdapter; import org.geoserver.backuprestore.RestoreExecutionAdapter; import org.geoserver.catalog.CatalogException; import org.geoserver.platform.resource.Files; import org.geoserver.platform.resource.Resource; import org.geoserver.rest.ResourceNotFoundException; import org.geoserver.rest.RestBaseController; import org.geotools.factory.Hints; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.geotools.util.logging.Logging; import org.opengis.filter.Filter; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.StepExecution; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; import com.thoughtworks.xstream.converters.collections.CollectionConverter; import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.ClassAliasingMapper; import com.thoughtworks.xstream.mapper.Mapper; public abstract class AbstractBackupRestoreController extends RestBaseController { protected static final Logger LOGGER = Logging.getLogger(BackupController.class); protected Backup backupFacade; /** * * @author Alessio Fabiani, GeoSolutions S.A.S. * */ static public class ArchiveFileResourceConverter extends AbstractSingleValueConverter { @SuppressWarnings("rawtypes") @Override public boolean canConvert(Class type) { return Resource.class.isAssignableFrom(type); } @Override public String toString(Object obj) { return ((Resource)obj).path(); } @Override public Object fromString(String str) { return Files.asResource(new File(str)); } } /** * @return the backupFacade */ public Backup getBackupFacade() { return backupFacade; } protected String getExecutionIdFilter(String executionId) { if(executionId.endsWith(".xml") || executionId.endsWith(".zip")) { executionId = executionId.substring(0, executionId.length() - 4); } else if(executionId.endsWith(".json")) { executionId = executionId.substring(0, executionId.length() - 5); } return executionId; } /** * * @param allowAll * @param mustExist * @return */ protected Object lookupBackupExecutionsContext(String i, boolean allowAll, boolean mustExist) { if (i != null) { BackupExecutionAdapter backupExecution = null; try { backupExecution = getBackupFacade().getBackupExecutions().get(Long.parseLong(i)); } catch (NumberFormatException e) { } if (backupExecution == null && mustExist) { throw new ResourceNotFoundException("No such backup execution: " + i); } return backupExecution; } else { if (allowAll) { return new ArrayList<BackupExecutionAdapter>(getBackupFacade().getBackupExecutions().values()); } throw new ResourceNotFoundException("No backup execution specified"); } } /** * * @param allowAll * @param mustExist * @return */ protected Object lookupRestoreExecutionsContext(String i, boolean allowAll, boolean mustExist) { if (i != null) { RestoreExecutionAdapter restoreExecution = null; try { restoreExecution = getBackupFacade().getRestoreExecutions().get(Long.parseLong(i)); } catch (NumberFormatException e) { } if (restoreExecution == null && mustExist) { throw new ResourceNotFoundException("No such restore execution: " + i); } return restoreExecution; } else { if (allowAll) { return new ArrayList<RestoreExecutionAdapter>(getBackupFacade().getRestoreExecutions().values()); } throw new ResourceNotFoundException("No backup execution specified"); } } @SuppressWarnings({ "rawtypes", "unchecked" }) protected Hints asParams(List<String> options) { Hints hints = new Hints(new HashMap(2)); if (options != null) { for (String option : options) { if (option.startsWith(Backup.PARAM_DRY_RUN_MODE)) { if (option.indexOf("=")>0) { if (!option.toLowerCase().endsWith("true")) { continue; } } hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_DRY_RUN_MODE), Backup.PARAM_DRY_RUN_MODE)); } if (option.startsWith(Backup.PARAM_BEST_EFFORT_MODE)) { if (option.indexOf("=")>0) { if (!option.toLowerCase().endsWith("true")) { continue; } } hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_BEST_EFFORT_MODE), Backup.PARAM_BEST_EFFORT_MODE)); } } } return hints; } /** * @param xStream */ protected void intializeXStreamContext(XStream xStream) { //Adapter xStream.alias("backup", BackupExecutionAdapter.class); xStream.alias("restore", RestoreExecutionAdapter.class); // setup white list of accepted classes xStream.allowTypes(new String[] { "org.geoserver.platform.resource.Files$ResourceAdaptor" }); xStream.allowTypesByWildcard(new String[] { "org.geoserver.backuprestore.**" }); // Configure aliases and converters xStream.omitField(RestoreExecutionAdapter.class, "restoreCatalog"); Class synchronizedListType = Collections.synchronizedList(Collections.EMPTY_LIST).getClass(); xStream.alias("synchList", synchronizedListType); ClassAliasingMapper optionsMapper = new ClassAliasingMapper(xStream.getMapper()); optionsMapper.addClassAlias("option", String.class); xStream.registerLocalConverter(AbstractExecutionAdapter.class, "options", new CollectionConverter(optionsMapper) { @Override public boolean canConvert(Class type) { return Collection.class.isAssignableFrom(type); } }); ClassAliasingMapper warningsMapper = new ClassAliasingMapper(xStream.getMapper()); warningsMapper.addClassAlias(Level.WARNING.getName(), RuntimeException.class); warningsMapper.addClassAlias(Level.WARNING.getName(), CatalogException.class); xStream.registerLocalConverter(AbstractExecutionAdapter.class, "warningsList", new CollectionConverter(warningsMapper) { @Override public boolean canConvert(Class type) { return Collection.class.isAssignableFrom(type); } }); Class resourceAdaptorType = Files.asResource(new File("/")).getClass(); xStream.alias("resource", resourceAdaptorType); xStream.registerLocalConverter(AbstractExecutionAdapter.class, "archiveFile", new ArchiveFileResourceConverter()); //Delegate xStream.aliasAttribute(AbstractExecutionAdapter.class, "delegate", "execution"); xStream.omitField(JobExecution.class, "version"); xStream.omitField(JobExecution.class, "jobInstance"); xStream.omitField(JobExecution.class, "jobId"); xStream.omitField(JobExecution.class, "jobParameters"); xStream.omitField(JobExecution.class, "executionContext"); xStream.registerLocalConverter(AbstractExecutionAdapter.class, "delegate", new JobExecutionConverter(xStream.getMapper(), xStream.getReflectionProvider(), this.backupFacade)); xStream.registerLocalConverter(AbstractExecutionAdapter.class, "filter", new FilterConverter(xStream.getMapper(), xStream.getReflectionProvider())); ClassAliasingMapper stepExecutionsMapper = new ClassAliasingMapper(xStream.getMapper()); stepExecutionsMapper.addClassAlias("step", StepExecution.class); xStream.registerLocalConverter(JobExecution.class, "stepExecutions", new CollectionConverter(stepExecutionsMapper) { @Override public boolean canConvert(Class type) { return CopyOnWriteArraySet.class.isAssignableFrom(type); } @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext ctx) { CopyOnWriteArraySet execs = (CopyOnWriteArraySet) obj; Iterator iterator = execs.iterator(); while (iterator.hasNext()) { StepExecution exec = (StepExecution) iterator.next(); // Step Node - START writer.startNode("step"); writer.startNode("name"); writer.setValue(exec.getStepName()); writer.endNode(); writer.startNode("status"); writer.setValue(exec.getStatus().name()); writer.endNode(); writer.startNode("exitStatus"); writer.startNode("exitCode"); writer.setValue(exec.getExitStatus().getExitCode()); writer.endNode(); writer.startNode("exitDescription"); writer.setValue(exec.getExitStatus().getExitDescription()); writer.endNode(); writer.endNode(); if (exec.getStartTime() != null) { writer.startNode("startTime"); writer.setValue(DateFormat.getInstance().format(exec.getStartTime())); writer.endNode(); } if (exec.getEndTime() != null) { writer.startNode("endTime"); writer.setValue(DateFormat.getInstance().format(exec.getEndTime())); writer.endNode(); } if (exec.getLastUpdated() != null) { writer.startNode("lastUpdated"); writer.setValue(DateFormat.getInstance().format(exec.getLastUpdated())); writer.endNode(); } writer.startNode("parameters"); for (Entry param : exec.getJobParameters().getParameters().entrySet()) { writer.startNode((String) param.getKey()); writer.setValue(((JobParameter) param.getValue()).getValue().toString()); writer.endNode(); } writer.endNode(); writer.startNode("readCount"); writer.setValue(String.valueOf(exec.getReadCount())); writer.endNode(); writer.startNode("writeCount"); writer.setValue(String.valueOf(exec.getWriteCount())); writer.endNode(); writer.startNode("failureExceptions"); for (Throwable ex : exec.getFailureExceptions()) { writer.startNode(Level.SEVERE.getName()); StringBuilder buf = new StringBuilder(); while (ex != null) { if (buf.length() > 0) { buf.append('\n'); } if (ex.getMessage() != null) { buf.append(ex.getMessage()); StringWriter errors = new StringWriter(); ex.printStackTrace(new PrintWriter(errors)); buf.append('\n').append(errors.toString()); } ex = ex.getCause(); } writer.setValue(buf.toString()); writer.endNode(); } writer.endNode(); // Step Node - END writer.endNode(); } } }); } /** * * @author Alessio Fabiani, GeoSolutions S.A.S. * */ static public class JobExecutionConverter extends ReflectionConverter { private Backup backupFacade; JobExecutionConverter(Mapper mapper, ReflectionProvider reflectionProvider, Backup backupFacade) { super(mapper, reflectionProvider); this.backupFacade = backupFacade; } @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) { super.marshal(obj,writer,context); JobExecution dl = (JobExecution) obj; Integer numSteps = 0; if (dl.getJobInstance().getJobName().equals(Backup.BACKUP_JOB_NAME)) { numSteps = backupFacade.getTotalNumberOfBackupSteps(); } else if (dl.getJobInstance().getJobName().equals(Backup.RESTORE_JOB_NAME)) { numSteps = backupFacade.getTotalNumberOfRestoreSteps(); } writer.startNode("progress"); final StringBuffer progress = new StringBuffer(); progress.append(dl.getStepExecutions().size()).append("/").append(numSteps); writer.setValue(progress.toString()); writer.endNode(); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { return super.unmarshal(reader,context); } @SuppressWarnings("rawtypes") @Override public boolean canConvert(Class clazz) { return clazz.equals(JobExecution.class); } } /** * * @author Alessio Fabiani, GeoSolutions S.A.S. * */ static public class FilterConverter extends ReflectionConverter { /** * @param mapper * @param reflectionProvider */ public FilterConverter(Mapper mapper, ReflectionProvider reflectionProvider) { super(mapper, reflectionProvider); } @SuppressWarnings("rawtypes") @Override public boolean canConvert(Class clazz) { return Filter.class.isAssignableFrom(clazz); } @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) { Filter filter = (Filter) obj; writer.setValue(ECQL.toCQL(filter)); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { Filter filter = null; String nodeName = reader.getNodeName(); if ("filter".equals(nodeName)) { try { filter = ECQL.toFilter(reader.getValue()); } catch (CQLException e) { throw new RuntimeException(e); } } return filter; } } public AbstractBackupRestoreController() { super(); } }