/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.modeshape.observer;
import static com.google.common.base.MoreObjects.toStringHelper;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_CREATION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_DELETION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_MODIFICATION;
import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_RELOCATION;
import static org.fcrepo.kernel.api.observer.OptionalValues.BASE_URL;
import static org.fcrepo.kernel.api.observer.OptionalValues.USER_AGENT;
import static javax.jcr.observation.Event.NODE_ADDED;
import static javax.jcr.observation.Event.NODE_MOVED;
import static javax.jcr.observation.Event.NODE_REMOVED;
import static javax.jcr.observation.Event.PROPERTY_ADDED;
import static javax.jcr.observation.Event.PROPERTY_CHANGED;
import static javax.jcr.observation.Event.PROPERTY_REMOVED;
import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
import static org.slf4j.LoggerFactory.getLogger;
import static java.time.Instant.ofEpochMilli;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singleton;
import static java.util.Objects.isNull;
import static java.util.Objects.requireNonNull;
import static java.util.UUID.randomUUID;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.Stream.empty;
import java.io.IOException;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.observation.Event;
import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
import org.fcrepo.kernel.api.observer.EventType;
import org.fcrepo.kernel.api.observer.FedoraEvent;
import org.fcrepo.kernel.modeshape.identifiers.HashConverter;
import org.slf4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
/**
* A very simple abstraction to prevent event-driven machinery downstream from the repository from relying directly
* on a JCR interface {@link Event}. Can represent either a single JCR event or several.
*
* @author ajs6f
* @since Feb 19, 2013
*/
public class FedoraEventImpl implements FedoraEvent {
private final static ObjectMapper MAPPER = new ObjectMapper();
private final static Logger LOGGER = getLogger(FedoraEventImpl.class);
private final String path;
private final String userID;
private final Instant date;
private final Map<String, String> info;
private final String eventID;
private final Set<String> eventResourceTypes;
private final Set<EventType> eventTypes = new HashSet<>();
private static final List<Integer> PROPERTY_TYPES = asList(Event.PROPERTY_ADDED,
Event.PROPERTY_CHANGED, Event.PROPERTY_REMOVED);
/**
* Create a new FedoraEvent
* @param type the Fedora EventType
* @param path the node path corresponding to this event
* @param resourceTypes the rdf types of the corresponding resource
* @param userID the acting user for this event
* @param date the timestamp for this event
* @param info supplementary information
*/
public FedoraEventImpl(final EventType type, final String path, final Set<String> resourceTypes,
final String userID, final Instant date, final Map<String, String> info) {
this(singleton(type), path, resourceTypes, userID, date, info);
}
/**
* Create a new FedoraEvent
* @param types a collection of Fedora EventTypes
* @param path the node path corresponding to this event
* @param resourceTypes the rdf types of the corresponding resource
* @param userID the acting user for this event
* @param date the timestamp for this event
* @param info supplementary information
*/
public FedoraEventImpl(final Collection<EventType> types, final String path, final Set<String> resourceTypes,
final String userID, final Instant date, final Map<String, String> info) {
requireNonNull(types, "FedoraEvent requires a non-null event type");
requireNonNull(path, "FedoraEvent requires a non-null path");
this.eventTypes.addAll(types);
this.path = path;
this.eventResourceTypes = resourceTypes;
this.userID = userID;
this.date = date;
this.info = isNull(info) ? emptyMap() : info;
this.eventID = "urn:uuid:" + randomUUID().toString();
}
/**
* @return the event types of the underlying JCR {@link Event}s
*/
@Override
public Set<EventType> getTypes() {
return eventTypes;
}
/**
* @return the RDF types of the underlying Fedora Resource
**/
@Override
public Set<String> getResourceTypes() {
return eventResourceTypes;
}
/**
* @return the path of the underlying JCR {@link Event}s
*/
@Override
public String getPath() {
return path;
}
/**
* @return the user ID of the underlying JCR {@link Event}s
*/
@Override
public String getUserID() {
return userID;
}
/**
* @return the date of the FedoraEvent
*/
@Override
public Instant getDate() {
return date;
}
/**
* Get the event ID.
* @return Event identifier to use for building event URIs (e.g., in an external triplestore).
**/
@Override
public String getEventID() {
return eventID;
}
/**
* Return a Map with any additional information about the event.
* @return a Map of additional information.
*/
@Override
public Map<String, String> getInfo() {
return info;
}
@Override
public String toString() {
return toStringHelper(this)
.add("Event types:", getTypes().stream()
.map(EventType::getName)
.collect(joining(", ")))
.add("Event resource types:", String.join(",", eventResourceTypes))
.add("Path:", getPath())
.add("Date: ", getDate()).toString();
}
private static final Map<Integer, EventType> translation = ImmutableMap.<Integer, EventType>builder()
.put(NODE_ADDED, RESOURCE_CREATION)
.put(NODE_REMOVED, RESOURCE_DELETION)
.put(PROPERTY_ADDED, RESOURCE_MODIFICATION)
.put(PROPERTY_REMOVED, RESOURCE_MODIFICATION)
.put(PROPERTY_CHANGED, RESOURCE_MODIFICATION)
.put(NODE_MOVED, RESOURCE_RELOCATION).build();
/**
* Get the Fedora event type for a JCR type
*
* @param i the integer value of a JCR type
* @return EventType
*/
public static EventType valueOf(final Integer i) {
final EventType type = translation.get(i);
if (isNull(type)) {
throw new IllegalArgumentException("Invalid event type: " + i);
}
return type;
}
/**
* Convert a JCR Event to a FedoraEvent
* @param event the JCR Event
* @return a FedoraEvent
*/
public static FedoraEvent from(final Event event) {
requireNonNull(event);
try {
@SuppressWarnings("unchecked")
final Map<String, String> info = new HashMap<>(event.getInfo());
final String userdata = event.getUserData();
try {
if (userdata != null && !userdata.isEmpty()) {
final JsonNode json = MAPPER.readTree(userdata);
if (json.has(BASE_URL)) {
String url = json.get(BASE_URL).asText();
while (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
info.put(BASE_URL, url);
}
if (json.has(USER_AGENT)) {
info.put(USER_AGENT, json.get(USER_AGENT).asText());
}
} else {
LOGGER.debug("Event UserData is empty!");
}
} catch (final IOException ex) {
LOGGER.warn("Error extracting user data: " + userdata, ex.getMessage());
}
final Set<String> resourceTypes = getResourceTypes(event).collect(toSet());
return new FedoraEventImpl(valueOf(event.getType()), cleanPath(event), resourceTypes,
event.getUserID(), ofEpochMilli(event.getDate()), info);
} catch (final RepositoryException ex) {
throw new RepositoryRuntimeException("Error converting JCR Event to FedoraEvent", ex);
}
}
/**
* Get the RDF Types of the resource corresponding to this JCR Event
* @param event the JCR event
* @return the types recorded on the resource associated to this event
*/
public static Stream<String> getResourceTypes(final Event event) {
if (event instanceof org.modeshape.jcr.api.observation.Event) {
try {
final org.modeshape.jcr.api.observation.Event modeEvent =
(org.modeshape.jcr.api.observation.Event) event;
final Stream.Builder<NodeType> types = Stream.builder();
for (final NodeType type : modeEvent.getMixinNodeTypes()) {
types.add(type);
}
types.add(modeEvent.getPrimaryNodeType());
return types.build().map(NodeType::getName);
} catch (final RepositoryException e) {
throw new RepositoryRuntimeException(e);
}
}
return empty(); // wasn't a ModeShape event, so we have no access to resource types
}
/**
* The JCR-based Event::getPath contains some Modeshape artifacts that must be removed or modified in
* order to correspond to the public resource path. For example, JCR Events will contain a trailing
* /jcr:content for Binaries, a trailing /propName for properties, and /#/ notation for URI fragments.
*/
private static String cleanPath(final Event event) throws RepositoryException {
// remove any trailing data for property changes
final String path = PROPERTY_TYPES.contains(event.getType()) ?
event.getPath().substring(0, event.getPath().lastIndexOf("/")) : event.getPath();
// reformat any hash URIs and remove any trailing /jcr:content
final HashConverter converter = new HashConverter();
return converter.reverse().convert(path.replaceAll("/" + JCR_CONTENT, ""));
}
}