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