From 45b6e5154c06a9f6b1e08d9a01d8f7217d0c8e83 Mon Sep 17 00:00:00 2001
From: ribeaudc <ribeaudc>
Date: Fri, 30 Nov 2007 07:32:31 +0000
Subject: [PATCH] [LMS-128] change: - 'tryTo*' renamed to 'try*' add: -
 'ILinkMaker' interface and its implementation 'HardLinkMaker'

SVN: 2863
---
 .../systemsx/cisd/bds/DataStructureV1_0.java  |  14 +-
 .../java/ch/systemsx/cisd/bds/Format.java     |   6 +-
 .../java/ch/systemsx/cisd/bds/Utilities.java  |   6 +-
 .../cisd/bds/handler/MappingFileHandler.java  |   5 +-
 .../cisd/bds/hcs/IHCSFormattedData.java       |  16 ++-
 .../cisd/bds/hcs/ImageHCSFormattedData.java   |  83 ++++++++++--
 .../systemsx/cisd/bds/storage/IDirectory.java |  16 ++-
 .../ch/systemsx/cisd/bds/storage/IFile.java   |   3 +
 .../ch/systemsx/cisd/bds/storage/INode.java   |   6 +-
 .../bds/storage/filesystem/Directory.java     |  36 ++++-
 .../cisd/bds/storage/filesystem/File.java     |  10 +-
 .../bds/storage/filesystem/HardLinkMaker.java | 123 ++++++++++++++++++
 .../bds/storage/filesystem/ILinkMaker.java    |  57 ++++++++
 .../cisd/bds/storage/filesystem/Link.java     |   6 +-
 .../storage/filesystem/LinkMakerProvider.java |  51 ++++++++
 .../bds/storage/filesystem/NodeFactory.java   |   6 +-
 .../bds/hcs/HCSDataStructureV1_0Test.java     |   8 +-
 .../bds/storage/filesystem/DirectoryTest.java |   6 +-
 .../storage/filesystem/HardLinkMakerTest.java | 102 +++++++++++++++
 19 files changed, 503 insertions(+), 57 deletions(-)
 create mode 100644 bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMaker.java
 create mode 100644 bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/ILinkMaker.java
 create mode 100644 bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/LinkMakerProvider.java
 create mode 100644 bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMakerTest.java

diff --git a/bds/source/java/ch/systemsx/cisd/bds/DataStructureV1_0.java b/bds/source/java/ch/systemsx/cisd/bds/DataStructureV1_0.java
index 532c330053a..0304ac73034 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/DataStructureV1_0.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/DataStructureV1_0.java
@@ -277,27 +277,27 @@ public class DataStructureV1_0 extends AbstractDataStructure
             throw new DataStructureException("Empty original data directory.");
         }
         IDirectory metaDataDirectory = getMetaDataDirectory();
-        if (metaDataDirectory.tryToGetNode(Format.FORMAT_DIR) == null && format == null)
+        if (metaDataDirectory.tryGetNode(Format.FORMAT_DIR) == null && format == null)
         {
             throw new DataStructureException("Unspecified format.");
         }
-        if (metaDataDirectory.tryToGetNode(ExperimentIdentifier.FOLDER) == null)
+        if (metaDataDirectory.tryGetNode(ExperimentIdentifier.FOLDER) == null)
         {
             throw new DataStructureException("Unspecified experiment identifier.");
         }
-        if (metaDataDirectory.tryToGetNode(ExperimentRegistratorDate.FILE_NAME) == null)
+        if (metaDataDirectory.tryGetNode(ExperimentRegistratorDate.FILE_NAME) == null)
         {
             throw new DataStructureException("Unspecified experiment registration date.");
         }
-        if (metaDataDirectory.tryToGetNode(ExperimentRegistrator.FOLDER) == null)
+        if (metaDataDirectory.tryGetNode(ExperimentRegistrator.FOLDER) == null)
         {
             throw new DataStructureException("Unspecified experiment registrator.");
         }
-        if (metaDataDirectory.tryToGetNode(MeasurementEntity.FOLDER) == null)
+        if (metaDataDirectory.tryGetNode(MeasurementEntity.FOLDER) == null)
         {
             throw new DataStructureException("Unspecified measurement entity.");
         }
-        if (metaDataDirectory.tryToGetNode(ProcessingType.PROCESSING_TYPE) == null)
+        if (metaDataDirectory.tryGetNode(ProcessingType.PROCESSING_TYPE) == null)
         {
             throw new DataStructureException("Unspecified processing type.");
         }
@@ -318,7 +318,7 @@ public class DataStructureV1_0 extends AbstractDataStructure
         super.performClosing();
         final IDirectory metaDataDirectory = getMetaDataDirectory();
         formatParameters.saveTo(getParametersDirectory());
