/* (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.web;
import static org.geoserver.backuprestore.web.BackupRestoreWebUtils.backupFacade;
import static org.geoserver.backuprestore.web.BackupRestoreWebUtils.humanReadableByteCount;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.NumberTextField;
import org.apache.wicket.markup.html.form.SubmitLink;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.util.resource.FileResourceStream;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.validation.validator.RangeValidator;
import org.geoserver.backuprestore.AbstractExecutionAdapter;
import org.geoserver.backuprestore.BackupExecutionAdapter;
import org.geoserver.backuprestore.RestoreExecutionAdapter;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.resource.Paths;
import org.geoserver.web.GeoServerSecuredPage;
import org.geoserver.web.GeoServerUnlockablePage;
import org.geoserver.web.wicket.GeoServerDialog;
import org.geoserver.web.wicket.Icon;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobExecutionNotRunningException;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.batch.core.launch.NoSuchJobExecutionException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
/**
* @author Alessio Fabiani, GeoSolutions S.A.S.
*
*/
public class BackupRestorePage<T extends AbstractExecutionAdapter> extends GeoServerSecuredPage implements GeoServerUnlockablePage {
public static final PackageResourceReference COMPRESS_ICON = new PackageResourceReference(
BackupRestorePage.class, "compress.png");
static final String DETAILS_LEVEL = "expand";
int expand = 0;
File backupFile;
GeoServerDialog dialog;
private PageParameters params;
private Class<T> clazz;
public BackupRestorePage(PageParameters pp) {
this(new BackupRestoreExecutionModel(pp.get("id").toLong(),
getType(pp.get("clazz").toString())), pp, getType(pp.get("clazz").toString()));
}
public BackupRestorePage(T bkp, PageParameters pp) {
this(new BackupRestoreExecutionModel(bkp, getType(pp.get("clazz").toString())), pp,
getType(pp.get("clazz").toString()));
}
/**
* @param string
* @return
*/
private static Class getType(String simpleName) {
if (BackupExecutionAdapter.class.getSimpleName().equals(simpleName)) {
return BackupExecutionAdapter.class;
} else if (RestoreExecutionAdapter.class.getSimpleName().equals(simpleName)) {
return RestoreExecutionAdapter.class;
}
return null;
}
public BackupRestorePage(IModel<T> model, PageParameters pp, Class<T> clazz) {
this.params = pp;
this.clazz = clazz;
initComponents(model);
}
public Class<T> getType() {
return this.clazz;
}
void initComponents(final IModel<T> model) {
add(new Label("id", new PropertyModel(model, "id")));
add(new Label("clazz", new Model(this.clazz.getSimpleName().substring(0,
this.clazz.getSimpleName().indexOf("Execution")))));
BackupRestoreExecutionsProvider provider = new BackupRestoreExecutionsProvider(getType()) {
@Override
protected List<Property<AbstractExecutionAdapter>> getProperties() {
return Arrays.asList(ID, STATE, STARTED, PROGRESS, ARCHIVEFILE, OPTIONS);
}
@Override
protected List<T> getItems() {
return Collections.singletonList(model.getObject());
}
};
final BackupRestoreExecutionsTable headerTable = new BackupRestoreExecutionsTable("header",
provider, getType());
headerTable.setOutputMarkupId(true);
headerTable.setFilterable(false);
headerTable.setPageable(false);
add(headerTable);
final T bkp = model.getObject();
boolean selectable = bkp.getStatus() != BatchStatus.COMPLETED;
add(new Icon("icon", COMPRESS_ICON));
add(new Label("title", new DataTitleModel(bkp))
.add(new AttributeModifier("title", new DataTitleModel(bkp, false))));
@SuppressWarnings("rawtypes")
Form<?> form = new Form("form");
add(form);
try {
if (params != null && params.getNamedKeys().contains(DETAILS_LEVEL)) {
if (params.get(DETAILS_LEVEL).toInt() > 0) {
expand = params.get(DETAILS_LEVEL).toInt();
}
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error parsing the 'details level' parameter: ",
params.get(DETAILS_LEVEL).toString());
}
form.add(new SubmitLink("refresh") {
@Override
public void onSubmit() {
setResponsePage(BackupRestorePage.class,
new PageParameters().add("id", params.get("id").toLong())
.add("clazz", getType().getSimpleName())
.add(DETAILS_LEVEL, expand));
}
});
NumberTextField<Integer> expand = new NumberTextField<Integer>("expand",
new PropertyModel<Integer>(this, "expand"));
expand.add(RangeValidator.minimum(0));
form.add(expand);
TextArea<String> details = new TextArea<String>("details", new BKErrorDetailsModel(bkp));
details.setOutputMarkupId(true);
details.setMarkupId("details");
add(details);
String location = bkp.getArchiveFile().path();
if (location == null) {
location = getGeoServerApplication().getGeoServer().getLogging().getLocation();
}
backupFile = new File(location);
if (!backupFile.isAbsolute()) {
// locate the geoserver.log file
GeoServerDataDirectory dd = getGeoServerApplication()
.getBeanOfType(GeoServerDataDirectory.class);
backupFile = dd.get(Paths.convert(backupFile.getPath())).file();
}
if (!backupFile.exists()) {
error("Could not find the Backup Archive file: " + backupFile.getAbsolutePath());
}
/***
* DOWNLOAD LINK
*/
final Link<Object> downLoadLink = new Link<Object>("download") {
@Override
public void onClick() {
IResourceStream stream = new FileResourceStream(backupFile) {
public String getContentType() {
return "application/zip";
}
};
ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(stream,
backupFile.getName());
handler.setContentDisposition(ContentDisposition.ATTACHMENT);
RequestCycle.get().scheduleRequestHandlerAfterCurrent(handler);
}
};
add(downLoadLink);
/***
* PAUSE LINK
*/
final AjaxLink pauseLink = new AjaxLink("pause") {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
@Override
public void onClick(AjaxRequestTarget target) {
AbstractExecutionAdapter bkp = model.getObject();
if (bkp.getStatus() == BatchStatus.STOPPED) {
setLinkEnabled((AjaxLink) downLoadLink.getParent().get("pause"), false, target);
} else {
try {
backupFacade().stopExecution(bkp.getId());
setResponsePage(BackupRestoreDataPage.class);
} catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
LOGGER.log(Level.WARNING, "", e);
getSession().error(e);
setResponsePage(BackupRestoreDataPage.class);
}
}
}
};
pauseLink.setEnabled(doSelectReady(bkp) && bkp.getStatus() != BatchStatus.STOPPED);
add(pauseLink);
/***
* RESUME LINK
*/
final AjaxLink resumeLink = new AjaxLink("resume") {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
@Override
public void onClick(AjaxRequestTarget target) {
AbstractExecutionAdapter bkp = model.getObject();
if (bkp.getStatus() != BatchStatus.STOPPED) {
setLinkEnabled((AjaxLink) downLoadLink.getParent().get("pause"), false, target);
} else {
try {
Long id = backupFacade().restartExecution(bkp.getId());
PageParameters pp = new PageParameters();
pp.add("id", id);
if (bkp instanceof BackupExecutionAdapter) {
pp.add("clazz", BackupExecutionAdapter.class.getSimpleName());
} else if (bkp instanceof RestoreExecutionAdapter) {
pp.add("clazz", RestoreExecutionAdapter.class.getSimpleName());
}
setResponsePage(BackupRestorePage.class, pp);
} catch (NoSuchJobExecutionException | JobInstanceAlreadyCompleteException
| NoSuchJobException | JobRestartException
| JobParametersInvalidException e) {
LOGGER.log(Level.WARNING, "", e);
getSession().error(e);
setResponsePage(BackupRestoreDataPage.class);
}
}
}
};
resumeLink.setEnabled(bkp.getStatus() == BatchStatus.STOPPED);
add(resumeLink);
/***
* ABANDON LINK
*/
final AjaxLink cancelLink = new AjaxLink("cancel") {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
@Override
public void onClick(AjaxRequestTarget target) {
AbstractExecutionAdapter bkp = model.getObject();
if (!doSelectReady(bkp)) {
setLinkEnabled((AjaxLink) downLoadLink.getParent().get("cancel"), false,
target);
} else {
try {
backupFacade().abandonExecution(bkp.getId());
PageParameters pp = new PageParameters();
pp.add("id", bkp.getId());
if (bkp instanceof BackupExecutionAdapter) {
pp.add("clazz", BackupExecutionAdapter.class.getSimpleName());
} else if (bkp instanceof RestoreExecutionAdapter) {
pp.add("clazz", RestoreExecutionAdapter.class.getSimpleName());
}
setResponsePage(BackupRestorePage.class, pp);
} catch (NoSuchJobExecutionException | JobExecutionAlreadyRunningException e) {
error(e);
LOGGER.log(Level.WARNING, "", e);
}
}
}
};
cancelLink.setEnabled(doSelectReady(bkp));
add(cancelLink);
/***
* DONE LINK
*/
final AjaxLink doneLink = new AjaxLink("done") {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
@Override
public void onClick(AjaxRequestTarget target) {
setResponsePage(BackupRestoreDataPage.class);
return;
}
};
add(doneLink);
/**
* FINALIZE
*/
add(dialog = new GeoServerDialog("dialog"));
}
/**
* @param bkp
* @return
*/
private boolean doSelectReady(AbstractExecutionAdapter bkp) {
if (bkp.getStatus() == BatchStatus.COMPLETED || bkp.getStatus() == BatchStatus.FAILED
|| bkp.getStatus() == BatchStatus.ABANDONED) {
return false;
}
return true;
}
@Override
public String getAjaxIndicatorMarkupId() {
return null;
}
class DataTitleModel<T extends AbstractExecutionAdapter>
extends LoadableDetachableModel<String> {
long contextId;
boolean abbrev;
DataTitleModel(T bkp) {
this(bkp, true);
}
DataTitleModel(T bkp, boolean abbrev) {
this.contextId = bkp.getId();
this.abbrev = abbrev;
}
@Override
protected String load() {
AbstractExecutionAdapter ctx = null;
if (getType() == BackupExecutionAdapter.class) {
ctx = backupFacade().getBackupExecutions().get(contextId);
} else if (getType() == RestoreExecutionAdapter.class) {
ctx = backupFacade().getRestoreExecutions().get(contextId);
}
String title = ctx.getArchiveFile() != null ? ctx.getArchiveFile().path()
: ctx.toString();
if (abbrev && title.length() > 70) {
// shorten it
title = title.substring(0, 20) + "[...]" + title.substring(title.length() - 50);
}
title = title + " ["
+ humanReadableByteCount(FileUtils.sizeOf(ctx.getArchiveFile().file()), false)
+ "]";
return title;
}
}
void setLinkEnabled(AjaxLink link, boolean enabled, AjaxRequestTarget target) {
link.setEnabled(enabled);
target.add(link);
}
class BKErrorDetailsModel<T extends AbstractExecutionAdapter>
extends LoadableDetachableModel<String> {
long contextId;
public BKErrorDetailsModel(AbstractExecutionAdapter bkp) {
this.contextId = bkp.getId();
}
@Override
protected String load() {
AbstractExecutionAdapter ctx = null;
if (getType() == BackupExecutionAdapter.class) {
ctx = backupFacade().getBackupExecutions().get(contextId);
} else if (getType() == RestoreExecutionAdapter.class) {
ctx = backupFacade().getRestoreExecutions().get(contextId);
}
StringBuilder buf = new StringBuilder();
if (!ctx.getAllFailureExceptions().isEmpty()) {
for (Throwable ex : ctx.getAllFailureExceptions()) {
ex = writeException(buf, ex, Level.SEVERE);
}
} else {
buf.append("\nNO Exceptions Detected.\n");
}
if (!ctx.getAllWarningExceptions().isEmpty()) {
for (Throwable ex : ctx.getAllWarningExceptions()) {
ex = writeException(buf, ex, Level.WARNING);
}
} else {
buf.append("\nNO Warnings Detected.\n");
}
return buf.toString();
}
/**
* @param buf
* @param ex
* @param severe
* @return
*/
private Throwable writeException(StringBuilder buf, Throwable ex, Level level) {
int cnt = 0;
while (ex != null) {
if (buf.length() > 0) {
buf.append('\n');
}
if (ex.getMessage() != null) {
buf.append(level).append(":");
buf.append(ex.getMessage());
cnt++;
}
if (BackupRestorePage.this.expand > 0 && BackupRestorePage.this.expand >= cnt) {
StringWriter errors = new StringWriter();
ex.printStackTrace(new PrintWriter(errors));
buf.append('\n').append(errors.toString());
}
ex = ex.getCause();
}
return ex;
}
}
}