/* (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.web.resources;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.ResourceStore;
import org.geoserver.platform.resource.Resources;
import org.geoserver.web.GeoServerSecuredPage;
import org.geoserver.web.treeview.TreeNode;
import org.geoserver.web.treeview.TreeView;
import org.geoserver.web.wicket.GeoServerDialog;
import org.geoserver.web.wicket.ParamResourceModel;
import org.geoserver.web.wicket.GeoServerDialog.DialogDelegate;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* The ResourceBrowser page.
*
* @author Niels Charlier
*
*/
public class PageResourceBrowser extends GeoServerSecuredPage {
private static final long serialVersionUID = 3979040405548783679L;
/**
* Behaviour for disabled button
*/
private static final AttributeAppender DISABLED_BEHAVIOR = new AttributeAppender("class", new Model<String>("disabled"), " ");
/**
* The extension that are recognised as simple text resources (and can be edited with simple text editor).
*/
private static final String[] TEXTUAL_EXTENSIONS = new String[] {"txt", "properties", "info", "xml", "sld", "rst", "log", "asc", "cfg",
"css", "ftl", "htm", "html", "js", "xsd", "prj", "meta", "pgw", "pal", "tfw", "url"};
/**
* The expanded states model.
*/
protected final ResourceExpandedStates expandedStates = new ResourceExpandedStates();
/**
* The clip board.
*/
protected final ClipBoard clipBoard;
public PageResourceBrowser() {
//create the root node
final ResourceNode rootNode = new ResourceNode(store().get(Paths.BASE), expandedStates);
rootNode.getExpanded().setObject(true);
//create tree view and clip board
final TreeView<Resource> treeView = new TreeView<Resource>("treeview", rootNode);
clipBoard = new ClipBoard(treeView);
//used for all pop-up dialogs.
final GeoServerDialog dialog = new GeoServerDialog("dialog");
dialog.setResizable(false);
//upload button
final AjaxLink<Void> btnUpload = new AjaxLink<Void>("upload") {
private static final long serialVersionUID = -6538820444407766106L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(225);
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = 1557172478015946688L;
private PanelUpload uploadPanel;
@Override
protected Component getContents(String id) {
uploadPanel = new PanelUpload(id,"/" + treeView.getSelectedNode().getObject().path());
return uploadPanel;
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
uploadPanel.getFeedbackMessages().clear();
if (uploadPanel.getFileUpload() == null) {
uploadPanel.error(new ParamResourceModel("fileRequired", getPage()).getString());
} else {
String dir = uploadPanel.getDirectory();
Resource dest = store().get(Paths.path(dir, uploadPanel.getFileUpload().getClientFileName()));
if (Resources.exists(dest)) {
uploadPanel.error(new ParamResourceModel("resourceExists", getPage()).getString().replace("%", "/" + dest.path()));
} else {
try (OutputStream os = dest.out()) {
IOUtils.copy(uploadPanel.getFileUpload().getInputStream(), os);
treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);
return true;
}
catch (IOException | IllegalStateException e) {
uploadPanel.error(e.getMessage());
}
}
}
target.add(uploadPanel.getFeedbackPanel());
return false;
}
});
}
};
//new resource button
final AjaxLink<Void> btnNew = new AjaxLink<Void>("new") {
private static final long serialVersionUID = 8112272759002275843L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(525);
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = -8898887236980594842L;
private PanelEdit editPanel;
@Override
protected Component getContents(String id) {
//pick a non-existing resource name (can be changed by user)
String dest = "/" + Paths.path(treeView.getSelectedNode().getObject().path(), "new.txt");
for (int i = 1; Resources.exists(store().get(dest)); i++) {
dest = "/" + Paths.path(treeView.getSelectedNode().getObject().path(), "new." + i + ".txt");
}
editPanel = new PanelEdit(id, dest, true, "");
return editPanel;
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
editPanel.getFeedbackMessages().clear();
Resource dest = store().get(editPanel.getResource());
if (Resources.exists(dest)) {
editPanel.error(new ParamResourceModel("resourceExists", getPage()).getString().replace("%", "/" + dest.path()));
} else {
try (OutputStream os = dest.out()) {
String newContents = editPanel.getContents();
if (newContents != null) {
os.write(newContents.getBytes());
if (!newContents.endsWith("\n")) {
os.write(System.lineSeparator().getBytes());
}
}
//select newly created node
treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);
return true;
} catch (IOException | IllegalStateException e) {
error(e.getMessage());
}
}
target.add(editPanel.getFeedbackPanel());
return false;
}
});
}
};
//download button
final Link<Void> btnDownload = new Link<Void>("download") {
private static final long serialVersionUID = 2746429086122117005L;
@Override
public void onClick() {
Resource res = treeView.getSelectedNode().getObject();
getRequestCycle().scheduleRequestHandlerAfterCurrent(
new ResourceStreamRequestHandler(new WicketResourceAdaptor(res))
.setFileName(res.name())
.setContentDisposition(ContentDisposition.ATTACHMENT));
}
};
//edit button
final AjaxLink<Void> btnEdit = new AjaxLink<Void>("edit") {
private static final long serialVersionUID = 6690936054046040647L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(500);
final Resource resource = treeView.getSelectedNode().getObject();
final String contents;
try (InputStream is = resource.in()) {
contents = IOUtils.toString(is);
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = -8898887236980594842L;
private PanelEdit editPanel;
@Override
protected Component getContents(String id) {
editPanel = new PanelEdit(id, "/" + resource.path(), false, contents);
return editPanel;
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
editPanel.getFeedbackMessages().clear();
try (OutputStream os = resource.out()) {
String newContents = editPanel.getContents();
if (newContents != null) {
os.write(newContents.getBytes());
if (!newContents.endsWith("\n")) {
os.write(System.lineSeparator().getBytes());
}
}
return true;
} catch (IOException | IllegalStateException e) {
error(e.getMessage());
}
target.add(editPanel.getFeedbackPanel());
return false;
}
});
} catch (IOException | IllegalStateException e) {
error(e.getMessage());
target.add(getFeedbackPanel());
}
}
};
//paste button
final AjaxLink<Void> btnPaste = new AjaxLink<Void>("paste") {
private static final long serialVersionUID = 2647829118342823975L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(240);
final List<Resource> sources = new ArrayList<Resource>();
for (TreeNode<Resource> node : clipBoard.getItems()) {
sources.add(node.getObject());
}
final List<TreeNode<Resource>> newSelected = new ArrayList<TreeNode<Resource>>();
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = -8898887236980594842L;
private PanelPaste pastePanel;
@Override
protected Component getContents(String id) {
pastePanel = new PanelPaste(id, listResources(sources),
"/" + treeView.getSelectedNode().getObject().path(), clipBoard.isCopy());
return pastePanel;
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
pastePanel.getFeedbackMessages().clear();
String dir = pastePanel.getDirectory();
Iterator<Resource> it = sources.iterator();
while (it.hasNext()) {
Resource src = it.next();
Resource dest = store().get(Paths.path(dir, src.name()));
if (clipBoard.isCopy() && Resources.serializable(dest).equals(src)) {
//if we are copying a resource to its own directory, we will give it a new name.
for (int i = 1; Resources.exists(dest); i++) {
dest = store().get(Paths.path(dir,
FilenameUtils.getExtension(src.name()).isEmpty() ? src.name() + "." + i :
FilenameUtils.getBaseName(src.name()) + "." + i + "." +
FilenameUtils.getExtension(src.name())));
}
}
if (Resources.exists(dest)) {
pastePanel.error(new ParamResourceModel("resourceExists", getPage()).getString().replace("%", "/" + dest.path()));
} else {
try {
if (clipBoard.isCopy()) {
try (InputStream is = src.in()) {
try (OutputStream os = dest.out()) {
IOUtils.copy(is, os);
}
}
} else {
if (!store().move(src.path(), dest.path())) {
throw new IOException(new ParamResourceModel("moveFailed", getPage()).getString().replace("%", "/" + dest.path()));
}
}
it.remove();
newSelected.add(new ResourceNode(dest, expandedStates));
} catch (IOException | IllegalStateException e) {
pastePanel.error(e.getMessage());
}
}
}
//we select all the newly created nodes.
treeView.setSelectedNodes(newSelected, target);
//clear clipboard from moved resources (copied resources will remain,
//in case user wants to copy them multiple times)
clipBoard.clearRemoved();
//we leave modal only if operation was complete
if (!sources.isEmpty()) {
pastePanel.getSourceField().setModelObject(listResources(sources));
target.add(pastePanel.getFeedbackPanel());
target.add(pastePanel.getSourceField());
return false;
}
return true;
}
});
}
};
//copy button
final AjaxLink<Void> btnCopy = new AjaxLink<Void>("copy") {
private static final long serialVersionUID = 3883958793500232081L;
@Override
public void onClick(AjaxRequestTarget target) {
clipBoard.setItems(treeView.getSelectedNodes(), true, target);
target.add(treeView.getSelectedViews());
}
};
//cut button
final AjaxLink<Void> btnCut = new AjaxLink<Void>("cut") {
private static final long serialVersionUID = 2647829118342823975L;
@Override
public void onClick(AjaxRequestTarget target) {
clipBoard.setItems(treeView.getSelectedNodes(), false, target);
enable(btnPaste, treeView.getSelectedNode() != null && !treeView.getSelectedNode().isLeaf());
target.add(treeView.getSelectedViews());
target.add(btnPaste);
}
};
//rename button
final AjaxLink<Void> btnRename = new AjaxLink<Void>("rename") {
private static final long serialVersionUID = 2647829118342823975L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(150);
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = -8898887236980594842L;
private PanelRename renamePanel;
@Override
protected Component getContents(String id) {
renamePanel = new PanelRename(id, treeView.getSelectedNode().getObject().name());
return renamePanel;
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
renamePanel.getFeedbackMessages().clear();
Resource src = treeView.getSelectedNode().getObject();
Resource dest = treeView.getSelectedNode().getObject().parent().get(renamePanel.getName());
if (Resources.exists(dest)) {
renamePanel.error(new ParamResourceModel("resourceExists", getPage()).getString().replace("%", "/" + dest.path()));
} else {
Boolean expandedModel = expandedStates.getResourceExpandedState(src).getObject();
if (!src.renameTo(dest)) {
renamePanel.error(new ParamResourceModel("renameFailed", getPage()).getString());
} else {
if (clipBoard.getItems().contains(new ResourceNode(src, expandedStates))) {
clipBoard.clearRemoved();
clipBoard.addItem(new ResourceNode(dest, expandedStates), target);
}
//we have a new expanded state. if the original node was expanded, we expand this one as well.
//(child nodes might still loose their expanded state though)
expandedStates.getResourceExpandedState(dest).setObject(expandedModel);
//select the new node
treeView.setSelectedNode(new ResourceNode(dest, expandedStates), target);
return true;
}
}
target.add(renamePanel.getFeedbackPanel());
return false;
}
});
}
};
//delete button
final AjaxLink<Void> btnDelete = new AjaxLink<Void>("delete") {
private static final long serialVersionUID = -7370119488741589880L;
@Override
public void onClick(AjaxRequestTarget target) {
dialog.setInitialHeight(100);
final List<Resource> toBeDeleted = new ArrayList<Resource>();
for (TreeNode<Resource> selectedNode : treeView.getSelectedNodes()) {
toBeDeleted.add(selectedNode.getObject());
}
dialog.showOkCancel(target, new DialogDelegate() {
private static final long serialVersionUID = 1557172478015946688L;
@Override
protected Component getContents(String id) {
return new Label(id, new ParamResourceModel("confirmDelete", getPage()).getString() + " " +
listResources(toBeDeleted));
}
@Override
protected boolean onSubmit(AjaxRequestTarget target, Component contents) {
for (Resource res : toBeDeleted) {
if (!res.delete()) {
error(new ParamResourceModel("deleteFailed", getPage()).getString().replace("%", res.path()));
target.add(getFeedbackPanel());
}
}
//if deleted node was on clipboard, remove it form the clipboard
clipBoard.clearRemoved();
//remove selection
treeView.setSelectedNodes(Collections.emptySet(), target);
return true;
}
});
}
};
//update menu buttons enabled states according to current selection
treeView.addSelectionListener(target -> {
Collection<TreeNode<Resource>> nodes = treeView.getSelectedNodes();
boolean containsRoot = false;
boolean containsDir = false;
for (TreeNode<Resource> node : nodes) {
if (!node.isLeaf()) {
containsDir = true;
}
if (node.getObject().path().isEmpty()) {
containsRoot = true;
}
}
TreeNode<Resource> node = treeView.getSelectedNode();
enable(btnUpload, node != null && !node.isLeaf());
enable(btnNew, node != null && !node.isLeaf());
enable(btnDownload, node != null && node.isLeaf());
enable(btnEdit, node != null && node.isLeaf() && isTextual(node.getObject()));
enable(btnRename, node != null && !node.getObject().path().isEmpty());
enable(btnPaste, node != null && clipBoard.getItems().size() > 0 && !node.isLeaf());
enable(btnCopy, nodes.size() > 0 && !containsDir);
enable(btnCut, nodes.size() > 0 && !containsRoot);
enable(btnDelete, nodes.size() > 0 && !containsRoot);
target.add(btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete);
});
//initialize and add buttons
initButtons(btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete);
add(dialog, btnUpload, btnNew, btnDownload, btnEdit, btnCopy, btnCut, btnPaste, btnRename, btnDelete, treeView);
}
/**
* The TreeView
*
* @return the TreeView
*/
@SuppressWarnings("unchecked")
protected TreeView<Resource> treeView() {
return (TreeView<Resource>) get("treeview");
}
/**
* The resource store
*
* @return resource store
*/
protected ResourceStore store() {
return getGeoServerApplication().getResourceLoader();
}
/**
* Initialize the buttons (start with all disabled)
*/
protected static void initButtons(AbstractLink... buttons) {
for (AbstractLink button : buttons) {
button.setEnabled(false);
button.add(DISABLED_BEHAVIOR);
button.setOutputMarkupId(true);
}
}
/**
* Enable/disable a button
*
* @param button the button
* @param enabled enabled state
*/
protected static void enable(AbstractLink button, boolean enabled) {
if (enabled != button.isEnabled()) {
button.setEnabled(enabled);
if (enabled) {
button.remove(DISABLED_BEHAVIOR);
} else {
button.add(DISABLED_BEHAVIOR);
}
}
}
private static String listResources(Collection<Resource> resources) {
if (resources.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (Resource res : resources) {
builder.append("/" + res.path());
builder.append(", ");
}
builder.setLength(builder.length() - 2);
return builder.toString();
}
/**
* Guess if a resource is textual or not (by extension)
*
* @param resource the resource
* @return whether that resource is likely textual or not.
*/
private static boolean isTextual(Resource resource) {
if (resource.getType() != Resource.Type.RESOURCE) {
return false;
}
int i = resource.name().lastIndexOf(".");
if (i >= 0) {
String ext = resource.name().substring(i + 1).toLowerCase();
for (String t : TEXTUAL_EXTENSIONS) {
if(ext.equals(t)) {
return true;
}
}
return false;
}
return true; //no extension, assume textual
}
}