diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png
new file mode 100644
index 0000000000000000000000000000000000000000..64f766e96cdd8e30306eaacf593c2d2aee6be593
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..928a67adf2a62e66427878eb9d5c36816dc218a1
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png
new file mode 100644
index 0000000000000000000000000000000000000000..8eb9f9719b1c18624d51d26d21abeb258051b8e8
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png
new file mode 100644
index 0000000000000000000000000000000000000000..606a7eec9f055c41eb7ef8b53db4d921c0ec5b62
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..759383221ff7738a2f996458b77ff5b4b773df8e
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png
new file mode 100644
index 0000000000000000000000000000000000000000..23e66f27906dc14733b7503b70c3375b533ee66b
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png
new file mode 100644
index 0000000000000000000000000000000000000000..2720997e003ca22d9f7bb25262296beaf24d2270
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..2eabfbfef7400e116804337e26ce5bbf375f7c3c
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef6a273c888eeaf379ffbfa4643054577ee83c9d
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix
new file mode 100755
index 0000000000000000000000000000000000000000..cb61c432b3edb182a53360f10881caee7a143a15
--- /dev/null
+++ b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<obitXML version="1">
+  <Experiment description="This is an ND2 file with one series." name="Test_Experiment" 
+              openBISIdentifier="/TEST/TEST-PROJECT/MICROSCOPY_IMAGE_DROPBOX_TEST" openBISSpaceIdentifier="/TEST">
+    <MicroscopyFile fileSize="2.8 MiB" name="example.nd2" 
+                    openBISExperimentIdentifier="/TEST/TEST-PROJECT/MICROSCOPY_IMAGE_DROPBOX_TEST" 
+                    openBISSpaceIdentifier="/TEST" relativeFileName="pontia/Experiment/example.nd2"/>
+  </Experiment>
+</obitXML>
\ No newline at end of file
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2 b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2
new file mode 100644
index 0000000000000000000000000000000000000000..9d014d17219caaae04d8d7e2b252442f92aa9d5f
Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2 differ
diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois
new file mode 100755
index 0000000000000000000000000000000000000000..b7a04663bf01671a0097f517df0adef2ff971334
--- /dev/null
+++ b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois
@@ -0,0 +1 @@
+pontia/Experiment/Experiment_properties.oix
diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py
new file mode 100644
index 0000000000000000000000000000000000000000..569644f4d566b99f01cd1899d8c0dc6ccc81e2b9
--- /dev/null
+++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py
@@ -0,0 +1,418 @@
+"""
+Created on Feb 6, 2014
+
+@author: Aaron Ponti
+"""
+
+from loci.formats import FormatTools
+from loci.formats import ChannelSeparator
+from loci.formats import ChannelFiller
+from loci.formats import MetadataTools
+
+class BioFormatsProcessor:
+    """The BioFormatsProcessor class scans a file using the bio-formats library and
+    extracts relevant metadata information for registration."""
+
+    # File path
+    _filePath = None
+
+    # LOCI reader object
+    _reader = None
+
+    # OME metadata store object
+    _metadataStore = None
+
+    # Number of series in the file
+    _nSeries = 0
+
+    # Array of metadata disctionaries (one per series)
+    _metadata = []
+
+
+    def __init__(self, filePath):
+        # Microscopy file path
+        self._filePath = filePath
+
+        # Initialize the LOCI reader
+        self._reader = ChannelSeparator(ChannelFiller())
+        self._reader.setMetadataStore(MetadataTools.createOMEXMLMetadata())
+
+        # Set the filename
+        self._reader.setId(self._filePath)
+
+        # Store a reference to the OME metadata store
+        self._metadataStore = self._reader.getMetadataStore()
+
+        # Initialize the metadata structure
+        self._initMetadata()
+
+
+    def extractMetadata(self):
+        """Extract and store needed metadata"""
+
+        # Dataset sizes
+        self._getDatasetSizes()
+
+        # Data types
+        self._getDataTypes()
+
+        # Voxel sizes
+        self._getVoxelSizes()
+
+        # Acquisition dates
+        self._getAcquisitionDates()
+
+        # Mark thumbnails
+        self._markThumbnails()
+
+        # Channel names
+        self._getChannelNames()
+
+        # Channel colors
+        self._getChannelColors()
+
+        # Get wavelengths
+        self._getWavelengths()
+
+        # Get time stamps
+        self._getTimeStamps()
+
+
+    def getMetadata(self):
+        """
+        Return the extracted metadata.
+        """
+
+        return self._metadata
+
+
+    def getNumSeries(self):
+        """
+        Return the number of series in file.
+        """
+
+        return self._nSeries
+
+
+    # # #
+    # # # PRIVATE METHODS
+    # # #
+
+    def _initMetadata(self):
+
+        # Initialize metadata dictionary
+        metadata = {'seriesNumber'      : 0,
+                    'nImages'           : 0,
+                    'sizeX'             : 0,
+                    'sizeY'             : 0,
+                    'sizeZ'             : 0,
+                    'sizeC'             : 0,
+                    'sizeT'             : 0,
+                    'voxelSizeX'        : 0,
+                    'voxelSizeY'        : 0,
+                    'voxelSizeZ'        : 0,
+                    'datatype'          : None,
+                    'isLittleEndian'    : False,
+                    'isSigned'          : False,
+                    'isThumbnail'       : False,
+                    'acquisitionDate'   : '',
+                    'channelNames'      : [],
+                    'channelColors'     : [],
+                    'emWavelengths'     : [],
+                    'exWavelengths'     : [],
+                    'NumericalAperture' : 0,
+                    'timestamps'        : []}
+
+        # Get and store the number of series
+        self._nSeries = self._reader.getSeriesCount()
+
+        # Initialize metadata dictionaries (one per series)
+        n = 0
+        while n < self._nSeries:
+            self._metadata.append(metadata.copy())
+            n += 1
+
+
+    def _getAcquisitionDates(self):
+        """
+        Extract the acquisition dates from all series.
+        """
+
+        # Get and store all acquisition dates
+        for n in range(self._nSeries):
+
+            # Change to current series
+            self._reader.setSeries(n)
+
+            acqDate = self._metadataStore.getImageAcquisitionDate(n)
+            if acqDate is None:
+                acqDate = ''
+            else:
+                acqDate = acqDate.getValue()
+
+            self._metadata[n]['acquisitionDate'] = acqDate
+
+
+    def _getChannelColors(self):
+        """
+        Extracts the colors as RGBA vectors of all channels from all series.
+        """
+
+        # Get and store all channel names
+        for n in range(self._nSeries):
+
+            # Initialize
+            self._metadata[n]['channelColors'] = []
+
+            # Change to series
+            self._reader.setSeries(n)
+
+            # Get the number of channels in this series
+            nChannels = self._reader.getSizeC()
+
+            for ch in range(nChannels):
+
+                # Get the channel color
+                color = self._metadataStore.getChannelColor(n, ch)
+                if color is None:
+                    colorsRGBA = [255, 255, 255, 255]
+                else:
+                    colorsRGBA = [color.getRed(), color.getGreen(),
+                                  color.getBlue(), color.getAlpha()]
+
+                # Store color for current channel in current series
+                self._metadata[n]['channelColors'].append(colorsRGBA)
+
+
+    def _getChannelNames(self):
+        """
+        Extracts the names of all channels from all series.
+        """
+
+        # Get and store all channel names
+        for n in range(self._nSeries):
+
+            # Initialize
+            self._metadata[n]['channelNames'] = []
+
+            # Change to series
+            self._reader.setSeries(n)
+
+            # Get the number of channels in this series
+            nChannels = self._reader.getSizeC()
+
+            for ch in range(nChannels):
+
+                # Get channel name
+                name = self._metadataStore.getChannelName(n, ch)
+                if name is None:
+                    name = "No name"
+
+                # Remove 0-byte at the end of the string if present
+                if name.endswith('\x00'):
+                    name = name[:-1]
+
+                # Store name for current channel in current series
+                self._metadata[n]['channelNames'].append(name)
+
+
+    def _getDatasetSizes(self):
+        """
+        Extract dataset sizes for all series.
+        """
+
+        # Get and store all dataset sizes for all series
+        for n in range(self._nSeries):
+
+            # Change to current series
+            self._reader.setSeries(n)
+
+            # Get sizes
+            self._metadata[n]['nImages'] = self._reader.getImageCount();
+            self._metadata[n]['sizeX'] = self._reader.getSizeX();
+            self._metadata[n]['sizeY'] = self._reader.getSizeY();
+            self._metadata[n]['sizeZ'] = self._reader.getSizeZ();
+            self._metadata[n]['sizeC'] = self._reader.getSizeC();
+            self._metadata[n]['sizeT'] = self._reader.getSizeT();
+
+
+    def _getDataTypes(self):
+        """
+        Get data types for all series.
+        """
+
+        # Get and store all dataset sizes for all series
+        for n in range(self._nSeries):
+
+            # Change to current series
+            self._reader.setSeries(n)
+
+            # Get the pixel type
+            pixelType = self._reader.getPixelType()
+
+            # Bytes per pixel
+            BytesPerPixel = FormatTools.getBytesPerPixel(pixelType)
+            if BytesPerPixel == 1:
+                datatype = 'uint8'
+            elif BytesPerPixel == 2:
+                datatype = 'uint16'
+            elif BytesPerPixel == 4:
+                # This is 32-bit floating point
+                datatype = 'single'
+            else:
+                datatype = "unsupported"
+
+            # Is the data type signed?
+            isSigned = FormatTools.isSigned(pixelType)
+
+            # Endianity
+            isLittleEndian = self._reader.isLittleEndian()
+
+            # Store data type information
+            self._metadata[n]['datatype'] = datatype
+            self._metadata[n]['isLittleEndian'] = isLittleEndian
+            self._metadata[n]['isSigned'] = isSigned
+
+
+    def _getNAs(self):
+        """
+        Get the numerical aperture for all series.
+        """
+
+        # Get and store all numerical apertures
+        for n in range(self._nSeries):
+
+            # Initialize
+            self._metadata[n]['NumericalAperture'] = []
+
+            # Change to series
+            self._reader.setSeries(n)
+
+            # Get the number of instruments and objectives for this series
+            # Expected: 1 and 1.
+            nInstr = self._metadataStore.getInstrumentCount()
+            nObj = self._metadataStore.getObjectiveCount()
+
+            for i in range(nInstr):
+                for o in range(nObj):
+                    NA = self._metadataStore.getObjectiveLensNA(i, o)
+                    self._metadata[n]['NumericalAperture'].append(NA)
+
+
+    def _getTimeStamps(self):
+        """
+        Extracts the timestamps from all series.
+        """
+
+        # Get and store all channel names
+        for n in range(self._nSeries):
+
+            # Initialize
+            self._metadata[n]['timestamps'] = []
+
+            # Change to series
+            self._reader.setSeries(n)
+
+            # Number of time stamps to retrieve
+            nTimepoints = self._reader.getSizeT()
+
+            # Iterate over the keys and get the values
+            for tp in range(nTimepoints):
+
+                option = 'timestamp ' + str(tp)
+                t = self._reader.getSeriesMetadataValue(option)
+                if t is None:
+                    t = "NaN"
+
+                # Store timestamps for current series
+                self._metadata[n]['timestamps'].append(t)
+
+
+    def _getVoxelSizes(self):
+        """
+        Extract voxel sizes for all series.
+        """
+
+        # Get and store all voxel sizes for all series
+        for n in range(self._nSeries):
+
+            # Change to current series
+            self._reader.setSeries(n)
+
+            # Voxel size X
+            voxelX = self._metadataStore.getPixelsPhysicalSizeX(n)
+            if voxelX is None:
+                voxelX = 0;
+            else:
+                voxelX = voxelX.getValue()
+
+            self._metadata[n]['voxelSizeX'] = voxelX
+
+            # Voxel size Y
+            voxelY = self._metadataStore.getPixelsPhysicalSizeY(n)
+            if voxelY is None:
+                voxelY = 0;
+            else:
+                voxelY = voxelY.getValue()
+
+            self._metadata[n]['voxelSizeY'] = voxelY
+
+            # Voxel size Z
+            voxelZ = self._metadataStore.getPixelsPhysicalSizeZ(n)
+            if voxelZ is None:
+                voxelZ = 0;
+            else:
+                voxelZ = voxelZ.getValue()
+
+            self._metadata[n]['voxelSizeZ'] = voxelZ
+
+
+    def _getWavelengths(self):
+        """
+        Extracts the excitation and emission wavelengths of all channels
+        from all series.
+        """
+
+        # Get and store all channel names
+        for n in range(self._nSeries):
+
+            # Initialize
+            self._metadata[n]['exWavelengths'] = []
+            self._metadata[n]['emWavelengths'] = []
+
+            # Change to series
+            self._reader.setSeries(n)
+
+            # Get the number of channels in this series
+            nChannels = self._reader.getSizeC()
+
+            for ch in range(nChannels):
+
+                # Get and store emission wavelength for current channel in
+                # current series
+                em = self._metadataStore.getChannelEmissionWavelength(n, ch)
+                if em is None:
+                    em = "NaN"
+                self._metadata[n]['emWavelengths'].append(em)
+
+                # Get and store excitation wavelength for current channel in
+                # current series
+                ex = self._metadataStore.getChannelExcitationWavelength(n, ch)
+                if ex is None:
+                    ex = "NaN"
+                self._metadata[n]['exWavelengths'].append(ex)
+
+
+    def _markThumbnails(self):
+        """
+        Mark which series contain thumbnails.
+        """
+
+        # Mark all series whether they are thumbnails or not
+        for n in range(self._nSeries):
+
+            # Change to current series
+            self._reader.setSeries(n)
+
+            # Check if thumbnail
+            self._metadata[n]['isThumbnail'] = self._reader.isThumbnailSeries()
diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py
new file mode 100644
index 0000000000000000000000000000000000000000..282a4271fc79e50c8471b5562ebaec2e8a1a478b
--- /dev/null
+++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py
@@ -0,0 +1,22 @@
+"""
+@author: Aaron Ponti
+"""
+
+import os
+import logging
+import re
+
+from Processor import Processor
+
+
+def process(transaction):
+    """Dropbox entry point.
+
+    @param transaction, the transaction object
+    """
+
+    # Create a Processor
+    processor = Processor(transaction)
+
+    # Run
+    processor.run()
diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f7a3988de02caac79e3e6475e950f6a6b63b200
--- /dev/null
+++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py
@@ -0,0 +1,197 @@
+"""
+Created on Feb 20, 2014
+
+@author: Aaron Ponti
+"""
+
+import re
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import SimpleImageDataConfig
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import SimpleImageContainerDataConfig
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import ChannelColor
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import ImageIdentifier
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import ImageMetadata
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import OriginalDataStorageFormat
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import ChannelColorRGB
+from ch.systemsx.cisd.openbis.dss.etl.dto.api import Channel
+
+class MicroscopySingleDatasetConfig(SimpleImageContainerDataConfig):
+    """Image data configuration class for multi datasets image files."""
+
+    # MetadataReader used to extract relevant metadata information using
+    # the LOCI bio-formats library.
+    _metadataReader = None
+
+    # Number of the series to register (for a multi-series dataset.
+    _seriesNum = 0
+
+    def __init__(self, metadataReader, seriesNum=0):
+        """Constructor.
+
+        @param metadataReader: MetadataReader object (with extracted metadata)
+        @param seriesNum: Int Number of the series to register. All other series in
+                   the file will be ignored.
+        """
+
+        # Store the MetadataReader
+        self._metadataReader = metadataReader
+
+        # Store the series number
+        self._seriesNum = seriesNum
+
+        # This is microscopy data
+        self.setMicroscopyData(True)
+
+        # Store raw data in original form
+        self.setOriginalDataStorageFormat(OriginalDataStorageFormat.UNCHANGED)
+
+        # Set the image library
+        self.setImageLibrary("BioFormats")
+
+        # Disable thumbnail generation by ImageMagick
+        self.setUseImageMagicToGenerateThumbnails(False)
+
+        # Enable thumbnail generation
+        self.setGenerateThumbnails(True)
+
+        # Set the recognized extensions
+        self.setRecognizedImageExtensions(['lsm', 'stk', 'lif', 'nd2'])
+
+        # Set the dataset type
+        self.setDataSetType("MICROSCOPY_IMG")
+
+    def createChannel(self, channelCode):
+        """Create a channel from the channelCode with the name as read from
+        the file via the MetadataReader.
+
+        @param channelCode Code of the channel as generated by extractImagesMetadata().
+        """
+
+        # Get the indices of series and channel from the channel code
+        (seriesIndx, channelIndx) = self._getSeriesAndChannelNumbers(channelCode)
+
+        if seriesIndx != self._seriesNum:
+            return
+
+        # Try extracting the channel colors for the given series
+        try:
+            channelNames = self._metadataReader.getMetadata()[seriesIndx]['channelNames']
+        except IndexError:
+            raise("Could not channel name for series " + str(seriesIndx) + " from MetadataReader.")
+
+        # Try extracting the name
+        try:
+            name = channelNames[channelIndx]
+        except:
+            raise("Could not extract name with index " + channelIndx)
+
+        # In case no name was found, assign default name
+        if name == "":
+            name = "No name"
+
+        # Return the color
+        return Channel(channelCode, name)
+
+
+    def extractImagesMetadata(self, imagePath, imageIdentifiers):
+        """Overrides extractImagesMetadata method making sure to store
+        both series and channel indices in the channel code to be reused
+        later to extract color information and other metadata.
+
+        The channel code is in the form SERIES-(\d+)_CHANNEL-(\d+).
+
+        Only metadata for the relevant series number is returned!
+
+        @param imagePath Full path to the file to process
+        @param imageIdentifiers Array of ImageIdentifier's
+
+        @see constructor.
+        """
+
+        # Initialize array of metadata entries
+        metaData = []
+
+        # Iterate over all image identifiers
+        for id in imageIdentifiers:
+
+            # Extract the info from the image identifier
+            ch = id.colorChannelIndex
+            plane = id.focalPlaneIndex
+            series = id.seriesIndex
+            timepoint = id.timeSeriesIndex
+
+            # Make sure to process only the relevant series
+            if series != self._seriesNum:
+                continue
+
+            # Build the channel code
+            channelCode = "SERIES-" + str(series) + "_CHANNEL-" + str(ch)
+
+            # Initialize a new ImageMetadata object
+            imageMetadata = ImageMetadata();
+
+            # Fill in all information
+            imageMetadata.imageIdentifier = id
+            imageMetadata.seriesNumber = series
+            imageMetadata.timepoint = timepoint
+            imageMetadata.depth = plane
+            imageMetadata.channelCode = channelCode
+            imageMetadata.well = "IGNORED"
+            imageMetadata.tileNumber = 1
+
+            # Append metadata for current image
+            metaData.append(imageMetadata)
+        return metaData
+
+
+    def getChannelColorRGB(self, channelCode):
+        """Returns a ChannelColorRGB instantiated with the RGB color components
+        extracted from the file by the MetadataReader.
+
+        @param channelCode Code of the channel as generated by extractImagesMetadata().
+        """
+
+        # Get the indices of series and channel from the channel code
+        (seriesIndx, channelIndx) = self._getSeriesAndChannelNumbers(channelCode)
+
+        if seriesIndx != self._seriesNum:
+            return
+
+        # Try extracting the channel colors for the given series
+        try:
+            channelColors = \
+            self._metadataReader.getMetadata()[seriesIndx]['channelColors']
+        except IndexError:
+            raise("Could not extract channel colors for series " + str(seriesIndx) + " from MetadataReader.")
+
+        # Try extracting the color
+        try:
+            color = channelColors[channelIndx]
+            R = color[0]
+            G = color[1]
+            B = color[2]
+        except:
+            raise("Could not extract color with index " + channelIndx)
+
+        return ChannelColorRGB(R, G, B)
+
+
+    def _getSeriesAndChannelNumbers(self, channelCode):
+        """Extract series and channel number from channel code in
+        the form SERIES-(\d+)_CHANNEL-(\d+) to a tuple
+        (seriesIndx, channelIndx).
+
+        @param channelCode Code of the channel as generated by extractImagesMetadata().
+        """
+
+        # Get the indices of series and channel from the channel code
+        p = re.compile("SERIES-(\d+)_CHANNEL-(\d+)")
+        m = p.match(channelCode)
+        if m is None or len(m.groups()) != 2:
+            raise Exception("Could not extract series and channel number!")
+
+        # Now assign the indices
+        seriesIndx = int(m.group(1))
+        channelIndx = int(m.group(2))
+
+        # Return them
+        return seriesIndx, channelIndx
diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py
new file mode 100644
index 0000000000000000000000000000000000000000..8364cc88b1fdb383d317f85ddf4eac69e8b3f435
--- /dev/null
+++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py
@@ -0,0 +1,169 @@
+"""
+Created on Feb 20, 2014
+
+@author: Aaron Ponti
+"""
+
+import java.io.File
+import os
+import re
+import xml.etree.ElementTree as xml
+from datetime import datetime
+from BioFormatsProcessor import BioFormatsProcessor
+from MicroscopySingleDatasetConfig import MicroscopySingleDatasetConfig
+
+class Processor:
+    """The Processor class performs all steps required for registering datasets
+    from the assigned dropbox folder."""
+
+    # A transaction object passed by openBIS
+    _transaction = None
+
+    # The incoming folder to process (a java.io.File object)
+    _incoming = ""
+
+    # Constructor
+    def __init__(self, transaction):
+
+        # Store arguments
+        self._transaction = transaction
+        self._incoming = transaction.getIncoming()
+
+    def getCustomTimeStamp(self):
+        """Create an univocal time stamp based on the current date and time
+        (works around incomplete API of Jython 2.5)."""
+
+        t = datetime.now()
+        return t.strftime("%y%d%m%H%M%S") + unicode(t)[20:]
+
+    def getSubFolders(self):
+        """Return a list of subfolders of the passed incoming directory.
+
+        @return list of subfolders (String)
+        """
+
+        incomingStr = self._incoming.getAbsolutePath()
+        return [name for name in os.listdir(incomingStr)
+                if os.path.isdir(os.path.join(incomingStr, name))]
+
+    def createExperiment(self, expId, expType):
+        # Create the experiment
+        exp = self._transaction.createNewExperiment(expId, expType)
+        if not exp:
+            raise Exception("Could not create experiment " + expId + "!")
+
+        return exp
+
+    def processExperiment(self, experimentNode, openBISExpType):
+        """Register an IExperimentUpdatable based on the Experiment XML node.
+
+        @param experimentNode An XML node corresponding to an Experiment
+        @param openBISExpType The experiment type
+        @return IExperimentUpdatable experiment
+        """
+
+        # Get the openBIS identifier
+        openBISIdentifier = experimentNode.attrib.get("openBISIdentifier")
+
+        openBISExperiment = self.createExperiment(openBISIdentifier, openBISExpType)
+        if not openBISExperiment:
+            raise Exception("Could not create experiment " + openBISIdentifier)
+        return openBISExperiment
+
+
+    def processMicroscopyFile(self, microscopyFileNode, openBISExperiment, propertiesFile):
+        """Register the Microscopy File using the parsed properties file.
+
+        @param microscopyFileNode An XML node corresponding to a microscopy
+        file (dataset)
+        @param openBISExperiment An ISample object representing an Experiment
+        """
+
+        # Assign the file to the dataset (we will use the absolute path)
+        relativeFileName = microscopyFileNode.attrib.get("relativeFileName")
+        fileName = os.path.join(self._incoming.getAbsolutePath(), relativeFileName)
+
+        # Instantiate a BioFormatsProcessor
+        bioFormatsProcessor = BioFormatsProcessor(fileName)
+
+        # Extract and store metadata
+        bioFormatsProcessor.extractMetadata()
+
+        # Log the number of series found
+        num_series = bioFormatsProcessor.getNumSeries()
+        singleDatasetConfig = MicroscopySingleDatasetConfig(bioFormatsProcessor)
+        dataset = self._transaction.createNewImageDataSet(singleDatasetConfig, java.io.File(fileName))
+        self._transaction.moveFile(fileName, dataset)
+        sample = self._transaction.createNewSampleWithGeneratedCode("TEST", "MICROSCOPY_SAMPLE")
+        sample.setExperiment(openBISExperiment)
+        dataset.setSample(sample)
+    
+
+    def register(self, tree, propertiesFile):
+        """Register the Experiment using the parsed properties file.
+
+        @param tree ElementTree parsed from the properties XML file
+        """
+
+        # Get the root node (obitXML)
+        root = tree.getroot()
+
+        # Iterate over the children (Experiments)
+        for experimentNode in root:
+            # The tag of the immediate children of the root experimentNode
+            # must be Experiment
+            if experimentNode.tag != "Experiment":
+                raise Exception("Expected Experiment node, found " + experimentNode.tag)
+
+            # Process an Experiment XML node and get/create an IExperimentUpdatable
+            openBISExperiment = self.processExperiment(experimentNode, "MICROSCOPY_EXPERIMENT")
+
+            # Process children of the Experiment
+            for microscopyFileNode in experimentNode:
+                if microscopyFileNode.tag != "MicroscopyFile":
+                    raise Exception("Expected MicroscopyFile node (found " + microscopyFileNode.tag + "!")
+
+                # Process the MicroscopyFile node
+                self.processMicroscopyFile(microscopyFileNode, openBISExperiment, propertiesFile)
+
+
+    def run(self):
+        """Run the registration."""
+
+        # Make sure that incoming is a folder
+        if not self._incoming.isDirectory():
+            raise Exception("Incoming MUST be a folder!")
+
+        # There must be just one subfolder: the user subfolder
+        subFolders = self.getSubFolders()
+        if len(subFolders) != 1:
+            raise Exception("Expected user subfolder!")
+
+        # Set the user folder
+        userFolder = os.path.join(self._incoming.getAbsolutePath(), subFolders[0])
+
+        # In the user subfolder we must find the data_structure.ois file
+        dataFileName = os.path.join(userFolder, "data_structure.ois")
+        if not os.path.exists(dataFileName):
+            raise Exception("File data_structure.ois not found!")
+
+        # Now read the data structure file and store all the pointers to
+        # the properties files. The paths are stored relative to self._incoming,
+        # so we can easily build the full file paths.
+        propertiesFileList = []
+        f = open(dataFileName)
+        try:
+            for line in f:
+                line = re.sub('[\r\n]', '', line)
+                propertiesFile = os.path.join(self._incoming.getAbsolutePath(), line)
+                propertiesFileList.append(propertiesFile)
+        finally:
+            f.close()
+
+        # Process (and ultimately register) all experiments
+        for propertiesFile in propertiesFileList:
+            # Read the properties file into an ElementTree
+            tree = xml.parse(propertiesFile)
+
+            # Now register the experiment
+            self.register(tree, propertiesFile)
diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..70ba6f9993bd9e36b2a3e78abffce26fb7406a2c
--- /dev/null
+++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties
@@ -0,0 +1,23 @@
+# The directory to watch for new data sets
+incoming-dir = ${incoming-root-dir}/incoming-MicroscopyImageDropboxTest
+
+# The handler class.
+top-level-data-set-handler = ch.systemsx.cisd.openbis.dss.etl.jython.v2.JythonPlateDataSetHandlerV2
+
+# The script to execute, reloaded and recompiled each time a file/folder is placed in the dropbox
+script-path = MicroscopyDropbox.py
+
+# The appropriate storage processor
+storage-processor = ch.systemsx.cisd.openbis.dss.etl.MicroscopyStorageProcessor
+
+# Defines how the drop box decides if a folder is ready to process: either by a 'marker-file' or a time out which is called 'auto-detection'
+# The time out is set globally in the service.properties and is called 'quiet-period'. This means when the number of seconds is over and no changes have
+# been made to the incoming folder the drop will start to register. The marker file must have the following naming schema: '.MARKER_is_finished_<incoming_folder_name>'
+#incoming-data-completeness-condition = marker-file
+incoming-data-completeness-condition = auto-detection
+
+# Storage processor
+storage-processor.data-source = imaging-db
+
+# Enable development mode
+development-mode = true
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java
index b94231feca6206610d1aec0826162ace7ff82438..ccffd57692511c8af48f13cc69d29b50cb2ff1d7 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java
@@ -18,10 +18,19 @@ package ch.systemsx.cisd.openbis.screening.systemtests;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
 
 import org.apache.commons.io.FileUtils;
 import org.testng.annotations.BeforeTest;
 
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetRelatedEntities;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
+
 /**
  *
  * @author Franz-Josef Elmer
@@ -31,10 +40,15 @@ public abstract class AbstractImageDropboxTestCase extends AbstractScreeningSyst
     @BeforeTest
     public void dropAnExampleDataSet() throws Exception
     {
+        registerAdditionalOpenbisMetaData();
         File exampleDataSet = createTestDataContents();
         moveFileToIncoming(exampleDataSet);
         waitUntilDataSetImported(FINISHED_POST_REGISTRATION_CONDITION);
     }
+    
+    protected void registerAdditionalOpenbisMetaData()
+    {
+    }
 
     private File createTestDataContents() throws IOException
     {
@@ -56,6 +70,49 @@ public abstract class AbstractImageDropboxTestCase extends AbstractScreeningSyst
     {
         return 60;
     }
+
+    protected AbstractExternalData getRegisteredContainerDataSet()
+    {
+        List<AbstractExternalData> dataSets = getRegisteredDataSets();
+        for (AbstractExternalData dataSet : dataSets)
+        {
+            if (dataSet.getDataSetType().getDataSetKind().equals(DataSetKind.CONTAINER))
+            {
+                return dataSet;
+            }
+        }
+        fail("No container data set found: " + dataSets);
+        return null; // never reached but needed for the compiler
+    }
+
+    protected List<AbstractExternalData> getRegisteredDataSets()
+    {
+        String code = translateIntoCamelCase(getClass().getSimpleName()).toUpperCase();
+        ExperimentIdentifier identifier = ExperimentIdentifierFactory.parse("/TEST/TEST-PROJECT/" + code);
+        Experiment experiment = commonServer.getExperimentInfo(sessionToken, identifier);
+        List<AbstractExternalData> dataSets 
+                = commonServer.listRelatedDataSets(sessionToken, new DataSetRelatedEntities(Arrays.asList(experiment)), false);
+        return dataSets;
+    }
     
+    private String translateIntoCamelCase(String string)
+    {
+        StringBuilder builder = new StringBuilder();
+        for (char c : string.toCharArray())
+        {
+            if (Character.isUpperCase(c))
+            {
+                if (builder.length() > 0)
+                {
+                    builder.append('_');
+                }
+                builder.append(Character.toLowerCase(c));
+            } else
+            {
+                builder.append(c);
+            }
+        }
+        return builder.toString();
+    }
 
 }
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..44a293edb0c57e4dac367f88d93bbcff2255c473
--- /dev/null
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2014 ETH Zuerich, SIS
+ *
+ * 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.openbis.screening.systemtests;
+
+import java.io.File;
+import java.util.Collections;
+
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.openbis.generic.shared.ICommonServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SampleType;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifierFactory;
+
+/**
+ * 
+ *
+ * @author Franz-Josef Elmer
+ */
+public class MicroscopyImageDropboxTest extends AbstractImageDropboxTestCase
+{
+
+    @Override
+    protected void registerAdditionalOpenbisMetaData()
+    {
+        System.out.println("MicroscopyImageDropboxTest.registerAdditionalOpenbisMetaData()");
+        commonServer = (ICommonServer) applicationContext
+                .getBean(ch.systemsx.cisd.openbis.generic.shared.ResourceNames.COMMON_SERVER);
+        sessionToken = commonServer.tryAuthenticate("admin", "a").getSessionToken();
+        ExperimentType experimentType = new ExperimentType();
+        experimentType.setCode("MICROSCOPY_EXPERIMENT");
+        commonServer.registerExperimentType(sessionToken, experimentType);
+        SampleType sampleType = new SampleType();
+        sampleType.setCode("MICROSCOPY_SAMPLE");
+        sampleType.setGeneratedCodePrefix("M-");
+        commonServer.registerSampleType(sessionToken, sampleType);
+        DataSetType dataSetType = new DataSetType("MICROSCOPY_IMG");
+        dataSetType.setDataSetKind(DataSetKind.PHYSICAL);
+        commonServer.registerDataSetType(sessionToken, dataSetType);
+        dataSetType = new DataSetType("MICROSCOPY_IMG_CONTAINER");
+        dataSetType.setDataSetKind(DataSetKind.CONTAINER);
+        commonServer.registerDataSetType(sessionToken, dataSetType);
+        commonServer.registerSpace(sessionToken, "TEST", null);
+        commonServer.registerProject(sessionToken, ProjectIdentifierFactory.parse("/TEST/TEST-PROJECT"), "", 
+                null, Collections.<NewAttachment>emptySet());
+    }
+
+    @Override
+    protected String getDataFolderToDrop()
+    {
+        return "aarons_example";
+    }
+    
+    @Test
+    public void test()
+    {
+        AbstractExternalData dataSet = getRegisteredContainerDataSet();
+        ImageChecker imageChecker = new ImageChecker();
+        imageChecker.check(new File(getTestDataFolder(), "Merged_Default.png"), 
+                new ImageLoader(dataSet, sessionToken));
+        imageChecker.check(new File(getTestDataFolder(), "Merged_256x256.png"), 
+                new ImageLoader(dataSet, sessionToken).mode("thumbnail256x256"));
+        imageChecker.check(new File(getTestDataFolder(), "Merged_512x512.png"), 
+                new ImageLoader(dataSet, sessionToken).mode("thumbnail512x512"));
+        imageChecker.check(new File(getTestDataFolder(), "C1_Default.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1"));
+        imageChecker.check(new File(getTestDataFolder(), "C1_256x256.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1").mode("thumbnail256x256"));
+        imageChecker.check(new File(getTestDataFolder(), "C1_512x512.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1").mode("thumbnail512x512"));
+        imageChecker.check(new File(getTestDataFolder(), "C01_Default.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1"));
+        imageChecker.check(new File(getTestDataFolder(), "C01_256x256.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1").mode("thumbnail256x256"));
+        imageChecker.check(new File(getTestDataFolder(), "C01_512x512.png"), 
+                new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1").mode("thumbnail512x512"));
+
+    }
+
+}
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java
index c368f8605c620732d7b540ddb7fa7466c7495eb6..b485912efe64e0125d182b86401bc508c41265ef 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java
@@ -17,18 +17,10 @@
 package ch.systemsx.cisd.openbis.screening.systemtests;
 
 import java.io.File;
-import java.util.List;
 
 import org.testng.annotations.Test;
 
-import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetRelatedEntities;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListSampleCriteria;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
-import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
-import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
 
 /**
  * 
@@ -47,17 +39,7 @@ public class SimpleImageDropboxTest extends AbstractImageDropboxTestCase
     @Test
     public void test() throws Exception
     {
-        ExperimentIdentifier identifier = ExperimentIdentifierFactory.parse("/TEST/TEST-PROJECT/SIMPLE_IMAGE_DROPBOX_TEST");
-        Experiment experiment = commonServer.getExperimentInfo(sessionToken, identifier);
-        ListSampleCriteria sampleCriteria = ListSampleCriteria.createForExperiment(TechId.create(experiment));
-        List<Sample> samples = commonServer.listSamples(sessionToken, sampleCriteria);
-        Sample plate = samples.get(0);
-        assertEquals("/TEST/SIMPLE_IMAGE_DROPBOX_TEST", plate.getIdentifier());
-        assertEquals(1, samples.size());
-        List<AbstractExternalData> dataSets2 
-                = commonServer.listRelatedDataSets(sessionToken, new DataSetRelatedEntities(samples), false);
-        AbstractExternalData dataSet = dataSets2.get(0);
-        assertEquals(1, dataSets2.size());
+        AbstractExternalData dataSet = getRegisteredContainerDataSet();
         ImageChecker imageChecker = new ImageChecker();
         imageChecker.check(new File(getTestDataFolder(), "1_1_Merged_Default.png"), 
                 new ImageLoader(dataSet, sessionToken));