1   package eu.fbk.knowledgestore;
2   
3   import java.io.Serializable;
4   import java.util.Map;
5   
6   import javax.annotation.Nullable;
7   
8   import com.google.common.base.Function;
9   import com.google.common.base.Preconditions;
10  import com.google.common.collect.ImmutableMap;
11  import com.google.common.collect.ImmutableSet;
12  
13  import org.openrdf.model.Statement;
14  import org.openrdf.model.URI;
15  
16  import eu.fbk.knowledgestore.data.Data;
17  import eu.fbk.knowledgestore.data.Record;
18  import eu.fbk.knowledgestore.data.Stream;
19  import eu.fbk.knowledgestore.vocabulary.KSR;
20  
21  /**
22   * The outcome of the invocation of a KnowledgeStore operation.
23   * <p>
24   * An {@code Outcome} instance encodes the outcome of an {@code Operation} invocation. It stores:
25   * </p>
26   * <ul>
27   * <li>the {@link Status} code specifying whether and how the invocation was successful or failed
28   * (see {@link #getStatus()});</li>
29   * <li>the invocation ID, which is generated by the system and may link to additional information
30   * logged on client/server sides (see {@link #getInvocationID()});</li>
31   * <li>an optional object ID, in case the invocation was applied to a specific object (see
32   * {@link #getObjectID()});</li>
33   * <li>an optional message, providing additional information about the outcome (see
34   * {@link #getMessage()})</li>
35   * </ul>
36   * <p>
37   * {@code Outcome} instances can be created using one of the static factory methods
38   * {@link #create(Status, URI)}, {@link #create(Status, URI, URI)},
39   * {@link #create(Status, URI, URI, String)}. Equality and comparison are performed based on the
40   * invocation ID. This class is immutable and thus thread safe.
41   * </p>
42   */
43  public final class Outcome implements Comparable<Outcome>, Serializable {
44  
45      private static final long serialVersionUID = 1L;
46  
47      private final Status status;
48  
49      private final URI invocationID;
50  
51      @Nullable
52      private final URI objectID;
53  
54      @Nullable
55      private final String message;
56  
57      private Outcome(final Status status, final URI invocationID, @Nullable final URI objectID,
58              @Nullable final String message) {
59          this.status = Preconditions.checkNotNull(status);
60          this.invocationID = Preconditions.checkNotNull(invocationID);
61          this.objectID = objectID;
62          this.message = message;
63      }
64  
65      /**
66       * Creates a new {@code Outcome} with the status code and invocation ID supplied.
67       * 
68       * @param status
69       *            the status code, not null
70       * @param invocationID
71       *            the invocation ID, not null
72       * @return the created {@code Outcome}
73       */
74      public static Outcome create(final Status status, final URI invocationID) {
75          return create(status, invocationID, null, null);
76      }
77  
78      /**
79       * Creates a new {@code Outcome} with the status code, invocation ID and optional object ID
80       * supplied.
81       * 
82       * @param status
83       *            the status code, not null
84       * @param invocationID
85       *            the invocation ID, not null
86       * @param objectID
87       *            the optional object ID, possibly null
88       * @return the created {@code Outcome}
89       */
90      public static Outcome create(final Status status, final URI invocationID,
91              @Nullable final URI objectID) {
92          return create(status, invocationID, objectID, null);
93      }
94  
95      /**
96       * Creates a new {@code Outcome} with the status code, invocation ID, optional object ID and
97       * optional message supplied.
98       * 
99       * @param status
100      *            the status code, not null
101      * @param invocationID
102      *            the invocation ID, not null
103      * @param objectID
104      *            the optional object ID, possibly null
105      * @param message
106      *            the optional message, possibly null
107      * @return the created {@code Outcome}
108      */
109     public static Outcome create(final Status status, final URI invocationID,
110             @Nullable final URI objectID, @Nullable final String message) {
111         return new Outcome(status, invocationID, objectID, message);
112     }
113 
114     /**
115      * Creates a new {@code Outcome} starting from the {@code Record} supplied. The record ID is
116      * used as the invocation ID, while properties {@link KSR#STATUS}, {@link KSR#OBJECT} and
117      * {@link KSR#MESSAGE} are read, respectively, to recover the status code, the optional object
118      * ID and the optional message.
119      * 
120      * @param record
121      *            the record
122      * @return the created {@code Outcome}
123      */
124     public static Outcome create(final Record record) {
125         final URI invocationID = record.getID();
126         final Status status = Status.valueOf(record.getUnique(KSR.STATUS, URI.class));
127         final URI objectID = record.getUnique(KSR.OBJECT, URI.class);
128         final String message = record.getUnique(KSR.MESSAGE, String.class);
129         return create(status, invocationID, objectID, message);
130     }
131 
132     /**
133      * Returns the status code for this outcome. This property can be checked to determine whether
134      * the invocation was successful or resulted in an error, with different status codes
135      * specifying different success or error conditions.
136      * 
137      * @return the status code, not null
138      */
139     public Status getStatus() {
140         return this.status;
141     }
142 
143     /**
144      * Returns the ID of the invocation, which may link to relevant logged information.
145      * 
146      * @return the invocation ID, not null
147      */
148     public URI getInvocationID() {
149         return this.invocationID;
150     }
151 
152     /**
153      * Returns the ID of the processed object, if applicable.
154      * 
155      * @return the object ID, null if not applicable
156      */
157     @Nullable
158     public URI getObjectID() {
159         return this.objectID;
160     }
161 
162     /**
163      * Return an optional message providing further information about the outcome.
164      * 
165      * @return the optional message
166      */
167     @Nullable
168     public String getMessage() {
169         return this.message;
170     }
171 
172     /**
173      * {@inheritDoc} Comparison is done on the invocation ID.
174      */
175     @Override
176     public int compareTo(final Outcome other) {
177         return Data.getTotalComparator().compare(this.invocationID, other.invocationID);
178     }
179 
180     /**
181      * {@inheritDoc} Two {@code Outcome} instances are equal if the have the same invocation ID.
182      */
183     @Override
184     public boolean equals(@Nullable final Object object) {
185         if (object == this) {
186             return true;
187         }
188         if (!(object instanceof Outcome)) {
189             return false;
190         }
191         final Outcome other = (Outcome) object;
192         return this.invocationID.equals(other.invocationID);
193     }
194 
195     /**
196      * {@inheritDoc} The returned code depends on the invocation ID.
197      */
198     @Override
199     public int hashCode() {
200         return this.invocationID.hashCode();
201     }
202 
203     /**
204      * Return a {@code Record} version of this {@code Outcome} object. The returned record is
205      * identified by this invocation ID; it is associated to the status URI via property
206      * {@link KSR#STATUS}, to the optional object ID via property {@link KSR#OBJECT} and to the
207      * optional message via property {@link KSR#MESSAGE}.
208      * 
209      * @return the corresponding record
210      */
211     public Record toRecord() {
212         final Record record = Record.create(this.invocationID);
213         record.add(KSR.STATUS, this.status.getURI());
214         if (this.objectID != null) {
215             record.add(KSR.OBJECT, this.objectID);
216         }
217         if (this.message != null) {
218             record.add(KSR.MESSAGE, Data.getValueFactory().createLiteral(this.message));
219         }
220         return record;
221     }
222 
223     /**
224      * {@inheritDoc} The method returns a string of the form
225      * {@code status invocationID (objectID) message}.
226      */
227     @Override
228     public String toString() {
229         final StringBuilder builder = new StringBuilder();
230         builder.append(this.status);
231         builder.append(' ');
232         builder.append(Data.toString(this.invocationID, Data.getNamespaceMap()));
233         if (this.objectID != null) {
234             builder.append(' ');
235             builder.append('(');
236             builder.append(Data.toString(this.objectID, Data.getNamespaceMap()));
237             builder.append(')');
238         }
239         if (this.message != null) {
240             builder.append(' ');
241             builder.append(this.message);
242         }
243         return builder.toString();
244     }
245 
246     /**
247      * Performs outcome-to-RDF encoding by converting a stream of outcomes in a stream of RDF
248      * statements.
249      * 
250      * @param stream
251      *            the stream of outcomes to encode.
252      * @return the resulting stream of statements
253      */
254     public static Stream<Statement> encode(final Stream<? extends Outcome> stream) {
255         Preconditions.checkNotNull(stream);
256         return Record.encode(stream.transform(new Function<Outcome, Record>() {
257 
258             @Override
259             public Record apply(final Outcome outcome) {
260                 return outcome.toRecord();
261             }
262 
263         }, 0), ImmutableSet.of(KSR.INVOCATION));
264     }
265 
266     /**
267      * Performs RDF-to-outcome decoding by converting a stream of RDF statements in a stream of
268      * outcomes. Parameter {@code chunked} specifies whether the input statement stream is
269      * chunked, i.e., organized as a sequence of statement chunks with each chunk containing the
270      * statements for an outcome. Chunked RDF streams noticeably speed up decoding, and are always
271      * produced by the KnowledgeStore API. Chunking information may be set to null (e.g., because
272      * unknown at the time the method is called): in this case, it will be read from metadata
273      * attribute {@code "chunked"} attached to the stream; reading will happen just before
274      * decoding will take place, i.e., when a terminal stream operation will be called.
275      * 
276      * @param stream
277      *            the stream of statements to decode
278      * @param chunked
279      *            true if the input statement stream is chunked, null if to be read from stream
280      *            metadata
281      * @return the resulting stream of outcomes
282      */
283     public static Stream<Outcome> decode(final Stream<Statement> stream,
284             @Nullable final Boolean chunked) {
285         Preconditions.checkNotNull(stream);
286         return Record.decode(stream, ImmutableSet.of(KSR.INVOCATION), chunked).transform(
287                 new Function<Record, Outcome>() {
288 
289                     @Override
290                     public Outcome apply(final Record record) {
291                         return Outcome.create(record);
292                     }
293 
294                 }, 0);
295     }
296 
297     /**
298      * Enumeration of {@code Outcome} status codes.
299      * <p>
300      * This enumeration lists the possible status codes for {@code Outcome} objects. A status code
301      * is identified by an URI (see {@link #getURI()}), is briefly explained through a comment
302      * string (see {@link #getComment()}) and may be mapped to an HTTP status code (see
303      * {@link #getHTTPStatus()}). Methods {@link #isOK()}, {@link #isError()} determine,
304      * respectively, if the status code denotes a success or error situation. Lookup of status
305      * codes based on URI is supported by method {@link #valueOf(URI)}.
306      * </p>
307      */
308     public enum Status {
309 
310         /**
311          * Success status specifying successful completion of a bulk operation invocation for all
312          * the objects involved.
313          * 
314          * @see KSR#OK_BULK
315          */
316         OK_BULK(KSR.OK_BULK, "Bulk operation succeeded for all affected objects", 200, true),
317 
318         /**
319          * Success status specifying that an object has been created.
320          * 
321          * @see KSR#OK_CREATED
322          */
323         OK_CREATED(KSR.OK_CREATED, "Object created", 201, true),
324 
325         /**
326          * Success status specifying that an object has been modified.
327          * 
328          * @see KSR#OK_MODIFIED
329          */
330         OK_MODIFIED(KSR.OK_MODIFIED, "Object modified", 200, true),
331 
332         /**
333          * Success status specifying that it was not necessary to modify an object.
334          * 
335          * @see KSR#OK_UNMODIFIED
336          */
337         OK_UNMODIFIED(KSR.OK_UNMODIFIED, "Object not modified", 200, true),
338 
339         /**
340          * Success status specifying that an object has been deleted.
341          * 
342          * @see KSR#OK_DELETED
343          */
344         OK_DELETED(KSR.OK_DELETED, "Object deleted", 200, true),
345 
346         /**
347          * Error status specifying that a bulk operation invocation failed for one or more of the
348          * involved objects.
349          * 
350          * @see KSR#ERROR_BULK
351          */
352         ERROR_BULK(KSR.ERROR_BULK, "Bulk operation failed for at least one affected object", 200,
353                 false),
354 
355         /**
356          * Error status specifying that an operation invocation failed as its result would not be
357          * acceptable to the client.
358          * 
359          * @see KSR#ERROR_PRECONDITION_FAILED
360          */
361         ERROR_NOT_ACCEPTABLE(KSR.ERROR_NOT_ACCEPTABLE,
362                 "Operation failed as result would not be acceptable to client", 406, false),
363 
364         /**
365          * Error status specifying that the referenced object does not exist.
366          * 
367          * @see KSR#ERROR_OBJECT_NOT_FOUND
368          */
369         ERROR_OBJECT_NOT_FOUND(KSR.ERROR_OBJECT_NOT_FOUND,
370                 "Operation failed as target object does not exist", 404, false),
371 
372         /**
373          * Error status specifying that the referenced object already exists.
374          * 
375          * @see KSR#ERROR_OBJECT_ALREADY_EXISTS
376          */
377         ERROR_OBJECT_ALREADY_EXISTS(KSR.ERROR_OBJECT_ALREADY_EXISTS,
378                 "Operation failed as target object already exists", 409, false),
379 
380         /**
381          * Error status specifying that an object indirectly referenced by the operation
382          * invocation does not exist.
383          * 
384          * @see KSR#ERROR_DEPENDENCY_NOT_FOUND
385          */
386         ERROR_DEPENDENCY_NOT_FOUND(KSR.ERROR_DEPENDENCY_NOT_FOUND,
387                 "Operation failed as object it depends on does not exist", 400, false),
388 
389         /**
390          * Error status specifying that input arguments are missing or wrong.
391          * 
392          * @see KSR#ERROR_INVALID_INPUT
393          */
394         ERROR_INVALID_INPUT(KSR.ERROR_INVALID_INPUT,
395                 "Operation failed as required input arguments are missing or wrong", 400, false),
396 
397         /**
398          * Error status specifying that the invoked operation has not been executed because the
399          * requester has not enough privileges.
400          * 
401          * @see KSR#ERROR_FORBIDDEN
402          */
403         ERROR_FORBIDDEN(KSR.ERROR_FORBIDDEN, "Operation forbidden", 403, false),
404 
405         /**
406          * Error status specifying that the invoked operation was forcedly interrupted by the
407          * client, server or due to a connectivity problem.
408          * 
409          * @see KSR#ERROR_INTERRUPTED
410          */
411         ERROR_INTERRUPTED(KSR.ERROR_INTERRUPTED, "Operation interrupted", 503, false),
412 
413         /**
414          * Error status specifying that the invocation failed due to an unexpected error.
415          * 
416          * @see KSR#ERROR_UNEXPECTED
417          */
418         ERROR_UNEXPECTED(KSR.ERROR_UNEXPECTED, "Unexpected error", 500, false),
419 
420         /**
421          * Error status specifying that the outcome of the invoked operation is unknown.
422          * 
423          * @see KSR#UNKNOWN
424          */
425         UNKNOWN(KSR.UNKNOWN, "Unknown outcome", 503, false);
426 
427         @Nullable
428         private static Map<URI, Status> uriToStatusMap = null;
429 
430         private URI uri;
431 
432         private String comment;
433 
434         private int httpStatus;
435 
436         private boolean isOK;
437 
438         private boolean isError;
439 
440         private Status(final URI uri, final String comment, final int httpStatus,
441                 @Nullable final boolean okOrError) {
442             this.uri = uri;
443             this.comment = comment;
444             this.httpStatus = httpStatus;
445             this.isOK = okOrError == Boolean.TRUE;
446             this.isError = okOrError == Boolean.FALSE;
447         }
448 
449         /**
450          * Returns the URI univocally identifying this status code. The URI can be used for
451          * serializing / deserializing this {@code Status} object.
452          * 
453          * @return the URI for this status code
454          */
455         public URI getURI() {
456             return this.uri;
457         }
458 
459         /**
460          * Returns a constant comment string describing this status code.
461          * 
462          * @return a comment string
463          */
464         public String getComment() {
465             return this.comment;
466         }
467 
468         /**
469          * Returns the HTTP status code corresponding to this {@code Outcome} status code. Note
470          * that multiple {@code Outcome} status codes may map to the same HTTP status code.
471          * 
472          * @return the corresponding HTTP status code
473          */
474         public int getHTTPStatus() {
475             return this.httpStatus;
476         }
477 
478         /**
479          * Helper method to determine whether the status code denotes success.
480          * 
481          * @return true, if this status code denotes success
482          */
483         public boolean isOK() {
484             return this.isOK;
485         }
486 
487         /**
488          * Helper method to determine whether the status code denotes error.
489          * 
490          * @return true, if this status code denotes error
491          */
492         public boolean isError() {
493             return this.isError;
494         }
495 
496         /**
497          * Lookups the status code with the URI specified.
498          * 
499          * @param uri
500          *            the URI of the status code to lookup
501          * @return the corresponding status code, on success
502          * @throws IllegalArgumentException
503          *             if there is no status for the URI specified
504          */
505         public static Status valueOf(final URI uri) throws IllegalArgumentException {
506             if (uriToStatusMap == null) {
507                 final ImmutableMap.Builder<URI, Status> builder = ImmutableMap.builder();
508                 for (final Status status : Status.values()) {
509                     builder.put(status.uri, status);
510                 }
511                 uriToStatusMap = builder.build();
512             }
513             final Status status = uriToStatusMap.get(uri);
514             if (status == null) {
515                 throw new IllegalArgumentException("Invalid status URI: " + uri);
516             }
517             return status;
518         }
519 
520         /**
521          * Return the {@code Status} that more closely matches the HTTP status code specified.
522          * Note that only part of the statuses of this enuemration are returned, as the mapping
523          * from {@code Status} to HTTP statuses is N:1.
524          * 
525          * @param httpStatus
526          *            the HTTP status
527          * @return the corresponding {@code Status}, defaulting to {@link #ERROR_UNEXPECTED}
528          */
529         public static Status valueOf(final int httpStatus) {
530             if (httpStatus == 400) {
531                 return Status.ERROR_INVALID_INPUT;
532             } else if (httpStatus == 401 || httpStatus == 403) {
533                 return Status.ERROR_FORBIDDEN;
534             } else if (httpStatus == 404) {
535                 return Status.ERROR_OBJECT_NOT_FOUND;
536             } else if (httpStatus == 406) {
537                 return Status.ERROR_NOT_ACCEPTABLE;
538             }
539             return Outcome.Status.ERROR_UNEXPECTED;
540         }
541 
542     }
543 
544 }