-        if (metaDataDirectory.tryToGetNode(Format.FORMAT_DIR) == null && format != null)
+        if (metaDataDirectory.tryGetNode(Format.FORMAT_DIR) == null && format != null)
         {
             format.saveTo(metaDataDirectory);
         }
diff --git a/bds/source/java/ch/systemsx/cisd/bds/Format.java b/bds/source/java/ch/systemsx/cisd/bds/Format.java
index 8e477ca9984..ec569b375ae 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/Format.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/Format.java
@@ -40,13 +40,13 @@ public class Format implements IStorable
      */
     static Format loadFrom(IDirectory directory)
     {
-        INode dir = directory.tryToGetNode(FORMAT_DIR);
+        INode dir = directory.tryGetNode(FORMAT_DIR);
         if (dir instanceof IDirectory == false)
         {
             throw new DataStructureException("Not a directory: " + dir);
         }
         IDirectory formatDir = (IDirectory) dir;
-        INode file = formatDir.tryToGetNode(FORMAT_CODE_FILE);
+        INode file = formatDir.tryGetNode(FORMAT_CODE_FILE);
         if (file instanceof IFile == false)
         {
             throw new DataStructureException("Not a plain file: " + file);
@@ -55,7 +55,7 @@ public class Format implements IStorable
         String formatCode = codeFile.getStringContent().trim();
         Version formatVersion = Version.loadFrom(formatDir);
         String variant = null;
-        file = formatDir.tryToGetNode(FORMAT_VARIANT_FILE);
+        file = formatDir.tryGetNode(FORMAT_VARIANT_FILE);
         if (file != null)
         {
             if (file instanceof IFile == false)
diff --git a/bds/source/java/ch/systemsx/cisd/bds/Utilities.java b/bds/source/java/ch/systemsx/cisd/bds/Utilities.java
index ba177890da0..012b32ad31d 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/Utilities.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/Utilities.java
@@ -36,7 +36,7 @@ public class Utilities
      */
     public static IDirectory getOrCreateSubDirectory(IDirectory directory, String name)
     {
-        INode node = directory.tryToGetNode(name);
+        INode node = directory.tryGetNode(name);
         if (node == null)
         {
             return directory.makeDirectory(name);
@@ -58,7 +58,7 @@ public class Utilities
      */
     public final static IDirectory getSubDirectory(final IDirectory directory, final String name)
     {
-        INode node = directory.tryToGetNode(name);
+        INode node = directory.tryGetNode(name);
         if (node == null)
         {
             throw new DataStructureException(String.format("No directory named '%s' found in directory '%s'.", name,
@@ -111,7 +111,7 @@ public class Utilities
     {
         assert directory != null : String.format("Given directory can not be null.");
         assert name != null : String.format("Given name can not be null.");
-        final INode node = directory.tryToGetNode(name);
+        final INode node = directory.tryGetNode(name);
         if (node == null)
         {
             throw new DataStructureException("File '" + name + "' missing in '" + directory + "'.");
diff --git a/bds/source/java/ch/systemsx/cisd/bds/handler/MappingFileHandler.java b/bds/source/java/ch/systemsx/cisd/bds/handler/MappingFileHandler.java
index 405643b6445..d0222927db5 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/handler/MappingFileHandler.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/handler/MappingFileHandler.java
@@ -44,9 +44,11 @@ public final class MappingFileHandler implements IDataStructureHandler
     private final IDirectory mappingDirectory;
 
     /** The root of {@link Reference#getPath()}. Usually the path to <code>standard</code> directory. */
+    @SuppressWarnings("unused")
     private final IDirectory pathRoot;
 
     /** The root of {@link Reference#getOriginalPath()}. Usually the path to <code>original</code> directory. */
+    @SuppressWarnings("unused")
     private final IDirectory originalPathRoot;
 
     /**
@@ -119,7 +121,8 @@ public final class MappingFileHandler implements IDataStructureHandler
             standardOriginalMapping.put(path, new Reference(path, referenceDefinition.substring(i2 + 1), type));
         }
     }
-
+    
+    // TODO 2007-11-30, Christian Ribeaud: list all nodes present in the standard directory. 
     private final String createMappingFile()
     {
         final StringWriter writer = new StringWriter();
diff --git a/bds/source/java/ch/systemsx/cisd/bds/hcs/IHCSFormattedData.java b/bds/source/java/ch/systemsx/cisd/bds/hcs/IHCSFormattedData.java
index 8549e0f1f7e..0b408224a6a 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/hcs/IHCSFormattedData.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/hcs/IHCSFormattedData.java
@@ -35,9 +35,17 @@ public interface IHCSFormattedData extends IFormattedData
      * corresponding <code>INode</code> (found in <code>data/standard</code> directory).
      * 
      * @return this could be, for instance, a {@link ILink} pointing to the <code>data/original</code> directory or a
-     *         {@link IFile} that can be extracted somewhere. Never returns <code>null</code>.
-     * @throws DataStructureException if a problem occurs while trying to find out the node in the whole data structure.
+     *         {@link IFile} that can be extracted somewhere. Might return <code>null</code>.
      */
-    public INode getNodeAt(final int channel, final Location plateLocation, final Location wellLocation)
-            throws DataStructureException;
+    public INode tryGetStandardNodeAt(final int channel, final Location plateLocation, final Location wellLocation);
+
+    /**
+     * Adds a new node at given coordinates and returns it.
+     * 
+     * @return the new <code>INode</code> just added. Never returns <code>null</code>.
+     * @param originalFileName name of the <code>original</code> directory file that is going to be added in the
+     *            <code>standard</code> directory.
+     */
+    public INode addStandardNode(final String originalFileName, final int channel, final Location plateLocation,
+            final Location wellLocation) throws DataStructureException;
 }
diff --git a/bds/source/java/ch/systemsx/cisd/bds/hcs/ImageHCSFormattedData.java b/bds/source/java/ch/systemsx/cisd/bds/hcs/ImageHCSFormattedData.java
index 32c8fb762dc..fbdbd1dfe5a 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/hcs/ImageHCSFormattedData.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/hcs/ImageHCSFormattedData.java
@@ -29,6 +29,7 @@ import ch.systemsx.cisd.bds.IFormattedData;
 import ch.systemsx.cisd.bds.Utilities;
 import ch.systemsx.cisd.bds.storage.IDirectory;
 import ch.systemsx.cisd.bds.storage.INode;
+import ch.systemsx.cisd.bds.storage.StorageException;
 
 /**
  * {@link IFormattedData} implementation for <i>HCS (High-Content Screening) with Images</i>. It is associated with
@@ -58,6 +59,11 @@ public final class ImageHCSFormattedData extends AbstractFormattedData implement
         super(context);
     }
 
+    private final boolean containsOriginalData()
+    {
+        return (Boolean) getFormatParameters().getValue(ImageHCSFormat1_0.CONTAINS_ORIGINAL_DATA);
+    }
+
     private final Geometry getWellGeometry()
     {
         return (Geometry) getFormatParameters().getValue(WellGeometry.WELL_GEOMETRY);
@@ -108,27 +114,86 @@ public final class ImageHCSFormattedData extends AbstractFormattedData implement
         return ROW + wellLocation.y + "_" + COLUMN + wellLocation.x + ".tiff";
     }
 
+    private final IDirectory getStandardDataDirectory()
+    {
+        return Utilities.getSubDirectory(dataDirectory, DataStructureV1_0.DIR_STANDARD);
+    }
+
+    private final IDirectory getOriginalDataDirectory()
+    {
+        return Utilities.getSubDirectory(dataDirectory, DataStructureV1_0.DIR_ORIGINAL);
+    }
+
+    private final static String getPlateColumnDir(final Location plateLocation)
+    {
+        return COLUMN + plateLocation.x;
+    }
+
+    private final static String getPlateRowDirName(final Location plateLocation)
+    {
+        return ROW + plateLocation.y;
+    }
+
+    private final static String getChannelName(final int channel)
+    {
+        return Channel.CHANNEL + channel;
+    }
+
     //
     // IHCSFormattedData
     //
 
-    public final INode getNodeAt(final int channel, final Location plateLocation, final Location wellLocation)
-            throws DataStructureException
+    public final INode tryGetStandardNodeAt(final int channel, final Location plateLocation, final Location wellLocation)
     {
         checkChannel(channel);
         checkLocation(getPlateGeometry(), plateLocation);
         checkLocation(getWellGeometry(), wellLocation);
-        final IDirectory standardDir = Utilities.getSubDirectory(dataDirectory, DataStructureV1_0.DIR_STANDARD);
-        final IDirectory channelDir = Utilities.getSubDirectory(standardDir, Channel.CHANNEL + channel);
-        final IDirectory plateRowDir = Utilities.getSubDirectory(channelDir, ROW + plateLocation.y);
-        final IDirectory plateColumnDir = Utilities.getSubDirectory(plateRowDir, COLUMN + plateLocation.x);
-        final INode node = plateColumnDir.tryToGetNode(createWellFileName(wellLocation));
-        if (node == null)
+        final IDirectory standardDir = getStandardDataDirectory();
+        final IDirectory channelDir = Utilities.getSubDirectory(standardDir, getChannelName(channel));
+        final IDirectory plateRowDir = Utilities.getSubDirectory(channelDir, getPlateRowDirName(plateLocation));
+        final IDirectory plateColumnDir = Utilities.getSubDirectory(plateRowDir, getPlateColumnDir(plateLocation));
+        return plateColumnDir.tryGetNode(createWellFileName(wellLocation));
+    }
+
+    public final INode addStandardNode(final String originalFileName, int channel, final Location plateLocation,
+            final Location wellLocation)
+    {
+        // This will check all parameters but originalFileName.
+        INode node = tryGetStandardNodeAt(channel, plateLocation, wellLocation);
+        if (node != null)
         {
             throw new DataStructureException(String.format(
-                    "No node could be found at channel %d, plate location '%s' and well location '%s'.", channel,
+                    "A node already exists at channel %d, plate location '%s' and well location '%s'.", channel,
                     plateLocation, wellLocation));
         }
+        assert originalFileName != null : "Given original file name can not be null.";
+        final IDirectory standardDir = getStandardDataDirectory();
+        final IDirectory channelDir = Utilities.getOrCreateSubDirectory(standardDir, getChannelName(channel));
+        final IDirectory plateRowDir = Utilities.getOrCreateSubDirectory(channelDir, getPlateRowDirName(plateLocation));
+        final IDirectory plateColumnDir =
+                Utilities.getOrCreateSubDirectory(plateRowDir, getPlateColumnDir(plateLocation));
+        final String wellFileName = createWellFileName(wellLocation);
+        final INode originalNode = getOriginalDataDirectory().tryGetNode(originalFileName);
+        if (originalNode == null)
+        {
+            throw new DataStructureException(String.format("No original node with name '%s' could be found.",
+                    originalFileName));
+        }
+        if (containsOriginalData())
+        {
+            node = plateColumnDir.tryAddLink(wellFileName, originalNode);
+        } else
+        {
+            node = plateColumnDir.tryAddNode(wellFileName, originalNode);
+        }
+        if (node == null)
+        {
+            throw new StorageException(
+                    String
+                            .format(
+                                    "Original file name '%s' could not be added at channel %d, plate location '%s' and well location '%s'.",
+                                    originalFileName, channel, plateLocation, wellLocation));
+        }
         return node;
     }
 
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/IDirectory.java b/bds/source/java/ch/systemsx/cisd/bds/storage/IDirectory.java
index 7f0ef500185..90195b00029 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/IDirectory.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/IDirectory.java
@@ -30,7 +30,7 @@ public interface IDirectory extends INode, Iterable<INode>
      * 
      * @return <code>null</code> if there is no child node named <code>name</code>.
      */
-    public INode tryToGetNode(final String name);
+    public INode tryGetNode(final String name);
 
     /**
      * Makes a new subdirectory in this directory. Does nothing if the subdirectory already exists.
@@ -38,7 +38,7 @@ public interface IDirectory extends INode, Iterable<INode>
      * @param name Name of the new subdirectory.
      * @return the new subdirectory.
      */
-    public IDirectory makeDirectory(String name);
+    public IDirectory makeDirectory(final String name);
 
     /**
      * Adds the specified real file to this directory. The content of <code>file</code> will be copied. If it is a
@@ -62,6 +62,16 @@ public interface IDirectory extends INode, Iterable<INode>
 
     /**
      * Adds the link named <code>name</code> to this directory which refers to the specified node.
+     * 
+     * @return <code>null</code> if the operation did not succeed.
+     */
+    public ILink tryAddLink(final String name, final INode node);
+
+    /**
+     * Adds given <code>node</code> to this <code>IDirectory</code> and returns a new node with given
+     * <code>name</code> (rename operation).
+     * 
+     * @return <code>null</code> if the operation did not succeed.
      */
-    public ILink addLink(final String name, final INode node);
+    public INode tryAddNode(final String name, final INode node);
 }
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/IFile.java b/bds/source/java/ch/systemsx/cisd/bds/storage/IFile.java
index 1fce846462f..05bbdcb129e 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/IFile.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/IFile.java
@@ -43,6 +43,9 @@ public interface IFile extends INode
 
     /**
      * Returns the content of this file node as a list of <code>String</code> objects.
+     * <p>
+     * This is useful when you know that the file content is composed of several lines.
+     * </p>
      */
     public List<String> getStringContentList();
 
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/INode.java b/bds/source/java/ch/systemsx/cisd/bds/storage/INode.java
index 24de1be2923..6496d08bc0d 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/INode.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/INode.java
@@ -16,8 +16,6 @@
 
 package ch.systemsx.cisd.bds.storage;
 
-import java.io.File;
-
 /**
  * Abstraction of a node in a hierarchical data structure.
  * 
@@ -46,11 +44,11 @@ public interface INode
      * All descendants are also extracted. This is a copy operation.
      * </p>
      */
-    public void extractTo(final File directory);
+    public void extractTo(final java.io.File directory);
 
     /**
      * Moves this node and all descendants to the specified directory of the file system. This node will be
      * automatically removed from its parent.
      */
-    public void moveTo(final File directory);
+    public void moveTo(final java.io.File directory);
 }
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Directory.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Directory.java
index c51790b7ba9..6729aadf881 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Directory.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Directory.java
@@ -27,7 +27,6 @@ import ch.systemsx.cisd.bds.storage.ILink;
 import ch.systemsx.cisd.bds.storage.INode;
 import ch.systemsx.cisd.bds.storage.StorageException;
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
-import ch.systemsx.cisd.common.exceptions.NotImplementedException;
 import ch.systemsx.cisd.common.utilities.FileUtilities;
 
 /**
@@ -38,7 +37,7 @@ import ch.systemsx.cisd.common.utilities.FileUtilities;
 final class Directory extends AbstractNode implements IDirectory
 {
 
-    Directory(java.io.File directory)
+    Directory(final java.io.File directory)
     {
         super(directory);
         if (directory.isDirectory() == false)
@@ -47,11 +46,17 @@ final class Directory extends AbstractNode implements IDirectory
         }
     }
 
+    private final static java.io.File getNodeFile(final INode node)
+    {
+        assert node instanceof AbstractNode : "Must be an instance of AbstractNode.";
+        return ((AbstractNode) node).nodeFile;
+    }
+
     //
     // IDirectory
     //
 
-    public final INode tryToGetNode(final String name)
+    public final INode tryGetNode(final String name)
     {
         assert name != null : "Given name can not be null.";
         final java.io.File[] files = FileUtilities.listFiles(nodeFile);
@@ -122,9 +127,19 @@ final class Directory extends AbstractNode implements IDirectory
         return NodeFactory.createNode(newFile);
     }
 
-    public final ILink addLink(final String name, final INode node)
+    public final ILink tryAddLink(final String name, final INode node)
     {
-        throw new NotImplementedException();
+        assert node != null : "Node can not be null.";
+        assert name != null : "Name can not be null.";
+        final java.io.File file = getNodeFile(node);
+        final java.io.File fileLink = LinkMakerProvider.getDefaultLinkMaker().tryCreateLink(file, nodeFile, name);
+        if (fileLink != null)
+        {
+            final Link link = NodeFactory.createLinkNode(fileLink);
+            link.setParent(this);
+            return link;
+        }
+        return null;
     }
 
     public final Iterator<INode> iterator()
@@ -170,11 +185,18 @@ final class Directory extends AbstractNode implements IDirectory
         }
     }
 
+    public final INode tryAddNode(final String name, final INode node)
+    {
+        assert node != null : "Node can not be null";
+        assert name != null : "Name can not be null.";
+        final java.io.File file = getNodeFile(node);
+        return addFile(file, true);
+    }
+
     public final void removeNode(final INode node)
     {
         assert node != null : "Node could not be null";
-        AbstractNode abstractNode = (AbstractNode) node;
-        final java.io.File file = abstractNode.nodeFile;
+        final java.io.File file = getNodeFile(node);
         if (file.isDirectory())
         {
             if (FileUtilities.deleteRecursively(file, null) == false)
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/File.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/File.java
index 559e0c5c98b..791d86e8828 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/File.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/File.java
@@ -34,15 +34,19 @@ import ch.systemsx.cisd.common.utilities.FileUtilities;
  * 
  * @author Franz-Josef Elmer
  */
-class File extends AbstractNode implements IFile
+final class File extends AbstractNode implements IFile
 {
-    File(java.io.File file)
+    File(final java.io.File file)
     {
         super(file);
         assert file.isFile() : "Not a file " + file.getAbsolutePath();
     }
 
-    public byte[] getBinaryContent()
+    //
+    // IFile
+    //
+
+    public final byte[] getBinaryContent()
     {
         InputStream inputStream = getInputStream();
         try
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMaker.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMaker.java
new file mode 100644
index 00000000000..a3e52766485
--- /dev/null
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMaker.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.bds.storage.filesystem;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.logging.BufferedAppender;
+import ch.systemsx.cisd.common.utilities.OSUtilities;
+import ch.systemsx.cisd.common.utilities.ProcessExecutionHelper;
+
+/**
+ * An <code>ILinkMaker</code> implementation which is able to make hard links using the operating system.
+ * <p>
+ * Note that the current implementation only works for files.
+ * </p>
+ * 
+ * @author Christian Ribeaud
+ */
+public final class HardLinkMaker implements ILinkMaker
+{
+    static final String ALREADY_EXISTS_FORMAT = "Given file '%s' already exists.";
+
+    private static final String HARD_LINK_EXEC = "ln";
+
+    private static HardLinkMaker instance;
+
+    private final String linkExecPath;
+
+    private final BufferedAppender loggerRecorder;
+
+    private HardLinkMaker(final String linkExecPath)
+    {
+        this.linkExecPath = linkExecPath;
+        loggerRecorder = new BufferedAppender("%m", Level.ERROR);
+    }
+
+    private final List<String> createLnCmdLine(final File srcFile, final File destFile)
+    {
+        List<String> tokens = new ArrayList<String>();
+        tokens.add(linkExecPath);
+        tokens.add(srcFile.getAbsolutePath());
+        // The destination file does not yet exist. Is going to be created and is the link.
+        tokens.add(destFile.getAbsolutePath());
+        return tokens;
+    }
+
+    /**
+     * Returns the unique instance of this class.
+     */
+    static synchronized final HardLinkMaker getInstance() throws EnvironmentFailureException
+    {
+        if (instance == null)
+        {
+            final File lnExec = OSUtilities.findExecutable(HARD_LINK_EXEC);
+            if (lnExec == null)
+            {
+                throw EnvironmentFailureException.fromTemplate(
+                        "No hard link executable '%s' could be found in path '%s'.", HARD_LINK_EXEC, OSUtilities
+                                .getSafeOSPath());
+            }
+            instance = new HardLinkMaker(lnExec.getAbsolutePath());
+        }
+        return instance;
+    }
+
+    //
+    // ILinkMaker
+    //
+
+    public final File tryCreateLink(final java.io.File file, final java.io.File destDir, final String nameOrNull)
+            throws EnvironmentFailureException
+    {
+        assert file != null && file.isFile() : "Given file can not be null and must be a file.";
+        assert destDir != null && destDir.isDirectory() : "Given destination directory can not be null and must be a directory.";
+        final String destName;
+        if (nameOrNull == null)
+        {
+            destName = file.getName();
+        } else
+        {
+            destName = nameOrNull;
+        }
+        final File destFile = new File(destDir, destName);
+        if (destFile.exists())
+        {
+            throw new IllegalArgumentException(String.format(ALREADY_EXISTS_FORMAT, destFile.getAbsolutePath()));
+        }
+        final List<String> cmd = createLnCmdLine(file, destFile);
+        final Logger rootLogger = Logger.getRootLogger();
+        loggerRecorder.resetLogContent();
+        final boolean ok = ProcessExecutionHelper.runAndLog(cmd, rootLogger, rootLogger);
+        if (ok == false)
+        {
+            String message = loggerRecorder.getLogContent();
+            if (message.length() == 0)
+            {
+                return null;
+            }
+            throw new EnvironmentFailureException(message);
+        }
+        return destFile;
+    }
+}
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/ILinkMaker.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/ILinkMaker.java
new file mode 100644
index 00000000000..81d1e80d612
--- /dev/null
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/ILinkMaker.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.bds.storage.filesystem;
+
+import java.io.File;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+
+/**
+ * Kind of strategy how to 'serialize' a <code>ILink</code> implementation.
+ * 
+ * @author Christian Ribeaud
+ */
+public interface ILinkMaker
+{
+
+    /**
+     * An default <code>ILinkMaker</code> implementation which always returns <code>null</code> without trying to do
+     * anything.
+     */
+    public final static ILinkMaker DEFAULT = new ILinkMaker()
+        {
+
+            //
+            // ILinkMaker
+            //
+
+            public final File tryCreateLink(final File file, final File destDir, final String nameOrNull)
+                    throws EnvironmentFailureException
+            {
+                return null;
+            }
+        };
+
+    /**
+     * Tries to create a link to given <var>file</var> in given <var>destDir</var>.
+     * 
+     * @param nameOrNull the link name in the destination directory.
+     * @return might returns <code>null</code> if the application was not able to create the link.
+     */
+    public File tryCreateLink(final File file, final File destDir, final String nameOrNull)
+            throws EnvironmentFailureException;
+}
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Link.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Link.java
index 604de64e9b8..5d11cc79273 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Link.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/Link.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.bds.storage.filesystem;
 import java.io.File;
 
 import ch.systemsx.cisd.bds.storage.IDirectory;
+import ch.systemsx.cisd.bds.storage.IFile;
 import ch.systemsx.cisd.bds.storage.ILink;
 import ch.systemsx.cisd.bds.storage.INode;
 
@@ -29,7 +30,6 @@ import ch.systemsx.cisd.bds.storage.INode;
  */
 final class Link implements ILink
 {
-
     private final String name;
 
     private final INode reference;
@@ -40,7 +40,7 @@ final class Link implements ILink
     {
         assert name != null : "A name must be specified.";
         assert reference != null : "Reference can not be null.";
-        assert reference instanceof ILink : "Link of link not supported.";
+        assert reference instanceof IFile && reference.isValid() : "Given reference must be a valid IFile implementation";
         this.name = name;
         this.reference = reference;
     }
@@ -88,7 +88,7 @@ final class Link implements ILink
         }
         if (parent != null)
         {
-            final INode node = parent.tryToGetNode(name);
+            final INode node = parent.tryGetNode(name);
             return node != null && node.isValid();
         }
         return true;
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/LinkMakerProvider.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/LinkMakerProvider.java
new file mode 100644
index 00000000000..290b3f9d437
--- /dev/null
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/LinkMakerProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.bds.storage.filesystem;
+
+import ch.systemsx.cisd.common.utilities.OSUtilities;
+
+/**
+ * A provider of {@link ILinkMaker} implementations.
+ * 
+ * @author Christian Ribeaud
+ */
+public final class LinkMakerProvider
+{
+
+    private LinkMakerProvider()
+    {
+        // This class can not be instantiated.
+    }
+
+    /**
+     * Returns a default <code>ILinkMaker</code> implementation.
+     * <p>
+     * Never returns <code>null</code> but could return the default implementation.
+     * </p>
+     * 
+     * @see ILinkMaker#DEFAULT
+     */
+    public final static ILinkMaker getDefaultLinkMaker()
+    {
+        if (OSUtilities.isWindows())
+        {
+            return ILinkMaker.DEFAULT;
+        }
+        return HardLinkMaker.getInstance();
+    }
+
+}
diff --git a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/NodeFactory.java b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/NodeFactory.java
index 9e394e94777..b647b9e5ad1 100644
--- a/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/NodeFactory.java
+++ b/bds/source/java/ch/systemsx/cisd/bds/storage/filesystem/NodeFactory.java
@@ -67,12 +67,12 @@ public final class NodeFactory
         }
     }
 
-    private static Link createLinkNode(final java.io.File file) throws EnvironmentFailureException
+    public final static Link createLinkNode(final java.io.File file) throws EnvironmentFailureException
     {
         final String absolutePath = file.getAbsolutePath();
         final String canonicalPath = getCanonicalPath(file);
-        assert absolutePath.equals(canonicalPath) == false : "Given file must be a link";
-        return new Link(file.getName(), createNode(new java.io.File(getCanonicalPath(file))));
+        assert absolutePath.equals(canonicalPath) == false : "Given file must be a link.";
+        return new Link(file.getName(), createNode(new java.io.File(canonicalPath)));
     }
 
     /**
diff --git a/bds/sourceTest/java/ch/systemsx/cisd/bds/hcs/HCSDataStructureV1_0Test.java b/bds/sourceTest/java/ch/systemsx/cisd/bds/hcs/HCSDataStructureV1_0Test.java
index a4b19b867d6..7608680e656 100644
--- a/bds/sourceTest/java/ch/systemsx/cisd/bds/hcs/HCSDataStructureV1_0Test.java
+++ b/bds/sourceTest/java/ch/systemsx/cisd/bds/hcs/HCSDataStructureV1_0Test.java
@@ -126,7 +126,7 @@ public final class HCSDataStructureV1_0Test extends AbstractFileSystemTestCase
         final IHCSFormattedData formattedData = (IHCSFormattedData) dataStructure.getFormattedData();
         try
         {
-            formattedData.getNodeAt(3, new Location(1, 1), new Location(1, 1));
+            formattedData.tryGetStandardNodeAt(3, new Location(1, 1), new Location(1, 1));
             fail("3 > 2");
         } catch (IndexOutOfBoundsException ex)
         {
@@ -134,7 +134,7 @@ public final class HCSDataStructureV1_0Test extends AbstractFileSystemTestCase
         }
         try
         {
-            formattedData.getNodeAt(2, new Location(1, 1), new Location(1, 1));
+            formattedData.tryGetStandardNodeAt(2, new Location(1, 1), new Location(1, 1));
             fail("No directory named 'channel2' found.");
         } catch (DataStructureException ex)
         {
@@ -142,13 +142,13 @@ public final class HCSDataStructureV1_0Test extends AbstractFileSystemTestCase
         }
         try
         {
-            formattedData.getNodeAt(1, new Location(1, 3), new Location(1, 1));
+            formattedData.tryGetStandardNodeAt(1, new Location(1, 3), new Location(1, 1));
             fail("Given geometry '2x3' does not contain location '[x=1,y=3]'.");
         } catch (IllegalArgumentException ex)
         {
             assertTrue(ex.getMessage().indexOf("does not contain location") > -1);
         }
-        final INode node = formattedData.getNodeAt(1, new Location(1, 1), new Location(1, 1));
+        final INode node = formattedData.tryGetStandardNodeAt(1, new Location(1, 1), new Location(1, 1));
         assertEquals("row1_column1.tiff", node.getName());
         dataStructure.close();
     }
diff --git a/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/DirectoryTest.java b/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/DirectoryTest.java
index 597de4c2dfd..d06c00f24d2 100644
--- a/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/DirectoryTest.java
+++ b/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/DirectoryTest.java
@@ -59,16 +59,16 @@ public final class DirectoryTest extends AbstractFileSystemTestCase
         File copiedRealDir = new File(dest, "dir");
         assertTrue(copiedRealDir.isDirectory());
         IDirectory cd = (IDirectory) copiedDir;
-        INode node = cd.tryToGetNode("p1");
+        INode node = cd.tryGetNode("p1");
         assertNotNull(node);
         assertTrue(node instanceof IFile);
         assertEquals("property 1\n", ((IFile) node).getStringContent());
         assertEquals("property 1\n", FileUtilities.loadToString(new File(copiedRealDir, "p1")));
-        node = cd.tryToGetNode("subdir");
+        node = cd.tryGetNode("subdir");
         assertEquals("subdir", node.getName());
         assertNotNull(node);
         assertTrue(node instanceof IDirectory);
-        node = ((IDirectory) node).tryToGetNode("p2");
+        node = ((IDirectory) node).tryGetNode("p2");
         File copiedRealSubDir = new File(copiedRealDir, "subdir");
         assertTrue(copiedRealSubDir.isDirectory());
         assertEquals("p2", node.getName());
diff --git a/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMakerTest.java b/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMakerTest.java
new file mode 100644
index 00000000000..ed77511f4e3
--- /dev/null
+++ b/bds/sourceTest/java/ch/systemsx/cisd/bds/storage/filesystem/HardLinkMakerTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.bds.storage.filesystem;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.utilities.AbstractFileSystemTestCase;
+
+/**
+ * Test cases for corresponding {@link HardLinkMaker} class.
+ * 
+ * @author Christian Ribeaud
+ */
+@Test(groups =
+    { "requires_unix" })
+public final class HardLinkMakerTest extends AbstractFileSystemTestCase
+{
+
+    private static final String FILE_CONTENT = "This is my content";
+
+    private static final String SOURCE_FILE_NAME = "source.txt";
+
+    private File createFile() throws IOException
+    {
+        final File srcFile = new File(new File(workingDirectory, "dir"), SOURCE_FILE_NAME);
+        assertFalse(srcFile.exists());
+        FileUtils.writeStringToFile(srcFile, FILE_CONTENT);
+        assertTrue(srcFile.exists());
+        return srcFile;
+    }
+
+    @Test
+    public final void testCreateLinkParameters() throws IOException
+    {
+        try
+        {
+            HardLinkMaker.getInstance().tryCreateLink(null, workingDirectory, null);
+            fail("Null value not accepted.");
+        } catch (AssertionError ex)
+        {
+            // Nothing to do here.
+        }
+        try
+        {
+            HardLinkMaker.getInstance().tryCreateLink(workingDirectory, workingDirectory, null);
+            fail("First parameter must be an existing file.");
+        } catch (AssertionError ex)
+        {
+            // Nothing to do here.
+        }
+        final File srcFile = createFile();
+        final File destinationDir = new File(workingDirectory, "dir");
+        try
+        {
+            HardLinkMaker.getInstance().tryCreateLink(srcFile, destinationDir, null);
+            fail("File already exits.");
+        } catch (final IllegalArgumentException ex)
+        {
+            assertEquals(String.format(HardLinkMaker.ALREADY_EXISTS_FORMAT, new File(destinationDir, srcFile.getName())
+                    .getAbsolutePath()), ex.getMessage());
+        }
+
+    }
+
+    @Test
+    public final void testCreateLink() throws IOException
+    {
+        final File srcFile = createFile();
+        HardLinkMaker.getInstance().tryCreateLink(srcFile, workingDirectory, null);
+        final File destFile = new File(workingDirectory, SOURCE_FILE_NAME);
+        assertTrue(destFile.exists());
+        assertEquals(FILE_CONTENT, FileUtils.readFileToString(destFile));
+        assertTrue(srcFile.delete());
+        assertFalse(srcFile.exists());
+        // We removed the source file but the destination file (the hard link) still persists.
+        assertTrue(destFile.exists());
+        assertEquals(FILE_CONTENT, FileUtils.readFileToString(destFile));
+    }
+}
\ No newline at end of file
-- 
GitLab