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