/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.web.wicket.browser; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.filechooser.FileSystemView; import org.apache.commons.io.FilenameUtils; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.markup.html.form.ChoiceRenderer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.web.wicket.ParamResourceModel; import org.geotools.util.logging.Logging; public class GeoServerFileChooser extends Panel { private static final long serialVersionUID = -6246944669686555266L; static Boolean HIDE_FS = null; static { HIDE_FS = Boolean.valueOf(GeoServerExtensions.getProperty("GEOSERVER_FILEBROWSER_HIDEFS")); } static File USER_HOME = null; static { // try to safely determine the user home location try { File hf = null; String home = System.getProperty("user.home"); if(home != null) { hf = new File(home); } if(hf != null && hf.exists()) { USER_HOME = hf; } } catch(Throwable t) { // that's ok, we might not be able to get the user home } } static final Logger LOGGER = Logging.getLogger(GeoServerFileChooser.class); FileBreadcrumbs breadcrumbs; FileDataView fileTable; boolean hideFileSystem = false; IModel<File> file; public GeoServerFileChooser(String id, IModel<File> file) { this(id, file, HIDE_FS); } /** * Constructor with optional flag to control how file system resources are exposed. * <p> * When <tt>hideFileSyste</tt> is set to <tt>true</tt> only the data directory is exposed * in the file browser. * </p> */ public GeoServerFileChooser(String id, IModel<File> file, boolean hideFileSystem) { super(id, file); this.file = file; this.hideFileSystem = hideFileSystem; // build the roots ArrayList<File> roots = new ArrayList<File>(); if (!hideFileSystem) { roots.addAll(Arrays.asList(File.listRoots())); } Collections.sort(roots); // TODO: find a better way to deal with the data dir GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class); File dataDirectory = loader.getBaseDirectory(); roots.add(0, dataDirectory ); // add the home directory as well if it was possible to determine it at all if(!hideFileSystem && USER_HOME != null) { roots.add(1, USER_HOME); } // find under which root the selection should be placed File selection = (File) file.getObject(); // first check if the file is a relative reference into the data dir if(selection != null) { File relativeToDataDir = loader.url(selection.getPath()); if(relativeToDataDir != null) { selection = relativeToDataDir; } } // select the proper root File selectionRoot = null; if(selection != null && selection.exists()) { for (File root : roots) { if(isSubfile(root, selection.getAbsoluteFile())) { selectionRoot = root; break; } } // if the file is not part of the known search paths, give up // and switch back to the data directory if(selectionRoot == null) { selectionRoot = dataDirectory; file = new Model<File>(selectionRoot); } else { if(!selection.isDirectory()) { file = new Model<File>(selection.getParentFile()); } else { file = new Model<File>(selection); } } } else { selectionRoot = dataDirectory; file = new Model<File>(selectionRoot); } this.file = file; setDefaultModel(file); // the root chooser final DropDownChoice<File> choice = new DropDownChoice<File>("roots", new Model<File>(selectionRoot), new Model<ArrayList<File>>(roots), new FileRootsRenderer()); choice.add(new AjaxFormComponentUpdatingBehavior("change") { private static final long serialVersionUID = -1527567847101388940L; @Override protected void onUpdate(AjaxRequestTarget target) { File selection = (File) choice.getModelObject(); breadcrumbs.setRootFile(selection); updateFileBrowser(selection, target); } }); choice.setOutputMarkupId(true); add(choice); // the breadcrumbs breadcrumbs = new FileBreadcrumbs("breadcrumbs", new Model<File>(selectionRoot), file) { private static final long serialVersionUID = -6995769189316700797L; @Override protected void pathItemClicked(File file, AjaxRequestTarget target) { updateFileBrowser(file, target); } }; breadcrumbs.setOutputMarkupId(true); add(breadcrumbs); // the file tables fileTable = new FileDataView("fileTable", new FileProvider(file)) { private static final long serialVersionUID = -5481794219862786117L; @Override protected void linkNameClicked(File file, AjaxRequestTarget target) { updateFileBrowser(file, target); } }; fileTable.setOutputMarkupId(true); add(fileTable); } void updateFileBrowser(File file, AjaxRequestTarget target) { if(file.isDirectory()) { directoryClicked(file, target); } else if(file.isFile()) { fileClicked(file, target); } } /** * Called when a file name is clicked. By default it does nothing */ protected void fileClicked(File file, AjaxRequestTarget target) { // do nothing, subclasses will override } /** * Action undertaken as a directory is clicked. Default behavior is to drill down into * the directory. * @param file * @param target */ protected void directoryClicked(File file, AjaxRequestTarget target) { // explicitly change the root model, inform the other components the model has changed GeoServerFileChooser.this.file.setObject(file); fileTable.getProvider().setDirectory(new Model<File>(file)); breadcrumbs.setSelection(file); target.add(fileTable); target.add(breadcrumbs); } private boolean isSubfile(File root, File selection) { if(selection == null || "".equals(selection.getPath())) return false; if(selection.equals(root)) return true; return isSubfile(root, selection.getParentFile()); } /** * * @param fileFilter */ public void setFilter(IModel<? extends FileFilter> fileFilter) { fileTable.provider.setFileFilter(fileFilter); } /** * Set the file table fixed height. Set it to null if you don't want fixed height * with overflow, and to a valid CSS measure if you want it instead. * Default value is "25em" * @param height */ public void setFileTableHeight(String height) { fileTable.setTableHeight(height); } // /** // * If the file is in the data directory builds a data dir relative path, otherwise // * returns an absolute path // * @param file // * // */ // public String getRelativePath(File file) { // File dataDirectory = GeoserverDataDirectory.getGeoserverDataDirectory(); // if(isSubfile(dataDirectory, file)) { // File curr = file; // String path = null; // // paranoid check to avoid infinite loops // while(curr != null && !curr.equals(dataDirectory)){ // if(path == null) { // path = curr.getName(); // } else { // path = curr.getName() + "/" + path; // } // curr = curr.getParentFile(); // } // return "file:" + path; // } // // return "file://" + file.getAbsolutePath(); // } // class FileRootsRenderer extends ChoiceRenderer<File> { private static final long serialVersionUID = 1389015915737006638L; public Object getDisplayValue(File f) { if (f == USER_HOME) { return new ParamResourceModel("userHome", GeoServerFileChooser.this) .getString(); } else { GeoServerResourceLoader loader = GeoServerExtensions .bean(GeoServerResourceLoader.class); if (f.equals(loader.getBaseDirectory())) { return new ParamResourceModel("dataDirectory", GeoServerFileChooser.this).getString(); } } try { final String displayName = FileSystemView.getFileSystemView() .getSystemDisplayName(f); if (displayName != null && !displayName.trim().isEmpty()) { return displayName.trim(); } return FilenameUtils.getPrefix(f.getAbsolutePath()); } catch (Exception e) { LOGGER.log(Level.FINE, "Failed to get file display name, " + "on Windows this might be related to a known java bug http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6973685", e); // on windows we can get the occasional NPE due to // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6973685 } return f.getName(); } public String getIdValue(File f, int count) { return "" + count; } } }