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