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