/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you 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 org.fcrepo.integration.kernel.modeshape.observer;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.SECONDS;
import static com.jayway.awaitility.Awaitility.await;
import static com.jayway.awaitility.Duration.ONE_HUNDRED_MILLISECONDS;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_RESOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_CREATION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_DELETION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_MODIFICATION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_RELOCATION;
import static org.fcrepo.kernel.api.utils.ContentDigest.DIGEST_ALGORITHM.SHA1;
import static org.fcrepo.kernel.api.utils.ContentDigest.asURI;
import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession;
import static org.junit.Assert.assertEquals;
import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
import static org.modeshape.jcr.api.JcrConstants.NT_FILE;
import static org.modeshape.jcr.api.JcrConstants.NT_FOLDER;
import static org.modeshape.jcr.api.JcrConstants.NT_RESOURCE;
import java.io.ByteArrayInputStream;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.fcrepo.integration.kernel.modeshape.AbstractIT;
import org.fcrepo.kernel.api.FedoraRepository;
import org.fcrepo.kernel.api.FedoraSession;
import org.fcrepo.kernel.api.exception.InvalidChecksumException;
import org.fcrepo.kernel.api.models.Container;
import org.fcrepo.kernel.api.models.FedoraBinary;
import org.fcrepo.kernel.api.observer.EventType;
import org.fcrepo.kernel.api.observer.FedoraEvent;
import org.fcrepo.kernel.api.services.ContainerService;
import org.fcrepo.kernel.api.services.NodeService;
import org.fcrepo.kernel.modeshape.FedoraBinaryImpl;
import org.fcrepo.kernel.modeshape.rdf.impl.DefaultIdentifierTranslator;
import org.fcrepo.kernel.modeshape.services.NodeServiceImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.jcr.api.JcrTools;
import org.modeshape.jcr.api.ValueFactory;
import org.springframework.test.context.ContextConfiguration;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import org.apache.jena.rdf.model.Resource;
/**
* <p>SimpleObserverIT class.</p>
*
* @author awoods
* @author acoburn
*/
@ContextConfiguration({"/spring-test/eventing.xml", "/spring-test/repo.xml"})
public class SimpleObserverIT extends AbstractIT {
private volatile List<FedoraEvent> events;
private Integer eventBusMessageCount;
@Inject
private FedoraRepository repository;
@Inject
private EventBus eventBus;
@Inject
private ContainerService containerService;
@Test
public void testEventBusPublishing() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
se.getRootNode().addNode("/object1").addMixin(FEDORA_CONTAINER);
se.getRootNode().addNode("/object2").addMixin(FEDORA_CONTAINER);
session.commit();
session.expire();
// Should be two messages, for each time
// each node becomes a Fedora object
awaitEvent("/object1", RESOURCE_CREATION, REPOSITORY_NAMESPACE + "Container");
awaitEvent("/object2", RESOURCE_CREATION, REPOSITORY_NAMESPACE + "Container");
assertEquals("Where are my messages!?", (Integer) 2, eventBusMessageCount);
}
@Test
public void contentEventCollapsing() throws RepositoryException, InvalidChecksumException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final JcrTools jcrTools = new JcrTools();
final Node n = jcrTools.findOrCreateNode(se.getRootNode(), "/object3", NT_FOLDER, NT_FILE);
n.addMixin(FEDORA_RESOURCE);
final String content = "test content";
final String checksum = "1eebdf4fdc9fc7bf283031b93f9aef3338de9052";
((ValueFactory) se.getValueFactory()).createBinary(new ByteArrayInputStream(content.getBytes()), null);
final Node contentNode = jcrTools.findOrCreateChild(n, JCR_CONTENT, NT_RESOURCE);
contentNode.addMixin(FEDORA_BINARY);
final FedoraBinary binary = new FedoraBinaryImpl(contentNode);
binary.setContent( new ByteArrayInputStream(content.getBytes()), "text/plain",
new HashSet<>(asList(asURI(SHA1.algorithm, checksum))), "text.txt", null);
try {
session.commit();
} finally {
session.expire();
}
awaitEvent("/object3", RESOURCE_CREATION, REPOSITORY_NAMESPACE + "Binary");
assertEquals("Node and content events not collapsed!", (Integer) 1, eventBusMessageCount);
}
@Test
public void testMoveEvent() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final NodeService ns = new NodeServiceImpl();
final Node n = se.getRootNode().addNode("/object4");
n.addMixin(FEDORA_CONTAINER);
n.addNode("/child1").addMixin(FEDORA_CONTAINER);
n.addNode("/child2").addMixin(FEDORA_CONTAINER);
session.commit();
ns.moveObject(session, "/object4", "/object5");
session.commit();
session.expire();
awaitEvent("/object4", RESOURCE_CREATION);
awaitEvent("/object4/child1", RESOURCE_CREATION);
awaitEvent("/object4/child2", RESOURCE_CREATION);
awaitEvent("/object5", RESOURCE_RELOCATION);
awaitEvent("/object5/child1", RESOURCE_RELOCATION);
awaitEvent("/object5/child2", RESOURCE_RELOCATION);
awaitEvent("/object4", RESOURCE_DELETION);
awaitEvent("/object4/child1", RESOURCE_DELETION);
awaitEvent("/object4/child2", RESOURCE_DELETION);
assertEquals("Move operation didn't generate additional events", (Integer) 9, eventBusMessageCount);
}
@Test
public void testMoveContainedEvent() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final NodeService ns = new NodeServiceImpl();
final Node n = se.getRootNode().addNode("/object6");
n.addMixin(FEDORA_CONTAINER);
final Node child = n.addNode("/object7");
child.addMixin(FEDORA_CONTAINER);
child.addNode("/child1").addMixin(FEDORA_CONTAINER);
child.addNode("/child2").addMixin(FEDORA_CONTAINER);
session.commit();
ns.moveObject(session, "/object6/object7", "/object6/object8");
session.commit();
session.expire();
awaitEvent("/object6", RESOURCE_CREATION);
awaitEvent("/object6/object7", RESOURCE_CREATION);
awaitEvent("/object6/object7/child1", RESOURCE_CREATION);
awaitEvent("/object6/object7/child2", RESOURCE_CREATION);
awaitEvent("/object6/object8", RESOURCE_RELOCATION);
awaitEvent("/object6/object8/child1", RESOURCE_RELOCATION);
awaitEvent("/object6/object8/child2", RESOURCE_RELOCATION);
awaitEvent("/object6/object7", RESOURCE_DELETION);
awaitEvent("/object6/object7/child1", RESOURCE_DELETION);
awaitEvent("/object6/object7/child2", RESOURCE_DELETION);
// should produce two of these
awaitEvent("/object6", RESOURCE_MODIFICATION);
assertEquals("Move operation didn't generate additional events", (Integer) 12, eventBusMessageCount);
}
@Test
public void testHashUriEvent() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(se);
final Container obj = containerService.findOrCreate(session, "/object9");
final Resource subject = subjects.reverse().convert(obj);
obj.updateProperties(subjects, "PREFIX dc: <http://purl.org/dc/elements/1.1/>\n" +
"PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n" +
"INSERT { <" + subject + "> dc:contributor <" + subject + "#contributor> .\n" +
"<" + subject + "#contributor> foaf:name \"some creator\" . } WHERE {}",
obj.getTriples(subjects, PROPERTIES));
session.commit();
session.expire();
// these first two are part of a single event
awaitEvent("/object9", RESOURCE_CREATION);
awaitEvent("/object9", RESOURCE_MODIFICATION);
awaitEvent("/object9#contributor", RESOURCE_MODIFICATION);
// FIXME -- it is unclear where this event is coming from; clearly from the hashURI,
// but it doesn't seem right.
awaitEvent("", RESOURCE_MODIFICATION);
// FIXME -- as hinted above, there is an extra event here somehow related to the hashURI.
assertEquals("Where are my events?", (Integer) 3, eventBusMessageCount);
}
@Test
public void testDirectContainerEvent() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(se);
final Container obj1 = containerService.findOrCreate(session, "/object10");
final Container obj2 = containerService.findOrCreate(session, "/object11");
final Resource subject2 = subjects.reverse().convert(obj2);
obj1.updateProperties(subjects, "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" +
"PREFIX pcdm: <http://pcdm.org/models#>\n" +
"INSERT { <> a ldp:DirectContainer ;\n" +
"ldp:membershipResource <" + subject2 + "> ;\n" +
"ldp:hasMemberRelation pcdm:hasMember . } WHERE {}", obj1.getTriples(subjects, PROPERTIES));
try {
session.commit();
awaitEvent("/object10", RESOURCE_CREATION);
awaitEvent("/object10", RESOURCE_MODIFICATION);
awaitEvent("/object11", RESOURCE_CREATION);
assertEquals("Where are my events?", (Integer) 3, eventBusMessageCount);
final Container obj3 = containerService.findOrCreate(session, "/object10/child");
session.commit();
awaitEvent("/object10/child", RESOURCE_CREATION);
awaitEvent("/object10", RESOURCE_MODIFICATION);
awaitEvent("/object11", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 6, eventBusMessageCount);
obj3.delete();
session.commit();
} finally {
session.expire();
}
awaitEvent("/object10/child", RESOURCE_DELETION);
awaitEvent("/object10", RESOURCE_MODIFICATION);
awaitEvent("/object11", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 9, eventBusMessageCount);
}
@Test
public void testIndirectContainerEvent() throws RepositoryException {
final FedoraSession session = repository.login();
final Session se = getJcrSession(session);
final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(se);
final Container obj1 = containerService.findOrCreate(session, "/object12");
final Container obj2 = containerService.findOrCreate(session, "/object13");
final Resource subject2 = subjects.reverse().convert(obj2);
obj1.updateProperties(subjects, "PREFIX ldp: <http://www.w3.org/ns/ldp#>\n" +
"PREFIX pcdm: <http://pcdm.org/models#>\n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/>\n" +
"INSERT { <> a ldp:IndirectContainer ;\n" +
"ldp:membershipResource <" + subject2 + "> ;\n" +
"ldp:hasMemberRelation pcdm:hasMember ;\n" +
"ldp:insertedContentRelation ore:proxyFor. } WHERE {}", obj1.getTriples(subjects, PROPERTIES));
try {
session.commit();
awaitEvent("/object12", RESOURCE_CREATION);
awaitEvent("/object12", RESOURCE_MODIFICATION);
awaitEvent("/object13", RESOURCE_CREATION);
assertEquals("Where are my events?", (Integer) 3, eventBusMessageCount);
final Container obj3 = containerService.findOrCreate(session, "/object12/child");
obj3.updateProperties(subjects, "PREFIX ore: <http://www.openarchives.org/ore/terms/>\n" +
"INSERT { <> ore:proxyFor <info:example/test> } WHERE {}", obj3.getTriples(subjects, PROPERTIES));
session.commit();
awaitEvent("/object12/child", RESOURCE_CREATION);
awaitEvent("/object12", RESOURCE_MODIFICATION);
awaitEvent("/object13", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 6, eventBusMessageCount);
final Container obj4 = containerService.findOrCreate(session, "/object12/child2");
session.commit();
awaitEvent("/object12/child2", RESOURCE_CREATION);
awaitEvent("/object12", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 8, eventBusMessageCount);
// Update a property that is irrelevant for the indirect container
obj4.updateProperties(subjects, "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n" +
"INSERT { <> rdfs:label \"some label\" } WHERE {}", obj4.getTriples(subjects, PROPERTIES));
session.commit();
awaitEvent("/object12/child2", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 9, eventBusMessageCount);
// Update a property that is relevant for the indirect container
obj4.updateProperties(subjects, "PREFIX ore: <http://www.openarchives.org/ore/terms/>\n" +
"INSERT { <> ore:proxyFor \"some proxy\" } WHERE {}", obj4.getTriples(subjects, PROPERTIES));
session.commit();
awaitEvent("/object12/child2", RESOURCE_MODIFICATION);
awaitEvent("/object13", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 11, eventBusMessageCount);
obj3.delete();
session.commit();
} finally {
session.expire();
}
awaitEvent("/object12/child", RESOURCE_DELETION);
awaitEvent("/object12", RESOURCE_MODIFICATION);
awaitEvent("/object13", RESOURCE_MODIFICATION);
assertEquals("Where are my events?", (Integer) 14, eventBusMessageCount);
}
private void awaitEvent(final String id, final EventType eventType, final String resourceType) {
await().atMost(5, SECONDS).pollInterval(ONE_HUNDRED_MILLISECONDS).until(() -> events.stream().anyMatch(evt ->
evt.getPath().equals(id) && evt.getTypes().contains(eventType)
&& (resourceType == null || evt.getResourceTypes().contains(resourceType))));
}
private void awaitEvent(final String id, final EventType eventType) {
awaitEvent(id, eventType, null);
}
@Subscribe
public void countMessages(final FedoraEvent e) {
eventBusMessageCount++;
events.add(e);
}
@Before
public void acquireConnections() {
eventBusMessageCount = 0;
events = new CopyOnWriteArrayList<>();
eventBus.register(this);
}
@After
public void releaseConnections() {
eventBus.unregister(this);
}
}