diff --git a/pom.xml b/pom.xml index 91b5b993f..8369ce5ed 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 13.1.0 + 17.0.0 diff --git a/src/main/java/org/scijava/convert/FileListConverters.java b/src/main/java/org/scijava/convert/FileListConverters.java new file mode 100644 index 000000000..386279774 --- /dev/null +++ b/src/main/java/org/scijava/convert/FileListConverters.java @@ -0,0 +1,162 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck + * Institute of Molecular Cell Biology and Genetics, University of + * Konstanz, and KNIME GmbH. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.convert; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.scijava.Priority; +import org.scijava.plugin.Plugin; +import org.scijava.util.StringUtils; + +/** + * A collection of {@link Converter} plugins for going between {@link String}, + * {@link File} and {@code File[]}. + * + * @author Jan Eglinger + * @author Curtis Rueden + */ +public class FileListConverters { + // -- String to File (list) converters -- + + @Plugin(type = Converter.class, priority = Priority.NORMAL) + public static class StringToFileConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + return (T) new File((String) src); + } + + @Override + public Class getOutputType() { + return File.class; + } + + @Override + public Class getInputType() { + return String.class; + } + + } + + @Plugin(type = Converter.class, priority = Priority.NORMAL) + public static class StringToFileArrayConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + final String[] tokens = StringUtils.splitUnquoted((String) src, ","); + final List fileList = new ArrayList<>(); + for (final String filePath : tokens) { + fileList.add(new File(filePath.replaceAll("^\"|\"$", ""))); + } + return (T) fileList.toArray(new File[fileList.size()]); + } + + @Override + public Class getOutputType() { + return File[].class; + } + + @Override + public Class getInputType() { + return String.class; + } + + } + + // TODO add StringToFileListConverter + + // -- File (list) to String converters -- + + @Plugin(type = Converter.class, priority = Priority.NORMAL) + public static class FileToStringConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + return (T) ((File) src).getAbsolutePath(); + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public Class getInputType() { + return File.class; + } + + } + + @Plugin(type = Converter.class, priority = Priority.NORMAL) + public static class FileArrayToStringConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + final List result = Arrays.asList((File[]) src).stream().map( + f -> { + final String path = f.getAbsolutePath(); + return path.contains(",") ? "\"" + path + "\"" : path; + }).collect(Collectors.toList()); + return (T) String.join(",", result); + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public Class getInputType() { + return File[].class; + } + + } + + // TODO add FileListToStringConverter +} diff --git a/src/main/java/org/scijava/module/DefaultModuleService.java b/src/main/java/org/scijava/module/DefaultModuleService.java index 2bcf13d24..bdad241f5 100644 --- a/src/main/java/org/scijava/module/DefaultModuleService.java +++ b/src/main/java/org/scijava/module/DefaultModuleService.java @@ -297,9 +297,7 @@ public void save(final ModuleItem item, final T value) { return; } - // FIXME: Convert to string, instead of just calling toString. - // Otherwise many things (e.g. File[]) are persisted improperly. - final String sValue = value == null ? "" : value.toString(); + final String sValue = value == null ? "" : convertService.convert(value, String.class); // do not persist if object cannot be converted back from a string if (!convertService.supports(sValue, item.getType())) return; diff --git a/src/main/java/org/scijava/ui/DefaultUIService.java b/src/main/java/org/scijava/ui/DefaultUIService.java index 76aae7e50..e88f94e55 100644 --- a/src/main/java/org/scijava/ui/DefaultUIService.java +++ b/src/main/java/org/scijava/ui/DefaultUIService.java @@ -321,15 +321,15 @@ public File chooseFile(final File file, final String style) { } @Override - public File[] chooseFiles(File[] files, FileFilter filter) { + public File[] chooseFiles(File parent, File[] files, FileFilter filter, String style) { final UserInterface ui = getDefaultUI(); - return ui == null ? null : ui.chooseFiles(files, filter); + return ui == null ? null : ui.chooseFiles(parent, files, filter, style); } @Override - public List chooseFiles(List fileList, FileFilter filter) { + public List chooseFiles(File parent, List fileList, FileFilter filter, String style) { final UserInterface ui = getDefaultUI(); - return ui == null ? null : ui.chooseFiles(fileList, filter); + return ui == null ? null : ui.chooseFiles(parent, fileList, filter, style); } @Override diff --git a/src/main/java/org/scijava/ui/FileListPreprocessor.java b/src/main/java/org/scijava/ui/FileListPreprocessor.java index 6cc5ae83f..6a8971898 100644 --- a/src/main/java/org/scijava/ui/FileListPreprocessor.java +++ b/src/main/java/org/scijava/ui/FileListPreprocessor.java @@ -56,7 +56,9 @@ public void process(final Module module) { final File[] files = fileInput.getValue(module); // show file chooser dialog box - final File[] result = uiService.chooseFiles(files, null); + // TODO decide how to create filter from style attributes + // TODO retrieve parent folder?? + final File[] result = uiService.chooseFiles(null, files, null, fileInput.getWidgetStyle()); if (result == null) { cancel(""); return; diff --git a/src/main/java/org/scijava/ui/UIService.java b/src/main/java/org/scijava/ui/UIService.java index 7191679dd..9b711bd05 100644 --- a/src/main/java/org/scijava/ui/UIService.java +++ b/src/main/java/org/scijava/ui/UIService.java @@ -304,7 +304,7 @@ DialogPrompt.Result showDialog(String message, String title, * @param files The initial value displayed in the file chooser prompt. * @param filter A filter allowing to restrict the choice of files */ - File[] chooseFiles(File[] files, FileFilter filter); + File[] chooseFiles(File parent, File[] files, FileFilter filter, String style); /** * Prompts the user to select one or multiple files. @@ -315,7 +315,7 @@ DialogPrompt.Result showDialog(String message, String title, * @param fileList The initial value displayed in the file chooser prompt. * @param filter A filter allowing to restrict the choice of files */ - List chooseFiles(List fileList, FileFilter filter); + List chooseFiles(File parent, List fileList, FileFilter filter, String style); /** * Displays a popup context menu for the given display at the specified diff --git a/src/main/java/org/scijava/ui/UserInterface.java b/src/main/java/org/scijava/ui/UserInterface.java index 544991cd5..385df24d8 100644 --- a/src/main/java/org/scijava/ui/UserInterface.java +++ b/src/main/java/org/scijava/ui/UserInterface.java @@ -191,26 +191,30 @@ default File chooseFile(String title, File file, String style) { /** * Prompts the user to choose a list of files. * + * @param parent Parent folder for file selection * @param files The initial value displayed in the file chooser prompt. * @param filter A filter allowing to restrict file choice. + * @param style File selection style (files, directories, or both) and optional filters * @return The selected {@link File}s chosen by the user, or null if the * user cancels the prompt. */ - default File[] chooseFiles(File[] files, FileFilter filter) { + default File[] chooseFiles(File parent, File[] files, FileFilter filter, String style) { throw new UnsupportedOperationException(); } /** * Prompts the user to choose a list of files. * + * @param parent Parent folder for file selection * @param fileList The initial value displayed in the file chooser prompt. * @param filter A filter allowing to restrict file choice. + * @param style File selection style (files, directories, or both) and optional filters * @return The selected {@link File}s chosen by the user, or null if the * user cancels the prompt. */ - default List chooseFiles(List fileList, FileFilter filter) { + default List chooseFiles(File parent, List fileList, FileFilter filter, String style) { final File[] initialFiles = fileList.toArray(new File[fileList.size()]); - final File[] chosenFiles = chooseFiles(initialFiles, filter); + final File[] chosenFiles = chooseFiles(parent, initialFiles, filter, style); return chosenFiles == null ? null : Arrays.asList(chosenFiles); } diff --git a/src/main/java/org/scijava/util/StringUtils.java b/src/main/java/org/scijava/util/StringUtils.java index 9a09e5939..c2155e277 100644 --- a/src/main/java/org/scijava/util/StringUtils.java +++ b/src/main/java/org/scijava/util/StringUtils.java @@ -63,6 +63,7 @@ import java.io.File; import java.text.DecimalFormatSymbols; +import java.util.regex.Pattern; /** * Useful methods for working with {@link String}s. @@ -80,6 +81,16 @@ private StringUtils() { // NB: prevent instantiation of utility class. } + /** + * Splits a string only at separators outside of quotation marks ({@code "}). + * Does not handle escaped quotes. + */ + public static String[] splitUnquoted(final String s, final String separator) { + // See https://stackoverflow.com/a/1757107/1919049 + return s.split(Pattern.quote(separator) + + "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); + } + /** Normalizes the decimal separator for the user's locale. */ public static String sanitizeDouble(String value) { value = value.replaceAll("[^0-9,\\.]", ""); diff --git a/src/main/java/org/scijava/widget/FileListWidget.java b/src/main/java/org/scijava/widget/FileListWidget.java index d76c3a1fe..b7f2203a5 100644 --- a/src/main/java/org/scijava/widget/FileListWidget.java +++ b/src/main/java/org/scijava/widget/FileListWidget.java @@ -34,5 +34,24 @@ import java.io.File; public interface FileListWidget extends InputWidget { - // NB: No changes to interface. + /** + * Widget style to allow file selection only + * + * @see org.scijava.plugin.Parameter#style() + */ + String FILES_ONLY = "files"; + + /** + * Widget style to allow directory selection only + * + * @see org.scijava.plugin.Parameter#style() + */ + String DIRECTORIES_ONLY = "directories"; + + /** + * Widget style to allow selection of both files and directories + * + * @see org.scijava.plugin.Parameter#style() + */ + String FILES_AND_DIRECTORIES = "both"; } diff --git a/src/test/java/org/scijava/convert/FileListConverterTest.java b/src/test/java/org/scijava/convert/FileListConverterTest.java new file mode 100644 index 000000000..ed3610e11 --- /dev/null +++ b/src/test/java/org/scijava/convert/FileListConverterTest.java @@ -0,0 +1,103 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck + * Institute of Molecular Cell Biology and Genetics, University of + * Konstanz, and KNIME GmbH. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.convert; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import java.io.File; + +import org.junit.Test; +import org.scijava.convert.FileListConverters.FileArrayToStringConverter; +import org.scijava.convert.FileListConverters.FileToStringConverter; +import org.scijava.convert.FileListConverters.StringToFileArrayConverter; +import org.scijava.convert.FileListConverters.StringToFileConverter; + +public class FileListConverterTest { + + @Test + public void testStringToFileConverter() { + final StringToFileConverter conv = new StringToFileConverter(); + final String path = "C:\\temp\\f,i;l-ename.txt"; + assertTrue("Cannot convert from String to File", + conv.canConvert(String.class, File.class)); + assertFalse("Can erroneously convert from String to File[]", + conv.canConvert(String.class, File[].class)); + assertEquals(new File(path), + conv.convert(path, File.class)); + } + + @Test + public void testStringToFileArrayConverter() { + final StringToFileArrayConverter conv = new StringToFileArrayConverter(); + final String path = "\"C:\\temp\\f,i;l-ename.txt\",C:\\temp"; + assertTrue("Cannot convert from String to File[]", + conv.canConvert(String.class, File[].class)); + assertFalse("Can erroneously convert from String to File", + conv.canConvert(String.class, File.class)); + assertEquals("Wrong array length", 2, + conv.convert(path, File[].class).length); + assertEquals("Wrong file name", new File("C:\\temp\\f,i;l-ename.txt"), + conv.convert(path, File[].class)[0]); + assertEquals("Wrong file name", new File("C:\\temp"), + conv.convert(path, File[].class)[1]); + } + + @Test + public void testFileToStringConverter() { + final FileToStringConverter conv = new FileToStringConverter(); + final File file = new File("C:\\temp\\f,i;l-ename.txt"); + assertTrue("Cannot convert from File to String", + conv.canConvert(File.class, String.class)); + assertFalse("Can erroneously convert from File[] to String", + conv.canConvert(File[].class, String.class)); + assertEquals(file.getAbsolutePath(), + conv.convert(file, String.class)); + } + + @Test + public void testFileArrayToStringConverter() { + final FileArrayToStringConverter conv = new FileArrayToStringConverter(); + final File[] fileArray = new File[2]; + fileArray[0] = new File("C:\\temp\\f,i;l-ename.txt"); + fileArray[1] = new File("C:\\temp"); + final String expected = "\"" + fileArray[0].getAbsolutePath() + "\"," + fileArray[1].getAbsolutePath(); + assertTrue("Cannot convert from File[] to String", + conv.canConvert(File[].class, String.class)); + assertFalse("Can erroneously convert from File to String", + conv.canConvert(File.class, String.class)); + assertEquals("Wrong output string", expected, + conv.convert(fileArray, String.class)); + } +} diff --git a/src/test/java/org/scijava/util/StringUtilsTest.java b/src/test/java/org/scijava/util/StringUtilsTest.java index 88089986b..9559fd83c 100644 --- a/src/test/java/org/scijava/util/StringUtilsTest.java +++ b/src/test/java/org/scijava/util/StringUtilsTest.java @@ -32,6 +32,7 @@ package org.scijava.util; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -47,6 +48,21 @@ */ public class StringUtilsTest { + /** Tests {@link StringUtils#splitUnquoted}. */ + @Test + public void testSplitUnquoted() { + // See https://stackoverflow.com/a/1757107/1919049 + final String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; + final String[] expected = { + "foo", + "bar", + "c;qual=\"baz,blurb\"", + "d;junk=\"quux,syzygy\"" + }; + final String[] actual = StringUtils.splitUnquoted(line, ","); + assertArrayEquals(expected, actual); + } + @Test public void isNullOrEmptyFalseIfString() throws Exception { assertFalse(StringUtils.isNullOrEmpty("Fresh out of Red Leicester"));