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.kernel.modeshape.observer; 019 020import static com.google.common.base.MoreObjects.toStringHelper; 021import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_CREATION; 022import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_DELETION; 023import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_MODIFICATION; 024import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_RELOCATION; 025import static org.fcrepo.kernel.api.observer.EventType.INBOUND_REFERENCE; 026import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FEDORA_JCR_REFERENCE; 027import static org.fcrepo.kernel.api.observer.OptionalValues.BASE_URL; 028import static org.fcrepo.kernel.api.observer.OptionalValues.USER_AGENT; 029import static javax.jcr.observation.Event.NODE_ADDED; 030import static javax.jcr.observation.Event.NODE_MOVED; 031import static javax.jcr.observation.Event.NODE_REMOVED; 032import static javax.jcr.observation.Event.PROPERTY_ADDED; 033import static javax.jcr.observation.Event.PROPERTY_CHANGED; 034import static javax.jcr.observation.Event.PROPERTY_REMOVED; 035import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 036import static org.slf4j.LoggerFactory.getLogger; 037import static java.time.Instant.ofEpochMilli; 038import static java.util.Arrays.asList; 039import static java.util.Collections.emptyMap; 040import static java.util.Collections.singleton; 041import static java.util.Objects.isNull; 042import static java.util.Objects.requireNonNull; 043import static java.util.UUID.randomUUID; 044import static java.util.stream.Collectors.joining; 045import static java.util.stream.Collectors.toSet; 046import static java.util.stream.Stream.empty; 047 048import java.io.IOException; 049import java.net.URI; 050import java.time.Instant; 051import java.util.Collection; 052import java.util.HashMap; 053import java.util.HashSet; 054import java.util.List; 055import java.util.Map; 056import java.util.Set; 057import java.util.stream.Stream; 058 059import javax.jcr.RepositoryException; 060import javax.jcr.nodetype.NodeType; 061import javax.jcr.observation.Event; 062 063import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 064import org.fcrepo.kernel.api.observer.EventType; 065import org.fcrepo.kernel.api.observer.FedoraEvent; 066import org.fcrepo.kernel.modeshape.identifiers.HashConverter; 067import org.fcrepo.kernel.modeshape.utils.FedoraSessionUserUtil; 068 069import org.slf4j.Logger; 070import com.fasterxml.jackson.databind.ObjectMapper; 071import com.fasterxml.jackson.databind.JsonNode; 072import com.google.common.collect.ImmutableMap; 073 074/** 075 * A very simple abstraction to prevent event-driven machinery downstream from the repository from relying directly 076 * on a JCR interface {@link Event}. Can represent either a single JCR event or several. 077 * 078 * @author ajs6f 079 * @since Feb 19, 2013 080 */ 081public class FedoraEventImpl implements FedoraEvent { 082 083 private final static ObjectMapper MAPPER = new ObjectMapper(); 084 085 private final static Logger LOGGER = getLogger(FedoraEventImpl.class); 086 087 private final String path; 088 private final String userID; 089 private final URI userURI; 090 private final Instant date; 091 private final Map<String, String> info; 092 private final String eventID; 093 private final Set<String> eventResourceTypes; 094 095 private final Set<EventType> eventTypes = new HashSet<>(); 096 097 private static final List<Integer> PROPERTY_TYPES = asList(Event.PROPERTY_ADDED, 098 Event.PROPERTY_CHANGED, Event.PROPERTY_REMOVED, FEDORA_JCR_REFERENCE); 099 100 /** 101 * Create a new FedoraEvent 102 * @param type the Fedora EventType 103 * @param path the node path corresponding to this event 104 * @param resourceTypes the rdf types of the corresponding resource 105 * @param userID the acting user for this event 106 * @param userURI the uri of the acting user for this event 107 * @param date the timestamp for this event 108 * @param info supplementary information 109 */ 110 public FedoraEventImpl(final EventType type, final String path, final Set<String> resourceTypes, 111 final String userID, final URI userURI, final Instant date, final Map<String, String> info) { 112 this(singleton(type), path, resourceTypes, userID, userURI, date, info); 113 } 114 115 /** 116 * Create a new FedoraEvent 117 * @param types a collection of Fedora EventTypes 118 * @param path the node path corresponding to this event 119 * @param resourceTypes the rdf types of the corresponding resource 120 * @param userID the acting user for this event 121 * @param userURI the uri of the acting user for this event 122 * @param date the timestamp for this event 123 * @param info supplementary information 124 */ 125 public FedoraEventImpl(final Collection<EventType> types, final String path, final Set<String> resourceTypes, 126 final String userID, final URI userURI, final Instant date, final Map<String, String> info) { 127 requireNonNull(types, "FedoraEvent requires a non-null event type"); 128 requireNonNull(path, "FedoraEvent requires a non-null path"); 129 130 this.eventTypes.addAll(types); 131 this.path = path; 132 this.eventResourceTypes = resourceTypes; 133 this.userID = userID; 134 this.userURI = userURI; 135 this.date = date; 136 this.info = isNull(info) ? emptyMap() : info; 137 this.eventID = "urn:uuid:" + randomUUID().toString(); 138 } 139 140 141 /** 142 * @return the event types of the underlying JCR {@link Event}s 143 */ 144 @Override 145 public Set<EventType> getTypes() { 146 return eventTypes; 147 } 148 149 /** 150 * @return the RDF types of the underlying Fedora Resource 151 **/ 152 @Override 153 public Set<String> getResourceTypes() { 154 return eventResourceTypes; 155 } 156 157 /** 158 * @return the path of the underlying JCR {@link Event}s 159 */ 160 @Override 161 public String getPath() { 162 return path; 163 } 164 165 /** 166 * @return the user ID of the underlying JCR {@link Event}s 167 */ 168 @Override 169 public String getUserID() { 170 return userID; 171 } 172 173 /** 174 * @return the user URI of the underlying JCR {@link Event}s 175 */ 176 @Override 177 public URI getUserURI() { 178 return userURI; 179 } 180 181 /** 182 * @return the date of the FedoraEvent 183 */ 184 @Override 185 public Instant getDate() { 186 return date; 187 } 188 189 /** 190 * Get the event ID. 191 * @return Event identifier to use for building event URIs (e.g., in an external triplestore). 192 **/ 193 @Override 194 public String getEventID() { 195 return eventID; 196 } 197 198 /** 199 * Return a Map with any additional information about the event. 200 * @return a Map of additional information. 201 */ 202 @Override 203 public Map<String, String> getInfo() { 204 205 return info; 206 } 207 208 @Override 209 public String toString() { 210 211 return toStringHelper(this) 212 .add("Event types:", getTypes().stream() 213 .map(EventType::getName) 214 .collect(joining(", "))) 215 .add("Event resource types:", String.join(",", eventResourceTypes)) 216 .add("Path:", getPath()) 217 .add("Date: ", getDate()).toString(); 218 } 219 220 private static final Map<Integer, EventType> translation = ImmutableMap.<Integer, EventType>builder() 221 .put(NODE_ADDED, RESOURCE_CREATION) 222 .put(NODE_REMOVED, RESOURCE_DELETION) 223 .put(PROPERTY_ADDED, RESOURCE_MODIFICATION) 224 .put(PROPERTY_REMOVED, RESOURCE_MODIFICATION) 225 .put(PROPERTY_CHANGED, RESOURCE_MODIFICATION) 226 .put(NODE_MOVED, RESOURCE_RELOCATION) 227 .put(FEDORA_JCR_REFERENCE, INBOUND_REFERENCE).build(); 228 229 /** 230 * Get the Fedora event type for a JCR type 231 * 232 * @param i the integer value of a JCR type 233 * @return EventType 234 */ 235 public static EventType valueOf(final Integer i) { 236 final EventType type = translation.get(i); 237 if (isNull(type)) { 238 throw new IllegalArgumentException("Invalid event type: " + i); 239 } 240 return type; 241 } 242 243 244 /** 245 * Convert a JCR Event to a FedoraEvent 246 * @param event the JCR Event 247 * @return a FedoraEvent 248 */ 249 public static FedoraEvent from(final Event event) { 250 requireNonNull(event); 251 try { 252 @SuppressWarnings("unchecked") 253 final Map<String, String> info = new HashMap<>(event.getInfo()); 254 255 final String userdata = event.getUserData(); 256 try { 257 if (userdata != null && !userdata.isEmpty()) { 258 final JsonNode json = MAPPER.readTree(userdata); 259 if (json.has(BASE_URL)) { 260 String url = json.get(BASE_URL).asText(); 261 while (url.endsWith("/")) { 262 url = url.substring(0, url.length() - 1); 263 } 264 info.put(BASE_URL, url); 265 } 266 if (json.has(USER_AGENT)) { 267 info.put(USER_AGENT, json.get(USER_AGENT).asText()); 268 } 269 } else { 270 LOGGER.debug("Event UserData is empty!"); 271 } 272 } catch (final IOException ex) { 273 LOGGER.warn("Error extracting user data: " + userdata, ex.getMessage()); 274 } 275 276 final Set<String> resourceTypes = getResourceTypes(event).collect(toSet()); 277 278 return new FedoraEventImpl(valueOf(event.getType()), cleanPath(event), resourceTypes, 279 event.getUserID(), FedoraSessionUserUtil.getUserURI(event.getUserID()), ofEpochMilli(event 280 .getDate()), info); 281 282 } catch (final RepositoryException ex) { 283 throw new RepositoryRuntimeException("Error converting JCR Event to FedoraEvent", ex); 284 } 285 } 286 287 /** 288 * Get the RDF Types of the resource corresponding to this JCR Event 289 * @param event the JCR event 290 * @return the types recorded on the resource associated to this event 291 */ 292 public static Stream<String> getResourceTypes(final Event event) { 293 if (event instanceof org.modeshape.jcr.api.observation.Event) { 294 try { 295 final org.modeshape.jcr.api.observation.Event modeEvent = 296 (org.modeshape.jcr.api.observation.Event) event; 297 final Stream.Builder<NodeType> types = Stream.builder(); 298 for (final NodeType type : modeEvent.getMixinNodeTypes()) { 299 types.add(type); 300 } 301 types.add(modeEvent.getPrimaryNodeType()); 302 return types.build().map(NodeType::getName); 303 } catch (final RepositoryException e) { 304 throw new RepositoryRuntimeException(e); 305 } 306 } 307 return empty(); // wasn't a ModeShape event, so we have no access to resource types 308 } 309 310 /** 311 * The JCR-based Event::getPath contains some Modeshape artifacts that must be removed or modified in 312 * order to correspond to the public resource path. For example, JCR Events will contain a trailing 313 * /jcr:content for Binaries, a trailing /propName for properties, and /#/ notation for URI fragments. 314 */ 315 private static String cleanPath(final Event event) throws RepositoryException { 316 // remove any trailing data for property changes 317 final String path = PROPERTY_TYPES.contains(event.getType()) ? 318 event.getPath().substring(0, event.getPath().lastIndexOf("/")) : event.getPath(); 319 320 // reformat any hash URIs and remove any trailing /jcr:content 321 final HashConverter converter = new HashConverter(); 322 return converter.reverse().convert(path.replaceAll("/" + JCR_CONTENT, "")); 323 } 324}