eu.fbk.knowledgestore.Outcome Maven / Gradle / Ivy
Show all versions of ks-core Show documentation
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.
*
* An {@code Outcome} instance encodes the outcome of an {@code Operation} invocation. It stores:
*
*
* - the {@link Status} code specifying whether and how the invocation was successful or failed
* (see {@link #getStatus()});
* - the invocation ID, which is generated by the system and may link to additional information
* logged on client/server sides (see {@link #getInvocationID()});
* - an optional object ID, in case the invocation was applied to a specific object (see
* {@link #getObjectID()});
* - an optional message, providing additional information about the outcome (see
* {@link #getMessage()})
*
*
* {@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.
*
*/
public final class Outcome implements Comparable, 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 encode(final Stream extends Outcome> stream) {
Preconditions.checkNotNull(stream);
return Record.encode(stream.transform(new Function() {
@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 decode(final Stream stream,
@Nullable final Boolean chunked) {
Preconditions.checkNotNull(stream);
return Record.decode(stream, ImmutableSet.of(KSR.INVOCATION), chunked).transform(
new Function() {
@Override
public Outcome apply(final Record record) {
return Outcome.create(record);
}
}, 0);
}
/**
* Enumeration of {@code Outcome} status codes.
*
* 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)}.
*
*/
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 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 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;
}
}
}