/* (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 java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FilenameUtils;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.time.Duration;
import org.geoserver.backuprestore.AbstractExecutionAdapter;
import org.geoserver.backuprestore.Backup;
import org.geoserver.backuprestore.BackupExecutionAdapter;
import org.geoserver.backuprestore.RestoreExecutionAdapter;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.platform.resource.Resources;
import org.geoserver.web.GeoServerApplication;
import org.geoserver.web.GeoServerSecuredPage;
import org.geoserver.web.GeoServerUnlockablePage;
import org.geoserver.web.data.workspace.WorkspaceChoiceRenderer;
import org.geoserver.web.data.workspace.WorkspaceDetachableModel;
import org.geoserver.web.data.workspace.WorkspacesModel;
import org.geoserver.web.wicket.GeoServerDialog;
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.launch.JobExecutionNotRunningException;
import org.springframework.batch.core.launch.NoSuchJobExecutionException;
/**
* First page of the backup wizard.
*
* @author Andrea Aime - OpenGeo
* @author Justin Deoliveira, OpenGeo
*/
@SuppressWarnings("serial")
public class BackupRestoreDataPage extends GeoServerSecuredPage implements GeoServerUnlockablePage {
static Logger LOGGER = Logging.getLogger(BackupRestoreDataPage.class);
WorkspaceDetachableModel workspace;
DropDownChoice workspaceChoice;
TextField workspaceNameTextField;
Component statusLabel;
BackupRestoreExecutionsTable backupRestoreExecutionsTable;
BackupRestoreExecutionsTable restoreExecutionsTable;
WebMarkupContainer newBackupRestorePanel;
GeoServerDialog dialog;
public BackupRestoreDataPage(PageParameters params) {
newBackupRestorePanel = new WebMarkupContainer("newBackupRestorePanel");
newBackupRestorePanel.setOutputMarkupId(true);
resetResourcePanel();
add(newBackupRestorePanel);
Catalog catalog = GeoServerApplication.get().getCatalog();
// workspace chooser
workspace = new WorkspaceDetachableModel(null);
workspaceChoice = new DropDownChoice("workspace", workspace, new WorkspacesModel(),
new WorkspaceChoiceRenderer());
workspaceChoice.setOutputMarkupId(true);
workspaceChoice.add(new AjaxFormComponentUpdatingBehavior("change") {
@Override
protected void onUpdate(AjaxRequestTarget target) {
updateTargetWorkspace(target);
}
});
workspaceChoice.setNullValid(true);
add(workspaceChoice);
WebMarkupContainer workspaceNameContainer = new WebMarkupContainer("workspaceNameContainer");
workspaceNameContainer.setOutputMarkupId(true);
add(workspaceNameContainer);
workspaceNameTextField = new TextField("workspaceName", new Model());
workspaceNameTextField.setOutputMarkupId(true);
boolean defaultWorkspace = catalog.getDefaultWorkspace() != null;
workspaceNameTextField.setVisible(!defaultWorkspace);
workspaceNameTextField.setRequired(!defaultWorkspace);
workspaceNameContainer.add(workspaceNameTextField);
/**
* Backup Panel
*/
Form backupForm = new Form("backupForm");
add(backupForm);
populateBackupForm(backupForm);
/**
* Restore Panel
*/
Form restoreForm = new Form("restoreForm");
add(restoreForm);
populateRestoreForm(restoreForm);
/**
*
*/
add(dialog = new GeoServerDialog("dialog"));
dialog.setInitialWidth(600);
dialog.setInitialHeight(400);
dialog.setMinimalHeight(150);
}
/**
* @param target
*/
protected void updateTargetWorkspace(AjaxRequestTarget target) {
WorkspaceInfo ws = (WorkspaceInfo) workspace.getObject();
//workspaceNameTextField.setVisible(ws == null);
//workspaceNameTextField.setRequired(ws == null);
/*if (target != null) {
target.add(workspaceNameTextField.getParent());
}*/
}
/**
*
*/
private void resetResourcePanel() {
if (newBackupRestorePanel.size() > 0) {
newBackupRestorePanel.remove("backupResource");
}
Panel p = new ResourceFilePanel("backupResource");
newBackupRestorePanel.add(p);
}
/**
* @param form
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void populateBackupForm(Form form) {
form.add(new CheckBox("backupOptOverwirte", new Model<Boolean>(false)));
form.add(new CheckBox("backupOptBestEffort", new Model<Boolean>(false)));
form.add(new CheckBox("backupOptCleanTemp", new Model<Boolean>(true)));
form.add(statusLabel = new Label("status", new Model()).setOutputMarkupId(true));
form.add(new AjaxSubmitLink("newBackupStart", form) {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(feedbackPanel);
}
protected void onSubmit(AjaxRequestTarget target, final Form<?> form) {
//update status to indicate we are working
statusLabel.add(AttributeModifier.replace("class", "working-link"));
statusLabel.setDefaultModelObject("Working");
target.add(statusLabel);
//enable cancel and disable this
Component cancel = form.get("cancel");
cancel.setEnabled(true);
target.add(cancel);
setEnabled(false);
target.add(this);
final AjaxSubmitLink self = this;
final Long jobid;
try {
jobid = launchBackupExecution(form);
} catch (Exception e) {
error(e);
LOGGER.log(Level.WARNING, "Error starting a new Backup", e);
return;
} finally {
//update the button back to original state
resetButtons(form, target, "newBackupStart");
target.add(feedbackPanel);
}
cancel.setDefaultModelObject(jobid);
this.add(new AbstractAjaxTimerBehavior(Duration.milliseconds(100)) {
@Override
protected void onTimer(AjaxRequestTarget target) {
Backup backupFacade = BackupRestoreWebUtils.backupFacade();
BackupExecutionAdapter exec = backupFacade.getBackupExecutions().get(jobid);
if (!exec.isRunning()) {
try {
if (exec.getAllFailureExceptions() != null &&
!exec.getAllFailureExceptions().isEmpty()) {
getSession().error(exec.getAllFailureExceptions().get(0));
setResponsePage(BackupRestoreDataPage.class);
}
else if (exec.isStopping()) {
//do nothing
}
else {
PageParameters pp = new PageParameters();
pp.add("id", exec.getId());
pp.add("clazz", BackupExecutionAdapter.class.getSimpleName());
setResponsePage(BackupRestorePage.class, pp);
}
}
catch(Exception e) {
error(e);
LOGGER.log(Level.WARNING, "", e);
}
finally {
stop(null);
//update the button back to original state
resetButtons(form, target, "newBackupStart");
target.add(feedbackPanel);
}
return;
}
String msg = exec != null ? exec.getStatus().toString() : "Working";
statusLabel.setDefaultModelObject(msg);
target.add(statusLabel);
};
@Override
public boolean canCallListenerInterface(Component component, Method method) {
if(self.equals(component) &&
method.getDeclaringClass().equals(org.apache.wicket.behavior.IBehaviorListener.class) &&
method.getName().equals("onRequest")){
return true;
}
return super.canCallListenerInterface(component, method);
}
});
}
private Long launchBackupExecution(Form<?> form) throws Exception {
ResourceFilePanel panel = (ResourceFilePanel) newBackupRestorePanel.get("backupResource");
Resource archiveFile = null;
try {
archiveFile = panel.getResource();
} catch (NullPointerException e) {
throw new Exception("Backup Archive File is Mandatory!");
}
if (archiveFile == null ||
archiveFile.getType() == Type.DIRECTORY ||
FilenameUtils.getExtension(archiveFile.name()).isEmpty()) {
throw new Exception("Backup Archive File is Mandatory and should not be a Directory or URI.");
}
Filter filter = null;
WorkspaceInfo ws = (WorkspaceInfo) workspace.getObject();
if (ws != null) {
filter = ECQL.toFilter("name = '" + ws.getName() +"'");
}
Hints hints = new Hints(new HashMap(2));
Boolean backupOptOverwirte = ((CheckBox) form.get("backupOptOverwirte")).getModelObject();
Boolean backupOptBestEffort = ((CheckBox) form.get("backupOptBestEffort")).getModelObject();
Boolean backupOptCleanTemp = ((CheckBox) form.get("backupOptCleanTemp")).getModelObject();
if (backupOptBestEffort) {
hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_BEST_EFFORT_MODE), Backup.PARAM_BEST_EFFORT_MODE));
}
if (backupOptCleanTemp) {
hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_CLEANUP_TEMP), Backup.PARAM_CLEANUP_TEMP));
}
Backup backupFacade = BackupRestoreWebUtils.backupFacade();
return backupFacade.runBackupAsync(archiveFile, backupOptOverwirte, filter, hints).getId();
}
});
form.add(new AjaxLink<Long>("cancel", new Model<Long>()) {
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
};
@Override
public void onClick(AjaxRequestTarget target) {
Long jobid = getModelObject();
if (jobid != null) {
try {
BackupRestoreWebUtils.backupFacade().stopExecution(jobid);
setResponsePage(BackupRestoreDataPage.class);
} catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
LOGGER.log(Level.WARNING, "", e);
}
}
setEnabled(false);
target.add(this);
}
}.setOutputMarkupId(true).setEnabled(false));
backupRestoreExecutionsTable = new BackupRestoreExecutionsTable("backups", new BackupRestoreExecutionsProvider(true, BackupExecutionAdapter.class) {
@Override
protected List<org.geoserver.web.wicket.GeoServerDataProvider.Property<AbstractExecutionAdapter>> getProperties() {
return Arrays.asList(ID, STATE, STARTED, PROGRESS, ARCHIVEFILE, OPTIONS);
}
}, true, BackupExecutionAdapter.class) {
protected void onSelectionUpdate(AjaxRequestTarget target) {
};
};
backupRestoreExecutionsTable.setOutputMarkupId(true);
backupRestoreExecutionsTable.setFilterable(false);
backupRestoreExecutionsTable.setSortable(false);
form.add(backupRestoreExecutionsTable);
}
/**
* @param form
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void populateRestoreForm(Form form) {
form.add(new CheckBox("restoreOptDryRun", new Model<Boolean>(false)));
form.add(new CheckBox("restoreOptBestEffort", new Model<Boolean>(false)));
form.add(new CheckBox("restoreOptCleanTemp", new Model<Boolean>(true)));
form.add(statusLabel = new Label("status", new Model()).setOutputMarkupId(true));
form.add(new AjaxSubmitLink("newRestoreStart", form) {
@Override
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
}
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(feedbackPanel);
}
protected void onSubmit(AjaxRequestTarget target, final Form<?> form) {
//update status to indicate we are working
statusLabel.add(AttributeModifier.replace("class", "working-link"));
statusLabel.setDefaultModelObject("Working");
target.add(statusLabel);
//enable cancel and disable this
Component cancel = form.get("cancel");
cancel.setEnabled(true);
target.add(cancel);
setEnabled(false);
target.add(this);
final AjaxSubmitLink self = this;
final Long jobid;
try {
jobid = launchRestoreExecution(form);
} catch (Exception e) {
error(e);
LOGGER.log(Level.WARNING, "Error starting a new Restore", e);
return;
} finally {
//update the button back to original state
resetButtons(form, target, "newRestoreStart");
target.add(feedbackPanel);
}
cancel.setDefaultModelObject(jobid);
this.add(new AbstractAjaxTimerBehavior(Duration.milliseconds(100)) {
@Override
protected void onTimer(AjaxRequestTarget target) {
Backup backupFacade = BackupRestoreWebUtils.backupFacade();
RestoreExecutionAdapter exec = backupFacade.getRestoreExecutions().get(jobid);
if (!exec.isRunning()) {
try {
if (exec.getAllFailureExceptions() != null &&
!exec.getAllFailureExceptions().isEmpty()) {
getSession().error(exec.getAllFailureExceptions().get(0));
setResponsePage(BackupRestoreDataPage.class);
}
else if (exec.isStopping()) {
//do nothing
}
else {
PageParameters pp = new PageParameters();
pp.add("id", exec.getId());
pp.add("clazz", RestoreExecutionAdapter.class.getSimpleName());
setResponsePage(BackupRestorePage.class, pp);
}
}
catch(Exception e) {
error(e);
LOGGER.log(Level.WARNING, "", e);
}
finally {
stop(null);
//update the button back to original state
resetButtons(form, target, "newRestoreStart");
target.add(feedbackPanel);
}
return;
}
String msg = exec != null ? exec.getStatus().toString() : "Working";
statusLabel.setDefaultModelObject(msg);
target.add(statusLabel);
};
@Override
public boolean canCallListenerInterface(Component component, Method method) {
if(self.equals(component) &&
method.getDeclaringClass().equals(org.apache.wicket.behavior.IBehaviorListener.class) &&
method.getName().equals("onRequest")){
return true;
}
return super.canCallListenerInterface(component, method);
}
});
}
private Long launchRestoreExecution(Form<?> form) throws Exception {
ResourceFilePanel panel = (ResourceFilePanel) newBackupRestorePanel.get("backupResource");
Resource archiveFile = null;
try {
archiveFile = panel.getResource();
} catch (NullPointerException e) {
throw new Exception("Restore Archive File is Mandatory!");
}
if (archiveFile == null || !Resources.exists(archiveFile) ||
archiveFile.getType() == Type.DIRECTORY ||
FilenameUtils.getExtension(archiveFile.name()).isEmpty()) {
throw new Exception("Restore Archive File is Mandatory, must exists and should not be a Directory or URI.");
}
Filter filter = null;
WorkspaceInfo ws = (WorkspaceInfo) workspace.getObject();
if (ws != null) {
filter = ECQL.toFilter("name = '" + ws.getName() +"'");
}
Hints hints = new Hints(new HashMap(2));
Boolean restoreOptDryRun = ((CheckBox) form.get("restoreOptDryRun")).getModelObject();
if (restoreOptDryRun) {
hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_DRY_RUN_MODE), Backup.PARAM_DRY_RUN_MODE));
}
Boolean restoreOptBestEffort = ((CheckBox) form.get("restoreOptBestEffort")).getModelObject();
if (restoreOptBestEffort) {
hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_BEST_EFFORT_MODE), Backup.PARAM_BEST_EFFORT_MODE));
}
Boolean restoreOptCleanTemp = ((CheckBox) form.get("restoreOptCleanTemp")).getModelObject();
if (restoreOptCleanTemp) {
hints.add(new Hints(new Hints.OptionKey(Backup.PARAM_CLEANUP_TEMP), Backup.PARAM_CLEANUP_TEMP));
}
Backup backupFacade = BackupRestoreWebUtils.backupFacade();
return backupFacade.runRestoreAsync(archiveFile, filter, hints).getId();
}
});
form.add(new AjaxLink<Long>("cancel", new Model<Long>()) {
protected void disableLink(ComponentTag tag) {
super.disableLink(tag);
tag.setName("a");
tag.addBehavior(AttributeModifier.replace("class", "disabled"));
};
@Override
public void onClick(AjaxRequestTarget target) {
Long jobid = getModelObject();
if (jobid != null) {
try {
BackupRestoreWebUtils.backupFacade().stopExecution(jobid);
setResponsePage(BackupRestoreDataPage.class);
} catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
LOGGER.log(Level.WARNING, "", e);
}
}
setEnabled(false);
target.add(this);
}
}.setOutputMarkupId(true).setEnabled(false));
restoreExecutionsTable = new BackupRestoreExecutionsTable("restores", new BackupRestoreExecutionsProvider(true, RestoreExecutionAdapter.class) {
@Override
protected List<org.geoserver.web.wicket.GeoServerDataProvider.Property<AbstractExecutionAdapter>> getProperties() {
return Arrays.asList(ID, STATE, STARTED, PROGRESS, ARCHIVEFILE, OPTIONS);
}
}, true, RestoreExecutionAdapter.class) {
protected void onSelectionUpdate(AjaxRequestTarget target) {
};
};
restoreExecutionsTable.setOutputMarkupId(true);
restoreExecutionsTable.setFilterable(false);
restoreExecutionsTable.setSortable(false);
form.add(restoreExecutionsTable);
}
protected void resetButtons(Form<?> form, AjaxRequestTarget target, String buttonId) {
form.get(buttonId).setEnabled(true);
statusLabel.setDefaultModelObject("");
statusLabel.add(AttributeModifier.replace("class", ""));
target.add(form.get(buttonId));
target.add(form.get("status"));
}
}