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}