From 8533bf892b25de12776ea43a7690ae7d94348567 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 29 Apr 2024 16:48:02 -0400 Subject: [PATCH] chore: add platform-test workflow (#74) * chore: add n5-aws-s3, n5-blosc dependencies * test: fix macro test for windows * pref: add uri validation * pref: rm file:// prefix from uri * test: uri validation --- .github/workflows/platform-test.yml | 44 +++ pom.xml | 8 +- .../janelia/saalfeldlab/n5/ij/N5Importer.java | 52 +++- .../n5/ij/N5ScalePyramidExporter.java | 3 - .../n5/ui/DatasetSelectorDialog.java | 45 ++- .../n5/ui/ImprovedFormattedTextField.java | 263 ++++++++++++++++++ .../janelia/saalfeldlab/n5/ij/MacroTests.java | 101 ++++--- .../metadata/ome/ngff/v04/WriteAxesTests.java | 108 ++++--- .../saalfeldlab/n5/ui/TestUriValidation.java | 89 ++++++ 9 files changed, 626 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/platform-test.yml create mode 100644 src/main/java/org/janelia/saalfeldlab/n5/ui/ImprovedFormattedTextField.java create mode 100644 src/test/java/org/janelia/saalfeldlab/n5/ui/TestUriValidation.java diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml new file mode 100644 index 00000000..019f001b --- /dev/null +++ b/.github/workflows/platform-test.yml @@ -0,0 +1,44 @@ +name: test + +on: + push: + branches: + - master + tags: + - "*-[0-9]+.*" + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install blosc (Windows) + if: matrix.os == 'windows-latest' + run: | + pip install blosc --no-input --target src/test/resources + mv src/test/resources/bin/* src/test/resources + - name: Install blosc (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + pip install blosc --no-input --target src/test/resources + mv src/test/resources/lib64/* src/test/resources + - name: Set up Java + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'zulu' + cache: 'maven' + - name: Maven Test + run: mvn -B clean test --file pom.xml diff --git a/pom.xml b/pom.xml index 36d15f57..151405f5 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.janelia.saalfeldlab n5-ij - 4.1.3-SNAPSHOT + 4.1.2-SNAPSHOT N5 ImageJ Bindings ImageJ convenience layer for N5 @@ -86,10 +86,12 @@ sign,deploy-to-scijava 3.2.0 + 4.1.2 + 1.1.1 + 4.1.0 2.2.0 7.0.0 - 4.1.0 - 1.4.2 + 1.4.3-SNAPSHOT 1.3.1 1.0.2 diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ij/N5Importer.java b/src/main/java/org/janelia/saalfeldlab/n5/ij/N5Importer.java index b3b48dd5..7af8bf9e 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/ij/N5Importer.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/ij/N5Importer.java @@ -27,7 +27,9 @@ import java.io.File; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -62,6 +64,7 @@ import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer; import org.janelia.saalfeldlab.n5.universe.N5Factory; import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.N5Factory.StorageFormat; import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadataParser; import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMultiScaleMetadata; @@ -108,6 +111,7 @@ import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Pair; import net.imglib2.util.Util; import net.imglib2.view.Views; @@ -855,11 +859,17 @@ public N5Reader apply(final String n5UriOrPath) { if (n5UriOrPath == null || n5UriOrPath.isEmpty()) return null; - - String rootPath = null ; + String rootPath = null; if (n5UriOrPath.contains("?")) { + try { - rootPath = new N5URI(n5UriOrPath).getContainerPath(); + // need to strip off storage format for n5uri to correctly remove query; + final Pair fmtUri = N5Factory.StorageFormat.parseUri(n5UriOrPath); + final StorageFormat format = fmtUri.getA(); + + final N5URI n5uri = from(fmtUri.getB().toString()); + // add the format prefix back if it was present + rootPath = format == null ? n5uri.getContainerPath() : format.toString().toLowerCase() + ":" + n5uri.getContainerPath(); } catch (URISyntaxException e) {} } @@ -877,6 +887,42 @@ public N5Reader apply(final String n5UriOrPath) { } } + /** + * Generate an {@link N5URI} from a String. + * + * @param uriOrPath + * a string representation of a uri or a path string. + * @return the {@link N5URI} + */ + private static N5URI from(final String uriOrPath) { + + try { + return new N5URI(new URI(uriOrPath)); + } catch (Throwable ignore) {} + + try { + final String[] split = uriOrPath.split("\\?"); + final URI tmp = Paths.get(split[0]).toUri(); + if (split.length == 1) + return new N5URI(tmp); + else { + StringBuffer buildUri = new StringBuffer(); + buildUri.append(tmp.toString()); + buildUri.append("?"); + for (int i = 1; i < split.length; i++) + buildUri.append(split[i]); + + return new N5URI(new URI(buildUri.toString())); + } + } catch (Throwable ignore) {} + + try { + return new N5URI(N5URI.encodeAsUri(uriOrPath)); + } catch (URISyntaxException e) { + throw new N5Exception(e); + } + } + private static String upToLastExtension(final String path) { final int i = path.lastIndexOf('.'); diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ij/N5ScalePyramidExporter.java b/src/main/java/org/janelia/saalfeldlab/n5/ij/N5ScalePyramidExporter.java index 3e17996b..2edf7f31 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/ij/N5ScalePyramidExporter.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/ij/N5ScalePyramidExporter.java @@ -1514,9 +1514,6 @@ private static class DetailedOverwriteWarningDialog extends JDialog { public DetailedOverwriteWarningDialog(final Frame parent, final String root, final String dataset) { super(parent,"WARNING", true); initComponents(root, dataset); - final Dimension dims = new Dimension(600, 250); - setSize(dims); - setPreferredSize(dims); setResizable(false); setLocationRelativeTo(null); } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ui/DatasetSelectorDialog.java b/src/main/java/org/janelia/saalfeldlab/n5/ui/DatasetSelectorDialog.java index 041043eb..1cef8af6 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/ui/DatasetSelectorDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/ui/DatasetSelectorDialog.java @@ -50,13 +50,13 @@ import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFileChooser; +import javax.swing.JFormattedTextField.AbstractFormatter; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; -import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; @@ -72,7 +72,11 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.Collator; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -105,7 +109,7 @@ public class DatasetSelectorDialog { private JFrame dialog; - private JTextField containerPathText; + private ImprovedFormattedTextField containerPathText; private JCheckBox virtualBox; @@ -366,8 +370,7 @@ private JFrame buildDialog() { tabs.addTab("Metadata Translation", translationPanel.buildPanel()); tabs.addTab("Translation Result", translationResultPanel.buildPanel()); - containerPathText = new JTextField(); - containerPathText.setText(initialContainerPath); + containerPathText = new ImprovedFormattedTextField(new UriValidator(), initialContainerPath); containerPathText.setPreferredSize(new Dimension(frameSizeX / 3, containerPathText.getPreferredSize().height)); containerPathText.addActionListener(e -> openContainer(n5Fun, () -> getN5RootPath(), pathFun)); @@ -934,4 +937,38 @@ private static boolean pathsEqual(final String a, final String b) { return normalDatasetName(a, "/").equals(normalDatasetName(b, "/")); } + protected static class UriValidator extends AbstractFormatter { + + private static final long serialVersionUID = 6765664180035018335L; + + @Override + public Object stringToValue(String input) throws ParseException { + + if (input == null || input.isEmpty()) + return null; + + try { + final URI uri = new URI(input); + if (uri.isAbsolute()) + return uri; + } catch (Throwable ignore) {} + + try { + return Paths.get(input).toUri(); + } catch (Throwable ignore) {} + + throw new ParseException("input " + input + " not a valid URI", 0); + } + + @Override + public String valueToString(Object arg) throws ParseException { + + if( arg instanceof URI ) + return ((URI)arg).toString().replaceFirst("^file://", ""); + else + throw new ParseException("input " + arg + " not a valid URI", 0); + } + + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ui/ImprovedFormattedTextField.java b/src/main/java/org/janelia/saalfeldlab/n5/ui/ImprovedFormattedTextField.java new file mode 100644 index 00000000..f5999afa --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/ui/ImprovedFormattedTextField.java @@ -0,0 +1,263 @@ +package org.janelia.saalfeldlab.n5.ui; + +import javax.swing.JFormattedTextField; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import java.awt.Color; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; + +/** + *

+ * Extension of {@code JFormattedTextField} which solves some of the usability + * issues + *

+ * + *

+ * from + * https://stackoverflow.com/questions/1313390/is-there-any-way-to-accept-only-numeric-values-in-a-jtextfield?answertab=scoredesc#tab-top + *

+ */ +public class ImprovedFormattedTextField extends JFormattedTextField { + + private static final long serialVersionUID = 6986337989217402465L; + + private static final Color ERROR_BACKGROUND_COLOR = new Color(255, 215, 215); + + private static final Color ERROR_FOREGROUND_COLOR = null; + + private Color fBackground, fForeground; + + private Runnable updateCallback; + + private boolean runCallback; + + public ImprovedFormattedTextField(AbstractFormatter formatter) { + + super(formatter); + setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT); + updateBackgroundOnEachUpdate(); + + // improve the caret behavior + // see also + // http://tips4java.wordpress.com/2010/02/21/formatted-text-field-tips/ + addFocusListener(new MousePositionCorrectorListener()); + addFocusListener(new ContainerTextUpdateOnFocus(this)); + runCallback = true; + } + + /** + * Create a new {@code ImprovedFormattedTextField} instance which will use + * {@code aFormat} for the validation of the user input. The field will be + * initialized with {@code aValue}. + * + * @param formatter + * The formatter. May not be {@code null} + * @param aValue + * The initial value + */ + public ImprovedFormattedTextField(AbstractFormatter formatter, Object aValue) { + + this(formatter); + try { + setValue(new URI("")); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + public void setCallback(final Runnable updateCallback) { + + this.updateCallback = updateCallback; + } + + private void updateBackgroundOnEachUpdate() { + + getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void insertUpdate(DocumentEvent e) { + + update(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + + update(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + + update(); + } + + public void update() { + + updateBackground(); + if (runCallback && updateCallback != null) + updateCallback.run(); + } + }); + } + + /** + * Update the background color depending on the valid state of the current + * input. This provides visual feedback to the user + */ + private void updateBackground() { + + final boolean valid = validContent(); + if (ERROR_BACKGROUND_COLOR != null) { + setBackground(valid ? fBackground : ERROR_BACKGROUND_COLOR); + } + if (ERROR_FOREGROUND_COLOR != null) { + setForeground(valid ? fForeground : ERROR_FOREGROUND_COLOR); + } + } + + @Override + public void updateUI() { + + super.updateUI(); + fBackground = getBackground(); + fForeground = getForeground(); + } + + private boolean validContent() { + + final AbstractFormatter formatter = getFormatter(); + if (formatter != null) { + try { + formatter.stringToValue(getText()); + return true; + } catch (final ParseException e) { + return false; + } + } + return true; + } + + + public void setValue(Object value, boolean callback, boolean validate) { + + boolean validValue = true; + // before setting the value, parse it by using the format + try { + final AbstractFormatter formatter = getFormatter(); + if (formatter != null && validate ) { + formatter.stringToValue(getText()); + } + } catch (final ParseException e) { + validValue = false; + updateBackground(); + } + // only set the value when valid + if (validValue) { + final int old_caret_position = getCaretPosition(); + + final boolean before = runCallback; + runCallback = callback; + super.setValue(value); + runCallback = before; + + setCaretPosition(Math.min(old_caret_position, getText().length())); + } + } + + public void setValue(Object value, boolean callback) { + + setValue(value, callback, true); + } + + public void setValueNoCallback(Object value) { + + setValue(value, false); + } + + @Override + public void setValue(Object value) { + + setValue(value, true); + } + + @Override + protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { + + // do not let the formatted text field consume the enters. This allows + // to trigger an OK button by + // pressing enter from within the formatted text field + if (validContent()) { + return super.processKeyBinding(ks, e, condition, pressed) && ks != KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + } else { + return super.processKeyBinding(ks, e, condition, pressed); + } + } + + private static class MousePositionCorrectorListener extends FocusAdapter { + + @Override + public void focusGained(FocusEvent e) { + + /* + * After a formatted text field gains focus, it replaces its text + * with its current value, formatted appropriately of course. It + * does this after any focus listeners are notified. We want to make + * sure that the caret is placed in the correct position rather than + * the dumb default that is before the 1st character ! + */ + final JTextField field = (JTextField)e.getSource(); + final int dot = field.getCaret().getDot(); + final int mark = field.getCaret().getMark(); + if (field.isEnabled() && field.isEditable()) { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + + // Only set the caret if the textfield hasn't got a + // selection on it + if (dot == mark) { + field.getCaret().setDot(dot); + } + } + }); + } + } + } + + private static class ContainerTextUpdateOnFocus extends FocusAdapter { + + private final ImprovedFormattedTextField field; + + public ContainerTextUpdateOnFocus(ImprovedFormattedTextField field) { + + this.field = field; + } + + @Override + public void focusLost(FocusEvent e) { + + final AbstractFormatter formatter = field.getFormatter(); + if (formatter != null) { + try { + Object result = formatter.stringToValue((String)field.getText()); + field.setValue((URI)result, false, false); // no callback, no validation + } catch (ParseException ignore) { + ignore.printStackTrace(); + } + } + + } + } + +} diff --git a/src/test/java/org/janelia/saalfeldlab/n5/ij/MacroTests.java b/src/test/java/org/janelia/saalfeldlab/n5/ij/MacroTests.java index 0af52934..ac08ab10 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/ij/MacroTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/ij/MacroTests.java @@ -5,13 +5,13 @@ import java.io.File; import java.io.IOException; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; -import org.janelia.saalfeldlab.n5.N5Exception; -import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5Writer; -import org.janelia.saalfeldlab.n5.RunImportExportTest; import org.janelia.saalfeldlab.n5.TestExportImports; import org.janelia.saalfeldlab.n5.universe.N5Factory; import org.junit.After; @@ -30,9 +30,11 @@ public class MacroTests { - private File n5rootF; + private URI containerDir; - private File containerDir; + private URI n5rootF; + + private String dataset; private ImagePlus imp; @@ -40,19 +42,18 @@ public class MacroTests { public void before() { System.setProperty("java.awt.headless", "true"); - final String n5Root = "src/test/resources/test.n5"; - n5rootF = new File(n5Root); + try { + containerDir = new File(tempN5PathName()).getCanonicalFile().toURI(); + } catch (IOException e) {} - final URL configUrl = RunImportExportTest.class.getResource( "/plugins.config" ); - final File baseDir = new File( configUrl.getFile() ).getParentFile(); - containerDir = new File( baseDir, "macrotest.n5" ); - System.out.println( containerDir.getAbsolutePath() ); + n5rootF = Paths.get("src", "test", "resources", "test.n5").toUri(); + dataset = "dataset"; imp = NewImage.createImage("test", 8, 7, 9, 16, NewImage.FILL_NOISE); - final String format = N5ScalePyramidExporter.AUTO_FORMAT; + final String format = N5ScalePyramidExporter.N5_FORMAT; final N5ScalePyramidExporter writer = new N5ScalePyramidExporter(); - writer.setOptions( imp, containerDir.getAbsolutePath(), "dataset", format, "16,16,16", false, + writer.setOptions( imp, containerDir.toString(), dataset, format, "16,16,16", false, N5ScalePyramidExporter.NONE, N5ScalePyramidExporter.DOWN_SAMPLE, N5ScalePyramidExporter.RAW_COMPRESSION); writer.run(); // run() closes the n5 writer } @@ -60,25 +61,44 @@ public void before() { @After public void after() { - final N5Writer n5 = new N5Factory().openWriter(containerDir.getAbsolutePath()); + N5Writer n5 = new N5Factory().openWriter(containerDir.toString()); n5.remove(); } + private static String tempN5PathName() { + + try { + final File tmpFile = Files.createTempDirectory("n5-ij-macro-test-").toFile(); + tmpFile.deleteOnExit(); + return tmpFile.getCanonicalPath(); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + protected String tempN5Location() throws URISyntaxException { + + final String basePath = new File(tempN5PathName()).toURI().normalize().getPath(); + return new URI("file", null, basePath, null).toString(); + } + @Test - public void testMacroContentPath() { + public void testMacroContentPath() throws IOException { testMacroContentHelper("url=%s/%s"); } @Test - public void testMacroContentUri() { - testMacroContentHelper("url=%s?%s"); + public void testMacroContentUri() throws IOException { + System.out.println("testMacroContent URI style skip windows"); + final String os = System.getProperty("os.name").toLowerCase(); + if( !os.startsWith("windows")) + testMacroContentHelper("url=%s?%s"); } - public void testMacroContentHelper( String urlFormat ) { + public void testMacroContentHelper( String urlFormat ) throws IOException { - // URL final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer", - String.format( urlFormat + " hide", containerDir.getAbsolutePath(), "dataset" )); + String.format( urlFormat + " hide", containerDir.toString(), dataset )); final List res = plugin.getResult(); final ImagePlus imgImported = res.get(0); @@ -86,7 +106,7 @@ public void testMacroContentHelper( String urlFormat ) { final N5Importer pluginCrop = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer", String.format( urlFormat + " hide min=0,1,2 max=5,5,5", - containerDir.getAbsolutePath(), "dataset" )); + containerDir.toString(), "dataset" )); final List resCrop = pluginCrop.getResult(); final ImagePlus imgImportedCrop = resCrop.get(0); @@ -97,54 +117,55 @@ public void testMacroContentHelper( String urlFormat ) { final ImagePlus impCrop = ImageJFunctions.wrap(imgCrop, "imgCrop"); impCrop.setDimensions(1, 4, 1); - assertEquals( " cont crop w", impCrop.getWidth(), imgImportedCrop.getWidth()); - assertEquals( " cont crop h", impCrop.getHeight(), imgImportedCrop.getHeight()); - assertEquals( " cont crop d", impCrop.getNSlices(), imgImportedCrop.getNSlices()); - assertTrue( "equal content crop", TestExportImports.equal(impCrop, imgImportedCrop)); + assertEquals("cont crop w", impCrop.getWidth(), imgImportedCrop.getWidth()); + assertEquals("cont crop h", impCrop.getHeight(), imgImportedCrop.getHeight()); + assertEquals("cont crop d", impCrop.getNSlices(), imgImportedCrop.getNSlices()); + assertTrue("equal content crop", TestExportImports.equal(impCrop, imgImportedCrop)); } @Test public void testMacro() { final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer", - String.format("url=%s/%s hide", n5rootF.getAbsolutePath(), "cosem" )); + String.format("url=%s" + File.separator + "%s hide", n5rootF.toString(), "cosem" )); final List res = plugin.getResult(); - assertEquals(" crop num", 1, res.size()); + assertEquals("crop num", 1, res.size()); final ImagePlus img = res.get(0); - assertEquals(" crop w", 256, img.getWidth()); - assertEquals(" crop h", 256, img.getHeight()); - assertEquals(" crop d", 129, img.getNSlices() ); + assertEquals("crop w", 256, img.getWidth()); + assertEquals("crop h", 256, img.getHeight()); + assertEquals("crop d", 129, img.getNSlices()); } @Test public void testMacroVirtual() { + final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer", - String.format("url=%s/%s hide virtual", n5rootF.getAbsolutePath(), "cosem" )); + String.format("url=%s" + File.separator + "%s hide virtual", n5rootF.toString(), "cosem" )); final List res = plugin.getResult(); - assertEquals(" crop num", 1, res.size()); + assertEquals("crop num", 1, res.size()); final ImagePlus img = res.get(0); - assertTrue( " is virtual", (img.getStack() instanceof ImageJVirtualStack) ); + assertTrue( "is virtual", (img.getStack() instanceof ImageJVirtualStack) ); } @Test public void testMacroCrop() { + final String minString = "100,100,50"; final String maxString = "250,250,120"; - final N5Importer plugin = (N5Importer)IJ.runPlugIn("org.janelia.saalfeldlab.n5.ij.N5Importer", - String.format("url=%s/%s hide min=%s max=%s", - n5rootF.getAbsolutePath(), "cosem", minString, maxString )); + String.format("url=%s" + File.separator + "%s hide min=%s max=%s", + n5rootF.toString(), "cosem", minString, maxString )); final List res = plugin.getResult(); assertEquals(" crop num", 1, res.size()); final ImagePlus img = res.get(0); - assertEquals(" crop w", 151, img.getWidth()); - assertEquals(" crop h", 151, img.getHeight()); - assertEquals(" crop d", 71, img.getNSlices() ); + assertEquals("crop w", 151, img.getWidth()); + assertEquals("crop h", 151, img.getHeight()); + assertEquals("crop d", 71, img.getNSlices() ); } } diff --git a/src/test/java/org/janelia/saalfeldlab/n5/metadata/ome/ngff/v04/WriteAxesTests.java b/src/test/java/org/janelia/saalfeldlab/n5/metadata/ome/ngff/v04/WriteAxesTests.java index f2630482..9574055f 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/metadata/ome/ngff/v04/WriteAxesTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/metadata/ome/ngff/v04/WriteAxesTests.java @@ -1,10 +1,13 @@ package org.janelia.saalfeldlab.n5.metadata.ome.ngff.v04; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; @@ -14,6 +17,7 @@ import java.util.concurrent.Executors; import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.TestExportImports; import org.janelia.saalfeldlab.n5.ij.N5Importer; import org.janelia.saalfeldlab.n5.ij.N5ScalePyramidExporter; @@ -27,6 +31,7 @@ import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer; import org.janelia.saalfeldlab.n5.universe.N5Factory; import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.N5Factory.StorageFormat; import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.N5DatasetMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.N5GenericSingleScaleMetadataParser; @@ -38,11 +43,15 @@ import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.NgffSingleScaleAxesMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadataParser; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import com.google.gson.JsonElement; + import ij.ImagePlus; import ij.gui.NewImage; +import net.imglib2.util.Pair; public class WriteAxesTests { @@ -80,27 +89,36 @@ private static HashMap, ImageplusMetadata> defaultImagePlusMetadataW public void before() { /* To explicitly test headless */ -// System.setProperty("java.awt.headless", "true"); + System.setProperty("java.awt.headless", "true"); + impWriters = defaultImagePlusMetadataWriters(); } + private void remove(final String rootLocation) { + + final N5Writer zarr = new N5Factory().openWriter(rootLocation); + zarr.remove(); + } + @Test public void testXYZ() throws IOException, InterruptedException, ExecutionException { final int nc = 1; final int nz = 6; final int nt = 1; + final String dataset = ""; final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("xyz.zarr", imp ); + final String rootLocation = createDataset("xyz.zarr", dataset, imp ); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertTrue(Arrays.stream(ngffMeta.multiscales[0].axes).allMatch(x -> x.getUnit().equals(UNIT))); assertEquals(3, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); - final ImagePlus impRead = readImage( rootLocation ); - assertTrue( TestExportImports.equal(imp, impRead)); + final ImagePlus impRead = readImage(rootLocation); + assertTrue(TestExportImports.equal(imp, impRead)); + remove(rootLocation); } @Test @@ -109,16 +127,18 @@ public void testXYC() throws IOException, InterruptedException, ExecutionExcepti final int nc = 6; final int nz = 1; final int nt = 1; - final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("xyc.zarr", imp ); + final String dataset = ""; + final ImagePlus imp = createImage(nc, nz, nt); + final String rootLocation = createDataset("xyc.zarr", dataset, imp); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertEquals(2, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); - final ImagePlus impRead = readImage( rootLocation ); - assertTrue( TestExportImports.equal(imp, impRead)); + final ImagePlus impRead = readImage(rootLocation); + assertTrue(TestExportImports.equal(imp, impRead)); + remove(rootLocation); } @Test @@ -127,16 +147,18 @@ public void testXYT() throws IOException, InterruptedException, ExecutionExcepti final int nc = 1; final int nz = 1; final int nt = 6; - final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("xyt.zarr", imp ); + final String dataset = ""; + final ImagePlus imp = createImage(nc, nz, nt); + final String rootLocation = createDataset("xyt.zarr", dataset, imp); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertEquals(2, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); - final ImagePlus impRead = readImage( rootLocation ); - assertTrue( TestExportImports.equal(imp, impRead)); + final ImagePlus impRead = readImage(rootLocation); + assertTrue(TestExportImports.equal(imp, impRead)); + remove(rootLocation); } @Test @@ -145,16 +167,18 @@ public void testXYCZ() throws IOException, InterruptedException, ExecutionExcept final int nc = 3; final int nz = 2; final int nt = 1; - final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("xycz.zarr", imp ); + final String dataset = ""; + final ImagePlus imp = createImage(nc, nz, nt); + final String rootLocation = createDataset("xycz.zarr", dataset, imp); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertEquals(3, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); - final ImagePlus impRead = readImage( rootLocation ); - assertTrue( TestExportImports.equal(imp, impRead)); + final ImagePlus impRead = readImage(rootLocation); + assertTrue(TestExportImports.equal(imp, impRead)); + remove(rootLocation); } @Test @@ -163,16 +187,18 @@ public void testCZYX() throws IOException, InterruptedException, ExecutionExcept final int nc = 3; final int nz = 2; final int nt = 1; - final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("czyx.zarr", imp); + final String dataset = ""; + final ImagePlus imp = createImage(nc, nz, nt); + final String rootLocation = createDataset("czyx.zarr", dataset, imp); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertEquals(3, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(0, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); - final ImagePlus impRead = readImage( rootLocation ); - assertTrue( TestExportImports.equal(imp, impRead)); + final ImagePlus impRead = readImage(rootLocation); + assertTrue(TestExportImports.equal(imp, impRead)); + remove(rootLocation); // TODO other checks? } @@ -182,45 +208,59 @@ public void testXYCZT() throws IOException, InterruptedException, ExecutionExcep final int nc = 4; final int nz = 3; final int nt = 2; - final ImagePlus imp = createImage( nc, nz, nt ); - final String rootLocation = createDataset("xyczt.zarr", imp); + final String dataset = ""; + final ImagePlus imp = createImage(nc, nz, nt); + final String rootLocation = createDataset("xyczt.zarr", dataset, imp); - final OmeNgffMetadata ngffMeta = readMetadata(rootLocation); + final OmeNgffMetadata ngffMeta = readMetadata(rootLocation, dataset); assertEquals(3, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.SPACE)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.CHANNEL)).count()); assertEquals(1, Arrays.stream(ngffMeta.multiscales[0].axes).filter(x -> x.getType().equals(Axis.TIME)).count()); + remove(rootLocation); } - private ImagePlus createImage( final int nc, final int nz, final int nt ) { + private ImagePlus createImage(final int nc, final int nz, final int nt) { + final ImagePlus imp = NewImage.createImage("test", nx, ny, nc * nz * nt, 8, NewImage.FILL_NOISE); imp.setDimensions(nc, nz, nt); imp.getCalibration().setUnit(UNIT); return imp; } - private String createDataset(final String containerName, final ImagePlus imp ) + private String createDataset(final String containerName, final String dataset, final ImagePlus imp) throws IOException, InterruptedException, ExecutionException { final String rootLocation = tempPathName() + File.separator + containerName; - final String dataset = "/"; final String blockSizeArg = "32,32,32"; final String compression = N5ScalePyramidExporter.GZIP_COMPRESSION; final N5ScalePyramidExporter writer = new N5ScalePyramidExporter(); - writer.setOptions( imp, rootLocation, dataset, N5ScalePyramidExporter.AUTO_FORMAT, blockSizeArg, false, + writer.setOptions(imp, rootLocation, dataset, N5ScalePyramidExporter.ZARR_FORMAT, blockSizeArg, false, N5ScalePyramidExporter.DOWN_SAMPLE, N5Importer.MetadataOmeZarrKey, compression); writer.run(); // run() closes the n5 writer return rootLocation; } - private OmeNgffMetadata readMetadata(final String rootLocation ) { + private OmeNgffMetadata readMetadata(final String rootLocation, final String dataset) { final N5Reader zarr = new N5Factory().openReader(rootLocation); - final N5TreeNode node = N5DatasetDiscoverer.discover(zarr, Collections.singletonList(new N5GenericSingleScaleMetadataParser()), + final String prefix = String.format("%s - %s", rootLocation, dataset); + + assertNotNull(prefix + " reader null", zarr); + assertTrue(prefix + " root exists", zarr.exists("")); + + final N5TreeNode node = N5DatasetDiscoverer.discover(zarr, + Collections.singletonList(new N5GenericSingleScaleMetadataParser()), Collections.singletonList(new OmeNgffMetadataParser())); + assertNotNull(prefix + " node null", node); + assertNotNull( zarr.getAttribute(dataset, "", JsonElement.class).getAsJsonObject().get("multiscales")); + assertTrue(prefix + " is not group", zarr.exists(dataset)); + final N5Metadata meta = node.getMetadata(); + assertNotNull(prefix + " metadata null", meta); + assertTrue(prefix + " metadata not OmeNgff", (meta instanceof OmeNgffMetadata)); if( meta instanceof OmeNgffMetadata ) { return (OmeNgffMetadata) meta; } diff --git a/src/test/java/org/janelia/saalfeldlab/n5/ui/TestUriValidation.java b/src/test/java/org/janelia/saalfeldlab/n5/ui/TestUriValidation.java new file mode 100644 index 00000000..79c92b94 --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/ui/TestUriValidation.java @@ -0,0 +1,89 @@ +package org.janelia.saalfeldlab.n5.ui; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.ParseException; + +import org.janelia.saalfeldlab.n5.ui.DatasetSelectorDialog.UriValidator; +import org.junit.Test; + +public class TestUriValidation { + + @Test + public void testUriValidator() throws ParseException { + + final UriValidator urival = new UriValidator(); + + final String os = System.getProperty("os.name").toLowerCase(); + + final String[] names = new String[] {"b", "c"}; + final Path p = Paths.get("a", names); + + if (os.contains("windows")) { + // windows only + + assertThrows(ParseException.class, () -> { + urival.stringToValue(p.toString() + "?d/e"); + }); + assertThrows(ParseException.class, () -> { + urival.stringToValue(p.toString() + "?d/e#/f/g"); + }); + } + else { + // not windows + + // test some weird strings that can technically be interpreted as paths or uris + assertNotNull(urival.stringToValue(".")); + assertNotNull(urival.stringToValue("/..")); + assertNotNull(urival.stringToValue("\\\\")); + assertNotNull(urival.stringToValue("::")); + assertNotNull(urival.stringToValue("/a/\\//b").toString()); + assertNotNull(urival.stringToValue("://////").toString()); + assertNotNull(urival.stringToValue("..").toString()); + + assertNotNull(urival.stringToValue("/a/b/c")); + assertNotNull(urival.stringToValue("/a/b/c?d/e")); + assertNotNull(urival.stringToValue("/a/b/c?d/e#f/g")); + } + + // both windows and not + assertNotNull(urival.stringToValue(p.toString())); + assertNotNull(urival.stringToValue(p.toUri().toString())); + assertNotNull(urival.stringToValue(p.toUri().toString() + "?d/e")); + assertNotNull(urival.stringToValue(p.toUri().toString() + "?d/e#f/g")); + + assertNotNull(urival.stringToValue("file:///a/b/c")); + assertNotNull(urival.stringToValue("file:///a/b/c?d/e")); + assertNotNull(urival.stringToValue("file:///a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("s3:///a/b/c")); + assertNotNull(urival.stringToValue("s3:///a/b/c?d/e")); + assertNotNull(urival.stringToValue("s3:///a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("https://s3.us-east-1.amazonaws.com/a/b/c")); + assertNotNull(urival.stringToValue("https://s3.us-east-1.amazonaws.com/a/b/c?d/e")); + assertNotNull(urival.stringToValue("https://s3.us-east-1.amazonaws.com/a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("gs://storage.googleapis.com/a/b")); + assertNotNull(urival.stringToValue("gs://storage.googleapis.com/a/b/c?d/e")); + assertNotNull(urival.stringToValue("gs://storage.googleapis.com/a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("https://storage.googleapis.com/a/b")); + assertNotNull(urival.stringToValue("https://storage.googleapis.com/a/b/c?d/e")); + assertNotNull(urival.stringToValue("https://storage.googleapis.com/a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("zarr:/a/b/c")); + assertNotNull(urival.stringToValue("zarr:/a/b/c?d/e")); + assertNotNull(urival.stringToValue("zarr:/a/b/c?d/e#f/g")); + + assertNotNull(urival.stringToValue("zarr:file:///a/b/c")); + assertNotNull(urival.stringToValue("zarr:file:///a/b/c?d/e")); + assertNotNull(urival.stringToValue("zarr:file:///a/b/c?d/e#f/g")); + + } + +}