/* (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.geogig.geoserver.web.repository; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.Serializable; import java.net.URI; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.swing.filechooser.FileSystemView; import org.apache.commons.io.FilenameUtils; import org.apache.wicket.AttributeModifier; import org.apache.wicket.MetaDataKey; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider; 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.ChoiceRenderer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.image.Image; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.util.convert.IConverter; import org.geogig.geoserver.config.RepositoryInfo; import org.geogig.geoserver.config.RepositoryManager; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.web.GeoServerBasePage; import org.geoserver.web.wicket.ParamResourceModel; import org.geoserver.web.wicket.browser.FileBreadcrumbs; import org.geoserver.web.wicket.browser.FileProvider; import org.geoserver.web.wicket.browser.GeoServerFileChooser; import com.google.common.collect.Lists; /** * A geogig directory file chooser compoenent * <p> * Adapted from {@link GeoServerFileChooser} */ public class DirectoryChooser extends Panel { private static final long serialVersionUID = -5587014542924822012L; private final IModel<DirectoryFilter> fileFilter = new Model<>(new DirectoryFilter()); 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 } } private static final MetaDataKey<File> LAST_VISITED_DIRECTORY = new MetaDataKey<File>() { private static final long serialVersionUID = 1L; }; private FileBreadcrumbs breadcrumbs; private DirectoryDataView directoryListingTable; private final IModel<File> directory; private final boolean makeRepositoriesSelectable; private AjaxLink<?> accepdDirectoryLink; public DirectoryChooser(String contentId, IModel<File> directory) { this(contentId, directory, true); } public DirectoryChooser(final String contentId, IModel<File> initialDirectory, final boolean makeRepositoriesSelectable) { super(contentId, initialDirectory); getSession().bind();// so we can store the last visited directory as a session object this.makeRepositoriesSelectable = makeRepositoriesSelectable; if (initialDirectory.getObject() == null) { File lastUsed = getSession().getMetaData(LAST_VISITED_DIRECTORY); initialDirectory.setObject(lastUsed); } // build the roots ArrayList<File> roots = Lists.newArrayList(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 (USER_HOME != null) { roots.add(1, USER_HOME); } // find under which root the selection should be placed File selection = initialDirectory.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; initialDirectory = new Model<>(selectionRoot); } else { if (!selection.isDirectory()) { initialDirectory = new Model<>(selection.getParentFile()); } else { initialDirectory = new Model<>(selection); } } } else { selectionRoot = dataDirectory; initialDirectory = new Model<>(selectionRoot); } this.directory = initialDirectory; setDefaultModel(initialDirectory); // the root chooser final DropDownChoice<File> choice = new DropDownChoice<>("roots", new Model<>( selectionRoot), new Model<ArrayList<File>>(roots), new FileRootsRenderer()); choice.add(new AjaxFormComponentUpdatingBehavior("change") { private static final long serialVersionUID = -1113141016446727615L; @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<>(selectionRoot), initialDirectory) { private static final long serialVersionUID = 3637173832581301482L; @Override protected void pathItemClicked(File file, AjaxRequestTarget target) { updateFileBrowser(file, target); } }; breadcrumbs.setOutputMarkupId(true); add(breadcrumbs); // the file tables directoryListingTable = new DirectoryDataView("fileTable", new FileProvider( initialDirectory), this.makeRepositoriesSelectable) { private static final long serialVersionUID = -1559299096797421815L; @Override protected void linkNameClicked(File file, AjaxRequestTarget target) { updateFileBrowser(file, target); } }; directoryListingTable.setOutputMarkupId(true); directoryListingTable.setFileFilter(fileFilter); add(directoryListingTable); accepdDirectoryLink = new AjaxLink<File>("ok", this.directory) { private static final long serialVersionUID = 1L; @Override public void onClick(AjaxRequestTarget target) { // must of been set by #directoryClicked() File dir = getModelObject(); getSession().setMetaData(LAST_VISITED_DIRECTORY, dir); directorySelected(dir, target); } }; add(accepdDirectoryLink); accepdDirectoryLink.setVisible(!this.makeRepositoriesSelectable); } void updateFileBrowser(File file, AjaxRequestTarget target) { if (RepositoryManager.isGeogigDirectory(file)) { geogigDirectoryClicked(file, target); } else { getSession().setMetaData(LAST_VISITED_DIRECTORY, file); directoryClicked(file, target); } } /** * Called when a file name is clicked. By default it does nothing * @param file * @param target */ protected void geogigDirectoryClicked(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 DirectoryChooser.this.directory.setObject(file); directoryListingTable.setDirectory(new Model<>(file)); breadcrumbs.setSelection(file); target.add(directoryListingTable); target.add(breadcrumbs); } protected void directorySelected(File file, AjaxRequestTarget target) { // to be overriden } 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()); } /** * 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) { directoryListingTable.setTableHeight(height); } class FileRootsRenderer extends ChoiceRenderer<File> { private static final long serialVersionUID = -5804668199121599078L; @Override public Object getDisplayValue(File f) { if (f == USER_HOME) { return new ParamResourceModel("userHome", DirectoryChooser.this).getString(); } else { GeoServerResourceLoader loader = GeoServerExtensions .bean(GeoServerResourceLoader.class); if (f.equals(loader.getBaseDirectory())) { return new ParamResourceModel("dataDirectory", DirectoryChooser.this) .getString(); } } try { final String displayName = FileSystemView.getFileSystemView().getSystemDisplayName( f); if (displayName != null && displayName.length() > 0) { return displayName; } return FilenameUtils.getPrefix(f.getAbsolutePath()); } catch (Exception 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(); } @Override public String getIdValue(File f, int count) { return "" + count; } } private static final class DirectoryFilter implements FileFilter, Serializable { private static final long serialVersionUID = -2280505390702552L; @Override public boolean accept(File file) { return file.isDirectory(); } } static abstract class DirectoryDataView extends Panel { private static final PackageResourceReference FOLDER = new PackageResourceReference( GeoServerBasePage.class, "img/icons/silk/folder.png"); private static final PackageResourceReference GEOGIG_FOLDER = new PackageResourceReference( DirectoryDataView.class, "../geogig_16x16_babyblue.png"); private static final long serialVersionUID = -2932107412054607607L; private final boolean allowSelectingRepositories; private final IConverter FILE_NAME_CONVERTER = new StringConverter() { private static final long serialVersionUID = 2050812486536366790L; @Override public String convertToString(Object value, Locale locale) { File file = (File) value; if (file.isDirectory()) { if (RepositoryManager.isGeogigDirectory(file)) { return file.getName(); } return file.getName() + "/"; } else { return file.getName(); } } }; private static final IConverter FILE_LASTMODIFIED_CONVERTER = new StringConverter() { private static final long serialVersionUID = 7862772890388011374L; @Override public String convertToString(Object value, Locale locale) { File file = (File) value; long lastModified = file.lastModified(); if (lastModified == 0L) return null; else { return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) .format(new Date(file.lastModified())); } } }; private SortableDataProvider<File, String> provider; private final WebMarkupContainer fileContent; private String tableHeight = "25em"; @SuppressWarnings("unchecked") public DirectoryDataView(final String id, final FileProvider fileProvider, final boolean allowSelectingRepositories) { super(id); this.provider = fileProvider; this.allowSelectingRepositories = allowSelectingRepositories; final WebMarkupContainer table = new WebMarkupContainer("fileTable"); table.setOutputMarkupId(true); add(table); List<RepositoryInfo> all = RepositoryManager.get().getAll(); // maps canonical files to configured file to identify duplicates due to symlinks final Map<File, File> existingPaths = new HashMap<>(); for (RepositoryInfo info : all) { final URI uri = info.getLocation(); if ("file".equals(uri.getScheme())) { File configured = new File(uri); File canonical; try { canonical = configured.getCanonicalFile(); } catch (IOException e) { canonical = configured; } existingPaths.put(canonical, configured); } } DataView<File> fileTable = new DataView<File>("files", fileProvider) { private static final long serialVersionUID = 1345694542339080271L; @Override protected void populateItem(final Item<File> item) { // odd/even alternate style item.add(AttributeModifier.replace("class", item.getIndex() % 2 == 0 ? "even" : "odd")); final File file = item.getModelObject(); final boolean isGeogigDirectory = RepositoryManager.isGeogigDirectory(file); PackageResourceReference icon = isGeogigDirectory ? GEOGIG_FOLDER : FOLDER; item.add(new Image("icon", icon)); // navigation/selection links AjaxFallbackLink<File> link = new AjaxFallbackLink<File>("nameLink") { private static final long serialVersionUID = -644973941443812893L; @Override public void onClick(AjaxRequestTarget target) { linkNameClicked((File) item.getModelObject(), target); } }; Label nameLabel = new Label("name", item.getModel()) { private static final long serialVersionUID = -4028081066393114129L; @Override public IConverter getConverter(Class type) { return FILE_NAME_CONVERTER; } }; link.add(nameLabel); final Map<File, File> existing = existingPaths; File canonicalFile; try { canonicalFile = file.getCanonicalFile(); } catch (IOException e) { canonicalFile = file; } final boolean alreadyImported = isGeogigDirectory && existing.containsKey(canonicalFile); if (isGeogigDirectory) { if (alreadyImported) { link.setEnabled(false); File dupicate = existing.get(canonicalFile); nameLabel.add(AttributeModifier.replace("title", new ParamResourceModel( "DirectoryChooser$DirectoryDataView.repoExists", DirectoryDataView.this, dupicate.getAbsolutePath()) .getObject())); } else { link.setEnabled(DirectoryDataView.this.allowSelectingRepositories); } } item.add(link); // last modified and size labels item.add(new Label("lastModified", item.getModel()) { private static final long serialVersionUID = -4706544449170830483L; @Override public IConverter getConverter(Class type) { return FILE_LASTMODIFIED_CONVERTER; } }); } }; fileContent = new WebMarkupContainer("fileContent") { private static final long serialVersionUID = -4197754944388542068L; @Override protected void onComponentTag(ComponentTag tag) { if (tableHeight != null) { tag.getAttributes().put("style", "overflow:auto; height:" + tableHeight); } } }; fileContent.add(fileTable); table.add(fileContent); } protected abstract void linkNameClicked(File file, AjaxRequestTarget target); private static abstract class StringConverter implements IConverter { private static final long serialVersionUID = -6464669942374870999L; @Override public Object convertToObject(String value, Locale locale) { throw new UnsupportedOperationException("This converter works only for strings"); } } public SortableDataProvider<File, String> getProvider() { return provider; } public void setTableHeight(String tableHeight) { this.tableHeight = tableHeight; } public void setFileFilter(IModel<? extends FileFilter> fileFilter) { ((FileProvider) provider).setFileFilter(fileFilter); } public void setDirectory(Model<File> model) { ((FileProvider) provider).setDirectory(model); } } }