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("bar".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}