package eu.fbk.knowledgestore; import java.io.Serializable; import java.util.Map; import javax.annotation.Nullable; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.openrdf.model.Statement; import org.openrdf.model.URI; import eu.fbk.knowledgestore.data.Data; import eu.fbk.knowledgestore.data.Record; import eu.fbk.knowledgestore.data.Stream; import eu.fbk.knowledgestore.vocabulary.KSR; /** * The outcome of the invocation of a KnowledgeStore operation. * <p> * An {@code Outcome} instance encodes the outcome of an {@code Operation} invocation. It stores: * </p> * <ul> * <li>the {@link Status} code specifying whether and how the invocation was successful or failed * (see {@link #getStatus()});</li> * <li>the invocation ID, which is generated by the system and may link to additional information * logged on client/server sides (see {@link #getInvocationID()});</li> * <li>an optional object ID, in case the invocation was applied to a specific object (see * {@link #getObjectID()});</li> * <li>an optional message, providing additional information about the outcome (see * {@link #getMessage()})</li> * </ul> * <p> * {@code Outcome} instances can be created using one of the static factory methods * {@link #create(Status, URI)}, {@link #create(Status, URI, URI)}, * {@link #create(Status, URI, URI, String)}. Equality and comparison are performed based on the * invocation ID. This class is immutable and thus thread safe. * </p> */ public final class Outcome implements Comparable<Outcome>, Serializable { private static final long serialVersionUID = 1L; private final Status status; private final URI invocationID; @Nullable private final URI objectID; @Nullable private final String message; private Outcome(final Status status, final URI invocationID, @Nullable final URI objectID, @Nullable final String message) { this.status = Preconditions.checkNotNull(status); this.invocationID = Preconditions.checkNotNull(invocationID); this.objectID = objectID; this.message = message; } /** * Creates a new {@code Outcome} with the status code and invocation ID supplied. * * @param status * the status code, not null * @param invocationID * the invocation ID, not null * @return the created {@code Outcome} */ public static Outcome create(final Status status, final URI invocationID) { return create(status, invocationID, null, null); } /** * Creates a new {@code Outcome} with the status code, invocation ID and optional object ID * supplied. * * @param status * the status code, not null * @param invocationID * the invocation ID, not null * @param objectID * the optional object ID, possibly null * @return the created {@code Outcome} */ public static Outcome create(final Status status, final URI invocationID, @Nullable final URI objectID) { return create(status, invocationID, objectID, null); } /** * Creates a new {@code Outcome} with the status code, invocation ID, optional object ID and * optional message supplied. * * @param status * the status code, not null * @param invocationID * the invocation ID, not null * @param objectID * the optional object ID, possibly null * @param message * the optional message, possibly null * @return the created {@code Outcome} */ public static Outcome create(final Status status, final URI invocationID, @Nullable final URI objectID, @Nullable final String message) { return new Outcome(status, invocationID, objectID, message); } /** * Creates a new {@code Outcome} starting from the {@code Record} supplied. The record ID is * used as the invocation ID, while properties {@link KSR#STATUS}, {@link KSR#OBJECT} and * {@link KSR#MESSAGE} are read, respectively, to recover the status code, the optional object * ID and the optional message. * * @param record * the record * @return the created {@code Outcome} */ public static Outcome create(final Record record) { final URI invocationID = record.getID(); final Status status = Status.valueOf(record.getUnique(KSR.STATUS, URI.class)); final URI objectID = record.getUnique(KSR.OBJECT, URI.class); final String message = record.getUnique(KSR.MESSAGE, String.class); return create(status, invocationID, objectID, message); } /** * Returns the status code for this outcome. This property can be checked to determine whether * the invocation was successful or resulted in an error, with different status codes * specifying different success or error conditions. * * @return the status code, not null */ public Status getStatus() { return this.status; } /** * Returns the ID of the invocation, which may link to relevant logged information. * * @return the invocation ID, not null */ public URI getInvocationID() { return this.invocationID; } /** * Returns the ID of the processed object, if applicable. * * @return the object ID, null if not applicable */ @Nullable public URI getObjectID() { return this.objectID; } /** * Return an optional message providing further information about the outcome. * * @return the optional message */ @Nullable public String getMessage() { return this.message; } /** * {@inheritDoc} Comparison is done on the invocation ID. */ @Override public int compareTo(final Outcome other) { return Data.getTotalComparator().compare(this.invocationID, other.invocationID); } /** * {@inheritDoc} Two {@code Outcome} instances are equal if the have the same invocation ID. */ @Override public boolean equals(@Nullable final Object object) { if (object == this) { return true; } if (!(object instanceof Outcome)) { return false; } final Outcome other = (Outcome) object; return this.invocationID.equals(other.invocationID); } /** * {@inheritDoc} The returned code depends on the invocation ID. */ @Override public int hashCode() { return this.invocationID.hashCode(); } /** * Return a {@code Record} version of this {@code Outcome} object. The returned record is * identified by this invocation ID; it is associated to the status URI via property * {@link KSR#STATUS}, to the optional object ID via property {@link KSR#OBJECT} and to the * optional message via property {@link KSR#MESSAGE}. * * @return the corresponding record */ public Record toRecord() { final Record record = Record.create(this.invocationID); record.add(KSR.STATUS, this.status.getURI()); if (this.objectID != null) { record.add(KSR.OBJECT, this.objectID); } if (this.message != null) { record.add(KSR.MESSAGE, Data.getValueFactory().createLiteral(this.message)); } return record; } /** * {@inheritDoc} The method returns a string of the form * {@code status invocationID (objectID) message}. */ @Override public String toString() { final StringBuilder builder = new StringBuilder(); builder.append(this.status); builder.append(' '); builder.append(Data.toString(this.invocationID, Data.getNamespaceMap())); if (this.objectID != null) { builder.append(' '); builder.append('('); builder.append(Data.toString(this.objectID, Data.getNamespaceMap())); builder.append(')'); } if (this.message != null) { builder.append(' '); builder.append(this.message); } return builder.toString(); } /** * Performs outcome-to-RDF encoding by converting a stream of outcomes in a stream of RDF * statements. * * @param stream * the stream of outcomes to encode. * @return the resulting stream of statements */ public static Stream<Statement> encode(final Stream<? extends Outcome> stream) { Preconditions.checkNotNull(stream); return Record.encode(stream.transform(new Function<Outcome, Record>() { @Override public Record apply(final Outcome outcome) { return outcome.toRecord(); } }, 0), ImmutableSet.of(KSR.INVOCATION)); } /** * Performs RDF-to-outcome decoding by converting a stream of RDF statements in a stream of * outcomes. Parameter {@code chunked} specifies whether the input statement stream is * chunked, i.e., organized as a sequence of statement chunks with each chunk containing the * statements for an outcome. Chunked RDF streams noticeably speed up decoding, and are always * produced by the KnowledgeStore API. Chunking information may be set to null (e.g., because * unknown at the time the method is called): in this case, it will be read from metadata * attribute {@code "chunked"} attached to the stream; reading will happen just before * decoding will take place, i.e., when a terminal stream operation will be called. * * @param stream * the stream of statements to decode * @param chunked * true if the input statement stream is chunked, null if to be read from stream * metadata * @return the resulting stream of outcomes */ public static Stream<Outcome> decode(final Stream<Statement> stream, @Nullable final Boolean chunked) { Preconditions.checkNotNull(stream); return Record.decode(stream, ImmutableSet.of(KSR.INVOCATION), chunked).transform( new Function<Record, Outcome>() { @Override public Outcome apply(final Record record) { return Outcome.create(record); } }, 0); } /** * Enumeration of {@code Outcome} status codes. * <p> * This enumeration lists the possible status codes for {@code Outcome} objects. A status code * is identified by an URI (see {@link #getURI()}), is briefly explained through a comment * string (see {@link #getComment()}) and may be mapped to an HTTP status code (see * {@link #getHTTPStatus()}). Methods {@link #isOK()}, {@link #isError()} determine, * respectively, if the status code denotes a success or error situation. Lookup of status * codes based on URI is supported by method {@link #valueOf(URI)}. * </p> */ public enum Status { /** * Success status specifying successful completion of a bulk operation invocation for all * the objects involved. * * @see KSR#OK_BULK */ OK_BULK(KSR.OK_BULK, "Bulk operation succeeded for all affected objects", 200, true), /** * Success status specifying that an object has been created. * * @see KSR#OK_CREATED */ OK_CREATED(KSR.OK_CREATED, "Object created", 201, true), /** * Success status specifying that an object has been modified. * * @see KSR#OK_MODIFIED */ OK_MODIFIED(KSR.OK_MODIFIED, "Object modified", 200, true), /** * Success status specifying that it was not necessary to modify an object. * * @see KSR#OK_UNMODIFIED */ OK_UNMODIFIED(KSR.OK_UNMODIFIED, "Object not modified", 200, true), /** * Success status specifying that an object has been deleted. * * @see KSR#OK_DELETED */ OK_DELETED(KSR.OK_DELETED, "Object deleted", 200, true), /** * Error status specifying that a bulk operation invocation failed for one or more of the * involved objects. * * @see KSR#ERROR_BULK */ ERROR_BULK(KSR.ERROR_BULK, "Bulk operation failed for at least one affected object", 200, false), /** * Error status specifying that an operation invocation failed as its result would not be * acceptable to the client. * * @see KSR#ERROR_PRECONDITION_FAILED */ ERROR_NOT_ACCEPTABLE(KSR.ERROR_NOT_ACCEPTABLE, "Operation failed as result would not be acceptable to client", 406, false), /** * Error status specifying that the referenced object does not exist. * * @see KSR#ERROR_OBJECT_NOT_FOUND */ ERROR_OBJECT_NOT_FOUND(KSR.ERROR_OBJECT_NOT_FOUND, "Operation failed as target object does not exist", 404, false), /** * Error status specifying that the referenced object already exists. * * @see KSR#ERROR_OBJECT_ALREADY_EXISTS */ ERROR_OBJECT_ALREADY_EXISTS(KSR.ERROR_OBJECT_ALREADY_EXISTS, "Operation failed as target object already exists", 409, false), /** * Error status specifying that an object indirectly referenced by the operation * invocation does not exist. * * @see KSR#ERROR_DEPENDENCY_NOT_FOUND */ ERROR_DEPENDENCY_NOT_FOUND(KSR.ERROR_DEPENDENCY_NOT_FOUND, "Operation failed as object it depends on does not exist", 400, false), /** * Error status specifying that input arguments are missing or wrong. * * @see KSR#ERROR_INVALID_INPUT */ ERROR_INVALID_INPUT(KSR.ERROR_INVALID_INPUT, "Operation failed as required input arguments are missing or wrong", 400, false), /** * Error status specifying that the invoked operation has not been executed because the * requester has not enough privileges. * * @see KSR#ERROR_FORBIDDEN */ ERROR_FORBIDDEN(KSR.ERROR_FORBIDDEN, "Operation forbidden", 403, false), /** * Error status specifying that the invoked operation was forcedly interrupted by the * client, server or due to a connectivity problem. * * @see KSR#ERROR_INTERRUPTED */ ERROR_INTERRUPTED(KSR.ERROR_INTERRUPTED, "Operation interrupted", 503, false), /** * Error status specifying that the invocation failed due to an unexpected error. * * @see KSR#ERROR_UNEXPECTED */ ERROR_UNEXPECTED(KSR.ERROR_UNEXPECTED, "Unexpected error", 500, false), /** * Error status specifying that the outcome of the invoked operation is unknown. * * @see KSR#UNKNOWN */ UNKNOWN(KSR.UNKNOWN, "Unknown outcome", 503, false); @Nullable private static Map<URI, Status> uriToStatusMap = null; private URI uri; private String comment; private int httpStatus; private boolean isOK; private boolean isError; private Status(final URI uri, final String comment, final int httpStatus, @Nullable final boolean okOrError) { this.uri = uri; this.comment = comment; this.httpStatus = httpStatus; this.isOK = okOrError == Boolean.TRUE; this.isError = okOrError == Boolean.FALSE; } /** * Returns the URI univocally identifying this status code. The URI can be used for * serializing / deserializing this {@code Status} object. * * @return the URI for this status code */ public URI getURI() { return this.uri; } /** * Returns a constant comment string describing this status code. * * @return a comment string */ public String getComment() { return this.comment; } /** * Returns the HTTP status code corresponding to this {@code Outcome} status code. Note * that multiple {@code Outcome} status codes may map to the same HTTP status code. * * @return the corresponding HTTP status code */ public int getHTTPStatus() { return this.httpStatus; } /** * Helper method to determine whether the status code denotes success. * * @return true, if this status code denotes success */ public boolean isOK() { return this.isOK; } /** * Helper method to determine whether the status code denotes error. * * @return true, if this status code denotes error */ public boolean isError() { return this.isError; } /** * Lookups the status code with the URI specified. * * @param uri * the URI of the status code to lookup * @return the corresponding status code, on success * @throws IllegalArgumentException * if there is no status for the URI specified */ public static Status valueOf(final URI uri) throws IllegalArgumentException { if (uriToStatusMap == null) { final ImmutableMap.Builder<URI, Status> builder = ImmutableMap.builder(); for (final Status status : Status.values()) { builder.put(status.uri, status); } uriToStatusMap = builder.build(); } final Status status = uriToStatusMap.get(uri); if (status == null) { throw new IllegalArgumentException("Invalid status URI: " + uri); } return status; } /** * Return the {@code Status} that more closely matches the HTTP status code specified. * Note that only part of the statuses of this enuemration are returned, as the mapping * from {@code Status} to HTTP statuses is N:1. * * @param httpStatus * the HTTP status * @return the corresponding {@code Status}, defaulting to {@link #ERROR_UNEXPECTED} */ public static Status valueOf(final int httpStatus) { if (httpStatus == 400) { return Status.ERROR_INVALID_INPUT; } else if (httpStatus == 401 || httpStatus == 403) { return Status.ERROR_FORBIDDEN; } else if (httpStatus == 404) { return Status.ERROR_OBJECT_NOT_FOUND; } else if (httpStatus == 406) { return Status.ERROR_NOT_ACCEPTABLE; } return Outcome.Status.ERROR_UNEXPECTED; } } }