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.jms.Session.AUTO_ACKNOWLEDGE;
022import static org.fcrepo.jms.headers.DefaultMessageFactory.BASE_URL_HEADER_NAME;
023import static org.fcrepo.jms.headers.DefaultMessageFactory.EVENT_TYPE_HEADER_NAME;
024import static org.fcrepo.jms.headers.DefaultMessageFactory.IDENTIFIER_HEADER_NAME;
025import static org.fcrepo.jms.headers.DefaultMessageFactory.PROPERTIES_HEADER_NAME;
026import static org.fcrepo.jms.headers.DefaultMessageFactory.TIMESTAMP_HEADER_NAME;
027import static org.fcrepo.kernel.api.RdfLexicon.HAS_SIZE;
028import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
029import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
030import static org.jgroups.util.UUID.randomUUID;
031import static org.slf4j.LoggerFactory.getLogger;
032
033import java.io.ByteArrayInputStream;
034import java.util.HashSet;
035import java.util.Set;
036import javax.inject.Inject;
037import javax.jcr.Repository;
038import javax.jcr.RepositoryException;
039import javax.jcr.Session;
040import javax.jms.Connection;
041import javax.jms.JMSException;
042import javax.jms.Message;
043import javax.jms.MessageConsumer;
044import javax.jms.MessageListener;
045
046import org.apache.activemq.ActiveMQConnectionFactory;
047
048import org.fcrepo.kernel.api.exception.InvalidChecksumException;
049import org.fcrepo.kernel.api.models.FedoraResource;
050import org.fcrepo.kernel.api.observer.EventType;
051import org.fcrepo.kernel.api.models.Container;
052import org.fcrepo.kernel.api.services.BinaryService;
053import org.fcrepo.kernel.api.services.ContainerService;
054import org.fcrepo.kernel.modeshape.rdf.impl.DefaultIdentifierTranslator;
055
056import org.junit.After;
057import org.junit.Before;
058import org.junit.Test;
059import org.junit.runner.RunWith;
060import org.slf4j.Logger;
061import org.springframework.test.annotation.DirtiesContext;
062import org.springframework.test.context.ContextConfiguration;
063import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
064
065
066/**
067 * <p>
068 * HeadersJMSIT class.
069 * </p>
070 *
071 * @author ajs6f
072 */
073@RunWith(SpringJUnit4ClassRunner.class)
074@ContextConfiguration({ "/spring-test/headers-jms.xml", "/spring-test/repo.xml",
075    "/spring-test/eventing.xml" })
076@DirtiesContext
077public class HeadersJMSIT implements MessageListener {
078
079    /**
080     * Time to wait for a set of test messages, in milliseconds.
081     */
082    private static final long TIMEOUT = 20000;
083
084    private static final String testIngested = "/testMessageFromIngestion-" + randomUUID();
085
086    private static final String testRemoved = "/testMessageFromRemoval-" + randomUUID();
087
088    private static final String testFile = "/testMessageFromFile-" + randomUUID() + "/file1";
089
090    private static final String testMeta = "/testMessageFromMetadata-" + randomUUID();
091
092    private static final String NODE_ADDED_EVENT_TYPE
093            = REPOSITORY_NAMESPACE + EventType.NODE_ADDED;
094    private static final String NODE_REMOVED_EVENT_TYPE
095            = REPOSITORY_NAMESPACE + EventType.NODE_REMOVED;
096    private static final String PROP_ADDED_EVENT_TYPE
097            = REPOSITORY_NAMESPACE + EventType.PROPERTY_ADDED;
098    private static final String PROP_CHANGED_EVENT_TYPE
099            = REPOSITORY_NAMESPACE + EventType.PROPERTY_CHANGED;
100    private static final String PROP_REMOVED_EVENT_TYPE
101            = REPOSITORY_NAMESPACE + EventType.PROPERTY_REMOVED;
102
103    @Inject
104    private Repository repository;
105
106    @Inject
107    private BinaryService binaryService;
108
109    @Inject
110    private ContainerService containerService;
111
112    @Inject
113    private ActiveMQConnectionFactory connectionFactory;
114
115    private Connection connection;
116
117    private javax.jms.Session session;
118
119    private MessageConsumer consumer;
120
121    private volatile Set<Message> messages = new HashSet<>();
122
123    private static final Logger LOGGER = getLogger(HeadersJMSIT.class);
124
125    @Test(timeout = TIMEOUT)
126    public void testIngestion() throws RepositoryException {
127
128        LOGGER.debug("Expecting a {} event", NODE_ADDED_EVENT_TYPE);
129
130        final Session session = repository.login();
131        try {
132            containerService.findOrCreate(session, testIngested);
133            session.save();
134            awaitMessageOrFail(testIngested, NODE_ADDED_EVENT_TYPE, null);
135        } finally {
136            session.logout();
137        }
138    }
139
140    @Test(timeout = TIMEOUT)
141    public void testFileEvents() throws InvalidChecksumException, RepositoryException {
142
143        final Session session = repository.login();
144
145        try {
146            binaryService.findOrCreate(session, testFile)
147                .setContent(new ByteArrayInputStream("foo".getBytes()), "text/plain", null, null, null);
148            session.save();
149            awaitMessageOrFail(testFile, NODE_ADDED_EVENT_TYPE, HAS_SIZE.toString());
150
151            binaryService.find(session, testFile)
152                .setContent(new ByteArrayInputStream("bar".getBytes()), "text/plain", null, null, null);
153            session.save();
154            awaitMessageOrFail(testFile, PROP_CHANGED_EVENT_TYPE, HAS_SIZE.toString());
155
156            binaryService.find(session, testFile).delete();
157            session.save();
158            awaitMessageOrFail(testFile, NODE_REMOVED_EVENT_TYPE, null);
159        } finally {
160            session.logout();
161        }
162    }
163
164    @Test(timeout = TIMEOUT)
165    public void testMetadataEvents() throws RepositoryException {
166
167        final Session session = repository.login();
168        final DefaultIdentifierTranslator subjects = new DefaultIdentifierTranslator(session);
169
170        try {
171            final FedoraResource resource1 = containerService.findOrCreate(session, testMeta);
172            final String sparql1 = "insert data { <> <http://foo.com/prop> \"foo\" . }";
173            resource1.updateProperties(subjects, sparql1, resource1.getTriples(subjects, PROPERTIES));
174            session.save();
175            awaitMessageOrFail(testMeta, PROP_ADDED_EVENT_TYPE, "http://foo.com/prop");
176
177            final FedoraResource resource2 = containerService.findOrCreate(session, testMeta);
178            final String sparql2 = " delete { <> <http://foo.com/prop> \"foo\" . } "
179                + "insert { <> <http://foo.com/prop> \"bar\" . } where {}";
180            resource2.updateProperties(subjects, sparql2, resource2.getTriples(subjects, PROPERTIES));
181            session.save();
182            awaitMessageOrFail(testMeta, PROP_CHANGED_EVENT_TYPE, "http://foo.com/prop");
183        } finally {
184            session.logout();
185        }
186    }
187
188    private void awaitMessageOrFail(final String id, final String eventType, final String property) {
189        await().pollInterval(ONE_SECOND).until(() -> messages.stream().anyMatch(msg -> {
190            try {
191                return getPath(msg).equals(id) && getEventTypes(msg).contains(eventType)
192                        && (property == null || getProperties(msg).contains(property));
193            } catch (final JMSException e) {
194                throw propagate(e);
195            }
196        }));
197    }
198
199    @Test(timeout = TIMEOUT)
200    public void testRemoval() throws RepositoryException {
201
202        LOGGER.debug("Expecting a {} event", NODE_REMOVED_EVENT_TYPE);
203        final Session session = repository.login();
204        try {
205            final Container resource = containerService.findOrCreate(session, testRemoved);
206            session.save();
207            resource.delete();
208            session.save();
209            awaitMessageOrFail(testRemoved, NODE_REMOVED_EVENT_TYPE, null);
210        } finally {
211            session.logout();
212        }
213    }
214
215    @Override
216    public void onMessage(final Message message) {
217        try {
218            LOGGER.debug(
219                    "Received JMS message: {} with path: {}, timestamp: {}, event type: {}, properties: {},"
220                            + " and baseURL: {}", message.getJMSMessageID(), getPath(message), getTimestamp(message),
221                            getEventTypes(message), getProperties(message), getBaseURL(message));
222        } catch (final JMSException e) {
223            propagate(e);
224        }
225        messages.add(message);
226    }
227
228    @Before
229    public void acquireConnection() throws JMSException {
230        LOGGER.debug(this.getClass().getName() + " acquiring JMS connection.");
231        connection = connectionFactory.createConnection();
232        connection.start();
233        session = connection.createSession(false, AUTO_ACKNOWLEDGE);
234        consumer = session.createConsumer(session.createTopic("fedora"));
235        messages.clear();
236        consumer.setMessageListener(this);
237    }
238
239    @After
240    public void releaseConnection() throws JMSException {
241        // ignore any remaining or queued messages
242        consumer.setMessageListener(msg -> { });
243        // and shut the listening machinery down
244        LOGGER.debug(this.getClass().getName() + " releasing JMS connection.");
245        consumer.close();
246        session.close();
247        connection.close();
248    }
249
250    private static String getPath(final Message msg) throws JMSException {
251        final String id = msg.getStringProperty(IDENTIFIER_HEADER_NAME);
252        LOGGER.debug("Processing an event with identifier: {}", id);
253        return id;
254    }
255
256    private static String getEventTypes(final Message msg) throws JMSException {
257        final String type = msg.getStringProperty(EVENT_TYPE_HEADER_NAME);
258        LOGGER.debug("Processing an event with type: {}", type);
259        return type;
260    }
261
262    private static Long getTimestamp(final Message msg) throws JMSException {
263        return msg.getLongProperty(TIMESTAMP_HEADER_NAME);
264    }
265
266    private static String getBaseURL(final Message msg) throws JMSException {
267        return msg.getStringProperty(BASE_URL_HEADER_NAME);
268    }
269
270    private static String getProperties(final Message msg) throws JMSException {
271        return msg.getStringProperty(PROPERTIES_HEADER_NAME);
272    }
273
274}