001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006
007package org.fcrepo.kernel.impl.observer;
008
009import static java.nio.charset.StandardCharsets.UTF_8;
010
011import java.net.URI;
012import java.net.URLEncoder;
013import java.time.Instant;
014import java.util.HashSet;
015import java.util.Objects;
016import java.util.Set;
017
018import org.fcrepo.kernel.api.identifiers.FedoraId;
019import org.fcrepo.kernel.api.observer.Event;
020import org.fcrepo.kernel.api.observer.EventType;
021import org.fcrepo.kernel.api.operations.ResourceOperation;
022import org.fcrepo.kernel.impl.util.UserUtil;
023
024/**
025 * Converts a ResourceOperation into an Event.
026 *
027 * @author pwinckles
028 */
029public class ResourceOperationEventBuilder implements EventBuilder {
030
031    private FedoraId fedoraId;
032    private Set<EventType> types;
033    private Set<String> resourceTypes;
034    private String userID;
035    private String userAgent;
036    private String baseUrl;
037    private Instant date;
038    private String userAgentBaseUri;
039
040    /**
041     * Creates a new EventBuilder based on an ResourceOperation
042     *
043     * @param fedoraId the FedoraId the operation is on
044     * @param operation the ResourceOperation to create an event for
045     * @param userAgentBaseUri the base uri of the user agent, optional
046     * @return new builder
047     */
048    public static ResourceOperationEventBuilder fromResourceOperation(final FedoraId fedoraId,
049                                                                      final ResourceOperation operation,
050                                                                      final String userAgentBaseUri) {
051        final var builder = new ResourceOperationEventBuilder();
052        builder.fedoraId = fedoraId;
053        builder.date = Instant.now();
054        builder.resourceTypes = new HashSet<>();
055        builder.userID = operation.getUserPrincipal();
056        builder.types = new HashSet<>();
057        builder.types.add(mapOperationToEventType(operation));
058        builder.userAgentBaseUri = userAgentBaseUri;
059        return builder;
060    }
061
062    private static EventType mapOperationToEventType(final ResourceOperation operation) {
063        switch (operation.getType()) {
064            case CREATE:
065            case OVERWRITE_TOMBSTONE:
066                return EventType.RESOURCE_CREATION;
067            case UPDATE:
068            case UPDATE_HEADERS:
069                return EventType.RESOURCE_MODIFICATION;
070            case DELETE:
071                return EventType.RESOURCE_DELETION;
072            case PURGE:
073                return EventType.RESOURCE_PURGE;
074            case FOLLOW:
075                return EventType.INBOUND_REFERENCE;
076            default:
077                throw new IllegalStateException(
078                        String.format("There is no EventType mapping for ResourceOperation type %s on operation %s",
079                                operation.getType(), operation));
080        }
081    }
082
083    private ResourceOperationEventBuilder() {
084        // Intentionally left blank
085    }
086
087    @Override
088    public EventBuilder merge(final EventBuilder other) {
089        if (other == null) {
090            return this;
091        }
092
093        if (!(other instanceof ResourceOperationEventBuilder)) {
094            throw new IllegalStateException(
095                    String.format("Cannot merge EventBuilders because they are different types <%s> and <%s>",
096                            this.getClass(), other.getClass()));
097        }
098
099        final var otherCast = (ResourceOperationEventBuilder) other;
100
101        if (!this.fedoraId.equals(otherCast.fedoraId)) {
102            throw new IllegalStateException(
103                    String.format("Cannot merge events because they are for different resources: <%s> and <%s>",
104                            this, otherCast));
105        }
106
107        this.types.addAll(otherCast.types);
108        this.resourceTypes.addAll(otherCast.resourceTypes);
109
110        if (this.date.isBefore(otherCast.date)) {
111            this.date = otherCast.date;
112        }
113
114        return this;
115    }
116
117    @Override
118    public EventBuilder withResourceTypes(final Set<String> resourceTypes) {
119        this.resourceTypes = Objects.requireNonNullElse(resourceTypes, new HashSet<>());
120        return this;
121    }
122
123    @Override
124    public EventBuilder withBaseUrl(final String baseUrl) {
125        this.baseUrl = baseUrl;
126        return this;
127    }
128
129    @Override
130    public EventBuilder withUserAgent(final String userAgent) {
131        this.userAgent = (userAgent.contains(" ") ? URLEncoder.encode(userAgent, UTF_8) : userAgent);
132        return this;
133    }
134
135    @Override
136    public Event build() {
137        URI userUri = null;
138        if (userID != null) {
139            userUri = UserUtil.getUserURI(userID, userAgentBaseUri);
140        }
141        return new EventImpl(fedoraId, types, resourceTypes, userID, userUri, userAgent, baseUrl, date);
142    }
143
144    @Override
145    public String toString() {
146        return "ResourceOperationEventBuilder{" +
147                "fedoraId=" + fedoraId +
148                ", types=" + types +
149                ", resourceTypes=" + resourceTypes +
150                ", userID='" + userID + '\'' +
151                ", userAgent='" + userAgent + '\'' +
152                ", baseUrl='" + baseUrl + '\'' +
153                ", date=" + date +
154                '}';
155    }
156
157}