diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/StringUtils.java b/openbis-common/source/java/ch/systemsx/cisd/bds/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e6d79a290c6c1d4e968335195da789bcb7a86f9
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/StringUtils.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * Operations on {@link java.lang.String} that are <code>null</code> safe.
+ * 
+ * @author Christian Ribeaud
+ */
+public final class StringUtils
+{
+    public static final String EMPTY_STRING = "";
+
+    private StringUtils()
+    {
+        // Can not be instantiated
+    }
+
+    /**
+     * Whether given <var>value</var> is blank or not.
+     */
+    public final static boolean isBlank(final String value)
+    {
+        return value == null || value.trim().length() == 0;
+    }
+
+    /**
+     * Whether given <var>value</var> is empty or not.
+     */
+    public final static boolean isEmpty(final String value)
+    {
+        return value == null || value.length() == 0;
+    }
+
+    /** Returns an empty if given <var>stringOrNull</var> is <code>null</code>. */
+    public final static String emptyIfNull(final String stringOrNull)
+    {
+        return stringOrNull == null ? EMPTY_STRING : stringOrNull;
+    }
+}
diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Channel.java b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Channel.java
new file mode 100644
index 0000000000000000000000000000000000000000..028e546ce3dd06c4de8df16b773f766ec5628305
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Channel.java
@@ -0,0 +1,97 @@
+/*
+ * 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.hcs;
+
+
+/**
+ * A channel is composed of only one child: <code>wavelength</code>.
+ * <p>
+ * Each channel has its <code>counter</code> which uniquely identifies it. It implements
+ * {@link Comparable} based on the <code>wavelength</code> value.
+ * </p>
+ * 
+ * @author Christian Ribeaud
+ */
+public final class Channel implements Comparable<Channel>
+{
+    static final String CHANNEL = "channel";
+
+    static final String WAVELENGTH = "wavelength";
+
+    private final int counter;
+
+    private final int wavelength;
+
+    public Channel(final int counter, final int wavelength)
+    {
+        assert counter > 0 : "Given counter must be > 0.";
+        this.counter = counter;
+        this.wavelength = wavelength;
+    }
+
+    public final int getCounter()
+    {
+        return counter;
+    }
+
+    public final int getWavelength()
+    {
+        return wavelength;
+    }
+
+    //
+    // Object
+    //
+
+    @Override
+    public final boolean equals(final Object obj)
+    {
+        if (obj == this)
+        {
+            return true;
+        }
+        if (obj instanceof Channel == false)
+        {
+            return false;
+        }
+        final Channel channel = (Channel) obj;
+        return channel.counter == counter;
+    }
+
+    @Override
+    public final int hashCode()
+    {
+        return 17 * 37 + counter;
+    }
+
+    @Override
+    public final String toString()
+    {
+        return CHANNEL + counter + "[" + wavelength + "=" + getWavelength() + "]";
+    }
+
+    //
+    // Comparable
+    //
+
+    @Override
+    public final int compareTo(final Channel o)
+    {
+        assert o != null : "Unspecified Channel.";
+        return counter - o.counter;
+    }
+}
diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Geometry.java b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Geometry.java
new file mode 100644
index 0000000000000000000000000000000000000000..31855a613aef87b614d18f8b8827b7aa0fe3f82b
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Geometry.java
@@ -0,0 +1,150 @@
+/*
+ * 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.hcs;
+
+
+/**
+ * A <code>Geometry</code> is composed of 2 dimensions:
+ * <ul>
+ * <li>rows</li>
+ * <li>columns</li>
+ * </ul>
+ * <p>
+ * This class is not <code>abstract</code> but {@link #getGeometryDirectoryName()} must be
+ * overridden by subclasses in order to work properly.
+ * </p>
+ * 
+ * @author Christian Ribeaud
+ */
+public class Geometry
+{
+    /**
+     * The <code>rows</code>-<code>columns</code> separator in the string representation of
+     * this object.
+     */
+    private static final String X = "x";
+
+    static final String NOT_POSITIVE = "Given geometry component '%s' must be > 0 (%d <= 0).";
+
+    final static String ROWS = "rows";
+
+    final static String COLUMNS = "columns";
+
+    private final int rows;
+
+    private final int columns;
+
+    public Geometry(final int rows, final int columns)
+    {
+        assert columns > 0 : String.format(NOT_POSITIVE, "columns", columns);
+        this.columns = columns;
+        assert rows > 0 : String.format(NOT_POSITIVE, "rows", rows);
+        this.rows = rows;
+    }
+
+    /** Return the number of columns this <code>Geometry</code> is composed of. */
+    public final int getColumns()
+    {
+        return columns;
+    }
+
+    /** Return the number of rows this <code>Geometry</code> is composed of. */
+    public final int getRows()
+    {
+        return rows;
+    }
+
+    /**
+     * Loads a <code>Geometry</code> from given <var>toString</var>.
+     * 
+     * @param toString the output you get when calling {@link #toString()}.
+     * @return <code>null</code> if operation fails.
+     */
+    public static Geometry createFromString(final String toString)
+    {
+        assert toString != null : "Given string can not be null.";
+        final int index = toString.indexOf(X);
+        if (index > -1)
+        {
+            try
+            {
+                int rows = Integer.parseInt(toString.substring(0, index));
+                int columns = Integer.parseInt(toString.substring(index + X.length()));
+                return new Geometry(rows, columns);
+            } catch (NumberFormatException ex)
+            {
+                // Nothing to do here.
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Whether this <code>Geometry</code> contains given <var>location</var>, meaning that it is
+     * a valid <code>Location</code>.
+     */
+    public final boolean contains(final Location location)
+    {
+        assert location != null : "Given location can not be null.";
+        return location.getX() <= getColumns() && location.getY() <= getRows();
+    }
+
+    /**
+     * Returns the directory name where this <code>Geometry</code> is saved.
+     * <p>
+     * Currently this method is not supported and must be implemented.
+     * </p>
+     */
+    protected String getGeometryDirectoryName()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    //
+    // Object
+    //
+
+    @Override
+    public final boolean equals(Object obj)
+    {
+        if (obj == this)
+        {
+            return true;
+        }
+        if (obj instanceof Geometry == false)
+        {
+            return false;
+        }
+        final Geometry geometry = (Geometry) obj;
+        return geometry.rows == rows && geometry.columns == columns;
+    }
+
+    @Override
+    public final int hashCode()
+    {
+        int hashCode = 17;
+        hashCode = hashCode * 37 + getRows();
+        hashCode = hashCode * 37 + getColumns();
+        return hashCode;
+    }
+
+    @Override
+    public final String toString()
+    {
+        return getRows() + X + getColumns();
+    }
+}
\ No newline at end of file
diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Location.java b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Location.java
new file mode 100644
index 0000000000000000000000000000000000000000..d55369d7de3d8ff1e36d1743d89ffbd9d4df1916
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/Location.java
@@ -0,0 +1,205 @@
+/*
+ * 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.hcs;
+
+import java.io.Serializable;
+
+import ch.systemsx.cisd.common.geometry.ConversionUtils;
+import ch.systemsx.cisd.common.geometry.Point;
+
+/**
+ * A location in (x, y) coordinate space, specified in integer precision. X and Y start from 1. *
+ * 
+ * @author Christian Ribeaud
+ */
+public final class Location implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    static final String NOT_POSITIVE = "Given coordinate '%s' must be > 0 (%d <= 0).";
+
+    /**
+     * The <i>x</i> (or <i>column</i>) coordinate.
+     */
+    private final int x;
+
+    /**
+     * The <i>y</i> (or <i>row</i>) coordinate.
+     */
+    private final int y;
+
+    public Location(final int x, final int y)
+    {
+        assert x > 0 : String.format(NOT_POSITIVE, "x", x);
+        this.x = x;
+        assert y > 0 : String.format(NOT_POSITIVE, "y", y);
+        this.y = y;
+    }
+
+    public static final Location createLocationFromRowAndColumn(final int row, final int column)
+    {
+        return new Location(column, row);
+    }
+
+    /**
+     * For given <var>position</var> in given <code>geometry</code> returns corresponding
+     * <code>Location</code>. Position should be greater than 0.<br>
+     * Assumes that element index grows as column/row numbers grow, so for a 3x2 geometry elements
+     * would be numbered like:<br>
+     * 1 2 3<br>
+     * 4 5 6<br>
+     * 
+     * @return <code>null</code> if position is out of range.
+     */
+    public static final Location tryCreateLocationFromRowwisePosition(final int position,
+            final Geometry geometry)
+    {
+        return tryCreateLocationFromPosition(position, geometry, false);
+    }
+
+    /**
+     * For given <var>position</var> in given <code>geometry</code> returns corresponding
+     * <code>Location</code>. Position should be greater than 0.<br>
+     * Assumes that element index grows as row/column numbers grow, so for a 3x2 geometry elements
+     * would be numbered like:<br>
+     * 1 3 5<br>
+     * 2 4 6<br>
+     * 
+     * @return <code>null</code> if position is out of range.
+     */
+    public static final Location tryCreateLocationFromColumnwisePosition(final int position,
+            final Geometry geometry)
+    {
+        return tryCreateLocationFromPosition(position, geometry, true);
+    }
+
+    private static final Location tryCreateLocationFromPosition(final int position,
+            final Geometry geometry, boolean isColumnwise)
+    {
+        assert geometry != null : "Given geometry can not be null.";
+        int rows = geometry.getRows();
+        int columns = geometry.getColumns();
+        int divisor = (isColumnwise ? rows : columns);
+        final int max = columns * rows;
+        // Given position is within the range.
+        if (position > 0 && position <= max)
+        {
+            final int modulo = position % divisor;
+            final int x = modulo == 0 ? divisor : modulo;
+            final int y = (int) Math.ceil(position / (float) divisor);
+            if (isColumnwise)
+            {
+                return new Location(y, x);
+            } else
+            {
+                return new Location(x, y);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * For given matrix <var>coordinate</var> (<strong>which is given in transposed form</strong>)
+     * returns corresponding <code>Location</code>.
+     * 
+     * @return <code>null</code> if given <var>coordinate</var> is not a matrix coordinate.
+     */
+    public static final Location tryCreateLocationFromTransposedMatrixCoordinate(
+            final String coordinate)
+    {
+        try
+        {
+            Point point = ConversionUtils.parseSpreadsheetLocation(coordinate);
+            return new Location(point.getY() + 1, point.getX() + 1);
+        } catch (IllegalArgumentException ex)
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Do the matrix coordinate conversion for coordinates that are specified this way, but in two
+     * columns. <strong>Note that first the <var>columnCoord</var> is given and then the
+     * <var>rowCoord</var>.
+     * 
+     * @return <code>null</code> if given <var>coordinate</var> is not a matrix coordinate.
+     */
+    public static final Location tryCreateLocationFromTransposedSplitMatrixCoordinate(
+            final String columnCoord, final String rowCoord)
+    {
+        return tryCreateLocationFromTransposedMatrixCoordinate(columnCoord + rowCoord);
+    }
+
+    /**
+     * For given location returns corresponding matrix coordinate.
+     */
+    public final String createMatrixCoordinateFromLocation()
+    {
+        Point point = new Point(getY() - 1, getX() - 1);
+        return ConversionUtils.convertToSpreadsheetLocation(point);
+    }
+
+    //
+    // Object
+    //
+
+    @Override
+    public final boolean equals(Object obj)
+    {
+        if (obj == this)
+        {
+            return true;
+        }
+        if (obj instanceof Location == false)
+        {
+            return false;
+        }
+        final Location location = (Location) obj;
+        return location.x == x && location.y == y;
+    }
+
+    @Override
+    public final int hashCode()
+    {
+        int hashCode = 17;
+        hashCode = hashCode * 37 + x;
+        hashCode = hashCode * 37 + y;
+        return hashCode;
+    }
+
+    @Override
+    public final String toString()
+    {
+        return "[x=" + x + ",y=" + y + "]";
+    }
+
+    /**
+     * Returns the <i>x</i> (or <i>column</i>) coordinate.
+     */
+    public int getX()
+    {
+        return x;
+    }
+
+    /**
+     * Returns the <i>y</i> (or <i>row</i>) coordinate.
+     */
+    public int getY()
+    {
+        return y;
+    }
+}
\ No newline at end of file
diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/PlateGeometry.java b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/PlateGeometry.java
new file mode 100644
index 0000000000000000000000000000000000000000..6396ec2342cb3f75eb45f07974e4334cf8cb7a5d
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/PlateGeometry.java
@@ -0,0 +1,62 @@
+/*
+ * 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.hcs;
+
+
+/**
+ * An <code>AbstractGeometry</code> implementation suitable for <i>Plate</i>.
+ * 
+ * @author Christian Ribeaud
+ */
+public final class PlateGeometry extends Geometry
+{
+
+    /**
+     * Directory name which contains the plate geometry.
+     * <p>
+     */
+    public static final String PLATE_GEOMETRY = "plate_geometry";
+
+    public PlateGeometry(final Geometry geometry)
+    {
+        this(geometry.getRows(), geometry.getColumns());
+    }
+
+    public PlateGeometry(final int rows, final int columns)
+    {
+        super(rows, columns);
+    }
+
+    /**
+     * Creates a new <code>WellGeometry</code> from given <var>toString</var>.
+     */
+    public final static Geometry createFromString(final String toString)
+    {
+        return new PlateGeometry(Geometry.createFromString(toString));
+    }
+
+    //
+    // Geometry
+    //
+
+    @Override
+    protected final String getGeometryDirectoryName()
+    {
+        return PLATE_GEOMETRY;
+    }
+
+}
diff --git a/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/WellGeometry.java b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/WellGeometry.java
new file mode 100644
index 0000000000000000000000000000000000000000..55830ba4c77f6f60a47fa3c6e7f52c3c1e4b5ccd
--- /dev/null
+++ b/openbis-common/source/java/ch/systemsx/cisd/bds/hcs/WellGeometry.java
@@ -0,0 +1,62 @@
+/*
+ * 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.hcs;
+
+
+/**
+ * An <code>AbstractGeometry</code> implementation suitable for <i>Well</i>.
+ * 
+ * @author Christian Ribeaud
+ */
+public final class WellGeometry extends Geometry
+{
+
+    /**
+     * Directory name which contains the well geometry.
+     * <p>
+     */
+    public static final String WELL_GEOMETRY = "well_geometry";
+
+    public WellGeometry(final Geometry geometry)
+    {
+        this(geometry.getRows(), geometry.getColumns());
+    }
+
+    public WellGeometry(final int rows, final int columns)
+    {
+        super(rows, columns);
+    }
+
+    /**
+     * Creates a new <code>WellGeometry</code> from given <var>toString</var>.
+     */
+    public final static Geometry createFromString(final String toString)
+    {
+        return new WellGeometry(Geometry.createFromString(toString));
+    }
+
+    //
+    // Geometry
+    //
+
+    @Override
+    protected final String getGeometryDirectoryName()
+    {
+        return WELL_GEOMETRY;
+    }
+
+}