/*
* 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.jms.observer;
import static com.google.common.base.Strings.nullToEmpty;
import static com.jayway.awaitility.Awaitility.await;
import static com.jayway.awaitility.Duration.ONE_HUNDRED_MILLISECONDS;
import static java.util.UUID.randomUUID;
import static javax.jms.Session.AUTO_ACKNOWLEDGE;
import static org.fcrepo.jms.DefaultMessageFactory.BASE_URL_HEADER_NAME;
import static org.fcrepo.jms.DefaultMessageFactory.EVENT_TYPE_HEADER_NAME;
import static org.fcrepo.jms.DefaultMessageFactory.IDENTIFIER_HEADER_NAME;
import static org.fcrepo.jms.DefaultMessageFactory.RESOURCE_TYPE_HEADER_NAME;
import static org.fcrepo.jms.DefaultMessageFactory.TIMESTAMP_HEADER_NAME;
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.OptionalValues.BASE_URL;
import static org.fcrepo.kernel.api.observer.OptionalValues.USER_AGENT;
import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.ByteArrayInputStream;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.inject.Inject;
import javax.jcr.RepositoryException;
import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;
import org.apache.activemq.ActiveMQConnectionFactory;
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.FedoraResource;
import org.fcrepo.kernel.api.observer.EventType;
import org.fcrepo.kernel.api.models.Container;
import org.fcrepo.kernel.api.services.BinaryService;
import org.fcrepo.kernel.api.services.ContainerService;
import org.fcrepo.kernel.modeshape.rdf.impl.DefaultIdentifierTranslator;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
/**
* <p>
* AbstractJmsIT class.
* </p>
*
* @author ajs6f
*/
abstract class AbstractJmsIT implements MessageListener {
/**
* Time to wait for a set of test messages, in milliseconds.
*/
private static final long TIMEOUT = 20000;
private static final String testIngested = "/testMessageFromIngestion-" + randomUUID();
private static final String testRemoved = "/testMessageFromRemoval-" + randomUUID();
private static final String testFile = "/testMessageFromFile-" + randomUUID() + "/file1";
private static final String testMeta = "/testMessageFromMetadata-" + randomUUID();
private static final String RESOURCE_CREATION_EVENT_TYPE = EventType.RESOURCE_CREATION.getType();
private static final String RESOURCE_DELETION_EVENT_TYPE = EventType.RESOURCE_DELETION.getType();
private static final String RESOURCE_MODIFICATION_EVENT_TYPE = EventType.RESOURCE_MODIFICATION.getType();
private static final String TEST_USER_AGENT = "FedoraClient/1.0";
private static final String TEST_BASE_URL = "http://localhost:8080/rest";
@Inject
private FedoraRepository repository;
@Inject
private BinaryService binaryService;
@Inject
private ContainerService containerService;
@Inject
private ActiveMQConnectionFactory connectionFactory;
private Connection connection;
protected Session jmsSession;
private MessageConsumer consumer;
private volatile Set<Message> messages = new CopyOnWriteArraySet<>();
private static final Logger LOGGER = getLogger(AbstractJmsIT.class);
protected abstract Destination createDestination() throws JMSException;
@Test(timeout = TIMEOUT)
public void testIngestion() throws RepositoryException {
LOGGER.debug("Expecting a {} event", RESOURCE_CREATION.getType());
final FedoraSession session = repository.login();
session.addSessionData(BASE_URL, TEST_BASE_URL);
session.addSessionData(USER_AGENT, TEST_USER_AGENT);
try {
containerService.findOrCreate(session, testIngested);
session.commit();
awaitMessageOrFail(testIngested, RESOURCE_CREATION.getType(), null);
} finally {
session.expire();
}
}
@Test(timeout = TIMEOUT)
public void testFileEvents() throws InvalidChecksumException, RepositoryException {
final FedoraSession session = repository.login();
session.addSessionData(BASE_URL, TEST_BASE_URL);
session.addSessionData(USER_AGENT, TEST_USER_AGENT);
try {
binaryService.findOrCreate(session, testFile)
.setContent(new ByteArrayInputStream("foo".getBytes()), "text/plain", null, null, null);
session.commit();
awaitMessageOrFail(testFile, RESOURCE_CREATION.getType(), REPOSITORY_NAMESPACE + "Binary");
binaryService.find(session, testFile)
.setContent(new ByteArrayInputStream("barney".getBytes()), "text/plain", null, null, null);
session.commit();
awaitMessageOrFail(testFile, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Binary");
binaryService.find(session, testFile).delete();
session.commit();
awaitMessageOrFail(testFile, RESOURCE_DELETION.getType(), null);
} finally {
session.expire();
}
}
@Test(timeout = TIMEOUT)
public void testMetadataEvents() throws RepositoryException {
final FedoraSession session = repository.login();
session.addSessionData(BASE_URL, TEST_BASE_URL);
session.addSessionData(USER_AGENT, TEST_USER_AGENT);
final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(getJcrSession(session));
try {
final FedoraResource resource1 = containerService.findOrCreate(session, testMeta);
final String sparql1 = "insert data { <> <http://foo.com/prop> \"foo\" . }";
resource1.updateProperties(subjects, sparql1, resource1.getTriples(subjects, PROPERTIES));
session.commit();
awaitMessageOrFail(testMeta, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Container");
final FedoraResource resource2 = containerService.findOrCreate(session, testMeta);
final String sparql2 = " delete { <> <http://foo.com/prop> \"foo\" . } "
+ "insert { <> <http://foo.com/prop> \"bar\" . } where {}";
resource2.updateProperties(subjects, sparql2, resource2.getTriples(subjects, PROPERTIES));
session.commit();
awaitMessageOrFail(testMeta, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Resource");
} finally {
session.expire();
}
}
private void awaitMessageOrFail(final String id, final String eventType, final String type) {
await().pollInterval(ONE_HUNDRED_MILLISECONDS).until(() -> messages.stream().anyMatch(msg -> {
try {
return getPath(msg).equals(id) && getEventTypes(msg).contains(eventType)
&& getResourceTypes(msg).contains(nullToEmpty(type));
} catch (final JMSException e) {
throw new RuntimeException(e);
}
}));
}
@Test(timeout = TIMEOUT)
public void testRemoval() throws RepositoryException {
LOGGER.debug("Expecting a {} event", RESOURCE_DELETION.getType());
final FedoraSession session = repository.login();
session.addSessionData(BASE_URL, TEST_BASE_URL);
session.addSessionData(USER_AGENT, TEST_USER_AGENT);
try {
final Container resource = containerService.findOrCreate(session, testRemoved);
session.commit();
resource.delete();
session.commit();
awaitMessageOrFail(testRemoved, RESOURCE_DELETION.getType(), null);
} finally {
session.expire();
}
}
@Override
public void onMessage(final Message message) {
try {
LOGGER.debug(
"Received JMS message: {} with path: {}, timestamp: {}, event type: {}, properties: {},"
+ " and baseURL: {}", message.getJMSMessageID(), getPath(message), getTimestamp(message),
getEventTypes(message), getResourceTypes(message), getBaseURL(message));
} catch (final JMSException e) {
throw new RuntimeException(e);
}
messages.add(message);
}
@Before
public void acquireConnection() throws JMSException {
LOGGER.debug(this.getClass().getName() + " acquiring JMS connection.");
connection = connectionFactory.createConnection();
connection.start();
jmsSession = connection.createSession(false, AUTO_ACKNOWLEDGE);
consumer = jmsSession.createConsumer(createDestination());
messages.clear();
consumer.setMessageListener(this);
}
@After
public void releaseConnection() throws JMSException {
// ignore any remaining or queued messages
consumer.setMessageListener(msg -> { });
// and shut the listening machinery down
LOGGER.debug(this.getClass().getName() + " releasing JMS connection.");
consumer.close();
jmsSession.close();
connection.close();
}
private static String getPath(final Message msg) throws JMSException {
final String id = msg.getStringProperty(IDENTIFIER_HEADER_NAME);
LOGGER.debug("Processing an event with identifier: {}", id);
return id;
}
private static String getEventTypes(final Message msg) throws JMSException {
final String type = msg.getStringProperty(EVENT_TYPE_HEADER_NAME);
LOGGER.debug("Processing an event with type: {}", type);
return type;
}
private static Long getTimestamp(final Message msg) throws JMSException {
return msg.getLongProperty(TIMESTAMP_HEADER_NAME);
}
private static String getBaseURL(final Message msg) throws JMSException {
return msg.getStringProperty(BASE_URL_HEADER_NAME);
}
private static String getResourceTypes(final Message msg) throws JMSException {
return msg.getStringProperty(RESOURCE_TYPE_HEADER_NAME);
}
}