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