1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package eu.ehri.extension;
21
22 import com.fasterxml.jackson.core.type.TypeReference;
23 import com.fasterxml.jackson.databind.JsonMappingException;
24 import com.google.common.base.Charsets;
25 import com.google.common.collect.Iterables;
26 import com.google.common.collect.Lists;
27 import com.tinkerpop.blueprints.Vertex;
28 import com.tinkerpop.pipes.PipeFunction;
29 import eu.ehri.extension.base.AbstractAccessibleResource;
30 import eu.ehri.project.acl.AclManager;
31 import eu.ehri.project.api.EventsApi;
32 import eu.ehri.project.api.impl.ApiImpl;
33 import eu.ehri.project.core.Tx;
34 import eu.ehri.project.exceptions.AccessDenied;
35 import eu.ehri.project.exceptions.DeserializationError;
36 import eu.ehri.project.exceptions.ItemNotFound;
37 import eu.ehri.project.exceptions.PermissionDenied;
38 import eu.ehri.project.exceptions.SerializationError;
39 import eu.ehri.project.exceptions.ValidationError;
40 import eu.ehri.project.exporters.dc.DublinCore11Exporter;
41 import eu.ehri.project.exporters.dc.DublinCoreExporter;
42 import eu.ehri.project.models.AccessPoint;
43 import eu.ehri.project.models.Annotation;
44 import eu.ehri.project.models.Link;
45 import eu.ehri.project.models.PermissionGrant;
46 import eu.ehri.project.models.base.Accessible;
47 import eu.ehri.project.models.base.Accessor;
48 import eu.ehri.project.models.base.Annotatable;
49 import eu.ehri.project.models.base.Described;
50 import eu.ehri.project.models.base.Description;
51 import eu.ehri.project.models.base.Entity;
52 import eu.ehri.project.models.base.Linkable;
53 import eu.ehri.project.models.base.PermissionGrantTarget;
54 import eu.ehri.project.models.base.PermissionScope;
55 import eu.ehri.project.models.base.Versioned;
56 import eu.ehri.project.models.events.Version;
57 import eu.ehri.project.persistence.Bundle;
58 import eu.ehri.project.persistence.Mutation;
59 import org.neo4j.graphdb.GraphDatabaseService;
60 import org.w3c.dom.Document;
61
62 import javax.ws.rs.Consumes;
63 import javax.ws.rs.DELETE;
64 import javax.ws.rs.DefaultValue;
65 import javax.ws.rs.GET;
66 import javax.ws.rs.POST;
67 import javax.ws.rs.PUT;
68 import javax.ws.rs.Path;
69 import javax.ws.rs.PathParam;
70 import javax.ws.rs.Produces;
71 import javax.ws.rs.QueryParam;
72 import javax.ws.rs.core.Context;
73 import javax.ws.rs.core.MediaType;
74 import javax.ws.rs.core.Response;
75 import java.io.IOException;
76 import java.util.List;
77 import java.util.Set;
78 import java.util.function.Function;
79
80
81
82
83
84 @Path(GenericResource.ENDPOINT)
85 public class GenericResource extends AbstractAccessibleResource<Accessible> {
86
87 public static final String ENDPOINT = "entities";
88
89 public GenericResource(@Context GraphDatabaseService database) {
90 super(database, Accessible.class);
91 }
92
93
94
95
96
97
98
99
100
101
102
103
104 @GET
105 @Produces(MediaType.APPLICATION_JSON)
106 public Response list(@QueryParam("id") List<String> ids, @QueryParam("gid") List<Long> gids) {
107 try (Tx tx = beginTx()) {
108 PipeFunction<Vertex, Boolean> aclFilter = AclManager
109 .getAclFilterFunction(getRequesterUserProfile());
110 PipeFunction<Vertex, Boolean> contentFilter = aclManager
111 .getContentTypeFilterFunction();
112 Function<Vertex, Vertex> filter = v ->
113 (contentFilter.compute(v) && aclFilter.compute(v)) ? v : null;
114
115 Iterable<Vertex> byGid = Iterables.transform(gids, graph::getVertex);
116 Iterable<Vertex> byId = manager.getVertices(ids);
117 Iterable<Vertex> all = Iterables.concat(byGid, byId);
118 Response response = streamingVertexList(() -> Iterables.transform(all, filter::apply));
119 tx.success();
120 return response;
121 }
122 }
123
124
125
126
127
128
129
130
131
132
133
134
135 @POST
136 @Produces(MediaType.APPLICATION_JSON)
137 @Consumes(MediaType.APPLICATION_JSON)
138 public Response listFromJson(String json)
139 throws ItemNotFound, PermissionDenied, DeserializationError, IOException {
140 IdSet set = parseGraphIds(json);
141 return this.list(set.ids, set.gids);
142 }
143
144
145
146
147
148
149
150 @GET
151 @Produces(MediaType.APPLICATION_JSON)
152 @Path("{id:[^/]+}")
153 public Response get(@PathParam("id") String id) throws ItemNotFound, AccessDenied {
154 try (final Tx tx = beginTx()) {
155 Vertex item = manager.getVertex(id);
156
157
158 Accessor currentUser = getRequesterUserProfile();
159 if (item == null || !aclManager.getContentTypeFilterFunction().compute(item)) {
160 throw new ItemNotFound(id);
161 } else if (!AclManager.getAclFilterFunction(currentUser).compute(item)) {
162 throw new AccessDenied(currentUser.getId(), id);
163 }
164
165 Response response = single(graph.frame(item, Accessible.class));
166 tx.success();
167 return response;
168 }
169 }
170
171
172
173
174
175
176
177 @GET
178 @Produces(MediaType.APPLICATION_JSON)
179 @Path("{id:[^/]+}/access")
180 public Response visibility(@PathParam("id") String id)
181 throws PermissionDenied, ItemNotFound, SerializationError {
182 try (final Tx tx = beginTx()) {
183 Accessible item = manager.getEntity(id, Accessible.class);
184 Iterable<Accessor> accessors = item.getAccessors();
185 Response response = streamingList(() -> accessors);
186 tx.success();
187 return response;
188 }
189 }
190
191
192
193
194
195
196
197
198
199 @POST
200 @Produces(MediaType.APPLICATION_JSON)
201 @Path("{id:[^/]+}/access")
202 public Response setVisibility(@PathParam("id") String id,
203 @QueryParam(ACCESSOR_PARAM) List<String> accessorIds)
204 throws PermissionDenied, ItemNotFound, SerializationError {
205 try (final Tx tx = beginTx()) {
206 Accessible item = api().detail(id, Accessible.class);
207 Accessor current = getRequesterUserProfile();
208 Set<Accessor> accessors = getAccessors(accessorIds, current);
209 api().acl().setAccessors(item, accessors);
210 Response response = single(item);
211 tx.success();
212 return response;
213 }
214 }
215
216
217
218
219
220
221
222 @POST
223 @Produces(MediaType.APPLICATION_JSON)
224 @Path("{id:[^/]+}/promote")
225 public Response addPromotion(@PathParam("id") String id)
226 throws PermissionDenied, ItemNotFound {
227 try (final Tx tx = beginTx()) {
228 Response item = single(api().promote(id));
229 tx.success();
230 return item;
231 } catch (ApiImpl.NotPromotableError e) {
232 return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
233 .entity(e.getMessage()).build();
234 }
235 }
236
237
238
239
240
241
242
243 @DELETE
244 @Produces(MediaType.APPLICATION_JSON)
245 @Path("{id:[^/]+}/promote")
246 public Response removePromotion(@PathParam("id") String id)
247 throws PermissionDenied, ItemNotFound, ValidationError {
248 try (final Tx tx = beginTx()) {
249 Response response = single(api().removePromotion(id));
250 tx.success();
251 return response;
252 }
253 }
254
255
256
257
258
259
260
261 @POST
262 @Produces(MediaType.APPLICATION_JSON)
263 @Path("{id:[^/]+}/demote")
264 public Response addDemotion(@PathParam("id") String id)
265 throws PermissionDenied, ItemNotFound {
266 try (final Tx tx = beginTx()) {
267 Response item = single(api().demote(id));
268 tx.success();
269 return item;
270 } catch (ApiImpl.NotPromotableError e) {
271 return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
272 .entity(e.getMessage()).build();
273 }
274 }
275
276
277
278
279
280
281
282 @DELETE
283 @Produces(MediaType.APPLICATION_JSON)
284 @Path("{id:[^/]+}/demote")
285 public Response removeDemotion(@PathParam("id") String id)
286 throws PermissionDenied, ItemNotFound, ValidationError {
287 try (final Tx tx = beginTx()) {
288 Response item = single(api().removeDemotion(id));
289 tx.success();
290 return item;
291 }
292 }
293
294
295
296
297
298
299
300
301 @GET
302 @Produces(MediaType.APPLICATION_JSON)
303 @Path("{id:[^/]+}/events")
304 public Response events(
305 @PathParam("id") String id,
306 @QueryParam(AGGREGATION_PARAM) @DefaultValue("user") EventsApi.Aggregation aggregation)
307 throws ItemNotFound, AccessDenied {
308 try (final Tx tx = beginTx()) {
309 Accessible item = api().detail(id, Accessible.class);
310 EventsApi eventsApi = getEventsApi()
311 .withAggregation(aggregation);
312 Response response = streamingListOfLists(() -> eventsApi.aggregateForItem(item));
313 tx.success();
314 return response;
315 }
316 }
317
318
319
320
321
322
323
324 @GET
325 @Produces(MediaType.APPLICATION_JSON)
326 @Path("{id:[^/]+}/annotations")
327 public Response annotations(
328 @PathParam("id") String id) throws ItemNotFound {
329 try (final Tx tx = beginTx()) {
330 Annotatable entity = manager.getEntity(id, Annotatable.class);
331 Response response = streamingPage(() -> getQuery().page(
332 entity.getAnnotations(),
333 Annotation.class));
334 tx.success();
335 return response;
336 }
337 }
338
339
340
341
342
343
344
345 @GET
346 @Produces(MediaType.APPLICATION_JSON)
347 @Path("{id:[^/]+}/links")
348 public Response links(@PathParam("id") String id) throws ItemNotFound {
349 try (final Tx tx = beginTx()) {
350 Linkable entity = manager.getEntity(id, Linkable.class);
351 Response response = streamingPage(() -> getQuery().page(
352 entity.getLinks(),
353 Link.class));
354 tx.success();
355 return response;
356 }
357 }
358
359
360
361
362
363
364 @GET
365 @Produces(MediaType.APPLICATION_JSON)
366 @Path("{id:[^/]+}/permission-grants")
367 public Response permissionGrants(@PathParam("id") String id)
368 throws PermissionDenied, ItemNotFound {
369 try (final Tx tx = beginTx()) {
370 PermissionGrantTarget target = manager.getEntity(id, PermissionGrantTarget.class);
371 Response response = streamingPage(() -> getQuery()
372 .page(target.getPermissionGrants(),
373 PermissionGrant.class));
374 tx.success();
375 return response;
376 }
377 }
378
379
380
381
382
383
384 @GET
385 @Produces(MediaType.APPLICATION_JSON)
386 @Path("{id:[^/]+}/scope-permission-grants")
387 public Response permissionGrantsAsScope(@PathParam("id") String id)
388 throws ItemNotFound {
389 try (final Tx tx = beginTx()) {
390 PermissionScope scope = manager.getEntity(id, PermissionScope.class);
391 Response response = streamingPage(() -> getQuery()
392 .page(scope.getPermissionGrants(),
393 PermissionGrant.class));
394 tx.success();
395 return response;
396 }
397 }
398
399
400
401
402
403
404
405
406 @POST
407 @Consumes(MediaType.APPLICATION_JSON)
408 @Produces(MediaType.APPLICATION_JSON)
409 @Path("{id:[^/]+}/descriptions")
410 public Response createDescription(@PathParam("id") String id, Bundle bundle)
411 throws PermissionDenied, ValidationError,
412 DeserializationError, ItemNotFound {
413 try (final Tx tx = beginTx()) {
414 Described item = api().detail(id, Described.class);
415 Description desc = api().createDependent(id, bundle,
416 Description.class, getLogMessage());
417 item.addDescription(desc);
418 Response response = buildResponse(item, desc, Response.Status.CREATED);
419 tx.success();
420 return response;
421 } catch (SerializationError serializationError) {
422 throw new RuntimeException(serializationError);
423 }
424 }
425
426
427
428
429
430
431
432
433 @PUT
434 @Consumes(MediaType.APPLICATION_JSON)
435 @Produces(MediaType.APPLICATION_JSON)
436 @Path("{id:[^/]+}/descriptions")
437 public Response updateDescription(@PathParam("id") String id, Bundle bundle)
438 throws PermissionDenied, ValidationError,
439 DeserializationError, ItemNotFound, SerializationError {
440 try (final Tx tx = beginTx()) {
441 Described item = api().detail(id, Described.class);
442 Mutation<Description> desc = api().updateDependent(id, bundle,
443 Description.class, getLogMessage());
444 Response response = buildResponse(item, desc.getNode(), Response.Status.OK);
445 tx.success();
446 return response;
447 } catch (SerializationError serializationError) {
448 throw new RuntimeException(serializationError);
449 }
450 }
451
452
453
454
455
456
457
458
459
460 @PUT
461 @Consumes(MediaType.APPLICATION_JSON)
462 @Produces(MediaType.APPLICATION_JSON)
463 @Path("{id:[^/]+}/descriptions/{did:[^/]+}")
464 public Response updateDescriptionWithId(@PathParam("id") String id,
465 @PathParam("did") String did, Bundle bundle)
466 throws AccessDenied, PermissionDenied, ValidationError,
467 DeserializationError, ItemNotFound, SerializationError {
468 return updateDescription(id, bundle.withId(did));
469 }
470
471
472
473
474
475
476
477 @DELETE
478 @Path("{id:[^/]+}/descriptions/{did:[^/]+}")
479 public void deleteDescription(
480 @PathParam("id") String id, @PathParam("did") String did)
481 throws PermissionDenied, ItemNotFound, ValidationError, SerializationError {
482 try (final Tx tx = beginTx()) {
483 api().deleteDependent(id, did, getLogMessage());
484 tx.success();
485 } catch (SerializationError serializationError) {
486 throw new RuntimeException(serializationError);
487 }
488 }
489
490
491
492
493
494
495
496
497
498
499 @POST
500 @Consumes(MediaType.APPLICATION_JSON)
501 @Produces(MediaType.APPLICATION_JSON)
502 @Path("{id:[^/]+}/descriptions/{did:[^/]+}/access-points")
503 public Response createAccessPoint(@PathParam("id") String id,
504 @PathParam("did") String did, Bundle bundle)
505 throws PermissionDenied, ValidationError, DeserializationError, ItemNotFound {
506 try (final Tx tx = beginTx()) {
507 Described item = api().detail(id, Described.class);
508 Description desc = api().detail(did, Description.class);
509 AccessPoint rel = api().createDependent(id, bundle,
510 AccessPoint.class, getLogMessage());
511 desc.addAccessPoint(rel);
512 Response response = buildResponse(item, rel, Response.Status.CREATED);
513 tx.success();
514 return response;
515 } catch (SerializationError serializationError) {
516 throw new RuntimeException(serializationError);
517 }
518 }
519
520
521
522
523
524
525
526
527 @GET
528 @Path("{id:[^/]+}/versions")
529 public Response listVersions(@PathParam("id") String id) throws ItemNotFound, AccessDenied {
530 try (final Tx tx = beginTx()) {
531 Versioned item = api().detail(id, Versioned.class);
532 Response response = streamingPage(() -> getQuery().setStream(true)
533 .page(item.getAllPriorVersions(), Version.class));
534 tx.success();
535 return response;
536 }
537 }
538
539
540
541
542
543
544
545
546
547 @DELETE
548 @Path("{id:[^/]+}/descriptions/{did:[^/]+}/access-points/{apid:[^/]+}")
549 public void deleteAccessPoint(@PathParam("id") String id,
550 @PathParam("did") String did, @PathParam("apid") String apid)
551 throws AccessDenied, PermissionDenied, ValidationError,
552 DeserializationError, ItemNotFound {
553 try (final Tx tx = beginTx()) {
554 api().deleteDependent(id, apid, getLogMessage());
555 tx.success();
556 } catch (SerializationError serializationError) {
557 throw new RuntimeException(serializationError);
558 }
559 }
560
561 @GET
562 @Path("{id:[^/]+}/dc")
563 @Produces(MediaType.TEXT_XML)
564 public Document exportDc(
565 @PathParam("id") String id,
566 @QueryParam("lang") String langCode)
567 throws AccessDenied, ItemNotFound, IOException {
568 try (final Tx tx = beginTx()) {
569 Described item = api().detail(id, Described.class);
570 DublinCoreExporter exporter = new DublinCore11Exporter(api());
571 Document doc = exporter.export(item, langCode);
572 tx.success();
573 return doc;
574 }
575 }
576
577
578
579 private Response buildResponse(Described item, Entity data, Response.Status status)
580 throws SerializationError {
581 return Response.status(status).location(getItemUri(item))
582 .entity((getSerializer().entityToJson(data))
583 .getBytes(Charsets.UTF_8)).build();
584 }
585
586 private static class IdSet {
587 final List<String> ids;
588 final List<Long> gids;
589
590 IdSet(List<String> ids, List<Long> gids) {
591 this.ids = ids;
592 this.gids = gids;
593 }
594 }
595
596 private IdSet parseGraphIds(String json) throws IOException, DeserializationError {
597 try {
598 TypeReference<List<Object>> typeRef = new TypeReference<List<Object>>() {
599 };
600 List<Object> jsonValues = jsonMapper.readValue(json, typeRef);
601 List<String> ids = Lists.newArrayList();
602 List<Long> gids = Lists.newArrayList();
603
604 for (Object js : jsonValues) {
605 if (js instanceof Integer) {
606 gids.add(Long.valueOf((Integer) js));
607 } else if (js instanceof Long) {
608 gids.add((Long) js);
609 } else if (js instanceof String) {
610 ids.add((String) js);
611 }
612 }
613 return new IdSet(ids, gids);
614 } catch (JsonMappingException e) {
615 throw new DeserializationError(e.getMessage());
616 }
617 }
618 }