/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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 org.opencastproject.oaipmh.server;
// use the class extensions to mock concrete classes (DublinCoreCatalogService)
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.getCurrentArguments;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.opencastproject.metadata.dublincore.EncodingSchemeUtils.encodeDate;
import static org.opencastproject.security.util.SecurityUtil.createSystemUser;
import static org.opencastproject.util.data.Collections.nil;
import static org.opencastproject.util.data.functions.Misc.chuck;
import static org.xmlmatchers.XmlMatchers.hasXPath;
import static org.xmlmatchers.xpath.XpathReturnType.returningANumber;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageSupport;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.oaipmh.Granularity;
import org.opencastproject.oaipmh.OaiPmhConstants;
import org.opencastproject.oaipmh.harvester.OaiPmhNamespaceContext;
import org.opencastproject.oaipmh.persistence.AbstractOaiPmhDatabase;
import org.opencastproject.oaipmh.persistence.OaiPmhDatabase;
import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseImpl;
import org.opencastproject.oaipmh.util.XmlGen;
import org.opencastproject.security.api.DefaultOrganization;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.persistence.PersistenceUtil;
import org.opencastproject.workspace.api.Workspace;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.junit.Test;
import java.io.File;
import java.net.URI;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManagerFactory;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/** Second test suite for OAI-PMH including a fully functional persistence backend. */
public class OaiPmhRepositoryPersistenceTest {
private static final NamespaceContext NS_CTX = OaiPmhNamespaceContext.getContext();
private static final String FORMAT_PREFIX = OaiPmhConstants.OAI_DC_METADATA_FORMAT.getPrefix();
private static final String REPOSITORY_ID = "repo";
/** Turn an XmlGen into a Source. */
private static Source s(XmlGen g) {
final Source s = new DOMSource(g.generate());
print(s);
return s;
}
private static String enc(Date a) {
return encodeDate(a, Precision.Second).getValue();
}
@Test
public void testInsertRepoSecond() throws Exception {
final Date now = new Date();
final MediaPackage mp1 = MediaPackageSupport.loadFromClassPath("/mp1.xml");
final MediaPackage mp2 = MediaPackageSupport.loadFromClassPath("/mp2.xml");
final MediaPackage mp3 = MediaPackageSupport.loadFromClassPath("/mp3.xml");
final OaiPmhRepository repo = repo(oaiPmhDatabase(mp1, mp2, mp3), Granularity.SECOND);
assertThat(
"List all records yields 3 records",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, null, null, null))),
allOf(hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(),
equalTo(3.0)),
hasXPath("//oai20:ListRecords/oai20:record/oai20:header[oai20:identifier='10.0000/11']", NS_CTX),
hasXPath("//oai20:ListRecords/oai20:record/oai20:header[oai20:identifier='10.0000/12']", NS_CTX),
hasXPath("//oai20:ListRecords/oai20:record/oai20:header[oai20:identifier='10.0000/13']", NS_CTX)));
assertThat("List records from time in the past yields 3 records",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(now), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(3.0)));
final Date future = new Date(System.currentTimeMillis() + 1000);
assertThat("List records from time in the future yields no records",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(future), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(0.0)));
}
@Test
public void testInsertRepoDay() throws Exception {
final Date ref = new Date();
final MediaPackage mp1 = MediaPackageSupport.loadFromClassPath("/mp1.xml");
final MediaPackage mp2 = MediaPackageSupport.loadFromClassPath("/mp2.xml");
final MediaPackage mp3 = MediaPackageSupport.loadFromClassPath("/mp3.xml");
final OaiPmhRepository repo = repo(oaiPmhDatabase(mp1, mp2, mp3), Granularity.DAY);
final Date ref2 = new Date();
final long diff = (ref2.getTime() - ref.getTime()) / 1000;
assertThat("List records from time in the past",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(ref), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(3.0)));
assertThat("List records from time in the future. Attention: Test will fail during the last " + diff
+ " seconds of a day!",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(ref2), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(3.0)));
assertThat(
"List records from time in the future (5 days ahead)",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(new Date(ref.getTime() + 5 * 60 * 60 * 24
* 1000)), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(0.0)));
}
@Test
public void testIdentify() throws Exception {
assertThat("Identify reports the right date granularity",
s(repo(oaiPmhDatabase(), Granularity.DAY).selectVerb(params("Identify", null, null, null, null, null))),
hasXPath("//oai20:Identify[oai20:granularity='YYYY-MM-DD']", NS_CTX));
assertThat("Identify reports the right date granularity",
s(repo(oaiPmhDatabase(), Granularity.SECOND).selectVerb(params("Identify", null, null, null, null, null))),
hasXPath("//oai20:Identify[oai20:granularity='YYYY-MM-DDThh:mm:ssZ']", NS_CTX));
}
@Test
public void testSelectDateRange() throws Exception {
final Date ref1 = new Date();
final MediaPackage mp1 = MediaPackageSupport.loadFromClassPath("/mp1.xml");
final MediaPackage mp2 = MediaPackageSupport.loadFromClassPath("/mp2.xml");
final MediaPackage mp3 = MediaPackageSupport.loadFromClassPath("/mp3.xml");
final OaiPmhRepository repo = repo(oaiPmhDatabase(mp1), Granularity.SECOND);
// wait 1 second since the repo has a time granularity of seconds
Thread.sleep(1000);
final Date ref2 = new Date();
repo.getPersistence().store(mp2, REPOSITORY_ID);
// wait 1 second since the repo has a time granularity of seconds
Thread.sleep(1000);
final Date ref3 = new Date();
repo.getPersistence().store(mp3, REPOSITORY_ID);
assertThat("List records from time 1 yields all 3 records",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(ref1), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(3.0)));
assertThat("List records from time 3 yields only the record record inserted last",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(ref3), null, null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(1.0)));
assertThat("List records until time 3 yields the first two records",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, null, enc(ref3), null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(2.0)));
assertThat("List records from time 2 until time 3 yields the record inserted second",
s(repo.selectVerb(params("ListRecords", null, FORMAT_PREFIX, enc(ref2), enc(ref3), null))),
hasXPath("count(//oai20:ListRecords/oai20:record/oai20:header)", NS_CTX, returningANumber(), equalTo(1.0)));
}
@Test
public void testListMetadataFormats() throws Exception {
final MediaPackage mp1 = MediaPackageSupport.loadFromClassPath("/mp1.xml");
final OaiPmhRepository repo = repo(oaiPmhDatabase(), Granularity.SECOND);
// add the media package to two different repositories
repo.getPersistence().store(mp1, repo.getRepositoryId());
repo.getPersistence().store(mp1, "ANOTHER_REPO");
assertThat(
"ListMetadataFormat yields error response",
s(repo.selectVerb(params("ListMetadataFormats", "UNKNOWN_ID", null, null, null, null))),
allOf(hasXPath("//oai20:request[@verb='ListMetadataFormats']", NS_CTX),
hasXPath("//oai20:error[@code='idDoesNotExist']", NS_CTX)));
assertThat("ListMetadataFormat yields a response",
s(repo.selectVerb(params("ListMetadataFormats", mp1.getIdentifier().toString(), null, null, null, null))),
hasXPath("//oai20:ListMetadataFormats/oai20:metadataFormat", NS_CTX));
}
@Test
public void testBadVerb() throws Exception {
final OaiPmhRepository repo = repo(oaiPmhDatabase(), Granularity.SECOND);
assertThat("BadVerb error does not have verb attribute",
s(repo.selectVerb(params("TheBadVerb", null, null, null, null, null))),
hasXPath("count(//oai20:request[@verb])", NS_CTX, returningANumber(), equalTo(0.0)));
}
@Test
public void testListSets() throws Exception {
final OaiPmhRepository repo = repo(oaiPmhDatabase(), Granularity.SECOND);
assertThat(
"ListSets return noSetHierarchy response",
s(repo.selectVerb(params("ListSets", null, null, null, null, null))),
allOf(hasXPath("//oai20:error[@code='noSetHierarchy']", NS_CTX),
hasXPath("count(//oai20:ListSets/oai20:set)", NS_CTX, returningANumber(), equalTo(0.0))));
}
// --
/** Each param may be null. */
private static Params params(final String verb, final String identifier, final String metadataPrefix,
final String from, final String until, final String resumptionToken) {
return new Params() {
@Override
String getParameter(String key) {
if ("verb".equals(key))
return verb;
if ("identifier".equals(key))
return identifier;
if ("metadataPrefix".equals(key))
return metadataPrefix;
if ("from".equals(key))
return from;
if ("until".equals(key))
return until;
if ("resumptionToken".equals(key))
return resumptionToken;
return null;
}
@Override
String getRepositoryUrl() {
return "http://localhost:8080/oaipmh";
}
};
}
private static AbstractOaiPmhDatabase oaiPmhDatabase(MediaPackage... mps) {
try {
final Organization org = new DefaultOrganization();
final SecurityService secSvc = EasyMock.createNiceMock(SecurityService.class);
// security service
final User user = createSystemUser("admin", org);
expect(secSvc.getOrganization()).andReturn(org).anyTimes();
expect(secSvc.getUser()).andReturn(user).anyTimes();
EasyMock.replay(secSvc);
// workspace
final Workspace workspace = EasyMock.createNiceMock(Workspace.class);
final File episodeDublinCore = new File(OaiPmhRepositoryPersistenceTest.class.getResource(
"/episode-dublincore.xml").toURI());
final File seriesDublinCore = new File(OaiPmhRepositoryPersistenceTest.class
.getResource("/series-dublincore.xml").toURI());
expect(workspace.get(EasyMock.<URI> anyObject())).andAnswer(new IAnswer<File>() {
@Override
public File answer() throws Throwable {
final String uri = getCurrentArguments()[0].toString();
if ("dublincore.xml".equals(uri))
return episodeDublinCore;
if ("series-dublincore.xml".equals(uri))
return seriesDublinCore;
throw new Error("Workspace mock does not know about file " + uri);
}
}).anyTimes();
EasyMock.replay(workspace);
// oai-pmh database
final EntityManagerFactory emf = PersistenceUtil
.newTestEntityManagerFactory(OaiPmhDatabaseImpl.PERSISTENCE_UNIT_NAME);
final AbstractOaiPmhDatabase db = new AbstractOaiPmhDatabase() {
@Override
public EntityManagerFactory getEmf() {
return emf;
}
@Override
public SecurityService getSecurityService() {
return secSvc;
}
@Override
public SeriesService getSeriesService() {
return null;
}
@Override
public Workspace getWorkspace() {
return workspace;
}
@Override
public Date currentDate() {
return new Date();
}
};
for (MediaPackage mp : mps)
db.store(mp, REPOSITORY_ID);
return db;
} catch (Exception e) {
return chuck(e);
}
}
private static OaiPmhRepository repo(final AbstractOaiPmhDatabase persistence, final Granularity granularity) {
return new OaiPmhRepository() {
@Override
public Granularity getRepositoryTimeGranularity() {
return granularity;
}
@Override
public String getRepositoryName() {
return "Test OAI Repository";
}
@Override
public String getRepositoryId() {
return REPOSITORY_ID;
}
@Override
public OaiPmhDatabase getPersistence() {
return persistence;
}
@Override
public String getAdminEmail() {
return "admin@localhost.org";
}
@Override
public String saveQuery(ResumableQuery query) {
return "r-token";
}
@Override
public Option<ResumableQuery> getSavedQuery(String resumptionToken) {
return Option.some(new ResumableQuery(FORMAT_PREFIX, new Date(), new Date(), Option.<String> none()));
}
@Override
public int getResultLimit() {
return 3;
}
@Override
public List<MetadataProvider> getRepositoryMetadataProviders() {
return nil();
}
@Override
public Date currentDate() {
return new Date();
}
};
}
public static void print(Source source) {
try {
final Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.transform(source, new StreamResult(System.out));
} catch (TransformerException e) {
chuck(e);
}
}
}