/** * 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.scheduler.impl; import static net.fortuna.ical4j.model.Component.VEVENT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_AVAILABLE; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CONTRIBUTOR; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATED; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATOR; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_DESCRIPTION; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_EXTENT; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IS_REPLACED_BY; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LANGUAGE; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LICENSE; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_PUBLISHER; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_REPLACES; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_RIGHTS_HOLDER; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SPATIAL; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SUBJECT; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TYPE; import static org.opencastproject.util.EqualsUtil.eqMap; import static org.opencastproject.util.data.Collections.list; import static org.opencastproject.util.data.Collections.properties; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.some; import static org.opencastproject.util.data.Tuple.tuple; import static org.opencastproject.util.persistencefn.PersistenceUtil.mkTestEntityManagerFactory; import org.opencastproject.mediapackage.EName; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.message.broker.api.BaseMessage; import org.opencastproject.message.broker.api.MessageReceiver; import org.opencastproject.message.broker.api.MessageSender; import org.opencastproject.metadata.dublincore.DCMIPeriod; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.DublinCoreCatalogList; import org.opencastproject.metadata.dublincore.DublinCoreCatalogService; import org.opencastproject.metadata.dublincore.DublinCores; import org.opencastproject.metadata.dublincore.EncodingSchemeUtils; import org.opencastproject.metadata.dublincore.Precision; import org.opencastproject.scheduler.api.SchedulerException; import org.opencastproject.scheduler.api.SchedulerQuery; import org.opencastproject.scheduler.endpoint.SchedulerRestService; import org.opencastproject.scheduler.impl.persistence.SchedulerServiceDatabaseImpl; import org.opencastproject.scheduler.impl.solr.SchedulerServiceSolrIndex; import org.opencastproject.security.api.UnauthorizedException; import org.opencastproject.series.api.SeriesService; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.PathSupport; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Monadics; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.functions.Misc; import org.opencastproject.workflow.api.WorkflowDefinition; import org.opencastproject.workflow.api.WorkflowException; import org.opencastproject.workflow.api.WorkflowInstance; import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState; import org.opencastproject.workflow.api.WorkflowInstanceImpl; import org.opencastproject.workflow.api.WorkflowOperationInstance; import org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState; import org.opencastproject.workflow.api.WorkflowOperationInstanceImpl; import org.opencastproject.workflow.api.WorkflowService; import org.opencastproject.workspace.api.Workspace; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.component.VEvent; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.easymock.EasyMock; import org.easymock.IAnswer; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; public class SchedulerServiceImplTest { private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImplTest.class); private WorkflowService workflowService; private SeriesService seriesService; private Workspace workspace; private MessageSender messageSender; private MessageReceiver messageReceiver; private SchedulerServiceImpl schedSvc; private DublinCoreCatalogService dcSvc; private SchedulerServiceDatabaseImpl schedulerDatabase; // index private String indexStorage; private SchedulerServiceSolrIndex index; private String seriesIdentifier; private Map<String, String> wfProperties = new HashMap<String, String>(); private Map<String, String> wfPropertiesUpdated = new HashMap<String, String>(); @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { wfProperties.put("test", "true"); wfProperties.put("clear", "all"); wfPropertiesUpdated.put("test", "false"); wfPropertiesUpdated.put("skip", "true"); long startTime = System.currentTimeMillis(); indexStorage = PathSupport.concat("target", Long.toString(startTime)); index = new SchedulerServiceSolrIndex(indexStorage); dcSvc = new DublinCoreCatalogService(); index.setDublinCoreService(dcSvc); index.activate(null); schedulerDatabase = new SchedulerServiceDatabaseImpl(); schedulerDatabase.setEntityManagerFactory(mkTestEntityManagerFactory(SchedulerServiceDatabaseImpl.PERSISTENCE_UNIT)); dcSvc = new DublinCoreCatalogService(); schedulerDatabase.setDublinCoreService(dcSvc); schedulerDatabase.activate(null); WorkflowInstance workflowInstance = getSampleWorkflowInstance(); // workflow service workflowService = EasyMock.createMock(WorkflowService.class); EasyMock.expect( workflowService.start((WorkflowDefinition) EasyMock.anyObject(), (MediaPackage) EasyMock.anyObject(), (Map<String, String>) EasyMock.anyObject())).andAnswer(new IAnswer<WorkflowInstance>() { @Override public WorkflowInstance answer() throws Throwable { return getSampleWorkflowInstance(); } }).anyTimes(); EasyMock.expect(workflowService.getWorkflowById(EasyMock.anyLong())).andReturn(workflowInstance).anyTimes(); EasyMock.expect(workflowService.stop(EasyMock.anyLong())).andReturn(workflowInstance).anyTimes(); // update may be called multiple times workflowService.update((WorkflowInstance) EasyMock.anyObject()); EasyMock.expectLastCall().anyTimes(); seriesIdentifier = Long.toString(System.currentTimeMillis()); DublinCoreCatalog seriesCatalog = getSampleSeriesDublinCoreCatalog(seriesIdentifier); seriesService = EasyMock.createMock(SeriesService.class); EasyMock.expect(seriesService.getSeries(EasyMock.eq(seriesIdentifier))).andReturn(seriesCatalog).anyTimes(); workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect( workspace.put((String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(new URI("http://localhost:8080/test")).anyTimes(); messageSender = EasyMock.createNiceMock(MessageSender.class); final BaseMessage baseMessageMock = EasyMock.createNiceMock(BaseMessage.class); messageReceiver = EasyMock.createNiceMock(MessageReceiver.class); EasyMock.expect( messageReceiver.receiveSerializable(EasyMock.anyString(), EasyMock.anyObject(MessageSender.DestinationType.class))).andStubReturn( new FutureTask<Serializable>(new Callable<Serializable>() { @Override public Serializable call() throws Exception { return baseMessageMock; } })); EasyMock.replay(workflowService, seriesService, workspace, messageSender, baseMessageMock, messageReceiver); schedSvc = new SchedulerServiceImpl(); // Set the mocked interfaces schedSvc.setWorkflowService(workflowService); schedSvc.setSeriesService(seriesService); schedSvc.setIndex(index); schedSvc.setPersistence(schedulerDatabase); schedSvc.setWorkspace(workspace); schedSvc.setMessageSender(messageSender); schedSvc.setMessageReceiver(messageReceiver); schedSvc.setDublinCoreCatalogService(new DublinCoreCatalogService()); schedSvc.activate(null); } @After public void tearDown() throws Exception { schedSvc = null; index.deactivate(); index = null; FileUtils.deleteQuietly(new File(indexStorage)); schedulerDatabase = null; } protected WorkflowInstance getSampleWorkflowInstance() throws Exception { WorkflowInstanceImpl instance = new WorkflowInstanceImpl(); Random gen = new Random(System.currentTimeMillis()); instance.setId(gen.nextInt()); instance.setMediaPackage(MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().createNew()); instance.setState(WorkflowState.PAUSED); WorkflowOperationInstanceImpl op = new WorkflowOperationInstanceImpl(SchedulerServiceImpl.SCHEDULE_OPERATION_ID, OperationState.PAUSED); List<WorkflowOperationInstance> operations = new ArrayList<WorkflowOperationInstance>(); operations.add(op); instance.setOperations(operations); return instance; } protected DublinCoreCatalog getSampleSeriesDublinCoreCatalog(String seriesID) { DublinCoreCatalog dc = dcSvc.newInstance(); dc.set(PROPERTY_IDENTIFIER, seriesID); dc.set(PROPERTY_TITLE, "Demo series"); dc.set(PROPERTY_LICENSE, "demo"); dc.set(PROPERTY_PUBLISHER, "demo"); dc.set(PROPERTY_CREATOR, "demo"); dc.set(PROPERTY_SUBJECT, "demo"); dc.set(PROPERTY_SPATIAL, "demo"); dc.set(PROPERTY_RIGHTS_HOLDER, "demo"); dc.set(PROPERTY_EXTENT, "3600000"); dc.set(PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute)); dc.set(PROPERTY_LANGUAGE, "demo"); dc.set(PROPERTY_IS_REPLACED_BY, "demo"); dc.set(PROPERTY_TYPE, "demo"); dc.set(PROPERTY_AVAILABLE, EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute)); dc.set(PROPERTY_REPLACES, "demo"); dc.set(PROPERTY_CONTRIBUTOR, "demo"); dc.set(PROPERTY_DESCRIPTION, "demo"); return dc; } protected DublinCoreCatalog generateEvent(String captureDeviceID, Option<Long> eventId, Option<String> title, Date startTime, Date endTime) { DublinCoreCatalog dc = dcSvc.newInstance(); dc.set(PROPERTY_IDENTIFIER, Long.toString(eventId.getOrElse(1L))); dc.set(PROPERTY_TITLE, title.getOrElse("Demo event")); dc.set(PROPERTY_CREATOR, "demo"); dc.set(PROPERTY_SUBJECT, "demo"); dc.set(PROPERTY_TEMPORAL, EncodingSchemeUtils.encodePeriod(new DCMIPeriod(startTime, endTime), Precision.Second)); dc.set(PROPERTY_SPATIAL, captureDeviceID); dc.set(PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute)); dc.set(PROPERTY_LANGUAGE, "demo"); dc.set(PROPERTY_CONTRIBUTOR, "demo"); dc.set(PROPERTY_DESCRIPTION, "demo"); return dc; } protected DublinCoreCatalog generateEvent(String captureDeviceID, Date startTime, Date endTime) { return generateEvent(captureDeviceID, none(0L), none(""), startTime, endTime); } protected Properties generateCaptureAgentMetadata(String captureDeviceID) { Properties properties = new Properties(); properties.put("event.title", "Demo event"); properties.put("capture.device.id", captureDeviceID); return properties; } @Test public void testPersistence() throws Exception { DublinCoreCatalog event = generateEvent("demo", new Date(), new Date(System.currentTimeMillis() + 60000)); Long id = schedSvc.addEvent(event, wfProperties); Assert.assertNotNull(id); DublinCoreCatalog eventLoaded = schedSvc.getEventDublinCore(id); assertEquals(event.getFirst(PROPERTY_TITLE), eventLoaded.getFirst(PROPERTY_TITLE)); eventLoaded.set(PROPERTY_TITLE, "Something more"); schedSvc.updateEvent(id, eventLoaded, wfPropertiesUpdated); DublinCoreCatalog eventReloaded = schedSvc.getEventDublinCore(id); assertEquals("Something more", eventReloaded.getFirst(PROPERTY_TITLE)); Properties caProperties = generateCaptureAgentMetadata("demo"); schedSvc.updateCaptureAgentMetadata(caProperties, tuple(id, eventLoaded)); Assert.assertNotNull(schedSvc.getEventCaptureAgentConfiguration(id)); } @Test public void testEventManagement() throws Exception { DublinCoreCatalog event = generateEvent("testdevice", new Date(System.currentTimeMillis() - 2000), new Date(System.currentTimeMillis() + 60000)); event.set(PROPERTY_TITLE, "Demotitle"); Properties caProperties = generateCaptureAgentMetadata("testdevice"); Long id = schedSvc.addEvent(event, wfProperties); schedSvc.updateCaptureAgentMetadata(caProperties, tuple(id, schedSvc.getEventDublinCore(id))); // test iCalender export CalendarBuilder calBuilder = new CalendarBuilder(); Calendar cal; SchedulerQuery filter = new SchedulerQuery().setSpatial("testdevice"); try { String icalString = schedSvc.getCalendar(filter); cal = calBuilder.build(IOUtils.toInputStream(icalString, "UTF-8")); ComponentList vevents = cal.getComponents(VEVENT); for (int i = 0; i < vevents.size(); i++) { PropertyList attachments = ((VEvent) vevents.get(i)).getProperties(Property.ATTACH); for (int j = 0; j < attachments.size(); j++) { String attached = ((Property) attachments.get(j)).getValue(); String filename = ((Property) attachments.get(j)).getParameter("X-APPLE-FILENAME").getValue(); attached = new String(Base64.decodeBase64(attached)); if ("org.opencastproject.capture.agent.properties".equals(filename)) { Assert.assertTrue(attached.contains("capture.device.id=testdevice")); } if ("episode.xml".equals(filename)) { Assert.assertTrue(attached.contains("Demotitle")); } } } } catch (IOException e) { Assert.fail(e.getMessage()); } catch (ParserException e) { e.printStackTrace(); Assert.fail(e.getMessage()); } // test for upcoming events (it should not be in there). List<DublinCoreCatalog> upcoming = schedSvc.search(new SchedulerQuery().setStartsFrom(new Date())).getCatalogList(); Assert.assertTrue(upcoming.isEmpty()); List<DublinCoreCatalog> all = schedSvc.search(null).getCatalogList(); assertEquals(1, all.size()); all = schedSvc.search(new SchedulerQuery().setSpatial("somedevice")).getCatalogList(); Assert.assertTrue(upcoming.isEmpty()); // update event event.set(PROPERTY_TEMPORAL, EncodingSchemeUtils.encodePeriod(new DCMIPeriod(new Date( System.currentTimeMillis() + 180000), new Date(System.currentTimeMillis() + 600000)), Precision.Second)); schedSvc.updateEvent(id, event, wfPropertiesUpdated); // test for upcoming events (now it should be there) upcoming = schedSvc.search(new SchedulerQuery().setStartsFrom(new Date())).getCatalogList(); assertEquals(1, upcoming.size()); // delete event schedSvc.removeEvent(id); try { schedSvc.getEventDublinCore(id); Assert.fail(); } catch (NotFoundException e) { // this is an expected exception } } @Test public void testFindConflictingEvents() throws Exception { long currentTime = System.currentTimeMillis(); DublinCoreCatalog eventA = generateEvent("Device A", new Date(currentTime + 10 * 1000), new Date( currentTime + 3610000)); DublinCoreCatalog eventB = generateEvent("Device A", new Date(currentTime + 24 * 60 * 60 * 1000), new Date( currentTime + 25 * 60 * 60 * 1000)); DublinCoreCatalog eventC = generateEvent("Device C", new Date(currentTime - 60 * 60 * 1000), new Date( currentTime - 10 * 60 * 1000)); DublinCoreCatalog eventD = generateEvent("Device D", new Date(currentTime + 10 * 1000), new Date( currentTime + 3610000)); schedSvc.addEvent(eventA, wfProperties); schedSvc.addEvent(eventB, wfProperties); schedSvc.addEvent(eventC, wfProperties); schedSvc.addEvent(eventD, wfProperties); List<DublinCoreCatalog> allEvents = schedSvc.search(null).getCatalogList(); assertEquals(4, allEvents.size()); Date start = new Date(currentTime); Date end = new Date(currentTime + 60 * 60 * 1000); List<DublinCoreCatalog> events = schedSvc.findConflictingEvents("Some Other Device", start, end).getCatalogList(); assertEquals(0, events.size()); events = schedSvc.findConflictingEvents("Device A", start, end).getCatalogList(); assertEquals(1, events.size()); events = schedSvc.findConflictingEvents("Device A", "FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA", start, new Date(start.getTime() + (48 * 60 * 60 * 1000)), new Long(36000), "America/Chicago").getCatalogList(); assertEquals(2, events.size()); } @Test public void testCalendarCutoff() throws Exception { long currentTime = System.currentTimeMillis(); DublinCoreCatalog eventA = generateEvent("Device A", new Date(currentTime + 10 * 1000), new Date(currentTime + (60 * 60 * 1000))); DublinCoreCatalog eventB = generateEvent("Device A", new Date(currentTime + (20 * 24 * 60 * 60 * 1000)), new Date( currentTime + (20 * 25 * 60 * 60 * 1000))); schedSvc.addEvent(eventA, wfProperties); schedSvc.addEvent(eventB, wfProperties); Date start = new Date(currentTime); Date end = new Date(currentTime + 60 * 60 * 1000); SchedulerQuery filter = new SchedulerQuery().setSpatial("Device A").setEndsFrom(start).setStartsTo(end); List<DublinCoreCatalog> events = schedSvc.search(filter).getCatalogList(); assertEquals(1, events.size()); } /** * Create an event with a start date 1 minute in the past and an end date 60 minutes in to the future. Make sure the * event is listed when asking for the schedule of the capture agent. */ @Test public void testCalendarCutoffWithStartedEvent() throws Exception { long currentTime = System.currentTimeMillis(); Date startDate = new Date(currentTime - 10 * 1000); Date endDate = new Date(currentTime + (60 * 60 * 1000)); DublinCoreCatalog eventA = generateEvent("Device A", startDate, endDate); schedSvc.addEvent(eventA, wfProperties); Date start = new Date(currentTime); Date end = new Date(currentTime + 60 * 60 * 1000); SchedulerQuery filter = new SchedulerQuery().setSpatial("Device A").setEndsFrom(start).setStartsTo(end); List<DublinCoreCatalog> events = schedSvc.search(filter).getCatalogList(); assertEquals(1, events.size()); } /** * Make sure only current event is retrieved and excludes the one that has ended, MH-10991 * * @throws Exception */ public void testCalendarEndFrom() throws Exception { long currentTime = System.currentTimeMillis(); Date startDate = new Date(currentTime - 10 * 1000); Date endDate = new Date(currentTime + (60 * 60 * 1000)); DublinCoreCatalog eventCurrent = generateEvent("Device A", startDate, endDate); startDate = new Date(currentTime - (60 * 60 * 1000)); endDate = new Date(currentTime - (60 * 1000)); DublinCoreCatalog eventPast = generateEvent("Device A", startDate, endDate); schedSvc.addEvent(eventCurrent, wfProperties); schedSvc.addEvent(eventPast, wfProperties); Date now = new Date(currentTime); SchedulerQuery filter = new SchedulerQuery().setEndsFrom(now); List<DublinCoreCatalog> events = schedSvc.search(filter).getCatalogList(); assertEquals(1, events.size()); } @Test public void testSpatial() throws Exception { long currentTime = System.currentTimeMillis(); DublinCoreCatalog eventA = generateEvent("Device A", new Date(currentTime + 10 * 1000), new Date(currentTime + (60 * 60 * 1000))); DublinCoreCatalog eventB = generateEvent("Device B", new Date(currentTime + 10 * 1000), new Date(currentTime + (60 * 60 * 1000))); schedSvc.addEvent(eventA, wfProperties); schedSvc.addEvent(eventB, wfProperties); SchedulerQuery filter = new SchedulerQuery().setSpatial("Device"); List<DublinCoreCatalog> events = schedSvc.search(filter).getCatalogList(); assertEquals(0, events.size()); filter = new SchedulerQuery().setSpatial("Device A"); events = schedSvc.search(filter).getCatalogList(); assertEquals(1, events.size()); filter = new SchedulerQuery().setSpatial("Device B"); events = schedSvc.search(filter).getCatalogList(); assertEquals(1, events.size()); filter = new SchedulerQuery().setText("Device"); events = schedSvc.search(filter).getCatalogList(); assertEquals(2, events.size()); } @Test public void testCalendarNotModified() throws Exception { HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); EasyMock.replay(request); SchedulerRestService restService = new SchedulerRestService(); restService.setService(schedSvc); restService.setDublinCoreService(dcSvc); String device = "Test Device"; // Store an event final DublinCoreCatalog event = generateEvent(device, new Date(), new Date(System.currentTimeMillis() + 60000)); final long eventId = schedSvc.addEvent(event, wfProperties); // Request the calendar without specifying an etag. We should get a 200 with the icalendar in the response body Response response = restService.getCalendar(device, null, null, request); Assert.assertNotNull(response.getEntity()); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); final String etag = (String) response.getMetadata().getFirst(HttpHeaders.ETAG); EasyMock.reset(request); EasyMock.expect(request.getHeader("If-None-Match")).andAnswer(new IAnswer<String>() { @Override public String answer() throws Throwable { return etag; } }).anyTimes(); EasyMock.replay(request); // Request using the etag from the first response. We should get a 304 (not modified) response = restService.getCalendar(device, null, null, request); assertEquals(HttpServletResponse.SC_NOT_MODIFIED, response.getStatus()); Assert.assertNull(response.getEntity()); // Update the event and clear to cache to make sure it's reloaded schedSvc.updateEvent(eventId, event, wfPropertiesUpdated); schedSvc.lastModifiedCache.invalidateAll(); // Try using the same old etag. We should get a 200, since the event has changed response = restService.getCalendar(device, null, null, request); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); Assert.assertNotNull(response.getEntity()); String secondEtag = (String) response.getMetadata().getFirst(HttpHeaders.ETAG); Assert.assertNotNull(secondEtag); Assert.assertFalse(etag.equals(secondEtag)); } @Test public void testUpdateEvent() throws Exception { final long currentTime = System.currentTimeMillis(); final String initialTitle = "Recording 1"; final DublinCoreCatalog initalEvent = generateEvent("Device A", none(0L), some(initialTitle), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); final Long eventId = schedSvc.addEvent(initalEvent, wfProperties); schedSvc.updateCaptureAgentMetadata( properties(tuple("org.opencastproject.workflow.config.archiveOp", "true"), tuple("org.opencastproject.workflow.definition", "full")), tuple(eventId, initalEvent)); final Properties initalCaProps = schedSvc.getEventCaptureAgentConfiguration(eventId); logger.info("Added event " + eventId); checkEvent(eventId, initalCaProps, initialTitle); // do single update final String updatedTitle1 = "Recording 2"; final DublinCoreCatalog updatedEvent1 = generateEvent("Device A", some(eventId), some(updatedTitle1), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); schedSvc.updateEvent(eventId, updatedEvent1, wfPropertiesUpdated); checkEvent(eventId, initalCaProps, updatedTitle1); // do bulk update final String updatedTitle2 = "Recording 3"; final String expectedTitle2 = "Recording 3 1"; final DublinCoreCatalog updatedEvent2 = generateEvent("Device A", none(0L), some(updatedTitle2), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); schedSvc.updateEvents(list(eventId), updatedEvent2); checkEvent(eventId, initalCaProps, expectedTitle2); } @Test public void testEventStatus() throws Exception { final long currentTime = System.currentTimeMillis(); final String initialTitle = "Recording 1"; final DublinCoreCatalog initalEvent = generateEvent("Device A", none(0L), some(initialTitle), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); final Long eventId = schedSvc.addEvent(initalEvent, wfProperties); schedSvc.updateCaptureAgentMetadata( properties(tuple("org.opencastproject.workflow.config.archiveOp", "true"), tuple("org.opencastproject.workflow.definition", "full")), tuple(eventId, initalEvent)); final Properties initalCaProps = schedSvc.getEventCaptureAgentConfiguration(eventId); logger.info("Added event " + eventId); checkEvent(eventId, initalCaProps, initialTitle); String mediaPackageId = schedSvc.getMediaPackageId(eventId); Assert.assertFalse(schedSvc.isOptOut(mediaPackageId)); Assert.assertFalse(schedSvc.isBlacklisted(mediaPackageId)); // do opt out update schedSvc.updateOptOutStatus(mediaPackageId, true); Assert.assertTrue(schedSvc.isOptOut(mediaPackageId)); // do blacklist update schedSvc.updateBlacklistStatus(mediaPackageId, true); Assert.assertTrue(schedSvc.isBlacklisted(mediaPackageId)); } @Test /** * Test for failure updating past events * This test construct new SchedulerService to mock the getCurrentDate method * @throws Exception */ public void testUpdateExpiredEvent() throws Exception { SchedulerServiceImpl schedSvc2 = EasyMock.createMockBuilder(SchedulerServiceImpl.class) .addMockedMethod("getCurrentDate").createMock(); // Mock the getCurrentDate method to skip to the future long currentTime = System.currentTimeMillis(); Date futureSystemDate = new Date(currentTime + 6610000); EasyMock.expect(schedSvc2.getCurrentDate()).andReturn(futureSystemDate).anyTimes(); EasyMock.replay(schedSvc2); // Set the mocked interfaces schedSvc2.setWorkflowService(workflowService); schedSvc2.setSeriesService(seriesService); schedSvc2.setIndex(index); schedSvc2.setPersistence(schedulerDatabase); schedSvc2.setWorkspace(workspace); schedSvc2.setMessageSender(messageSender); schedSvc2.setMessageReceiver(messageReceiver); final String initialTitle = "Recording 1"; final DublinCoreCatalog initalEvent = generateEvent("Device A", none(0L), some(initialTitle), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); Long eventId = null; try { eventId = schedSvc2.addEvent(initalEvent, wfProperties); schedSvc2.updateCaptureAgentMetadata( properties(tuple("org.opencastproject.workflow.config.archiveOp", "true"), tuple("org.opencastproject.workflow.definition", "full")), tuple(eventId, initalEvent)); } catch (Exception e) { logger.info("Exception " + e.getClass().getCanonicalName() + " message " + e.getMessage()); } final Properties initalCaProps = schedSvc.getEventCaptureAgentConfiguration(eventId); logger.info("Added event " + eventId); checkEvent(eventId, initalCaProps, initialTitle); // test single update try { final String updatedTitle1 = "Recording 2"; final DublinCoreCatalog updatedEvent1 = generateEvent("Device A", some(eventId), some(updatedTitle1), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); schedSvc2.updateEvent(eventId, updatedEvent1, wfPropertiesUpdated); checkEvent(eventId, initalCaProps, updatedTitle1); Assert.fail("Schedule should not update a recording that has ended (single)"); } catch (SchedulerException e) { logger.info("Expected exception: " + e.getMessage()); } try { // test bulk update final String updatedTitle2 = "Recording 3"; final String expectedTitle2 = "Recording 3 1"; final DublinCoreCatalog updatedEvent2 = generateEvent("Device A", none(0L), some(updatedTitle2), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); schedSvc2.updateEvents(list(eventId), updatedEvent2); checkEvent(eventId, initalCaProps, expectedTitle2); Assert.fail("Schedule should not update a recording that has ended (multi)"); } catch (SchedulerException e) { logger.info("Expected exception: " + e.getMessage()); } finally { schedSvc2 = null; } } @Test /** * Test that opted out and blacklisted events don't end up in the calendar but regular events do. * @throws Exception */ public void testGetCalendarInputRegularOptedOutBlacklistedExpectsOnlyRegularEvents() throws Exception { SchedulerServiceImpl schedulerServiceImpl = EasyMock.createMockBuilder(SchedulerServiceImpl.class) .addMockedMethod("getCurrentDate").createMock(); // Mock the getCurrentDate method to skip to the future long currentTime = System.currentTimeMillis(); Date futureSystemDate = new Date(currentTime + 6610000); EasyMock.expect(schedulerServiceImpl.getCurrentDate()).andReturn(futureSystemDate).anyTimes(); EasyMock.replay(schedulerServiceImpl); // Set the mocked interfaces schedulerServiceImpl.setWorkflowService(workflowService); schedulerServiceImpl.setSeriesService(seriesService); schedulerServiceImpl.setIndex(index); schedulerServiceImpl.setPersistence(schedulerDatabase); schedulerServiceImpl.setWorkspace(workspace); schedulerServiceImpl.setMessageSender(messageSender); schedulerServiceImpl.setMessageReceiver(messageReceiver); schedulerServiceImpl.setDublinCoreCatalogService(new DublinCoreCatalogService()); int optedOutCount = 3; int blacklistedCount = 5; int bothCount = 7; int regularCount = 9; String optedOutPrefix = "OptedOut"; String blacklistedPrefix = "Blacklisted"; String bothPrefix = "Both"; String regularPrefix = "Regular"; List<Long> optedOutEvents = createEvents(optedOutPrefix, optedOutCount, schedulerServiceImpl, true, false); assertEquals(optedOutCount, optedOutEvents.size()); List<Long> blacklistedEvents = createEvents(blacklistedPrefix, blacklistedCount, schedulerServiceImpl, false, true); assertEquals(blacklistedCount, blacklistedEvents.size()); List<Long> bothOptedOutEventsAndBlacklisted = createEvents(bothPrefix, bothCount, schedulerServiceImpl, true, true); assertEquals(bothCount, bothOptedOutEventsAndBlacklisted.size()); List<Long> regularEvents = createEvents(regularPrefix, regularCount, schedulerServiceImpl, false, false); assertEquals(regularCount, regularEvents.size()); checkEventStatus(schedulerDatabase, optedOutEvents, true, false); checkEventStatus(schedulerDatabase, blacklistedEvents, false, true); checkEventStatus(schedulerDatabase, bothOptedOutEventsAndBlacklisted, true, true); checkEventStatus(schedulerDatabase, regularEvents, false, false); SchedulerQuery query = new SchedulerQuery(); String calendar = schedulerServiceImpl.getCalendar(query); assertEquals("There shouldn't be any events that are opted out.", -1, calendar.indexOf(optedOutPrefix)); assertEquals("There shouldn't be any events that are blacklisted.", -1, calendar.indexOf(blacklistedPrefix)); assertEquals("There shouldn't be any events that are both blacklisted and opted out.", -1, calendar.indexOf(bothPrefix)); assertEquals("All of the regular events should be in the calendar.", regularCount, getCountFromString(regularPrefix, calendar)); } private int getCountFromString(String searchTerm, String value) { int count = 0; int index = 0; while (value.indexOf(searchTerm, index) != -1) { count++; index = value.indexOf(searchTerm, index) + searchTerm.length(); } return count; } private void checkEventStatus(SchedulerServiceDatabase schedulerServiceDatabase, List<Long> events, boolean optedOut, boolean blacklisted) throws NotFoundException, SchedulerServiceDatabaseException { for (Long eventId : events) { assertEquals(optedOut, schedulerServiceDatabase.isOptOut(eventId)); assertEquals(blacklisted, schedulerServiceDatabase.isBlacklisted(eventId)); } } private List<Long> createEvents(String titlePrefix, int number, SchedulerServiceImpl schedulerServiceImpl, boolean optedout, boolean blacklisted) { List<Long> optedOutEvents = new ArrayList<Long>(); final long currentTime = System.currentTimeMillis(); for (int i = 0; i < number; i++) { final DublinCoreCatalog event = generateEvent("Device A", none(0L), some(titlePrefix + "-" + i), new Date( currentTime + 10 * 1000), new Date(currentTime + 3610000)); try { long eventId = schedulerServiceImpl.addEvent(event, wfProperties); String mediaPackageId = schedulerServiceImpl.getMediaPackageId(eventId); schedulerServiceImpl.updateOptOutStatus(mediaPackageId, optedout); schedulerServiceImpl.updateBlacklistStatus(mediaPackageId, blacklisted); optedOutEvents.add(eventId); } catch (Exception e) { logger.info("Exception " + e.getClass().getCanonicalName() + " message " + e.getMessage()); } } return optedOutEvents; } @Test public void getCutOffDateWorksForDaylightSavingsTime() { DateTimeZone zurichDateTimeZone = DateTimeZone.forID("Europe/Zurich"); /** * Is a date and time that isn't around a daylight savings time Jan 1st, 2013 @ 3:00am. After the buffer is * subtracted it should be Jan 1st, 2013 @ 2:00am local time, Jan 1st, 2013 1:00am GMT. */ DateTime normalTime = new DateTime(2013, 1, 2, 3, 7, zurichDateTimeZone); DateTime normalCutoff = SchedulerServiceImpl.getCutoffDate(3600, normalTime); assertEquals(2013, normalCutoff.getYear()); assertEquals(01, normalCutoff.getMonthOfYear()); assertEquals(02, normalCutoff.getDayOfMonth()); assertEquals(01, normalCutoff.getHourOfDay()); assertEquals(07, normalCutoff.getMinuteOfHour()); /** * Sunday, March 31, 2013, 2:00:00 AM clocks were turned forward 1 hour to Sunday, March 31, 2013, 3:00:00 AM local * daylight time instead. Therefore, March 31, 2013 @ 3:01am back 1 hour will be March 31, 2013 @ 1:01am Zurich time * and 0:01am GMT due to time zone difference of 1 hour. */ DateTime forwardTime = new DateTime(2013, 03, 31, 3, 1, zurichDateTimeZone); DateTime forwardCutOff = SchedulerServiceImpl.getCutoffDate(3600, forwardTime); assertEquals(2013, forwardCutOff.getYear()); assertEquals(03, forwardCutOff.getMonthOfYear()); assertEquals(31, forwardCutOff.getDayOfMonth()); assertEquals(00, forwardCutOff.getHourOfDay()); assertEquals(01, forwardCutOff.getMinuteOfHour()); /** * Sunday, October 27, 2013, 3:00:00 AM clocks were turned backward 1 hour to Sunday, October 27, 2013, 2:00:00 AM * local standard time instead. Therefore, Oct. 27th, 2013 @ 2:01am back 1 hour is 1:01 am local time and 23:01 GMT. */ DateTime backwardTime = new DateTime(2013, 10, 27, 2, 1, zurichDateTimeZone); DateTime backwardCutoff = SchedulerServiceImpl.getCutoffDate(3600, backwardTime); assertEquals(2013, backwardCutoff.getYear()); assertEquals(10, backwardCutoff.getMonthOfYear()); assertEquals(26, backwardCutoff.getDayOfMonth()); assertEquals(23, backwardCutoff.getHourOfDay()); assertEquals(01, backwardCutoff.getMinuteOfHour()); } @Test(expected = SchedulerException.class) public void removeScheduledRecordingsBeforeBufferInputSchedulerExceptionExpectsIllegalStateException() throws SchedulerException, SchedulerServiceDatabaseException { SchedulerServiceIndex index = EasyMock.createMock(SchedulerServiceIndex.class); EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andThrow( new SchedulerServiceDatabaseException("Mock exception")); EasyMock.replay(index); SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setIndex(index); service.setMessageSender(messageSender); service.setMessageReceiver(messageReceiver); service.removeScheduledRecordingsBeforeBuffer(0); } @Test public void removeScheduledRecordingsBeforeBufferInputEmptyFinishedSchedulesExpectsNoException() throws SchedulerException, SchedulerServiceDatabaseException { // Setup data LinkedList<DublinCoreCatalog> catalogs = new LinkedList<DublinCoreCatalog>(); DublinCoreCatalogList list = new DublinCoreCatalogList(catalogs, catalogs.size()); // Setup index SchedulerServiceIndex index = EasyMock.createMock(SchedulerServiceIndex.class); EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andReturn(list); EasyMock.replay(index); // Run test SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setIndex(index); service.removeScheduledRecordingsBeforeBuffer(0); } @Test public void removeScheduledRecordingsBeforeBufferInputOneEventEmptyExpectsNoEventDeleted() throws SchedulerException, SchedulerServiceDatabaseException { // Setup data DublinCoreCatalog catalog = EasyMock.createMock(DublinCoreCatalog.class); EasyMock.expect(catalog.getFirst(EasyMock.anyObject(EName.class))).andReturn(null); EasyMock.replay(catalog); LinkedList<DublinCoreCatalog> catalogs = new LinkedList<DublinCoreCatalog>(); catalogs.add(catalog); DublinCoreCatalogList list = new DublinCoreCatalogList(catalogs, catalogs.size()); // Setup index SchedulerServiceIndex index = EasyMock.createMock(SchedulerServiceIndex.class); EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andReturn(list); EasyMock.replay(index); // Run test SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setMessageSender(messageSender); service.setMessageReceiver(messageReceiver); service.setIndex(index); service.removeScheduledRecordingsBeforeBuffer(0); } private URI[] createMediapackageURIs(long[] ids) throws URISyntaxException { URI[] uris = new URI[ids.length]; for (int i = 0; i < ids.length; i++) { long id = ids[i]; URI uri = new URI("location" + id); uris[i] = uri; } return uris; } private Workspace createWorkspace(URI[] uris, boolean throwException) throws NotFoundException, IOException { Workspace workspace = EasyMock.createMock(Workspace.class); for (int i = 0; i < uris.length; i++) { workspace.delete(uris[i]); if (throwException && i == 0) { EasyMock.expectLastCall().andThrow(new NotFoundException("Mock Exception")); } else { EasyMock.expectLastCall(); } } EasyMock.replay(workspace); return workspace; } private WorkflowService createWorkflowService(long[] ids, URI[] uris) throws WorkflowException, NotFoundException, UnauthorizedException { WorkflowService workflowService = EasyMock.createMock(WorkflowService.class); for (int i = 0; i < ids.length; i++) { long id = ids[i]; URI uri = uris[i]; // Setup elements MediaPackageElement element = EasyMock.createMock(MediaPackageElement.class); EasyMock.expect(element.getURI()).andReturn(uri); EasyMock.replay(element); MediaPackageElement[] elements = { element }; MediaPackage mediaPackage = EasyMock.createMock(MediaPackage.class); EasyMock.expect(mediaPackage.getElements()).andReturn(elements); EasyMock.replay(mediaPackage); // Setup WorkflowInstance WorkflowInstance workflowServiceInstance = EasyMock.createMock(WorkflowInstance.class); EasyMock.expect(workflowServiceInstance.getMediaPackage()).andReturn(mediaPackage); EasyMock.replay(workflowServiceInstance); EasyMock.expect(workflowService.stop(id)).andReturn(workflowServiceInstance); } EasyMock.replay(workflowService); return workflowService; } private SchedulerServiceDatabase setupPersistence(long[] ids) throws NotFoundException, SchedulerServiceDatabaseException { SchedulerServiceDatabase persistence = EasyMock.createMock(SchedulerServiceDatabase.class); EasyMock.expect(persistence.getMediaPackageId(EasyMock.anyLong())).andReturn("uuid").anyTimes(); for (long id : ids) { persistence.deleteEvent(id); EasyMock.expectLastCall(); } EasyMock.replay(persistence); return persistence; } private SchedulerServiceIndex createIndex(long[] ids, boolean throwSchedulerException, boolean throwNullPointerException) throws SchedulerServiceDatabaseException { LinkedList<DublinCoreCatalog> catalogs = new LinkedList<DublinCoreCatalog>(); for (long id : ids) { DublinCoreCatalog catalog = EasyMock.createMock(DublinCoreCatalog.class); EasyMock.expect(catalog.getFirst(EasyMock.anyObject(EName.class))).andReturn(Long.toString(id)); EasyMock.replay(catalog); catalogs.add(catalog); } DublinCoreCatalogList list = new DublinCoreCatalogList(catalogs, catalogs.size()); SchedulerServiceIndex index = EasyMock.createMock(SchedulerServiceIndex.class); if (throwSchedulerException) { EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andThrow( new SchedulerException("Mock scheduler exception")); } else if (throwNullPointerException) { EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andThrow( new NullPointerException("Mock null exception")); } else { EasyMock.expect(index.search(EasyMock.anyObject(SchedulerQuery.class))).andReturn(list); for (long id : ids) { index.delete(id); EasyMock.expectLastCall(); } } EasyMock.replay(index); return index; } @Test public void removeScheduledRecordingsBeforeBufferInputOneEventOneIDExpectsEventDeleted() throws SchedulerException, NotFoundException, UnauthorizedException, SchedulerServiceDatabaseException, URISyntaxException, IOException, WorkflowException { // Setup data long[] ids = { 1L }; URI[] uris = createMediapackageURIs(ids); // Run test SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setIndex(createIndex(ids, false, false)); service.setWorkspace(createWorkspace(uris, false)); service.setWorkflowService(createWorkflowService(ids, uris)); service.setPersistence(setupPersistence(ids)); service.setMessageSender(messageSender); service.setMessageReceiver(messageReceiver); service.removeScheduledRecordingsBeforeBuffer(0); } @Test public void scanInputMultipleEventOneIDExpectsEventsDeleted() throws SchedulerException, NotFoundException, UnauthorizedException, SchedulerServiceDatabaseException, URISyntaxException, IOException, WorkflowException { // Setup data long[] ids = { 4L, 5L, 6L }; URI[] uris = createMediapackageURIs(ids); // Run test SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setIndex(createIndex(ids, false, false)); service.setWorkspace(createWorkspace(uris, false)); service.setWorkflowService(createWorkflowService(ids, uris)); service.setPersistence(setupPersistence(ids)); service.setMessageSender(messageSender); service.setMessageReceiver(messageReceiver); service.removeScheduledRecordingsBeforeBuffer(0); } @Test public void scanInputWorkSpaceExceptionExpectsProperEventDeleted() throws SchedulerException, NotFoundException, UnauthorizedException, SchedulerServiceDatabaseException, URISyntaxException, IOException, WorkflowException { // Setup data long[] ids = { 7L, 8L, 9L }; // Setup workspace URI[] uris = createMediapackageURIs(ids); // Run test SchedulerServiceImpl service = new SchedulerServiceImpl(); service.setIndex(createIndex(ids, false, false)); service.setWorkspace(createWorkspace(uris, true)); service.setWorkflowService(createWorkflowService(ids, uris)); service.setPersistence(setupPersistence(ids)); service.setMessageSender(messageSender); service.setMessageReceiver(messageReceiver); service.removeScheduledRecordingsBeforeBuffer(0); } private void checkEvent(long eventId, Properties initialCaProps, String title) throws Exception { final Properties updatedCaProps = (Properties) initialCaProps.clone(); updatedCaProps.setProperty("event.title", title); assertTrue("CA properties", eqMap(updatedCaProps, schedSvc.getEventCaptureAgentConfiguration(eventId))); assertEquals(Long.toString(eventId), schedSvc.getEventDublinCore(eventId).getFirst(PROPERTY_IDENTIFIER)); assertEquals("DublinCore title", title, schedSvc.getEventDublinCore(eventId).getFirst(PROPERTY_TITLE)); checkIcalFeed(updatedCaProps, title); } private void checkIcalFeed(Properties caProps, String title) throws Exception { final String cs = schedSvc.getCalendar(new SchedulerQuery()); final Calendar cal = new CalendarBuilder().build(new StringReader(cs)); assertEquals("number of entries", 1, cal.getComponents().size()); for (Object co : cal.getComponents()) { final Component c = (Component) co; assertEquals("SUMMARY property should contain the DC title", title, c.getProperty(Property.SUMMARY).getValue()); final Monadics.ListMonadic<Property> attachments = mlist(c.getProperties(Property.ATTACH)).map( Misc.<Object, Property> cast()); // episode dublin core final List<DublinCoreCatalog> dcsIcal = attachments .filter(byParamNameAndValue("X-APPLE-FILENAME", "episode.xml")).map(parseDc.o(decodeBase64).o(getValue)) .value(); assertEquals("number of episode DCs", 1, dcsIcal.size()); assertEquals("dcterms:title", title, dcsIcal.get(0).getFirst(PROPERTY_TITLE)); // capture agent properties final List<Properties> caPropsIcal = attachments .filter(byParamNameAndValue("X-APPLE-FILENAME", "org.opencastproject.capture.agent.properties")) .map(parseProperties.o(decodeBase64).o(getValue)).value(); assertEquals("number of CA property sets", 1, caPropsIcal.size()); assertTrue("CA properties", eqMap(caProps, caPropsIcal.get(0))); } } private Function<Property, Boolean> byParamNameAndValue(final String name, final String value) { return new Function<Property, Boolean>() { @Override public Boolean apply(Property p) { final Parameter param = p.getParameter(name); return param != null && param.getValue().equals(value); } }; } private static Function<Property, String> getValue = new Function<Property, String>() { @Override public String apply(Property property) { return property.getValue(); } }; private static Function<String, String> decodeBase64 = new Function<String, String>() { @Override public String apply(String base64) { return new String(Base64.decodeBase64(base64)); } }; private static Function<String, DublinCoreCatalog> parseDc = new Function<String, DublinCoreCatalog>() { @Override public DublinCoreCatalog apply(String s) { return DublinCores.read(IOUtils.toInputStream(s)); } }; private static Function<String, Properties> parseProperties = new Function.X<String, Properties>() { @Override public Properties xapply(String s) throws Exception { final Properties p = new Properties(); p.load(new StringReader(s)); return p; } }; }