diff --git a/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/handler.py b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea8f5446ef5867669c4b038d58ccf342c0360339
--- /dev/null
+++ b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/handler.py
@@ -0,0 +1,95 @@
+#! /usr/bin/env python
+from java.util import Date
+from java.text import SimpleDateFormat
+from xml.etree import ElementTree
+from xml.etree.ElementTree import Element, SubElement
+from com.lyncode.xoai.dataprovider import DataProvider
+from com.lyncode.xoai.dataprovider.model import Context, MetadataFormat, Item
+from com.lyncode.xoai.dataprovider.repository import Repository, RepositoryConfiguration
+from com.lyncode.xoai.dataprovider.parameters import OAIRequest
+from com.lyncode.xoai.dataprovider.handlers.results import ListItemIdentifiersResult, ListItemsResults
+from com.lyncode.xoai.model.oaipmh import OAIPMH, DeletedRecord, Granularity, Metadata
+from com.lyncode.xoai.xml import XmlWriter
+from ch.systemsx.cisd.openbis.dss.generic.server.oaipmh.xoai import SimpleItemIdentifier, SimpleItem, SimpleItemRepository, SimpleSetRepository
+from ch.systemsx.cisd.openbis.generic.shared.api.v1.dto import SearchCriteria
+from ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchCriteria import MatchClause, MatchClauseAttribute, MatchClauseTimeAttribute, CompareMode
+DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd")
+TIME_ZONE = "0"
+ 
+def handle(req, resp):
+    context = Context();
+    context.withMetadataFormat(MetadataFormat().withPrefix("testPrefix").withTransformer(MetadataFormat.identity()));
+    configuration = RepositoryConfiguration();
+    configuration.withMaxListSets(10);
+    configuration.withMaxListIdentifiers(10);
+    configuration.withMaxListRecords(10);
+    configuration.withAdminEmail("test@test");
+    configuration.withBaseUrl("http://localhost");
+    configuration.withDeleteMethod(DeletedRecord.NO);
+    configuration.withEarliestDate(Date(0));
+    configuration.withRepositoryName("TEST");
+    configuration.withGranularity(Granularity.Day);
+    repository = Repository();
+    repository.withConfiguration(configuration);
+    repository.withItemRepository(ItemRepository());
+    repository.withSetRepository(SimpleSetRepository());
+    provider = DataProvider(context, repository);
+    params = {}
+    for param in req.getParameterNames():
+        values = []
+        for value in req.getParameterValues(param):
+            values.append(value)
+        params[param] = values
+    request = OAIRequest(params);
+    response = provider.handle(request);
+    writer = XmlWriter(resp.getOutputStream());
+    response.write(writer);
+    writer.flush();
+ 
+class ItemRepository(SimpleItemRepository):
+   
+    def doGetItem(self, identifier):
+        criteria = SearchCriteria()
+        criteria.addMatchClause(MatchClause.createAttributeMatch(MatchClauseAttribute.CODE, identifier))
+        dataSets = searchService.searchForDataSets(criteria)
+         
+        if dataSets:
+            return createItem(dataSets[0])
+        else:
+            return None
+    def doGetItemIdentifiers(self, filters, offset, length, setSpec, fromDate, untilDate):
+        results = self.doGetItems(filters, offset, length, setSpec, fromDate, untilDate)
+        return ListItemIdentifiersResult(results.hasMore(), results.getResults(), results.getTotal())
+     
+    def doGetItems(self, filters, offset, length, setSpec, fromDate, untilDate):
+        criteria = SearchCriteria()
+        if fromDate:
+            criteria.addMatchClause(MatchClause.createTimeAttributeMatch(MatchClauseTimeAttribute.REGISTRATION_DATE, CompareMode.GREATER_THAN_OR_EQUAL, DATE_FORMAT.format(fromDate), TIME_ZONE))
+        if untilDate:
+            criteria.addMatchClause(MatchClause.createTimeAttributeMatch(MatchClauseTimeAttribute.REGISTRATION_DATE, CompareMode.LESS_THAN_OR_EQUAL, DATE_FORMAT.format(untilDate), TIME_ZONE))
+        dataSets = searchService.searchForDataSets(criteria)
+        if dataSets:
+            hasMoreResults = (offset + length) < len(dataSets)
+            results = [createItem(dataSet) for dataSet in dataSets[offset:(offset + length)]]
+            total = len(dataSets)
+            return ListItemsResults(hasMoreResults, results, total)
+        else:
+            return ListItemsResults(False, [], 0)
+ 
+ 
+def createItemMetadata(dataSet):
+    properties = Element("properties")
+     
+    for propertyCode in dataSet.getAllPropertyCodes():
+        property = SubElement(properties, "property")
+        property.set("code", propertyCode)
+        property.text = dataSet.getPropertyValue(propertyCode)
+         
+    return Metadata(ElementTree.tostring(properties))
+ 
+def createItem(dataSet):
+    item = SimpleItem()
+    item.setIdentifier(dataSet.getDataSetCode())
+    item.setDatestamp(Date())
+    item.setMetadata(createItemMetadata(dataSet))
+    return item
\ No newline at end of file
diff --git a/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/plugin.properties b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..2acdff1936dc414611f4f274d181f2a63bb48e0b
--- /dev/null
+++ b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/services/oaipmh/plugin.properties
@@ -0,0 +1,5 @@
+class = ch.systemsx.cisd.openbis.dss.generic.server.oaipmh.OaipmhServlet
+path = /oaipmh/*
+request-handler = ch.systemsx.cisd.openbis.dss.generic.server.oaipmh.JythonBasedRequestHandler
+request-handler.script-path = handler.py
+authentication-handler = ch.systemsx.cisd.openbis.dss.generic.server.oaipmh.BasicHttpAuthenticationHandler
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/OaipmhServletTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/OaipmhServletTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..80dda4b71992ef50d83dbfdf55a459684acb988d
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/OaipmhServletTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.datastoreserver.systemtests;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import junit.framework.Assert;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
+import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
+import ch.systemsx.cisd.openbis.generic.shared.api.v1.IGeneralInformationService;
+import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.DataSet;
+import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.util.TestInstanceHostUtils;
+
+/**
+ * @author pkupczyk
+ */
+@Test(groups =
+{ "slow" })
+public class OaipmhServletTest extends SystemTestCase
+{
+
+    private static final String GENERAL_INFORMATION_SERVICE_URL = TestInstanceHostUtils.getOpenBISUrl() + IGeneralInformationService.SERVICE_URL;
+
+    private static final String OAIPMH_SERVLET_URL = TestInstanceHostUtils.getDSSUrl() + "/oaipmh/";
+
+    private static final String USER_ID = "test";
+
+    private static final String USER_PASSWORD = "password";
+
+    private IGeneralInformationService generalInformationService;
+
+    private DocumentBuilder xmlBuilder;
+
+    private XPath xPath;
+
+    @BeforeClass
+    public void beforeClass() throws ParserConfigurationException
+    {
+        generalInformationService = HttpInvokerUtils.createServiceStub(IGeneralInformationService.class, GENERAL_INFORMATION_SERVICE_URL, 5000);
+        xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        xPath = XPathFactory.newInstance().newXPath();
+    }
+
+    @Test
+    public void testWithoutAuthorizationHeader()
+    {
+        GetMethod method = sendRequest(null, OAIPMH_SERVLET_URL + "?verb=Identify");
+        Assert.assertEquals(401, method.getStatusCode());
+    }
+
+    @Test
+    public void testWithIncorrectAuthorizationHeader()
+    {
+        GetMethod method = sendRequest("This is an invalid header", OAIPMH_SERVLET_URL + "?verb=Identify");
+        Assert.assertEquals(500, method.getStatusCode());
+    }
+
+    @Test
+    public void testWithIncorrectCredentials()
+    {
+        GetMethod method = sendRequest("incorrect", USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=Identify");
+        Assert.assertEquals(401, method.getStatusCode());
+    }
+
+    @Test
+    public void testIdentify() throws InterruptedException
+    {
+        GetMethod method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=Identify");
+        Assert.assertEquals(200, method.getStatusCode());
+        Document document = parseResponse(method);
+        Assert.assertEquals("TEST", evaluateToString(document, "/OAI-PMH/Identify/repositoryName"));
+    }
+
+    @Test
+    public void testListMetadataformats()
+    {
+        GetMethod method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListMetadataFormats");
+        Assert.assertEquals(200, method.getStatusCode());
+        Document document = parseResponse(method);
+        Assert.assertEquals("testPrefix", evaluateToString(document, "/OAI-PMH/ListMetadataFormats/metadataFormat/metadataPrefix"));
+    }
+
+    @Test
+    public void testListSets()
+    {
+        GetMethod method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListSets");
+        Assert.assertEquals(200, method.getStatusCode());
+        Document document = parseResponse(method);
+        Assert.assertEquals("This repository does not support sets", evaluateToString(document, "/OAI-PMH/error"));
+    }
+
+    @Test
+    public void testListIdentifiers()
+    {
+        String sessionToken = generalInformationService.tryToAuthenticateForAllServices(USER_ID, USER_PASSWORD);
+        List<DataSet> dataSets = generalInformationService.searchForDataSets(sessionToken, new SearchCriteria());
+
+        String resumptionToken = null;
+        int dataSetCount = 0;
+
+        do
+        {
+            GetMethod method = null;
+            if (resumptionToken == null)
+            {
+                method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListIdentifiers&metadataPrefix=testPrefix");
+            } else
+            {
+                method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListIdentifiers&resumptionToken=" + resumptionToken);
+            }
+            Assert.assertEquals(200, method.getStatusCode());
+
+            Document document = parseResponse(method);
+            dataSetCount += evaluateToNodeList(document, "/OAI-PMH/ListIdentifiers/header").getLength();
+            resumptionToken = evaluateToString(document, "/OAI-PMH/ListIdentifiers/resumptionToken");
+
+        } while (resumptionToken != null && !resumptionToken.isEmpty());
+
+        Assert.assertEquals(dataSets.size(), dataSetCount);
+    }
+
+    @Test
+    public void testListRecords()
+    {
+        String sessionToken = generalInformationService.tryToAuthenticateForAllServices(USER_ID, USER_PASSWORD);
+        List<DataSet> dataSets = generalInformationService.searchForDataSets(sessionToken, new SearchCriteria());
+
+        String resumptionToken = null;
+        int dataSetCount = 0;
+
+        do
+        {
+            GetMethod method = null;
+            if (resumptionToken == null)
+            {
+                method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListRecords&metadataPrefix=testPrefix");
+            } else
+            {
+                method = sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=ListRecords&resumptionToken=" + resumptionToken);
+            }
+            Assert.assertEquals(200, method.getStatusCode());
+
+            Document document = parseResponse(method);
+            dataSetCount += evaluateToNodeList(document, "/OAI-PMH/ListRecords/record").getLength();
+            resumptionToken = evaluateToString(document, "/OAI-PMH/ListRecords/resumptionToken");
+
+        } while (resumptionToken != null && !resumptionToken.isEmpty());
+
+        Assert.assertEquals(dataSets.size(), dataSetCount);
+    }
+
+    @Test
+    public void testGetRecord()
+    {
+        GetMethod method =
+                sendRequest(USER_ID, USER_PASSWORD, OAIPMH_SERVLET_URL + "?verb=GetRecord&metadataPrefix=testPrefix&identifier=20081105092159111-1");
+        Assert.assertEquals(200, method.getStatusCode());
+
+        Document document = parseResponse(method);
+        Assert.assertEquals("20081105092159111-1", evaluateToString(document, "/OAI-PMH/GetRecord/record/header/identifier"));
+        Assert.assertEquals("FEMALE", evaluateToString(document, "/OAI-PMH/GetRecord/record/metadata/properties/property[@code='GENDER']"));
+    }
+
+    private GetMethod sendRequest(String user, String password, String url)
+    {
+        String authorizationHeader = "Basic " + new String(Base64.encodeBase64(new String(user + ":" + password).getBytes()));
+        return sendRequest(authorizationHeader, url);
+    }
+
+    private GetMethod sendRequest(String authorizationHeader, String url)
+    {
+        try
+        {
+            operationLog.info("Sending OAI-PMH request: " + url);
+
+            HttpClient httpClient = new HttpClient();
+            GetMethod method = new GetMethod(url);
+            if (authorizationHeader != null)
+            {
+                method.setRequestHeader("Authorization", authorizationHeader);
+            }
+            httpClient.executeMethod(method);
+
+            operationLog.info("Received OAI-PMH response: " + method.getResponseBodyAsString());
+
+            return method;
+        } catch (HttpException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        } catch (IOException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        }
+    }
+
+    private Document parseResponse(GetMethod method)
+    {
+        try
+        {
+            String body = method.getResponseBodyAsString();
+            return xmlBuilder.parse(new ByteArrayInputStream(body.getBytes()));
+        } catch (IOException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        } catch (SAXException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        }
+    }
+
+    private String evaluateToString(Document document, String xpath)
+    {
+        return (String) evaluate(document, xpath, XPathConstants.STRING);
+    }
+
+    private NodeList evaluateToNodeList(Document document, String xpath)
+    {
+        return (NodeList) evaluate(document, xpath, XPathConstants.NODESET);
+    }
+
+    private Object evaluate(Document document, String xpath, QName returnType)
+    {
+        try
+        {
+            return xPath.compile(xpath).evaluate(document, returnType);
+        } catch (XPathExpressionException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        }
+    }
+
+}