001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.integration.jms.observer;
019
020import static com.google.common.base.Strings.nullToEmpty;
021import static com.google.common.base.Throwables.propagate;
022import static com.jayway.awaitility.Awaitility.await;
023import static com.jayway.awaitility.Duration.ONE_HUNDRED_MILLISECONDS;
024import static java.util.UUID.randomUUID;
025import static javax.jms.Session.AUTO_ACKNOWLEDGE;
026import static org.fcrepo.jms.DefaultMessageFactory.BASE_URL_HEADER_NAME;
027import static org.fcrepo.jms.DefaultMessageFactory.EVENT_TYPE_HEADER_NAME;
028import static org.fcrepo.jms.DefaultMessageFactory.IDENTIFIER_HEADER_NAME;
029import static org.fcrepo.jms.DefaultMessageFactory.RESOURCE_TYPE_HEADER_NAME;
030import static org.fcrepo.jms.DefaultMessageFactory.TIMESTAMP_HEADER_NAME;
031import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
032import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
033import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_CREATION;
034import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_DELETION;
035import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_MODIFICATION;
036import static org.slf4j.LoggerFactory.getLogger;
037
038import java.io.ByteArrayInputStream;
039import java.util.Set;
040import java.util.concurrent.CopyOnWriteArraySet;
041import javax.inject.Inject;
042import javax.jcr.Repository;
043import javax.jcr.RepositoryException;
044import javax.jcr.Session;
045import javax.jms.Connection;
046import javax.jms.JMSException;
047import javax.jms.Message;
048import javax.jms.MessageConsumer;
049import javax.jms.MessageListener;
050
051import org.apache.activemq.ActiveMQConnectionFactory;
052
053import org.fcrepo.kernel.api.exception.InvalidChecksumException;
054import org.fcrepo.kernel.api.models.FedoraResource;
055import org.fcrepo.kernel.api.observer.EventType;
056import org.fcrepo.kernel.api.models.Container;
057import org.fcrepo.kernel.api.services.BinaryService;
058import org.fcrepo.kernel.api.services.ContainerService;
059import org.fcrepo.kernel.modeshape.rdf.impl.DefaultIdentifierTranslator;
060
061import org.junit.After;
062import org.junit.Before;
063import org.junit.Test;
064import org.junit.runner.RunWith;
065import org.slf4j.Logger;
066import org.springframework.test.annotation.DirtiesContext;
067import org.springframework.test.context.ContextConfiguration;
068import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
069
070
071/**
072 * <p>
073 * JmsIT class.
074 * </p>
075 *
076 * @author ajs6f
077 */
078@RunWith(SpringJUnit4ClassRunner.class)
079@ContextConfiguration({ "/spring-test/jms.xml", "/spring-test/repo.xml",
080    "/spring-test/eventing.xml" })
081@DirtiesContext
082public class JmsIT implements MessageListener {
083
084    /**
085     * Time to wait for a set of test messages, in milliseconds.
086     */
087    private static final long TIMEOUT = 20000;
088
089    private static final String testIngested = "/testMessageFromIngestion-" + randomUUID();
090
091    private static final String testRemoved = "/testMessageFromRemoval-" + randomUUID();
092
093    private static final String testFile = "/testMessageFromFile-" + randomUUID() + "/file1";
094
095    private static final String testMeta = "/testMessageFromMetadata-" + randomUUID();
096
097    private static final String RESOURCE_CREATION_EVENT_TYPE = EventType.RESOURCE_CREATION.getType();
098    private static final String RESOURCE_DELETION_EVENT_TYPE = EventType.RESOURCE_DELETION.getType();
099    private static final String RESOURCE_MODIFICATION_EVENT_TYPE = EventType.RESOURCE_MODIFICATION.getType();
100
101    @Inject
102    private Repository repository;
103
104    @Inject
105    private BinaryService binaryService;
106
107    @Inject
108    private ContainerService containerService;
109
110    @Inject
111    private ActiveMQConnectionFactory connectionFactory;
112
113    private Connection connection;
114
115    private javax.jms.Session session;
116
117    private MessageConsumer consumer;
118
119    private volatile Set<Message> messages = new CopyOnWriteArraySet<>();
120
121    private static final Logger LOGGER = getLogger(JmsIT.class);
122
123    @Test(timeout = TIMEOUT)
124    public void testIngestion() throws RepositoryException {
125
126        LOGGER.debug("Expecting a {} event", RESOURCE_CREATION.getType());
127
128        final Session session = repository.login();
129        try {
130            containerService.findOrCreate(session, testIngested);
131            session.save();
132            awaitMessageOrFail(testIngested, RESOURCE_CREATION.getType(), null);
133        } finally {
134            session.logout();
135        }
136    }
137
138    @Test(timeout = TIMEOUT)
139    public void testFileEvents() throws InvalidChecksumException, RepositoryException {
140
141        final Session session = repository.login();
142
143        try {
144            binaryService.findOrCreate(session, testFile)
145                .setContent(new ByteArrayInputStream("foo".getBytes()), "text/plain", null, null, null);
146            session.save();
147            awaitMessageOrFail(testFile, RESOURCE_CREATION.getType(), REPOSITORY_NAMESPACE + "Binary");
148
149            binaryService.find(session, testFile)
150                .setContent(new ByteArrayInputStream("barney".getBytes()), "text/plain", null, null, null);
151            session.save();
152            awaitMessageOrFail(testFile, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Binary");
153
154            binaryService.find(session, testFile).delete();
155            session.save();
156            awaitMessageOrFail(testFile, RESOURCE_DELETION.getType(), null);
157        } finally {
158            session.logout();
159        }
160    }
161
162    @Test(timeout = TIMEOUT)
163    public void testMetadataEvents() throws RepositoryException {
164
165        final Session session = repository.login();
166        final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(session);
167
168        try {
169            final FedoraResource resource1 = containerService.findOrCreate(session, testMeta);
170            final String sparql1 = "insert data { <> <http://foo.com/prop> \"foo\" . }";
171            resource1.updateProperties(subjects, sparql1, resource1.getTriples(subjects, PROPERTIES));
172            session.save();
173            awaitMessageOrFail(testMeta, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Container");
174
175            final FedoraResource resource2 = containerService.findOrCreate(session, testMeta);
176            final String sparql2 = " delete { <> <http://foo.com/prop> \"foo\" . } "
177                + "insert { <> <http://foo.com/prop> \"bar\" . } where {}";
178            resource2.updateProperties(subjects, sparql2, resource2.getTriples(subjects, PROPERTIES));
179            session.save();
180            awaitMessageOrFail(testMeta, RESOURCE_MODIFICATION.getType(), REPOSITORY_NAMESPACE + "Resource");
181        } finally {
182            session.logout();
183        }
184    }
185
186    private void awaitMessageOrFail(final String id, final String eventType, final String type) {
187        await().pollInterval(ONE_HUNDRED_MILLISECONDS).until(() -> messages.stream().anyMatch(msg -> {
188            try {
189                return getPath(msg).equals(id) && getEventTypes(msg).contains(eventType)
190                        && getResourceTypes(msg).contains(nullToEmpty(type));
191            } catch (final JMSException e) {
192                throw propagate(e);
193            }
194        }));
195    }
196
197    @Test(timeout = TIMEOUT)
198    public void testRemoval() throws RepositoryException {
199
200        LOGGER.debug("Expecting a {} event", RESOURCE_DELETION.getType());
201        final Session session = repository.login();
202        try {
203            final Container resource = containerService.findOrCreate(session, testRemoved);
204            session.save();
205            resource.delete();
206            session.save();
207            awaitMessageOrFail(testRemoved, RESOURCE_DELETION.getType(), null);
208        } finally {
209            session.logout();
210        }
211    }
212
213    @Override
214    public void onMessage(final Message message) {
215        try {
216            LOGGER.debug(
217                    "Received JMS message: {} with path: {}, timestamp: {}, event type: {}, properties: {},"
218                            + " and baseURL: {}", message.getJMSMessageID(), getPath(message), getTimestamp(message),
219                            getEventTypes(message), getResourceTypes(message), getBaseURL(message));
220        } catch (final JMSException e) {
221            propagate(e);
222        }
223        messages.add(message);
224    }
225
226    @Before
227    public void acquireConnection() throws JMSException {
228        LOGGER.debug(this.getClass().getName() + " acquiring JMS connection.");
229        connection = connectionFactory.createConnection();
230        connection.start();
231        session = connection.createSession(false, AUTO_ACKNOWLEDGE);
232        consumer = session.createConsumer(session.createTopic("fedora"));
233        messages.clear();
234        consumer.setMessageListener(this);
235    }
236
237    @After
238    public void releaseConnection() throws JMSException {
239        // ignore any remaining or queued messages
240        consumer.setMessageListener(msg -> { });
241        // and shut the listening machinery down
242        LOGGER.debug(this.getClass().getName() + " releasing JMS connection.");
243        consumer.close();
244        session.close();
245        connection.close();
246    }
247
248    private static String getPath(final Message msg) throws JMSException {
249        final String id = msg.getStringProperty(IDENTIFIER_HEADER_NAME);
250        LOGGER.debug("Processing an event with identifier: {}", id);
251        return id;
252    }
253
254    private static String getEventTypes(final Message msg) throws JMSException {
255        final String type = msg.getStringProperty(EVENT_TYPE_HEADER_NAME);
256        LOGGER.debug("Processing an event with type: {}", type);
257        return type;
258    }
259
260    private static Long getTimestamp(final Message msg) throws JMSException {
261        return msg.getLongProperty(TIMESTAMP_HEADER_NAME);
262    }
263
264    private static String getBaseURL(final Message msg) throws JMSException {
265        return msg.getStringProperty(BASE_URL_HEADER_NAME);
266    }
267
268    private static String getResourceTypes(final Message msg) throws JMSException {
269        return msg.getStringProperty(RESOURCE_TYPE_HEADER_NAME);
270    }
271
272}