1   package eu.fbk.knowledgestore.server.http.jaxrs;
2   
3   import java.util.List;
4   
5   import javax.ws.rs.Consumes;
6   import javax.ws.rs.GET;
7   import javax.ws.rs.HttpMethod;
8   import javax.ws.rs.POST;
9   import javax.ws.rs.Path;
10  import javax.ws.rs.Produces;
11  import javax.ws.rs.QueryParam;
12  import javax.ws.rs.core.Response;
13  import javax.ws.rs.core.Response.Status;
14  
15  import com.google.common.base.MoreObjects;
16  import com.google.common.base.Preconditions;
17  import com.google.common.collect.ImmutableList;
18  import com.google.common.collect.ImmutableSet;
19  import com.google.common.collect.Iterables;
20  import com.google.common.collect.Lists;
21  
22  import org.codehaus.enunciate.jaxrs.ResponseCode;
23  import org.codehaus.enunciate.jaxrs.StatusCodes;
24  import org.codehaus.enunciate.jaxrs.TypeHint;
25  import org.openrdf.model.Literal;
26  import org.openrdf.model.Statement;
27  import org.openrdf.model.URI;
28  import org.openrdf.model.ValueFactory;
29  
30  import eu.fbk.knowledgestore.Operation;
31  import eu.fbk.knowledgestore.OperationException;
32  import eu.fbk.knowledgestore.Outcome;
33  import eu.fbk.knowledgestore.data.Criteria;
34  import eu.fbk.knowledgestore.data.Data;
35  import eu.fbk.knowledgestore.data.Record;
36  import eu.fbk.knowledgestore.data.Stream;
37  import eu.fbk.knowledgestore.data.XPath;
38  import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
39  import eu.fbk.knowledgestore.vocabulary.KSR;
40  
41  public abstract class Crud extends Resource {
42  
43      private static final long DEFAULT_RETRIEVE_LIMIT = 1000;
44  
45      private final URI recordType;
46  
47      Crud(final URI recordType) {
48          this.recordType = Preconditions.checkNotNull(recordType);
49      }
50  
51      final URI getRecordType() {
52          return this.recordType;
53      }
54  
55      @GET
56      @Produces(Protocol.MIME_TYPES_RDF)
57      @TypeHint(Stream.class)
58      @StatusCodes({ @ResponseCode(code = 200, condition = "if the request is acceptable and the "
59              + "query is being executed") })
60      public Response retrieve(
61              @QueryParam(Protocol.PARAMETER_CONDITION) final List<XPath> conditions,
62              @QueryParam(Protocol.PARAMETER_ID) final List<String> ids,
63              @QueryParam(Protocol.PARAMETER_PROPERTY) final List<String> properties,
64              @QueryParam(Protocol.PARAMETER_OFFSET) final Long offset,
65              @QueryParam(Protocol.PARAMETER_LIMIT) final Long limit) throws OperationException {
66  
67          
68          final long actualLimit = MoreObjects.firstNonNull(limit, DEFAULT_RETRIEVE_LIMIT);
69  
70          
71          final Operation.Retrieve operation;
72          try {
73              operation = getSession().retrieve(getRecordType()) 
74                      .timeout(getTimeout()) 
75                      .conditions(emptyToNull(conditions)) 
76                      .ids(emptyToNull(parseURIs(ids))) 
77                      .properties(emptyToNull(parseURIs(properties))) 
78                      .offset(offset) 
79                      .limit(actualLimit);
80          } catch (final IllegalArgumentException ex) {
81              throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
82                      ex.getMessage()), ex);
83          }
84  
85          
86          init(false, null, null, null);
87  
88          
89          Stream<Record> entity;
90          if (getMethod().equals(HttpMethod.HEAD)) {
91              entity = Stream.create();
92          } else {
93              entity = operation.exec();
94          }
95          entity.setProperty("types", ImmutableSet.of(getRecordType()));
96  
97          
98          return newResponseBuilder(Status.OK, closeOnCompletion(entity), Protocol.STREAM_OF_RECORDS)
99                  .build();
100     }
101 
102     @GET
103     @Path(Protocol.SUBPATH_COUNT)
104     @Produces(Protocol.MIME_TYPES_RDF)
105     @TypeHint(Stream.class)
106     @StatusCodes({ @ResponseCode(code = 200, condition = "if the request is acceptable and the "
107             + "requested count is being returned") })
108     public Response count( 
109             @QueryParam(Protocol.PARAMETER_CONDITION) final List<XPath> conditions, 
110             @QueryParam(Protocol.PARAMETER_ID) final List<String> ids) throws OperationException {
111 
112         
113         final Operation.Count operation;
114         try {
115             operation = getSession() 
116                     .count(getRecordType()) 
117                     .timeout(getTimeout()) 
118                     .conditions(emptyToNull(conditions)) 
119                     .ids(emptyToNull(parseURIs(ids)));
120         } catch (final IllegalArgumentException ex) {
121             throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
122                     ex.getMessage()), ex);
123         }
124 
125         
126         init(false, null, null, null);
127 
128         
129         Stream<Statement> entity;
130         if (getMethod().equals(HttpMethod.HEAD)) {
131             entity = Stream.create();
132         } else {
133             final long count = operation.exec();
134             final ValueFactory factory = Data.getValueFactory();
135             final Literal literal = factory.createLiteral(count);
136             final Statement statement = factory.createStatement(getInvocationID(), KSR.RESULT,
137                     literal);
138             entity = Stream.create(statement);
139         }
140 
141         
142         return newResponseBuilder(Status.OK, closeOnCompletion(entity),
143                 Protocol.STREAM_OF_STATEMENTS).build();
144     }
145 
146     @POST
147     @Path(Protocol.SUBPATH_CREATE)
148     @Consumes(Protocol.MIME_TYPES_RDF)
149     @Produces(Protocol.MIME_TYPES_RDF)
150     @TypeHint(Stream.class)
151     public Response create(final Stream<Record> records) throws OperationException {
152 
153         
154         closeOnCompletion(records);
155 
156         
157         final Operation.Create operation;
158         try {
159             operation = getSession() 
160                     .create(getRecordType()) 
161                     .timeout(getTimeout()) 
162                     .records(records);
163         } catch (final IllegalArgumentException ex) {
164             throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
165                     ex.getMessage()), ex);
166         }
167 
168         
169         init(true, null);
170 
171         
172         records.setProperty("types", ImmutableList.of(getRecordType()));
173         closeOnCompletion(records);
174 
175         
176         final List<Outcome> outcomes = Lists.newArrayList();
177         operation.exec(outcomes);
178 
179         
180         final Stream<Outcome> entity = Stream.create(outcomes);
181         entity.setProperty("types", ImmutableSet.of(KSR.INVOCATION));
182 
183         
184         
185         
186         
187         
188         
189         
190         
191         
192         
193         
194         
195 
196         
197         return newResponseBuilder(Status.OK, entity, Protocol.STREAM_OF_OUTCOMES).build();
198     }
199 
200     @POST
201     @Path(Protocol.SUBPATH_MERGE)
202     @Consumes(Protocol.MIME_TYPES_RDF)
203     @Produces(Protocol.MIME_TYPES_RDF)
204     @TypeHint(Stream.class)
205     public Response merge( 
206             @QueryParam(Protocol.PARAMETER_CRITERIA) final List<Criteria> criterias, 
207             final Stream<Record> records) throws OperationException {
208 
209         
210         closeOnCompletion(records);
211 
212         
213         final Operation.Merge operation;
214         try {
215             operation = getSession() 
216                     .merge(getRecordType()) 
217                     .timeout(getTimeout()) 
218                     .records(records) 
219                     .criteria(emptyToNull(criterias));
220         } catch (final IllegalArgumentException ex) {
221             throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
222                     ex.getMessage()), ex);
223         }
224 
225         
226         init(true, null);
227 
228         
229         records.setProperty("types", ImmutableList.of(getRecordType()));
230         closeOnCompletion(records);
231 
232         
233         final List<Outcome> outcomes = Lists.newArrayList();
234         operation.exec(outcomes);
235 
236         
237         final Stream<Outcome> entity = Stream.create(outcomes);
238         entity.setProperty("types", ImmutableSet.of(KSR.INVOCATION));
239 
240         
241         
242         
243         
244         
245         
246         
247         
248         
249         
250         
251         
252 
253         
254         return newResponseBuilder(Status.OK, entity, Protocol.STREAM_OF_OUTCOMES).build();
255     }
256 
257     @POST
258     @Path(Protocol.SUBPATH_UPDATE)
259     @Consumes(Protocol.MIME_TYPES_RDF)
260     @Produces(Protocol.MIME_TYPES_RDF)
261     @TypeHint(Stream.class)
262     public Response update( 
263             @QueryParam(Protocol.PARAMETER_CONDITION) final List<XPath> conditions, 
264             @QueryParam(Protocol.PARAMETER_ID) final List<URI> ids, 
265             @QueryParam(Protocol.PARAMETER_CRITERIA) final List<Criteria> criterias, 
266             final Stream<Record> records) throws OperationException {
267 
268         
269         closeOnCompletion(records);
270 
271         
272         final Operation.Update operation;
273         try {
274             operation = getSession() 
275                     .update(getRecordType()) 
276                     .timeout(getTimeout()) 
277                     .conditions(emptyToNull(conditions)) 
278                     .ids(emptyToNull(ids)) 
279                     .criteria(emptyToNull(criterias));
280         } catch (final IllegalArgumentException ex) {
281             throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
282                     ex.getMessage()), ex);
283         }
284 
285         
286         init(true, null);
287 
288         
289         records.setProperty("types", ImmutableList.of(getRecordType()));
290         final Record record = records.getUnique();
291 
292         
293         final List<Outcome> outcomes = Lists.newArrayList();
294         operation.record(record).exec(outcomes);
295 
296         
297         final Stream<Outcome> entity = Stream.create(outcomes);
298         entity.setProperty("types", ImmutableSet.of(KSR.INVOCATION));
299 
300         
301         
302         
303         
304         
305         
306         
307         
308         
309         
310         
311         
312 
313         
314         return newResponseBuilder(Status.OK, entity, Protocol.STREAM_OF_OUTCOMES).build();
315     }
316 
317     @POST
318     @Path(Protocol.SUBPATH_DELETE)
319     @Produces(Protocol.MIME_TYPES_RDF)
320     @TypeHint(Stream.class)
321     public Response delete( 
322             @QueryParam(Protocol.PARAMETER_CONDITION) final List<XPath> conditions, 
323             @QueryParam(Protocol.PARAMETER_ID) final List<URI> ids) throws OperationException {
324 
325         
326         final Operation.Delete operation;
327         try {
328             operation = getSession() 
329                     .delete(getRecordType()) 
330                     .timeout(getTimeout()) 
331                     .conditions(emptyToNull(conditions)) 
332                     .ids(emptyToNull(ids));
333         } catch (final IllegalArgumentException ex) {
334             throw new OperationException(newOutcome(Outcome.Status.ERROR_INVALID_INPUT,
335                     ex.getMessage()), ex);
336         }
337 
338         
339         init(true, null);
340 
341         
342         final List<Outcome> outcomes = Lists.newArrayList();
343         operation.exec(outcomes);
344 
345         
346         final Stream<Outcome> entity = Stream.create(outcomes);
347         entity.setProperty("types", ImmutableSet.of(KSR.INVOCATION));
348 
349         
350         
351         
352         
353         
354         
355         
356         
357         
358         
359         
360         
361 
362         
363         return newResponseBuilder(Status.OK, closeOnCompletion(entity),
364                 Protocol.STREAM_OF_OUTCOMES).build();
365     }
366 
367     private <T extends Iterable<?>> T emptyToNull(final T iterable) {
368         return iterable == null || Iterables.isEmpty(iterable) ? null : iterable;
369     }
370 
371     private List<URI> parseURIs(final Iterable<String> strings) {
372         final List<URI> uris = Lists.newArrayList();
373         for (final String string : strings) {
374             final int length = string.length();
375             boolean escape = false;
376             boolean qname = false;
377             int start = -1;
378             for (int i = 0; i < length; ++i) {
379                 final char ch = string.charAt(i);
380                 if (escape) {
381                     escape = false;
382                 } else if (start >= 0) {
383                     if (qname) {
384                         if (ch == ',' || ch == ';' || Character.isWhitespace(ch)) {
385                             uris.add((URI) Data.parseValue(string.substring(start, i),
386                                     Data.getNamespaceMap()));
387                             start = -1;
388                         }
389                     } else {
390                         if (ch == '\\') {
391                             escape = true;
392                         } else if (ch == '>') {
393                             uris.add((URI) Data.parseValue(string.substring(start, i + 1),
394                                     Data.getNamespaceMap()));
395                             start = -1;
396 
397                         }
398                     }
399                 } else if (ch == '<') {
400                     start = i;
401                     qname = false;
402                 } else if (!Character.isWhitespace(ch)) {
403                     start = i;
404                     qname = true;
405                 }
406             }
407             if (start >= 0) {
408                 if (qname) {
409                     uris.add((URI) Data.parseValue(string.substring(start), Data.getNamespaceMap()));
410                 } else {
411                     throw new IllegalArgumentException("Invalid ID(s): " + string);
412                 }
413             }
414         }
415 
416         return uris;
417     }
418 
419 }