1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.external.endpoint;
22
23 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
24 import static javax.servlet.http.HttpServletResponse.SC_OK;
25 import static org.apache.commons.lang3.StringUtils.isBlank;
26 import static org.apache.commons.lang3.StringUtils.trimToNull;
27 import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
28 import static org.opencastproject.external.common.ApiVersion.VERSION_1_1_0;
29 import static org.opencastproject.external.common.ApiVersion.VERSION_1_2_0;
30 import static org.opencastproject.external.common.ApiVersion.VERSION_1_5_0;
31 import static org.opencastproject.index.service.util.JSONUtils.collectionToJsonArray;
32 import static org.opencastproject.index.service.util.JSONUtils.safeString;
33 import static org.opencastproject.util.DateTimeSupport.toUTC;
34 import static org.opencastproject.util.RestUtil.getEndpointUrl;
35 import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
36 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
37
38 import org.opencastproject.elasticsearch.api.SearchIndexException;
39 import org.opencastproject.elasticsearch.api.SearchResult;
40 import org.opencastproject.elasticsearch.api.SearchResultItem;
41 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
42 import org.opencastproject.elasticsearch.index.objects.event.EventIndexSchema;
43 import org.opencastproject.elasticsearch.index.objects.series.Series;
44 import org.opencastproject.elasticsearch.index.objects.series.SeriesIndexSchema;
45 import org.opencastproject.elasticsearch.index.objects.series.SeriesSearchQuery;
46 import org.opencastproject.external.common.ApiMediaType;
47 import org.opencastproject.external.common.ApiResponseBuilder;
48 import org.opencastproject.external.common.ApiVersion;
49 import org.opencastproject.external.util.AclUtils;
50 import org.opencastproject.external.util.ExternalMetadataUtils;
51 import org.opencastproject.index.service.api.IndexService;
52 import org.opencastproject.index.service.exception.IndexServiceException;
53 import org.opencastproject.index.service.util.RequestUtils;
54 import org.opencastproject.index.service.util.RestUtils;
55 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
56 import org.opencastproject.metadata.dublincore.DublinCore;
57 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
58 import org.opencastproject.metadata.dublincore.MetadataField;
59 import org.opencastproject.metadata.dublincore.MetadataJson;
60 import org.opencastproject.metadata.dublincore.MetadataList;
61 import org.opencastproject.metadata.dublincore.SeriesCatalogUIAdapter;
62 import org.opencastproject.rest.RestConstants;
63 import org.opencastproject.security.api.AccessControlEntry;
64 import org.opencastproject.security.api.AccessControlList;
65 import org.opencastproject.security.api.AccessControlParser;
66 import org.opencastproject.security.api.Permissions;
67 import org.opencastproject.security.api.SecurityService;
68 import org.opencastproject.security.api.UnauthorizedException;
69 import org.opencastproject.series.api.SeriesException;
70 import org.opencastproject.series.api.SeriesService;
71 import org.opencastproject.systems.OpencastConstants;
72 import org.opencastproject.util.DateTimeSupport;
73 import org.opencastproject.util.NotFoundException;
74 import org.opencastproject.util.RestUtil;
75 import org.opencastproject.util.RestUtil.R;
76 import org.opencastproject.util.UrlSupport;
77 import org.opencastproject.util.data.Tuple;
78 import org.opencastproject.util.doc.rest.RestParameter;
79 import org.opencastproject.util.doc.rest.RestQuery;
80 import org.opencastproject.util.doc.rest.RestResponse;
81 import org.opencastproject.util.doc.rest.RestService;
82 import org.opencastproject.util.requests.SortCriterion;
83
84 import com.google.gson.JsonArray;
85 import com.google.gson.JsonObject;
86 import com.google.gson.JsonPrimitive;
87
88 import org.apache.commons.lang3.StringUtils;
89 import org.json.simple.JSONArray;
90 import org.json.simple.JSONObject;
91 import org.json.simple.parser.JSONParser;
92 import org.json.simple.parser.ParseException;
93 import org.osgi.service.component.ComponentContext;
94 import org.osgi.service.component.annotations.Activate;
95 import org.osgi.service.component.annotations.Component;
96 import org.osgi.service.component.annotations.Reference;
97 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
98 import org.slf4j.Logger;
99 import org.slf4j.LoggerFactory;
100
101 import java.net.URI;
102 import java.text.SimpleDateFormat;
103 import java.util.ArrayList;
104 import java.util.Date;
105 import java.util.List;
106 import java.util.Locale;
107 import java.util.Map;
108 import java.util.Map.Entry;
109 import java.util.Optional;
110 import java.util.TreeMap;
111 import java.util.stream.Collectors;
112
113 import javax.servlet.http.HttpServletResponse;
114 import javax.ws.rs.DELETE;
115 import javax.ws.rs.DefaultValue;
116 import javax.ws.rs.FormParam;
117 import javax.ws.rs.GET;
118 import javax.ws.rs.HeaderParam;
119 import javax.ws.rs.POST;
120 import javax.ws.rs.PUT;
121 import javax.ws.rs.Path;
122 import javax.ws.rs.PathParam;
123 import javax.ws.rs.Produces;
124 import javax.ws.rs.QueryParam;
125 import javax.ws.rs.WebApplicationException;
126 import javax.ws.rs.core.MediaType;
127 import javax.ws.rs.core.Response;
128 import javax.ws.rs.core.Response.Status;
129
130 @Path("/api/series")
131 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
132 ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
133 ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
134 ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
135 @RestService(
136 name = "externalapiseries",
137 title = "External API Series Service",
138 notes = {},
139 abstractText = "Provides resources and operations related to the series"
140 )
141 @Component(
142 immediate = true,
143 service = SeriesEndpoint.class,
144 property = {
145 "service.description=External API - Series Endpoint",
146 "opencast.service.type=org.opencastproject.external",
147 "opencast.service.path=/api/series"
148 }
149 )
150 @JaxrsResource
151 public class SeriesEndpoint {
152
153 private static final int CREATED_BY_UI_ORDER = 9;
154 private static final int DEFAULT_LIMIT = 100;
155
156 private static final Logger logger = LoggerFactory.getLogger(SeriesEndpoint.class);
157
158
159 protected String endpointBaseUrl;
160
161
162 private ElasticsearchIndex elasticsearchIndex;
163 private IndexService indexService;
164 private SecurityService securityService;
165 private SeriesService seriesService;
166
167
168 @Reference
169 void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
170 this.elasticsearchIndex = elasticsearchIndex;
171 }
172
173
174 @Reference
175 void setIndexService(IndexService indexService) {
176 this.indexService = indexService;
177 }
178
179
180 @Reference
181 void setSecurityService(SecurityService securityService) {
182 this.securityService = securityService;
183 }
184
185
186 @Reference
187 void setSeriesService(SeriesService seriesService) {
188 this.seriesService = seriesService;
189 }
190
191
192 @Activate
193 void activate(ComponentContext cc) {
194 logger.info("Activating External API - Series Endpoint");
195
196 final Tuple<String, String> endpointUrl = getEndpointUrl(cc, OpencastConstants.EXTERNAL_API_URL_ORG_PROPERTY,
197 RestConstants.SERVICE_PATH_PROPERTY);
198 endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
199 logger.debug("Configured service endpoint is {}", endpointBaseUrl);
200 }
201
202 @GET
203 @Path("")
204 @RestQuery(
205 name = "getseries",
206 description = "Returns a list of series.",
207 returnDescription = "",
208 restParameters = {
209 @RestParameter(name = "onlyWithWriteAccess", isRequired = false, description = "Whether only to get the "
210 + "series to which we have write access.", type = RestParameter.Type.BOOLEAN),
211 @RestParameter(name = "filter", isRequired = false, description = "Usage <Filter Name>:<Value to Filter "
212 + "With>. Filters can combine using a comma \",\". Available Filters: managedAcl, contributors, "
213 + "CreationDate, Creator, textFilter, language, license, organizers, subject, title. If API ver > 1.1.0 "
214 + "also: identifier, description, creator, publishers, rightsholder.", type = STRING),
215 @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting "
216 + "criteria. In the comma seperated list each type of sorting is specified as a pair such as: <Sort "
217 + "Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or "
218 + "descending order and is mandatory.", isRequired = false, type = STRING),
219 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.",
220 isRequired = false, type = RestParameter.Type.INTEGER),
221 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false,
222 type = RestParameter.Type.INTEGER),
223 @RestParameter(name = "withacl", isRequired = false, description = "Whether the acl should be included in "
224 + "the response.", type = RestParameter.Type.BOOLEAN)
225 },
226 responses = {
227 @RestResponse(description = "A (potentially empty) list of series is returned.",
228 responseCode = HttpServletResponse.SC_OK)
229 })
230 public Response getSeriesList(@HeaderParam("Accept") String acceptHeader, @QueryParam("filter") String filter,
231 @QueryParam("sort") String sort, @QueryParam("order") String order, @QueryParam("offset") int offset,
232 @QueryParam("limit") int limit, @QueryParam("onlyWithWriteAccess") Boolean onlyWithWriteAccess,
233 @QueryParam("withacl") Boolean withAcl) throws UnauthorizedException {
234 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
235 if (requestedVersion.isSmallerThan(VERSION_1_5_0)) {
236
237 withAcl = false;
238 }
239 try {
240 SeriesSearchQuery query = new SeriesSearchQuery(securityService.getOrganization().getId(),
241 securityService.getUser());
242 Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
243
244 if (offset > 0) {
245 query.withOffset(offset);
246 }
247
248
249 query.withLimit(limit < 1 ? DEFAULT_LIMIT : limit);
250
251
252 if (StringUtils.isNotBlank(filter)) {
253 for (String f : filter.split(",")) {
254 String[] filterTuple = f.split(":",2);
255 if (filterTuple.length < 2) {
256 logger.debug("Filter {} not valid: {}", filterTuple[0], filter);
257 continue;
258 }
259 String name = filterTuple[0];
260
261 String value;
262 if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
263
264 value = f.substring(name.length() + 1);
265 } else {
266 value = filterTuple[1];
267 }
268
269 if ("managedAcl".equals(name)) {
270 query.withManagedAcl(value);
271 } else if ("contributors".equals(name)) {
272 query.withContributor(value);
273 } else if ("CreationDate".equals(name)) {
274 try {
275 Tuple<Date, Date> fromAndToCreationRange = getFromAndToCreationRange(value.split("/")[0],
276 value.split("/")[1]);
277 query.withCreatedFrom(fromAndToCreationRange.getA());
278 query.withCreatedTo(fromAndToCreationRange.getB());
279 } catch (IllegalArgumentException e) {
280 return RestUtil.R.badRequest(e.getMessage());
281 } catch (ArrayIndexOutOfBoundsException e) {
282 String dateErrorMsg = String.format("Filter Series API error: Malformed date period. "
283 + "Correct UTC time period format: yyyy-MM-ddTHH:mm:ssZ/yyyy-MM-ddTHH:mm:ssZ, "
284 + "stated date period string: \"%s\"", value);
285 logger.warn(dateErrorMsg);
286 return RestUtil.R.badRequest(dateErrorMsg);
287 }
288 } else if ("Creator".equals(name)) {
289 query.withCreator(value);
290 } else if ("textFilter".equals(name)) {
291 query.withText(value);
292 } else if ("language".equals(name)) {
293 query.withLanguage(value);
294 } else if ("license".equals(name)) {
295 query.withLicense(value);
296 } else if ("organizers".equals(name)) {
297 query.withOrganizer(value);
298 } else if ("subject".equals(name)) {
299 query.withSubject(value);
300 } else if ("title".equals(name)) {
301 query.withTitle(value);
302 } else if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
303
304 if ("identifier".equals(name)) {
305 query.withIdentifier(value);
306 } else if ("description".equals(name)) {
307 query.withDescription(value);
308 } else if ("creator".equals(name)) {
309 query.withCreator(value);
310 } else if ("publishers".equals(name)) {
311 query.withPublisher(value);
312 } else if ("rightsholder".equals(name)) {
313 query.withRightsHolder(value);
314 } else {
315 logger.warn("Unknown filter criteria {}", name);
316 return Response.status(SC_BAD_REQUEST).build();
317 }
318 }
319 }
320 }
321
322 if (optSort.isPresent()) {
323 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
324 for (SortCriterion criterion : sortCriteria) {
325
326 switch (criterion.getFieldName()) {
327 case SeriesIndexSchema.TITLE:
328 query.sortByTitle(criterion.getOrder());
329 break;
330 case SeriesIndexSchema.CONTRIBUTORS:
331 query.sortByContributors(criterion.getOrder());
332 break;
333 case SeriesIndexSchema.CREATOR:
334 query.sortByOrganizers(criterion.getOrder());
335 break;
336 case EventIndexSchema.CREATED:
337 query.sortByCreatedDateTime(criterion.getOrder());
338 break;
339 default:
340 logger.info("Unknown sort criteria {}", criterion.getFieldName());
341 return Response.status(SC_BAD_REQUEST).build();
342 }
343 }
344 }
345
346 if (onlyWithWriteAccess != null && onlyWithWriteAccess) {
347 query.withoutActions();
348 query.withAction(Permissions.Action.WRITE);
349 }
350
351 logger.trace("Using Query: " + query.toString());
352
353 SearchResult<Series> result = elasticsearchIndex.getByQuery(query);
354 final boolean includeAcl = (withAcl != null && withAcl);
355 return queryResultToJson(result, includeAcl, requestedVersion);
356
357 } catch (Exception e) {
358 logger.warn("Could not perform search query", e);
359 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
360 }
361 }
362
363 private Response queryResultToJson(SearchResult<Series> result, boolean includeAcl, ApiVersion requestedVersion) {
364 JsonArray seriesArray = new JsonArray();
365
366 for (SearchResultItem<Series> item : result.getItems()) {
367 final Series s = item.getSource();
368 JsonArray subjects;
369 if (s.getSubject() == null) {
370 subjects = new JsonArray();
371 } else {
372 subjects = splitSubjectIntoArray(s.getSubject());
373 }
374
375 Date createdDate = s.getCreatedDateTime();
376 JsonObject seriesJson = new JsonObject();
377
378 seriesJson.addProperty("identifier", s.getIdentifier());
379 seriesJson.addProperty("title", s.getTitle());
380 seriesJson.addProperty("creator", safeString(s.getCreator()));
381 seriesJson.addProperty("created", createdDate != null ? toUTC(createdDate.getTime()) : "");
382 seriesJson.add("subjects", subjects);
383
384 seriesJson.add("contributors", collectionToJsonArray(s.getContributors()));
385 seriesJson.add("organizers", collectionToJsonArray(s.getOrganizers()));
386 seriesJson.add("publishers", collectionToJsonArray(s.getPublishers()));
387
388 if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
389 seriesJson.addProperty("description", safeString(s.getDescription()));
390 seriesJson.addProperty("language", safeString(s.getLanguage()));
391 seriesJson.addProperty("license", safeString(s.getLicense()));
392 seriesJson.addProperty("rightsholder", safeString(s.getRightsHolder()));
393
394 if (includeAcl) {
395 AccessControlList acl = getAclFromSeries(s);
396 seriesJson.add("acl", AclUtils.serializeAclToJson(acl));
397 }
398 }
399
400 seriesArray.add(seriesJson);
401 }
402
403 return ApiResponseBuilder.Json.ok(requestedVersion, seriesArray);
404 }
405
406
407
408
409
410
411
412
413 private static AccessControlList getAclFromSeries(Series series) {
414 AccessControlList activeAcl = new AccessControlList();
415 try {
416 if (series.getAccessPolicy() != null) {
417 activeAcl = AccessControlParser.parseAcl(series.getAccessPolicy());
418 }
419 } catch (Exception e) {
420 logger.error("Unable to parse access policy", e);
421 }
422 return activeAcl;
423 }
424
425 @GET
426 @Path("{seriesId}")
427 @RestQuery(
428 name = "getseries",
429 description = "Returns a single series.",
430 returnDescription = "",
431 pathParameters = {
432 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
433 },
434 restParameters = {
435 @RestParameter(name = "withacl", isRequired = false, type = RestParameter.Type.BOOLEAN,
436 description = "Whether the acl should be included in the response.")
437 },
438 responses = {
439 @RestResponse(description = "The series is returned.", responseCode = HttpServletResponse.SC_OK),
440 @RestResponse(description = "The specified series does not exist.",
441 responseCode = HttpServletResponse.SC_NOT_FOUND), })
442 public Response getSeries(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id,
443 @QueryParam("withacl") Boolean withAcl)
444 throws Exception {
445 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
446 if (requestedVersion.isSmallerThan(VERSION_1_5_0)) {
447
448 withAcl = false;
449 }
450
451 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
452 securityService.getUser());
453 if (optSeries.isPresent()) {
454 final Series s = optSeries.get();
455 JsonArray subjects;
456 if (s.getSubject() == null) {
457 subjects = new JsonArray();
458 } else {
459 subjects = splitSubjectIntoArray(s.getSubject());
460 }
461 Date createdDate = s.getCreatedDateTime();
462
463 JsonObject responseContent = new JsonObject();
464
465
466 responseContent.addProperty("identifier", s.getIdentifier());
467 responseContent.addProperty("title", s.getTitle());
468 responseContent.addProperty("description", safeString(s.getDescription()));
469 responseContent.addProperty("creator", safeString(s.getCreator()));
470 responseContent.add("subjects", subjects);
471 responseContent.addProperty("organization", s.getOrganization());
472 responseContent.addProperty("created", createdDate != null ? toUTC(createdDate.getTime()) : "");
473 responseContent.add("contributors", collectionToJsonArray(s.getContributors()));
474 responseContent.add("organizers", collectionToJsonArray(s.getOrganizers()));
475 responseContent.addProperty("opt_out", false);
476 responseContent.add("publishers", collectionToJsonArray(s.getPublishers()));
477
478 if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
479 responseContent.addProperty("language", safeString(s.getLanguage()));
480 responseContent.addProperty("license", safeString(s.getLicense()));
481 responseContent.addProperty("rightsholder", safeString(s.getRightsHolder()));
482
483 if (withAcl != null && withAcl) {
484 AccessControlList acl = getAclFromSeries(s);
485 JsonArray aclJsonArray = AclUtils.serializeAclToJson(acl);
486 responseContent.add("acl", aclJsonArray);
487 }
488 }
489
490 return ApiResponseBuilder.Json.ok(requestedVersion, responseContent);
491 }
492 return ApiResponseBuilder.notFound("Cannot find an series with id '%s'.", id);
493 }
494
495 private JsonArray splitSubjectIntoArray(final String subject) {
496 JsonArray array = new JsonArray();
497 if (subject != null && !subject.trim().isEmpty()) {
498 for (String part : subject.split(",")) {
499 array.add(new JsonPrimitive(part.trim()));
500 }
501 }
502 return array;
503 }
504
505 @GET
506 @Path("{seriesId}/metadata")
507 @RestQuery(
508 name = "getseriesmetadata",
509 description = "Returns a series' metadata of all types or returns a series' metadata collection of the given "
510 + "type when the query string parameter type is specified. For each metadata catalog there is a unique "
511 + "property called the flavor such as dublincore/series so the type in this example would be "
512 + "'dublincore/series'",
513 returnDescription = "",
514 pathParameters = {
515 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
516 },
517 restParameters = {
518 @RestParameter(name = "type", isRequired = false, description = "The type of metadata to return",
519 type = STRING)
520 },
521 responses = {
522 @RestResponse(description = "The series' metadata are returned.", responseCode = HttpServletResponse.SC_OK),
523 @RestResponse(description = "The specified series does not exist.",
524 responseCode = HttpServletResponse.SC_NOT_FOUND)
525 })
526 public Response getSeriesMetadata(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id,
527 @QueryParam("type") String type) throws Exception {
528 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
529 if (StringUtils.trimToNull(type) == null) {
530 return getAllMetadata(id, requestedVersion);
531 } else {
532 return getMetadataByType(id, type, requestedVersion);
533 }
534 }
535
536 private Response getAllMetadata(String id, ApiVersion requestedVersion) throws SearchIndexException {
537 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
538 securityService.getUser());
539 if (optSeries.isEmpty()) {
540 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
541 }
542
543 MetadataList metadataList = new MetadataList();
544 List<SeriesCatalogUIAdapter> catalogUIAdapters = indexService.getSeriesCatalogUIAdapters();
545 catalogUIAdapters.remove(indexService.getCommonSeriesCatalogUIAdapter());
546 for (SeriesCatalogUIAdapter adapter : catalogUIAdapters) {
547 final Optional<DublinCoreMetadataCollection> optSeriesMetadata = adapter.getFields(id);
548 if (optSeriesMetadata.isPresent()) {
549 metadataList.add(adapter.getFlavor().toString(), adapter.getUITitle(), optSeriesMetadata.get());
550 }
551 }
552 DublinCoreMetadataCollection collection = getSeriesMetadata(optSeries.get());
553 ExternalMetadataUtils.changeSubjectToSubjects(collection);
554 metadataList.add(indexService.getCommonSeriesCatalogUIAdapter(), collection);
555 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.listToJson(metadataList, false));
556 }
557
558 private Response getMetadataByType(String id, String type, ApiVersion requestedVersion) throws SearchIndexException {
559 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
560 securityService.getUser());
561 if (optSeries.isEmpty()) {
562 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
563 }
564
565
566 if (typeMatchesSeriesCatalogUIAdapter(type, indexService.getCommonSeriesCatalogUIAdapter())) {
567 DublinCoreMetadataCollection collection = getSeriesMetadata(optSeries.get());
568 ExternalMetadataUtils.changeSubjectToSubjects(collection);
569 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(collection, false));
570 }
571
572
573 List<SeriesCatalogUIAdapter> catalogUIAdapters = indexService.getSeriesCatalogUIAdapters();
574 catalogUIAdapters.remove(indexService.getCommonSeriesCatalogUIAdapter());
575
576 for (SeriesCatalogUIAdapter adapter : catalogUIAdapters) {
577 if (typeMatchesSeriesCatalogUIAdapter(type, adapter)) {
578 final Optional<DublinCoreMetadataCollection> optSeriesMetadata = adapter.getFields(id);
579 if (optSeriesMetadata.isPresent()) {
580 return ApiResponseBuilder.Json.ok(requestedVersion, MetadataJson.collectionToJson(optSeriesMetadata.get(),
581 true));
582 }
583 }
584 }
585 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for series with id '%s'.", type, id);
586 }
587
588
589
590
591
592
593
594
595 private DublinCoreMetadataCollection getSeriesMetadata(Series series) {
596 DublinCoreMetadataCollection metadata = indexService.getCommonSeriesCatalogUIAdapter().getRawFields();
597
598 MetadataField title = metadata.getOutputFields().get(DublinCore.PROPERTY_TITLE.getLocalName());
599 metadata.removeField(title);
600 MetadataField newTitle = new MetadataField(title);
601 newTitle.setValue(series.getTitle());
602 metadata.addField(newTitle);
603
604 MetadataField subject = metadata.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
605 metadata.removeField(subject);
606 MetadataField newSubject = new MetadataField(subject);
607 newSubject.setValue(series.getSubject());
608 metadata.addField(newSubject);
609
610 MetadataField description = metadata.getOutputFields().get(DublinCore.PROPERTY_DESCRIPTION.getLocalName());
611 metadata.removeField(description);
612 MetadataField newDescription = new MetadataField(description);
613 newDescription.setValue(series.getDescription());
614 metadata.addField(newDescription);
615
616 MetadataField language = metadata.getOutputFields().get(DublinCore.PROPERTY_LANGUAGE.getLocalName());
617 metadata.removeField(language);
618 MetadataField newLanguage = new MetadataField(language);
619 newLanguage.setValue(series.getLanguage());
620 metadata.addField(newLanguage);
621
622 MetadataField rightsHolder = metadata.getOutputFields().get(DublinCore.PROPERTY_RIGHTS_HOLDER.getLocalName());
623 metadata.removeField(rightsHolder);
624 MetadataField newRightsHolder = new MetadataField(rightsHolder);
625 newRightsHolder.setValue(series.getRightsHolder());
626 metadata.addField(newRightsHolder);
627
628 MetadataField license = metadata.getOutputFields().get(DublinCore.PROPERTY_LICENSE.getLocalName());
629 metadata.removeField(license);
630 MetadataField newLicense = new MetadataField(license);
631 newLicense.setValue(series.getLicense());
632 metadata.addField(newLicense);
633
634 MetadataField organizers = metadata.getOutputFields().get(DublinCore.PROPERTY_CREATOR.getLocalName());
635 metadata.removeField(organizers);
636 MetadataField newOrganizers = new MetadataField(organizers);
637 newOrganizers.setValue(StringUtils.join(series.getOrganizers(), ", "));
638 metadata.addField(newOrganizers);
639
640 MetadataField contributors = metadata.getOutputFields().get(DublinCore.PROPERTY_CONTRIBUTOR.getLocalName());
641 metadata.removeField(contributors);
642 MetadataField newContributors = new MetadataField(contributors);
643 newContributors.setValue(StringUtils.join(series.getContributors(), ", "));
644 metadata.addField(newContributors);
645
646 MetadataField publishers = metadata.getOutputFields().get(DublinCore.PROPERTY_PUBLISHER.getLocalName());
647 metadata.removeField(publishers);
648 MetadataField newPublishers = new MetadataField(publishers);
649 newPublishers.setValue(StringUtils.join(series.getPublishers(), ", "));
650 metadata.addField(newPublishers);
651
652
653 MetadataField createdBy = new MetadataField(
654 "createdBy",
655 null,
656 "EVENTS.SERIES.DETAILS.METADATA.CREATED_BY",
657 true,
658 false,
659 null,
660 null,
661 MetadataField.Type.TEXT,
662 null,
663 null,
664 CREATED_BY_UI_ORDER,
665 null,
666 null,
667 null,
668 null);
669 createdBy.setValue(series.getCreator());
670 metadata.addField(createdBy);
671
672 MetadataField uid = metadata.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName());
673 metadata.removeField(uid);
674 MetadataField newUID = new MetadataField(uid);
675 newUID.setValue(series.getIdentifier());
676 metadata.addField(newUID);
677
678 ExternalMetadataUtils.removeCollectionList(metadata);
679
680 return metadata;
681 }
682
683
684
685
686
687
688
689
690
691
692 private boolean typeMatchesSeriesCatalogUIAdapter(String type, SeriesCatalogUIAdapter catalog) {
693 if (StringUtils.trimToNull(type) == null) {
694 return false;
695 }
696 MediaPackageElementFlavor catalogFlavor = MediaPackageElementFlavor.parseFlavor(catalog.getFlavor().toString());
697 try {
698 MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(type);
699 return flavor.equals(catalogFlavor);
700 } catch (IllegalArgumentException e) {
701 return false;
702 }
703 }
704
705 private Optional<MediaPackageElementFlavor> getFlavor(String flavorString) {
706 try {
707 MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
708 return Optional.of(flavor);
709 } catch (IllegalArgumentException e) {
710 return Optional.empty();
711 }
712 }
713
714 @PUT
715 @Path("{seriesId}/metadata")
716 @RestQuery(
717 name = "updateseriesmetadata",
718 description = "Update a series' metadata of the given type. For a metadata catalog there is the flavor such as "
719 + "'dublincore/series' and this is the unique type.",
720 returnDescription = "",
721 pathParameters = {
722 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
723 },
724 restParameters = {
725 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to update",
726 type = STRING),
727 @RestParameter(name = "metadata", description = "Series metadata as Form param", isRequired = true,
728 type = STRING)
729 },
730 responses = {
731 @RestResponse(description = "The series' metadata have been updated.",
732 responseCode = HttpServletResponse.SC_OK),
733 @RestResponse(description = "The request is invalid or inconsistent.",
734 responseCode = HttpServletResponse.SC_BAD_REQUEST),
735 @RestResponse(description = "The specified series does not exist.",
736 responseCode = HttpServletResponse.SC_NOT_FOUND)
737 })
738 public Response updateSeriesMetadata(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id,
739 @QueryParam("type") String type, @FormParam("metadata") String metadataJSON) throws Exception {
740 if (StringUtils.trimToNull(metadataJSON) == null) {
741 return RestUtil.R.badRequest("Unable to update metadata for series as the metadata provided is empty.");
742 }
743 Map<String, String> updatedFields;
744 try {
745 updatedFields = RequestUtils.getKeyValueMap(metadataJSON);
746 } catch (ParseException e) {
747 logger.debug("Unable to update series '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
748 return RestUtil.R.badRequest(String.format("Unable to parse metadata fields as json from '%s' because '%s'",
749 metadataJSON, e.getMessage()));
750 } catch (IllegalArgumentException e) {
751 return RestUtil.R.badRequest(e.getMessage());
752 }
753
754 if (updatedFields == null || updatedFields.size() == 0) {
755 return RestUtil.R.badRequest(
756 String.format("Unable to parse metadata fields as json from '%s' because there were no fields to update.",
757 metadataJSON));
758 }
759
760 Optional<DublinCoreMetadataCollection> optCollection = Optional.empty();
761 SeriesCatalogUIAdapter adapter = null;
762
763 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
764 securityService.getUser());
765 if (optSeries.isEmpty()) {
766 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
767 }
768
769 MetadataList metadataList = new MetadataList();
770
771
772 if (typeMatchesSeriesCatalogUIAdapter(type, indexService.getCommonSeriesCatalogUIAdapter())) {
773 optCollection = Optional.of(getSeriesMetadata(optSeries.get()));
774 adapter = indexService.getCommonSeriesCatalogUIAdapter();
775 } else {
776 metadataList.add(indexService.getCommonSeriesCatalogUIAdapter(), getSeriesMetadata(optSeries.get()));
777 }
778
779
780 List<SeriesCatalogUIAdapter> catalogUIAdapters = indexService.getSeriesCatalogUIAdapters();
781 catalogUIAdapters.remove(indexService.getCommonSeriesCatalogUIAdapter());
782 if (catalogUIAdapters.size() > 0) {
783 for (SeriesCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
784 if (typeMatchesSeriesCatalogUIAdapter(type, catalogUIAdapter)) {
785 optCollection = catalogUIAdapter.getFields(id);
786 adapter = catalogUIAdapter;
787 } else {
788 Optional<DublinCoreMetadataCollection> current = catalogUIAdapter.getFields(id);
789 if (current.isPresent()) {
790 metadataList.add(catalogUIAdapter, current.get());
791 }
792 }
793 }
794 }
795
796 if (optCollection.isEmpty()) {
797 return ApiResponseBuilder.notFound("Cannot find a catalog with type '%s' for series with id '%s'.", type, id);
798 }
799
800 DublinCoreMetadataCollection collection = optCollection.get();
801
802 for (String key : updatedFields.keySet()) {
803 MetadataField field = collection.getOutputFields().get(key);
804 if (field == null) {
805 return ApiResponseBuilder.notFound(
806 "Cannot find a metadata field with id '%s' from event with id '%s' and the metadata type '%s'.", key,
807 id, type);
808 } else if (field.isRequired() && StringUtils.isBlank(updatedFields.get(key))) {
809 return R.badRequest(String.format(
810 "The series metadata field with id '%s' and the metadata type '%s' is required and can not be empty!.",
811 key, type));
812 }
813 collection.removeField(field);
814 collection.addField(MetadataJson.copyWithDifferentJsonValue(field, updatedFields.get(key)));
815 }
816
817 metadataList.add(adapter, collection);
818 indexService.updateAllSeriesMetadata(id, metadataList, elasticsearchIndex);
819 return ApiResponseBuilder.Json.ok(acceptHeader, "");
820 }
821
822 @DELETE
823 @Path("{seriesId}/metadata")
824 @RestQuery(
825 name = "deleteseriesmetadata",
826 description = "Deletes a series' metadata catalog of the given type. All fields and values of that catalog will "
827 + "be deleted.",
828 returnDescription = "",
829 pathParameters = {
830 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
831 },
832 restParameters = {
833 @RestParameter(name = "type", isRequired = true, description = "The type of metadata to delete",
834 type = STRING)
835 },
836 responses = {
837 @RestResponse(description = "The metadata have been deleted.",
838 responseCode = HttpServletResponse.SC_NO_CONTENT),
839 @RestResponse(description = "The main metadata catalog dublincore/series cannot be deleted as it has "
840 + "mandatory fields.", responseCode = HttpServletResponse.SC_FORBIDDEN),
841 @RestResponse(description = "The specified series does not exist.",
842 responseCode = HttpServletResponse.SC_NOT_FOUND)
843 })
844 public Response deleteSeriesMetadataByType(@HeaderParam("Accept") String acceptHeader,
845 @PathParam("seriesId") String id, @QueryParam("type") String type) throws Exception {
846 if (StringUtils.trimToNull(type) == null) {
847 return RestUtil.R
848 .badRequest(String.format("A type of catalog needs to be specified for series '%s' to delete it.", id));
849 }
850
851 Optional<MediaPackageElementFlavor> flavor = getFlavor(type);
852
853 if (flavor.isEmpty()) {
854 return RestUtil.R.badRequest(
855 String.format("Unable to parse flavor '%s' it should look something like dublincore/series.", type));
856 }
857
858 if (typeMatchesSeriesCatalogUIAdapter(type, indexService.getCommonSeriesCatalogUIAdapter())) {
859 return Response
860 .status(Status.FORBIDDEN).entity(String
861 .format("Unable to delete mandatory metadata catalog with type '%s' for series '%s'", type, id))
862 .build();
863 }
864
865 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
866 securityService.getUser());
867 if (optSeries.isEmpty()) {
868 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
869 }
870
871 try {
872 indexService.removeCatalogByFlavor(optSeries.get(), MediaPackageElementFlavor.parseFlavor(type));
873 } catch (NotFoundException e) {
874 return ApiResponseBuilder.notFound(e.getMessage());
875 }
876 return Response.noContent().build();
877 }
878
879 @GET
880 @Path("{seriesId}/acl")
881 @RestQuery(
882 name = "getseriesacl",
883 description = "Returns a series' access policy.",
884 returnDescription = "",
885 pathParameters = {
886 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
887 },
888 responses = {
889 @RestResponse(description = "The series' access policy is returned.",
890 responseCode = HttpServletResponse.SC_OK),
891 @RestResponse(description = "The specified series does not exist.",
892 responseCode = HttpServletResponse.SC_NOT_FOUND)
893 })
894 public Response getSeriesAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id)
895 throws Exception {
896 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
897 JSONParser parser = new JSONParser();
898 Optional<Series> optSeries = elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
899 securityService.getUser());
900 if (optSeries.isPresent()) {
901 Series series = optSeries.get();
902
903 if (series.getAccessPolicy() == null) {
904 return ApiResponseBuilder.notFound("Acl for series with id '%s' is not defined.", id);
905 }
906 JSONObject acl = (JSONObject) parser.parse(series.getAccessPolicy());
907
908 if (!((JSONObject) acl.get("acl")).containsKey("ace")) {
909 return ApiResponseBuilder.notFound("Cannot find acl for series with id '%s'.", id);
910 } else {
911 return ApiResponseBuilder.Json.ok(requestedVersion, ((JSONArray) ((JSONObject) acl.get("acl")).get("ace"))
912 .toJSONString());
913 }
914 }
915
916 return ApiResponseBuilder.notFound("Cannot find an series with id '%s'.", id);
917 }
918
919 @GET
920 @Path("{seriesId}/properties")
921 @RestQuery(
922 name = "getseriesproperties",
923 description = "Returns a series' properties",
924 returnDescription = "",
925 pathParameters = {
926 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
927 },
928 responses = {
929 @RestResponse(description = "The series' properties are returned.", responseCode = HttpServletResponse.SC_OK),
930 @RestResponse(description = "The specified series does not exist.",
931 responseCode = HttpServletResponse.SC_NOT_FOUND)
932 })
933 public Response getSeriesProperties(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id)
934 throws Exception {
935 if (elasticsearchIndex.getSeries(id, securityService.getOrganization().getId(),
936 securityService.getUser()).isPresent()) {
937 final Map<String, String> properties = seriesService.getSeriesProperties(id);
938
939 JsonObject json = new JsonObject();
940 for (Entry<String, String> entry : properties.entrySet()) {
941 json.addProperty(entry.getKey(), safeString(entry.getValue()));
942 }
943
944 return ApiResponseBuilder.Json.ok(acceptHeader, json);
945 } else {
946 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
947 }
948 }
949
950 @DELETE
951 @Path("{seriesId}")
952 @RestQuery(
953 name = "deleteseries",
954 description = "Deletes a series.",
955 returnDescription = "",
956 pathParameters = {
957 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
958 },
959 responses = {
960 @RestResponse(description = "The series has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
961 @RestResponse(description = "The specified series does not exist.",
962 responseCode = HttpServletResponse.SC_NOT_FOUND)
963 })
964 public Response deleteSeries(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String id)
965 throws NotFoundException {
966 try {
967 indexService.removeSeries(id);
968 return Response.noContent().build();
969 } catch (NotFoundException e) {
970 return ApiResponseBuilder.notFound("Cannot find a series with id '%s'.", id);
971 } catch (Exception e) {
972 logger.error("Unable to delete the series '{}' due to", id, e);
973 return Response.serverError().build();
974 }
975 }
976
977 @PUT
978 @Path("{seriesId}")
979 @RestQuery(
980 name = "updateallseriesmetadata",
981 description = "Update all series metadata.",
982 returnDescription = "",
983 pathParameters = {
984 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
985 },
986 restParameters = {
987 @RestParameter(name = "metadata", description = "Series metadata as Form param", isRequired = true,
988 type = STRING)
989 },
990 responses = {
991 @RestResponse(description = "The series' metadata have been updated.",
992 responseCode = HttpServletResponse.SC_OK),
993 @RestResponse(description = "The request is invalid or inconsistent.",
994 responseCode = HttpServletResponse.SC_BAD_REQUEST),
995 @RestResponse(description = "The specified series does not exist.",
996 responseCode = HttpServletResponse.SC_NOT_FOUND)
997 })
998 public Response updateSeriesMetadata(@HeaderParam("Accept") String acceptHeader,
999 @PathParam("seriesId") String seriesID, @FormParam("metadata") String metadataJSON)
1000 throws UnauthorizedException, NotFoundException, SearchIndexException {
1001 try {
1002 MetadataList metadataList = indexService.updateAllSeriesMetadata(seriesID, metadataJSON, elasticsearchIndex);
1003 return ApiResponseBuilder.Json.ok(acceptHeader, MetadataJson.listToJson(metadataList, true));
1004 } catch (IllegalArgumentException e) {
1005 logger.debug("Unable to update series '{}' with metadata '{}'", seriesID, metadataJSON, e);
1006 return RestUtil.R.badRequest(e.getMessage());
1007 } catch (IndexServiceException e) {
1008 logger.error("Unable to update series '{}' with metadata '{}'", seriesID, metadataJSON, e);
1009 return RestUtil.R.serverError();
1010 }
1011 }
1012
1013 @POST
1014 @Path("")
1015 @RestQuery(
1016 name = "createseries",
1017 description = "Creates a series.",
1018 returnDescription = "",
1019 restParameters = {
1020 @RestParameter(name = "metadata", isRequired = true, description = "Series metadata", type = STRING),
1021 @RestParameter(name = "acl", description = "A collection of roles with their possible action",
1022 isRequired = true, type = STRING),
1023 @RestParameter(name = "theme", description = "The theme ID to be applied to the series", isRequired = false,
1024 type = STRING)
1025 },
1026 responses = {
1027 @RestResponse(description = "A new series is created and its identifier is returned in the Location header.",
1028 responseCode = HttpServletResponse.SC_CREATED),
1029 @RestResponse(description = "The request is invalid or inconsistent..",
1030 responseCode = HttpServletResponse.SC_BAD_REQUEST),
1031 @RestResponse(description = "The user doesn't have the rights to create the series.",
1032 responseCode = HttpServletResponse.SC_UNAUTHORIZED)
1033 })
1034 public Response createNewSeries(@HeaderParam("Accept") String acceptHeader,
1035 @FormParam("metadata") String metadataParam, @FormParam("acl") String aclParam,
1036 @FormParam("theme") String themeIdParam) throws UnauthorizedException, NotFoundException {
1037 if (isBlank(metadataParam)) {
1038 return R.badRequest("Required parameter 'metadata' is missing or invalid");
1039 }
1040
1041 if (isBlank(aclParam)) {
1042 return R.badRequest("Required parameter 'acl' is missing or invalid");
1043 }
1044
1045 MetadataList metadataList;
1046 try {
1047 metadataList = deserializeMetadataList(metadataParam);
1048 } catch (ParseException e) {
1049 logger.debug("Unable to parse series metadata '{}'", metadataParam, e);
1050 return R.badRequest(String.format("Unable to parse metadata because '%s'", e.getMessage()));
1051 } catch (NotFoundException e) {
1052
1053 return R.badRequest(e.getMessage());
1054 } catch (IllegalArgumentException e) {
1055 logger.debug("Unable to create series with metadata '{}'", metadataParam, e);
1056 return R.badRequest(e.getMessage());
1057 }
1058 Map<String, String> options = new TreeMap<>();
1059 Optional<Long> optThemeId = Optional.empty();
1060 if (StringUtils.trimToNull(themeIdParam) != null) {
1061 try {
1062 Long themeId = Long.parseLong(themeIdParam);
1063 optThemeId = Optional.of(themeId);
1064 } catch (NumberFormatException e) {
1065 return R.badRequest(String.format("Unable to parse the theme id '%s' into a number", themeIdParam));
1066 }
1067 }
1068 AccessControlList acl;
1069 try {
1070 acl = AclUtils.deserializeJsonToAcl(aclParam, false);
1071 } catch (ParseException e) {
1072 logger.debug("Unable to parse acl '{}'", aclParam, e);
1073 return R.badRequest(String.format("Unable to parse acl '%s' because '%s'", aclParam, e.getMessage()));
1074 } catch (IllegalArgumentException e) {
1075 logger.debug("Unable to create new series with acl '{}'", aclParam, e);
1076 return R.badRequest(e.getMessage());
1077 }
1078
1079 try {
1080 String seriesId = indexService.createSeries(metadataList, options, Optional.of(acl), optThemeId);
1081 JsonObject json = new JsonObject();
1082 json.addProperty("identifier", safeString(seriesId));
1083 return ApiResponseBuilder.Json.created(acceptHeader, URI.create(getSeriesUrl(seriesId)), json);
1084 } catch (IndexServiceException e) {
1085 logger.error("Unable to create series with metadata '{}', acl '{}', theme '{}'",
1086 metadataParam, aclParam, themeIdParam, e);
1087 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
1088 }
1089 }
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102 protected MetadataList deserializeMetadataList(String json) throws ParseException, NotFoundException {
1103 MetadataList metadataList = new MetadataList();
1104 JSONParser parser = new JSONParser();
1105 JSONArray jsonCatalogs = (JSONArray) parser.parse(json);
1106 for (int i = 0; i < jsonCatalogs.size(); i++) {
1107 JSONObject catalog = (JSONObject) jsonCatalogs.get(i);
1108 if (catalog.get("flavor") == null || StringUtils.isBlank(catalog.get("flavor").toString())) {
1109 throw new IllegalArgumentException(
1110 "Unable to create new series as no flavor was given for one of the metadata collections");
1111 }
1112 String flavorString = catalog.get("flavor").toString();
1113
1114 MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
1115
1116 DublinCoreMetadataCollection collection = null;
1117 SeriesCatalogUIAdapter adapter = null;
1118 for (SeriesCatalogUIAdapter seriesCatalogUIAdapter : indexService.getSeriesCatalogUIAdapters()) {
1119 MediaPackageElementFlavor catalogFlavor = MediaPackageElementFlavor
1120 .parseFlavor(seriesCatalogUIAdapter.getFlavor().toString());
1121 if (catalogFlavor.equals(flavor)) {
1122 adapter = seriesCatalogUIAdapter;
1123 collection = seriesCatalogUIAdapter.getRawFields();
1124 }
1125 }
1126
1127 if (collection == null) {
1128 throw new IllegalArgumentException(
1129 String.format("Unable to find an SeriesCatalogUIAdapter with Flavor '%s'", flavorString));
1130 }
1131
1132 String fieldsJson = catalog.get("fields").toString();
1133 if (StringUtils.trimToNull(fieldsJson) != null) {
1134 Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson);
1135 for (String key : fields.keySet()) {
1136 if ("subjects".equals(key)) {
1137 MetadataField field = collection.getOutputFields().get("subject");
1138 if (field == null) {
1139 throw new NotFoundException(String.format(
1140 "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
1141 }
1142 collection.removeField(field);
1143 try {
1144 JSONArray subjects = (JSONArray) parser.parse(fields.get(key));
1145 collection.addField(
1146 MetadataJson.copyWithDifferentJsonValue(field, StringUtils.join(subjects.iterator(), ",")));
1147 } catch (ParseException e) {
1148 throw new IllegalArgumentException(
1149 String.format("Unable to parse the 'subjects' metadata array field because: %s", e.toString()));
1150 }
1151 } else {
1152 MetadataField field = collection.getOutputFields().get(key);
1153 if (field == null) {
1154 throw new NotFoundException(String.format(
1155 "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
1156 }
1157 collection.removeField(field);
1158 collection.addField(MetadataJson.copyWithDifferentJsonValue(field, fields.get(key)));
1159 }
1160 }
1161 }
1162 metadataList.add(adapter, collection);
1163 }
1164 return metadataList;
1165 }
1166
1167 @PUT
1168 @Path("{seriesId}/acl")
1169 @RestQuery(
1170 name = "updateseriesacl",
1171 description = "Updates a series' access policy.",
1172 returnDescription = "",
1173 pathParameters = {
1174 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
1175 },
1176 restParameters = {
1177 @RestParameter(name = "acl", isRequired = true, description = "Access policy", type = STRING),
1178 @RestParameter(name = "override", isRequired = false, description = "If true the series ACL will take "
1179 + "precedence over any existing episode ACL", type = STRING)
1180 },
1181 responses = {
1182 @RestResponse(description = "The access control list for the specified series is updated.",
1183 responseCode = HttpServletResponse.SC_OK),
1184 @RestResponse(description = "The specified series does not exist.",
1185 responseCode = HttpServletResponse.SC_NOT_FOUND)
1186 })
1187 public Response updateSeriesAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("seriesId") String seriesID,
1188 @FormParam("acl") String aclJson, @DefaultValue("false") @FormParam("override") boolean override)
1189 throws NotFoundException, SeriesException, UnauthorizedException {
1190 if (isBlank(aclJson)) {
1191 return R.badRequest("Missing form parameter 'acl'");
1192 }
1193
1194 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
1195 if (requestedVersion.isSmallerThan(VERSION_1_2_0)) {
1196
1197 override = false;
1198 }
1199
1200 JSONParser parser = new JSONParser();
1201 JSONArray acl;
1202 try {
1203 acl = (JSONArray) parser.parse(aclJson);
1204 } catch (ParseException e) {
1205 logger.debug("Could not parse ACL ({})", aclJson, e);
1206 return R.badRequest("Could not parse ACL");
1207 }
1208
1209 List<AccessControlEntry> accessControlEntries = ((List<?>) acl).stream()
1210 .map(a -> {
1211 JSONObject ace = (JSONObject) a;
1212 return new AccessControlEntry(
1213 (String) ace.get("role"),
1214 (String) ace.get("action"),
1215 (Boolean) ace.get("allow")
1216 );
1217 })
1218 .collect(Collectors.toList());
1219
1220 seriesService.updateAccessControl(seriesID, new AccessControlList(accessControlEntries), override);
1221 return ApiResponseBuilder.Json.ok(acceptHeader, aclJson);
1222 }
1223
1224 @SuppressWarnings("unchecked")
1225 @PUT
1226 @Path("{seriesId}/properties")
1227 @RestQuery(
1228 name = "updateseriesproperties",
1229 description = "Updates a series' properties",
1230 returnDescription = "",
1231 pathParameters = {
1232 @RestParameter(name = "seriesId", description = "The series id", isRequired = true, type = STRING)
1233 },
1234 restParameters = {
1235 @RestParameter(name = "properties", isRequired = true, description = "Series properties", type = STRING)
1236 },
1237 responses = {
1238 @RestResponse(description = "Successfully updated the series' properties.",
1239 responseCode = HttpServletResponse.SC_OK),
1240 @RestResponse(description = "The specified series does not exist.",
1241 responseCode = HttpServletResponse.SC_NOT_FOUND)
1242 })
1243 public Response updateSeriesProperties(@HeaderParam("Accept") String acceptHeader,
1244 @PathParam("seriesId") String seriesID, @FormParam("properties") String propertiesJson)
1245 throws NotFoundException, SeriesException, UnauthorizedException {
1246 if (StringUtils.isBlank(propertiesJson)) {
1247 return R.badRequest("Missing form parameter 'acl'");
1248 }
1249
1250 JSONParser parser = new JSONParser();
1251 JSONObject props;
1252 try {
1253 props = (JSONObject) parser.parse(propertiesJson);
1254 } catch (ParseException e) {
1255 logger.debug("Could not parse properties ({})", propertiesJson, e);
1256 return R.badRequest("Could not parse series properties");
1257 }
1258
1259 for (Object prop : props.entrySet()) {
1260 Entry<String, Object> field = (Entry<String, Object>) prop;
1261 seriesService.updateSeriesProperty(seriesID, field.getKey(), field.getValue().toString());
1262 }
1263
1264 return ApiResponseBuilder.Json.ok(acceptHeader, propertiesJson);
1265 }
1266
1267 @GET
1268 @Produces(MediaType.APPLICATION_JSON)
1269 @Path("series.json")
1270 @RestQuery(
1271 name = "listSeriesAsJson",
1272 description = "Returns the series matching the query parameters",
1273 returnDescription = "Returns the series search results as JSON",
1274 restParameters = {
1275 @RestParameter(
1276 name = "q",
1277 isRequired = false,
1278 description = "Free text search",
1279 type = STRING
1280 ),
1281 @RestParameter(
1282 name = "edit",
1283 isRequired = false,
1284 description = "Whether this query should return only series that are editable",
1285 type = BOOLEAN
1286 ),
1287 @RestParameter(
1288 name = "fuzzyMatch",
1289 isRequired = false,
1290 description = "Whether a partial match on series id is allowed, default is false",
1291 type = BOOLEAN
1292 ),
1293 @RestParameter(
1294 name = "seriesId",
1295 isRequired = false,
1296 description = "The series identifier",
1297 type = STRING
1298 ),
1299 @RestParameter(
1300 name = "seriesTitle",
1301 isRequired = false,
1302 description = "The series title",
1303 type = STRING
1304 ),
1305 @RestParameter(
1306 name = "creator",
1307 isRequired = false,
1308 description = "The series creator",
1309 type = STRING
1310 ),
1311 @RestParameter(
1312 name = "contributor",
1313 isRequired = false,
1314 description = "The series contributor",
1315 type = STRING
1316 ),
1317 @RestParameter(
1318 name = "publisher",
1319 isRequired = false,
1320 description = "The series publisher",
1321 type = STRING
1322 ),
1323 @RestParameter(
1324 name = "rightsholder",
1325 isRequired = false,
1326 description = "The series rights holder",
1327 type = STRING
1328 ),
1329 @RestParameter(
1330 name = "createdfrom",
1331 isRequired = false,
1332 description = "Filter results by created from (yyyy-MM-dd'T'HH:mm:ss'Z')",
1333 type = STRING
1334 ),
1335 @RestParameter(
1336 name = "createdto",
1337 isRequired = false,
1338 description = "Filter results by created to (yyyy-MM-dd'T'HH:mm:ss'Z')",
1339 type = STRING
1340 ),
1341 @RestParameter(
1342 name = "language",
1343 isRequired = false,
1344 description = "The series language",
1345 type = STRING
1346 ),
1347 @RestParameter(
1348 name = "license",
1349 isRequired = false,
1350 description = "The series license",
1351 type = STRING
1352 ),
1353 @RestParameter(
1354 name = "subject",
1355 isRequired = false,
1356 description = "The series subject",
1357 type = STRING
1358 ),
1359 @RestParameter(
1360 name = "description",
1361 isRequired = false,
1362 description = "The series description",
1363 type = STRING
1364 ),
1365 @RestParameter(
1366 name = "sort",
1367 isRequired = false,
1368 description = "The sort order. May include any of the following: TITLE, SUBJECT, "
1369 + "CREATOR, PUBLISHERS, CONTRIBUTORS, DESCRIPTION, CREATED_DATE_TIME, "
1370 + "LANGUAGE, RIGHTS_HOLDER, MANAGED_ACL, LICENCE. "
1371 + "Add '_DESC' to reverse the sort order (e.g. TITLE_DESC).",
1372 type = STRING
1373 ),
1374 @RestParameter(
1375 name = "offset",
1376 isRequired = false,
1377 description = "The offset",
1378 type = STRING
1379 ),
1380 @RestParameter(
1381 name = "count",
1382 isRequired = false,
1383 description = "Results per page (max 100)",
1384 type = STRING
1385 )
1386 },
1387 responses = {
1388 @RestResponse(
1389 responseCode = SC_OK,
1390 description = "The access control list."
1391 ),
1392 @RestResponse(
1393 responseCode = SC_UNAUTHORIZED,
1394 description = "If the current user is not authorized to perform this action"
1395 )
1396 }
1397 )
1398 public Response getSeriesAsJson(
1399 @QueryParam("q") String text,
1400 @QueryParam("seriesId") String seriesId,
1401 @QueryParam("edit") Boolean edit,
1402 @QueryParam("fuzzyMatch") Boolean fuzzyMatch,
1403 @QueryParam("seriesTitle") String seriesTitle,
1404 @QueryParam("creator") String creator,
1405 @QueryParam("contributor") String contributor,
1406 @QueryParam("publisher") String publisher,
1407 @QueryParam("rightsholder") String rightsHolder,
1408 @QueryParam("createdfrom") String createdFrom,
1409 @QueryParam("createdto") String createdTo,
1410 @QueryParam("language") String language,
1411 @QueryParam("license") String license,
1412 @QueryParam("subject") String subject,
1413 @QueryParam("description") String description,
1414 @QueryParam("sort") String sort,
1415 @QueryParam("offset") String offset,
1416 @QueryParam("count") String count
1417 ) throws UnauthorizedException {
1418 try {
1419 SearchResult<Series> items = getSeries(
1420 text, seriesId, edit, seriesTitle, creator, contributor, publisher,
1421 rightsHolder, createdFrom, createdTo, language, license, subject, description, sort,
1422 offset, count, fuzzyMatch);
1423
1424 return queryResultToJson(items, false, ApiVersion.VERSION_1_7_0);
1425
1426 } catch (UnauthorizedException e) {
1427 throw e;
1428 } catch (Exception e) {
1429 logger.warn("Could not perform search query: {}", e.getMessage());
1430 }
1431 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1432 }
1433
1434 private SearchResult<Series> getSeries(
1435 String text,
1436 String seriesId,
1437 Boolean edit,
1438 String seriesTitle,
1439 String creator,
1440 String contributor,
1441 String publisher,
1442 String rightsHolder,
1443 String createdFrom,
1444 String createdTo,
1445 String language,
1446 String license,
1447 String subject,
1448 String description,
1449 String sort,
1450 String offsetString,
1451 String countString,
1452 Boolean fuzzyMatch
1453 ) throws SeriesException, UnauthorizedException {
1454 int offset = 0;
1455 if (StringUtils.isNotEmpty(offsetString)) {
1456 try {
1457 offset = Integer.parseInt(offsetString);
1458 } catch (NumberFormatException e) {
1459 logger.warn("Bad start page parameter");
1460 }
1461 if (offset < 0) {
1462 offset = 0;
1463 }
1464 }
1465
1466 int count = DEFAULT_LIMIT;
1467 if (StringUtils.isNotEmpty(countString)) {
1468 try {
1469 count = Integer.parseInt(countString);
1470 } catch (NumberFormatException e) {
1471 logger.warn("Bad count parameter");
1472 }
1473 if (count < 1) {
1474 count = DEFAULT_LIMIT;
1475 }
1476 }
1477
1478 SeriesSearchQuery q = new SeriesSearchQuery(securityService.getOrganization().getId(), securityService.getUser());
1479 q.withLimit(count);
1480 q.withOffset(offset);
1481 if (edit != null) {
1482 q.withEdit(edit);
1483 }
1484 if (StringUtils.isNotEmpty(text)) {
1485 q.withText(fuzzyMatch.booleanValue(), text);
1486 }
1487 if (StringUtils.isNotEmpty(seriesId)) {
1488 q.withIdentifier(seriesId);
1489 }
1490 if (StringUtils.isNotEmpty(seriesTitle)) {
1491 q.withTitle(seriesTitle);
1492 }
1493 if (StringUtils.isNotEmpty(creator)) {
1494 q.withCreator(creator);
1495 }
1496 if (StringUtils.isNotEmpty(contributor)) {
1497 q.withContributor(contributor);
1498 }
1499 if (StringUtils.isNotEmpty(language)) {
1500 q.withLanguage(language);
1501 }
1502 if (StringUtils.isNotEmpty(license)) {
1503 q.withLicense(license);
1504 }
1505 if (StringUtils.isNotEmpty(subject)) {
1506 q.withSubject(subject);
1507 }
1508 if (StringUtils.isNotEmpty(publisher)) {
1509 q.withPublisher(publisher);
1510 }
1511 if (StringUtils.isNotEmpty(description)) {
1512 q.withDescription(description);
1513 }
1514 if (StringUtils.isNotEmpty(rightsHolder)) {
1515 q.withRightsHolder(rightsHolder);
1516 }
1517 try {
1518 if (StringUtils.isNotEmpty(createdFrom)) {
1519 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
1520 Date date = formatter.parse(createdFrom);
1521 q.withCreatedFrom(date);
1522 }
1523 if (StringUtils.isNotEmpty(createdTo)) {
1524 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
1525 Date date = formatter.parse(createdTo);
1526 q.withCreatedFrom(date);
1527 }
1528 } catch (java.text.ParseException e1) {
1529 logger.warn("Could not parse date parameter", e1);
1530 }
1531
1532 if (StringUtils.isNotBlank(sort)) {
1533 String enumKey;
1534 SortCriterion.Order order;
1535 if (sort.endsWith("_DESC")) {
1536 enumKey = sort.substring(0, sort.length() - "_DESC".length()).toUpperCase();
1537 order = SortCriterion.Order.Descending;
1538 } else {
1539 enumKey = sort;
1540 order = SortCriterion.Order.Ascending;
1541 }
1542
1543 try {
1544 switch (enumKey) {
1545 case SeriesIndexSchema.TITLE:
1546 q.sortByTitle(order);
1547 break;
1548 case SeriesIndexSchema.SUBJECT:
1549 q.sortBySubject(order);
1550 break;
1551 case SeriesIndexSchema.CREATOR:
1552 q.sortByCreator(order);
1553 break;
1554 case SeriesIndexSchema.PUBLISHERS:
1555 q.sortByPublishers(order);
1556 break;
1557 case SeriesIndexSchema.CONTRIBUTORS:
1558 q.sortByContributors(order);
1559 break;
1560 case SeriesIndexSchema.DESCRIPTION:
1561 q.sortByDescription(order);
1562 break;
1563 case SeriesIndexSchema.LANGUAGE:
1564 q.sortByLanguage(order);
1565 break;
1566 case SeriesIndexSchema.RIGHTS_HOLDER:
1567 q.sortByRightsHolder(order);
1568 break;
1569 case SeriesIndexSchema.LICENSE:
1570 q.sortByLicense(order);
1571 break;
1572 case SeriesIndexSchema.CREATED_DATE_TIME:
1573 q.sortByCreatedDateTime(order);
1574 break;
1575 case SeriesIndexSchema.MANAGED_ACL:
1576 q.sortByManagedAcl(order);
1577 break;
1578 default:
1579 logger.info("Unknown filter criteria {}", enumKey);
1580 throw new IllegalArgumentException("Unknown filter criteria " + enumKey);
1581 }
1582 } catch (IllegalArgumentException e) {
1583 logger.warn("No sort enum matches '{}'", enumKey);
1584 }
1585 }
1586
1587 try {
1588 return elasticsearchIndex.getByQuery(q);
1589 } catch (SearchIndexException e) {
1590 logger.error("Failed to execute search query: {}", e.getMessage());
1591 throw new SeriesException(e);
1592 }
1593 }
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606 private Tuple<Date, Date> getFromAndToCreationRange(String createdFrom, String createdTo) {
1607 Date createdFromDate = null;
1608 Date createdToDate = null;
1609 if ((StringUtils.isNotBlank(createdFrom) && StringUtils.isBlank(createdTo))
1610 || (StringUtils.isBlank(createdFrom) && StringUtils.isNotBlank(createdTo))) {
1611 logger.error("Both createdTo '{}' and createdFrom '{}' have to be specified or neither of them", createdTo,
1612 createdFrom);
1613 throw new IllegalArgumentException("Both createdTo '" + createdTo + "' and createdFrom '" + createdFrom
1614 + "' have to be specified or neither of them");
1615 } else {
1616
1617 if (StringUtils.isNotBlank(createdFrom)) {
1618 try {
1619 createdFromDate = new Date(DateTimeSupport.fromUTC(createdFrom));
1620 } catch (IllegalStateException e) {
1621 logger.error("Unable to parse createdFrom parameter '{}'", createdFrom, e);
1622 throw new IllegalArgumentException("Unable to parse createdFrom parameter.");
1623 } catch (java.text.ParseException e) {
1624 logger.error("Unable to parse createdFrom parameter '{}'", createdFrom, e);
1625 throw new IllegalArgumentException("Unable to parse createdFrom parameter.");
1626 }
1627 }
1628
1629 if (StringUtils.isNotBlank(createdTo)) {
1630 try {
1631 createdToDate = new Date(DateTimeSupport.fromUTC(createdTo));
1632 } catch (IllegalStateException e) {
1633 logger.error("Unable to parse createdTo parameter '{}'", createdTo, e);
1634 throw new IllegalArgumentException("Unable to parse createdTo parameter.");
1635 } catch (java.text.ParseException e) {
1636 logger.error("Unable to parse createdTo parameter '{}'", createdTo, e);
1637 throw new IllegalArgumentException("Unable to parse createdTo parameter.");
1638 }
1639 }
1640 }
1641 return new Tuple<>(createdFromDate, createdToDate);
1642 }
1643
1644 private String getSeriesUrl(String seriesId) {
1645 return UrlSupport.concat(endpointBaseUrl, seriesId);
1646 }
1647 }