1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.adminui.endpoint;
23
24 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
26 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
27 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
28 import static javax.servlet.http.HttpServletResponse.SC_OK;
29 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
30 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
31 import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
32 import static javax.ws.rs.core.Response.Status.NOT_FOUND;
33 import static javax.ws.rs.core.Response.Status.NO_CONTENT;
34 import static org.apache.commons.lang3.StringUtils.trimToNull;
35 import static org.opencastproject.adminui.endpoint.EndpointUtil.transformAccessControList;
36 import static org.opencastproject.index.service.util.JSONUtils.collectionToJsonArray;
37 import static org.opencastproject.index.service.util.JSONUtils.safeString;
38 import static org.opencastproject.index.service.util.RestUtils.notFound;
39 import static org.opencastproject.index.service.util.RestUtils.okJson;
40 import static org.opencastproject.index.service.util.RestUtils.okJsonList;
41 import static org.opencastproject.util.DateTimeSupport.toUTC;
42 import static org.opencastproject.util.RestUtil.R.badRequest;
43 import static org.opencastproject.util.RestUtil.R.conflict;
44 import static org.opencastproject.util.RestUtil.R.forbidden;
45 import static org.opencastproject.util.RestUtil.R.notFound;
46 import static org.opencastproject.util.RestUtil.R.ok;
47 import static org.opencastproject.util.RestUtil.R.serverError;
48 import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
49 import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
50 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
51 import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
52
53 import org.opencastproject.adminui.impl.AdminUIConfiguration;
54 import org.opencastproject.adminui.tobira.TobiraException;
55 import org.opencastproject.adminui.tobira.TobiraService;
56 import org.opencastproject.authorization.xacml.manager.api.AclService;
57 import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
58 import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
59 import org.opencastproject.authorization.xacml.manager.util.AccessInformationUtil;
60 import org.opencastproject.elasticsearch.api.SearchIndexException;
61 import org.opencastproject.elasticsearch.api.SearchResult;
62 import org.opencastproject.elasticsearch.api.SearchResultItem;
63 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
64 import org.opencastproject.elasticsearch.index.objects.event.Event;
65 import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
66 import org.opencastproject.elasticsearch.index.objects.series.Series;
67 import org.opencastproject.elasticsearch.index.objects.series.SeriesIndexSchema;
68 import org.opencastproject.elasticsearch.index.objects.series.SeriesSearchQuery;
69 import org.opencastproject.index.service.api.IndexService;
70 import org.opencastproject.index.service.exception.IndexServiceException;
71 import org.opencastproject.index.service.util.RestUtils;
72 import org.opencastproject.list.api.ListProviderException;
73 import org.opencastproject.list.api.ListProvidersService;
74 import org.opencastproject.list.common.provider.SeriesListProvider;
75 import org.opencastproject.list.common.query.SeriesListQuery;
76 import org.opencastproject.metadata.dublincore.DublinCore;
77 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
78 import org.opencastproject.metadata.dublincore.MetadataField;
79 import org.opencastproject.metadata.dublincore.MetadataJson;
80 import org.opencastproject.metadata.dublincore.MetadataList;
81 import org.opencastproject.metadata.dublincore.SeriesCatalogUIAdapter;
82 import org.opencastproject.rest.BulkOperationResult;
83 import org.opencastproject.security.api.AccessControlList;
84 import org.opencastproject.security.api.AccessControlParser;
85 import org.opencastproject.security.api.Permissions;
86 import org.opencastproject.security.api.SecurityService;
87 import org.opencastproject.security.api.UnauthorizedException;
88 import org.opencastproject.security.api.UserDirectoryService;
89 import org.opencastproject.series.api.SeriesException;
90 import org.opencastproject.series.api.SeriesService;
91 import org.opencastproject.systems.OpencastConstants;
92 import org.opencastproject.themes.Theme;
93 import org.opencastproject.themes.ThemesServiceDatabase;
94 import org.opencastproject.themes.persistence.ThemesServiceDatabaseException;
95 import org.opencastproject.util.NotFoundException;
96 import org.opencastproject.util.RestUtil;
97 import org.opencastproject.util.UrlSupport;
98 import org.opencastproject.util.data.Tuple;
99 import org.opencastproject.util.doc.rest.RestParameter;
100 import org.opencastproject.util.doc.rest.RestParameter.Type;
101 import org.opencastproject.util.doc.rest.RestQuery;
102 import org.opencastproject.util.doc.rest.RestResponse;
103 import org.opencastproject.util.doc.rest.RestService;
104 import org.opencastproject.util.requests.SortCriterion;
105 import org.opencastproject.util.requests.SortCriterion.Order;
106 import org.opencastproject.workflow.api.WorkflowInstance;
107
108 import com.google.gson.JsonObject;
109
110 import org.apache.commons.lang3.BooleanUtils;
111 import org.apache.commons.lang3.StringUtils;
112 import org.json.simple.JSONArray;
113 import org.json.simple.JSONObject;
114 import org.json.simple.parser.JSONParser;
115 import org.osgi.service.component.ComponentContext;
116 import org.osgi.service.component.annotations.Activate;
117 import org.osgi.service.component.annotations.Component;
118 import org.osgi.service.component.annotations.Modified;
119 import org.osgi.service.component.annotations.Reference;
120 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
121 import org.slf4j.Logger;
122 import org.slf4j.LoggerFactory;
123
124 import java.io.IOException;
125 import java.net.URI;
126 import java.util.ArrayList;
127 import java.util.Date;
128 import java.util.List;
129 import java.util.Map;
130 import java.util.Objects;
131 import java.util.Optional;
132 import java.util.regex.Pattern;
133
134 import javax.servlet.http.HttpServletResponse;
135 import javax.ws.rs.DELETE;
136 import javax.ws.rs.DefaultValue;
137 import javax.ws.rs.FormParam;
138 import javax.ws.rs.GET;
139 import javax.ws.rs.POST;
140 import javax.ws.rs.PUT;
141 import javax.ws.rs.Path;
142 import javax.ws.rs.PathParam;
143 import javax.ws.rs.Produces;
144 import javax.ws.rs.QueryParam;
145 import javax.ws.rs.WebApplicationException;
146 import javax.ws.rs.core.MediaType;
147 import javax.ws.rs.core.Response;
148 import javax.ws.rs.core.Response.Status;
149
150 @Path("/admin-ng/series")
151 @RestService(
152 name = "SeriesProxyService",
153 title = "UI Series",
154 abstractText = "This service provides the series data for the UI.",
155 notes = { "This service offers the series CRUD Operations for the admin UI.",
156 "<strong>Important:</strong> "
157 + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
158 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
159 + "DO NOT use this for integration of third-party applications.<em>"})
160 @Component(
161 immediate = true,
162 service = SeriesEndpoint.class,
163 property = {
164 "service.description=Admin UI - SeriesEndpoint Endpoint",
165 "opencast.service.type=org.opencastproject.adminui.SeriesEndpoint",
166 "opencast.service.path=/admin-ng/series",
167 }
168 )
169 @JaxrsResource
170 public class SeriesEndpoint {
171
172 private static final Logger logger = LoggerFactory.getLogger(SeriesEndpoint.class);
173
174 private static final int CREATED_BY_UI_ORDER = 9;
175
176
177 private static final int DEFAULT_LIMIT = 100;
178
179 public static final String THEME_KEY = "theme";
180
181 private Boolean deleteSeriesWithEventsAllowed = true;
182 private Boolean onlySeriesWithWriteAccessSeriesTab = false;
183 private Boolean onlySeriesWithWriteAccessEventsFilter = false;
184
185 public static final String SERIES_HASEVENTS_DELETE_ALLOW_KEY = "series.hasEvents.delete.allow";
186 public static final String SERIESTAB_ONLYSERIESWITHWRITEACCESS_KEY = "seriesTab.onlySeriesWithWriteAccess";
187 public static final String EVENTSFILTER_ONLYSERIESWITHWRITEACCESS_KEY = "eventsFilter.onlySeriesWithWriteAccess";
188 public static final Pattern TOBIRA_CONFIG =
189 Pattern.compile("^tobira\\.(?<organization>.*)\\.(?<key>origin|trustedKey)$");
190
191 private SeriesService seriesService;
192 private SecurityService securityService;
193 private AclServiceFactory aclServiceFactory;
194 private IndexService indexService;
195 private ListProvidersService listProvidersService;
196 private ElasticsearchIndex searchIndex;
197 private AdminUIConfiguration adminUIConfiguration;
198 private ThemesServiceDatabase themesServiceDatabase;
199 private UserDirectoryService userDirectoryService;
200
201
202 private String serverUrl = "http://localhost:8080";
203
204
205 @Reference
206 public void setSeriesService(SeriesService seriesService) {
207 this.seriesService = seriesService;
208 }
209
210
211 @Reference
212 public void setIndex(ElasticsearchIndex index) {
213 this.searchIndex = index;
214 }
215
216
217 @Reference
218 public void setIndexService(IndexService indexService) {
219 this.indexService = indexService;
220 }
221
222
223 @Reference
224 public void setListProvidersService(ListProvidersService listProvidersService) {
225 this.listProvidersService = listProvidersService;
226 }
227
228
229 @Reference
230 public void setSecurityService(SecurityService securityService) {
231 this.securityService = securityService;
232 }
233
234
235 @Reference
236 public void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
237 this.aclServiceFactory = aclServiceFactory;
238 }
239
240 private AclService getAclService() {
241 return aclServiceFactory.serviceFor(securityService.getOrganization());
242 }
243
244
245
246 @Reference
247 public void setAdminUIConfiguration(AdminUIConfiguration adminUIConfiguration) {
248 this.adminUIConfiguration = adminUIConfiguration;
249 }
250
251
252 @Reference
253 public void setThemesServiceDatabase(ThemesServiceDatabase themesServiceDatabase) {
254 this.themesServiceDatabase = themesServiceDatabase;
255 }
256
257
258 @Reference
259 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
260 this.userDirectoryService = userDirectoryService;
261 }
262
263 @Activate
264 protected void activate(ComponentContext cc, Map<String, Object> properties) {
265 if (cc != null) {
266 String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
267 logger.debug("Configured server url is {}", ccServerUrl);
268 if (ccServerUrl != null) {
269 this.serverUrl = ccServerUrl;
270 }
271
272 modified(properties);
273 }
274 logger.info("Activate series endpoint");
275 }
276
277
278 @Modified
279 public void modified(Map<String, Object> properties) {
280 if (properties == null) {
281 logger.info("No configuration available, using defaults");
282 return;
283 }
284
285 Object mapValue = properties.get(SERIES_HASEVENTS_DELETE_ALLOW_KEY);
286 if (mapValue != null) {
287 deleteSeriesWithEventsAllowed = BooleanUtils.toBoolean(mapValue.toString());
288 }
289
290 mapValue = properties.get(SERIESTAB_ONLYSERIESWITHWRITEACCESS_KEY);
291 onlySeriesWithWriteAccessSeriesTab = BooleanUtils.toBoolean(Objects.toString(mapValue, "true"));
292
293 mapValue = properties.get(EVENTSFILTER_ONLYSERIESWITHWRITEACCESS_KEY);
294 onlySeriesWithWriteAccessEventsFilter = BooleanUtils.toBoolean(Objects.toString(mapValue, "true"));
295
296 properties.forEach((key, value) -> {
297 var matches = TOBIRA_CONFIG.matcher(key);
298 if (!matches.matches()) {
299 return;
300 }
301 var tobira = TobiraService.getTobira(matches.group("organization"));
302 switch (matches.group("key")) {
303 case "origin":
304 tobira.setOrigin((String) value);
305 break;
306 case "trustedKey":
307 tobira.setTrustedKey((String) value);
308 break;
309 default:
310 throw new RuntimeException("unhandled Tobira config key");
311 }
312 });
313
314 logger.info("Configuration updated");
315 }
316
317 @GET
318 @Path("{seriesId}/access.json")
319 @SuppressWarnings("unchecked")
320 @Produces(MediaType.APPLICATION_JSON)
321 @RestQuery(
322 name = "getseriesaccessinformation",
323 description = "Get the access information of a series",
324 returnDescription = "The access information",
325 pathParameters = {
326 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier",
327 type = Type.STRING)
328 }, responses = {
329 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the "
330 + "request."),
331 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."),
332 @RestResponse(responseCode = SC_OK, description = "The access information ")
333 })
334 public Response getSeriesAccessInformation(@PathParam("seriesId") String seriesId) throws NotFoundException {
335 if (StringUtils.isBlank(seriesId)) {
336 return RestUtil.R.badRequest("Path parameter series ID is missing");
337 }
338
339 boolean hasProcessingEvents = hasProcessingEvents(seriesId);
340
341
342 JSONArray systemAclsJson = new JSONArray();
343 List<ManagedAcl> acls = getAclService().getAcls();
344 for (ManagedAcl acl : acls) {
345 systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl));
346 }
347
348 JSONObject seriesAccessJson = new JSONObject();
349 try {
350 AccessControlList seriesAccessControl = seriesService.getSeriesAccessControl(seriesId);
351 Optional<ManagedAcl> currentAcl = AccessInformationUtil.matchAclsLenient(acls, seriesAccessControl,
352 adminUIConfiguration.getMatchManagedAclRolePrefixes());
353 seriesAccessJson.put("current_acl", currentAcl.isPresent() ? currentAcl.get().getId() : 0);
354 seriesAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(seriesAccessControl));
355 seriesAccessJson.put("acl", transformAccessControList(seriesAccessControl, userDirectoryService));
356 seriesAccessJson.put("locked", hasProcessingEvents);
357 } catch (SeriesException e) {
358 logger.error("Unable to get ACL from series {}", seriesId, e);
359 return RestUtil.R.serverError();
360 }
361
362 JSONObject jsonReturnObj = new JSONObject();
363 jsonReturnObj.put("system_acls", systemAclsJson);
364 jsonReturnObj.put("series_access", seriesAccessJson);
365
366 return Response.ok(jsonReturnObj.toString()).build();
367 }
368
369 @GET
370 @Produces(MediaType.APPLICATION_JSON)
371 @Path("{seriesId}/metadata.json")
372 @RestQuery(
373 name = "getseriesmetadata",
374 description = "Returns the series metadata as JSON",
375 returnDescription = "Returns the series metadata as JSON",
376 pathParameters = {
377 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
378 },
379 responses = {
380 @RestResponse(responseCode = SC_OK, description = "The series metadata as JSON."),
381 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
382 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
383 + "perform this action")
384 })
385 public Response getSeriesMetadata(@PathParam("seriesId") String series) throws UnauthorizedException,
386 NotFoundException, SearchIndexException {
387 Optional<Series> optSeries = searchIndex.getSeries(series, securityService.getOrganization(),
388 securityService.getUser());
389 if (optSeries.isEmpty()) {
390 return notFound("Cannot find a series with id '%s'.", series);
391 }
392
393 MetadataList metadataList = new MetadataList();
394 List<SeriesCatalogUIAdapter> catalogUIAdapters = indexService.getSeriesCatalogUIAdapters();
395 catalogUIAdapters.remove(indexService.getCommonSeriesCatalogUIAdapter());
396 for (SeriesCatalogUIAdapter adapter : catalogUIAdapters) {
397 final Optional<DublinCoreMetadataCollection> optSeriesMetadata = adapter.getFields(series);
398 if (optSeriesMetadata.isPresent()) {
399 metadataList.add(adapter.getFlavor().toString(), adapter.getUITitle(), optSeriesMetadata.get());
400 }
401 }
402 metadataList.add(indexService.getCommonSeriesCatalogUIAdapter(), getSeriesMetadata(optSeries.get()));
403 return okJson(MetadataJson.listToJson(metadataList, true));
404 }
405
406
407
408
409
410
411
412
413 private DublinCoreMetadataCollection getSeriesMetadata(Series series) {
414 DublinCoreMetadataCollection metadata = indexService.getCommonSeriesCatalogUIAdapter().getRawFields();
415
416 MetadataField title = metadata.getOutputFields().get(DublinCore.PROPERTY_TITLE.getLocalName());
417 metadata.removeField(title);
418 MetadataField newTitle = new MetadataField(title);
419 newTitle.setValue(series.getTitle());
420 metadata.addField(newTitle);
421
422 MetadataField subject = metadata.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
423 metadata.removeField(subject);
424 MetadataField newSubject = new MetadataField(subject);
425 newSubject.setValue(series.getSubject());
426 metadata.addField(newSubject);
427
428 MetadataField description = metadata.getOutputFields().get(DublinCore.PROPERTY_DESCRIPTION.getLocalName());
429 metadata.removeField(description);
430 MetadataField newDescription = new MetadataField(description);
431 newDescription.setValue(series.getDescription());
432 metadata.addField(newDescription);
433
434 MetadataField language = metadata.getOutputFields().get(DublinCore.PROPERTY_LANGUAGE.getLocalName());
435 metadata.removeField(language);
436 MetadataField newLanguage = new MetadataField(language);
437 newLanguage.setValue(series.getLanguage());
438 metadata.addField(newLanguage);
439
440 MetadataField rightsHolder = metadata.getOutputFields().get(DublinCore.PROPERTY_RIGHTS_HOLDER.getLocalName());
441 metadata.removeField(rightsHolder);
442 MetadataField newRightsHolder = new MetadataField(rightsHolder);
443 newRightsHolder.setValue(series.getRightsHolder());
444 metadata.addField(newRightsHolder);
445
446 MetadataField license = metadata.getOutputFields().get(DublinCore.PROPERTY_LICENSE.getLocalName());
447 metadata.removeField(license);
448 MetadataField newLicense = new MetadataField(license);
449 newLicense.setValue(series.getLicense());
450 metadata.addField(newLicense);
451
452 MetadataField organizers = metadata.getOutputFields().get(DublinCore.PROPERTY_CREATOR.getLocalName());
453 metadata.removeField(organizers);
454 MetadataField newOrganizers = new MetadataField(organizers);
455 newOrganizers.setValue(series.getOrganizers());
456 metadata.addField(newOrganizers);
457
458 MetadataField contributors = metadata.getOutputFields().get(DublinCore.PROPERTY_CONTRIBUTOR.getLocalName());
459 metadata.removeField(contributors);
460 MetadataField newContributors = new MetadataField(contributors);
461 newContributors.setValue(series.getContributors());
462 metadata.addField(newContributors);
463
464 MetadataField publishers = metadata.getOutputFields().get(DublinCore.PROPERTY_PUBLISHER.getLocalName());
465 metadata.removeField(publishers);
466 MetadataField newPublishers = new MetadataField(publishers);
467 newPublishers.setValue(series.getPublishers());
468 metadata.addField(newPublishers);
469
470
471 MetadataField createdBy = new MetadataField(
472 "createdBy",
473 null,
474 "EVENTS.SERIES.DETAILS.METADATA.CREATED_BY",
475 true,
476 false,
477 null,
478 null,
479 MetadataField.Type.TEXT,
480 null,
481 null,
482 CREATED_BY_UI_ORDER,
483 null,
484 null,
485 null,
486 null);
487 createdBy.setValue(series.getCreator());
488 metadata.addField(createdBy);
489
490 MetadataField uid = metadata.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName());
491 metadata.removeField(uid);
492 MetadataField newUID = new MetadataField(uid);
493 newUID.setValue(series.getIdentifier());
494 metadata.addField(newUID);
495
496 return metadata;
497 }
498
499 @PUT
500 @Path("{seriesId}/metadata")
501 @RestQuery(
502 name = "updateseriesmetadata",
503 description = "Update the series metadata with the one given JSON",
504 returnDescription = "Returns OK if the metadata have been saved.",
505 pathParameters = {
506 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
507 },
508 restParameters = {
509 @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list "
510 + "of metadata to update")
511 },
512 responses = {
513 @RestResponse(responseCode = SC_OK, description = "The series metadata as JSON."),
514 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
515 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
516 + "perform this action")
517 })
518 public Response updateSeriesMetadata(@PathParam("seriesId") String seriesID,
519 @FormParam("metadata") String metadataJSON) throws UnauthorizedException, NotFoundException,
520 SearchIndexException {
521 try {
522 MetadataList metadataList = indexService.updateAllSeriesMetadata(seriesID, metadataJSON, searchIndex);
523 return okJson(MetadataJson.listToJson(metadataList, true));
524 } catch (IllegalArgumentException e) {
525 return RestUtil.R.badRequest(e.getMessage());
526 } catch (IndexServiceException e) {
527 return RestUtil.R.serverError();
528 }
529 }
530
531 @GET
532 @Path("new/metadata")
533 @RestQuery(
534 name = "getNewMetadata",
535 description = "Returns all the data related to the metadata tab in the new series modal as JSON",
536 returnDescription = "All the data related to the series metadata tab as JSON",
537 responses = {
538 @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the series metadata tab "
539 + "as JSON")
540 })
541 public Response getNewMetadata() {
542 MetadataList metadataList = indexService.getMetadataListWithAllSeriesCatalogUIAdapters();
543 final DublinCoreMetadataCollection metadataByAdapter = metadataList
544 .getMetadataByAdapter(indexService.getCommonSeriesCatalogUIAdapter());
545 if (metadataByAdapter != null) {
546 DublinCoreMetadataCollection collection = metadataByAdapter;
547 safelyRemoveField(collection, "identifier");
548 metadataList.add(indexService.getCommonSeriesCatalogUIAdapter(), collection);
549 }
550 return okJson(MetadataJson.listToJson(metadataList, true));
551 }
552
553 private void safelyRemoveField(DublinCoreMetadataCollection collection, String fieldName) {
554 MetadataField metadataField = collection.getOutputFields().get(fieldName);
555 if (metadataField != null) {
556 collection.removeField(metadataField);
557 }
558 }
559
560 @GET
561 @Path("new/themes")
562 @SuppressWarnings("unchecked")
563 @RestQuery(
564 name = "getNewThemes",
565 description = "Returns all the data related to the themes tab in the new series modal as JSON",
566 returnDescription = "All the data related to the series themes tab as JSON",
567 responses = {
568 @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the series themes tab as "
569 + "JSON")
570 })
571 public Response getNewThemes() {
572 SortCriterion sortCriterion = new SortCriterion("name", Order.Ascending);
573 ArrayList<SortCriterion> sortCriteria = new ArrayList<>();
574 sortCriteria.add(sortCriterion);
575 List<Theme> results = themesServiceDatabase.findThemes(
576 Optional.ofNullable(Integer.MAX_VALUE),
577 Optional.ofNullable(0),
578 sortCriteria,
579 Optional.empty(),
580 Optional.empty()
581 );
582
583 JSONObject themesJson = new JSONObject();
584 for (Theme theme : results) {
585 JSONObject themeInfoJson = new JSONObject();
586 themeInfoJson.put("name", theme.getName());
587 themeInfoJson.put("description", theme.getDescription());
588 themesJson.put(theme.getId().get(), themeInfoJson);
589 }
590
591 return Response.ok(themesJson.toJSONString()).build();
592 }
593
594 private TobiraService getTobira() {
595 return TobiraService.getTobira(securityService.getOrganization().getId());
596 }
597
598 @GET
599 @Path("new/tobira/page")
600 @Produces(MediaType.APPLICATION_JSON)
601 @RestQuery(
602 name = "getTobiraPage",
603 description = "Returns data about the page tree of a connected Tobira instance for use in the series creation "
604 + "wizard",
605 returnDescription = "Information about a given page in Tobira, and its direct children",
606 restParameters = {
607 @RestParameter(
608 name = "path",
609 isRequired = true,
610 type = STRING,
611 description = "The path of the page you want information about"
612 )},
613 responses = {
614 @RestResponse(
615 responseCode = SC_OK,
616 description = "Data about the given page in Tobira. Note that this does not mean the page exists!"),
617 @RestResponse(
618 responseCode = SC_NOT_FOUND,
619 description = "Nonexistent `path`"),
620 @RestResponse(
621 responseCode = SC_BAD_REQUEST,
622 description = "missing `path`"),
623 @RestResponse(
624 responseCode = SC_SERVICE_UNAVAILABLE,
625 description = "Tobira is not configured (correctly)")
626 })
627 public Response getTobiraPage(@QueryParam("path") String path) throws IOException, InterruptedException {
628 if (path == null) {
629 throw new WebApplicationException("`path` missing", BAD_REQUEST);
630 }
631
632 var tobira = getTobira();
633 if (!tobira.ready()) {
634 return Response.status(Status.SERVICE_UNAVAILABLE)
635 .entity("Tobira is not configured (correctly)")
636 .build();
637 }
638
639 try {
640 var page = (JSONObject) tobira.getPage(path).get("page");
641 if (page == null) {
642 throw new WebApplicationException(NOT_FOUND);
643 }
644 return Response.ok(page.toJSONString()).build();
645 } catch (TobiraException e) {
646 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
647 }
648 }
649
650 @POST
651 @Path("new")
652 @RestQuery(
653 name = "createNewSeries",
654 description = "Creates a new series by the given metadata as JSON",
655 returnDescription = "The created series id",
656 restParameters = {
657 @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON",
658 type = RestParameter.Type.TEXT)
659 },
660 responses = {
661 @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Returns the created series id"),
662 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "he request could not be "
663 + "fulfilled due to the incorrect syntax of the request"),
664 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If user doesn't have rights to create the "
665 + "series")
666 })
667 public Response createNewSeries(@FormParam("metadata") String metadata) throws UnauthorizedException {
668 try {
669 JSONObject metadataJson;
670 try {
671 metadataJson = (JSONObject) new JSONParser().parse(metadata);
672 } catch (Exception e) {
673 throw new IllegalArgumentException("Unable to parse metadata " + metadata, e);
674 }
675
676 if (metadataJson == null) {
677 throw new IllegalArgumentException("No metadata set to create series");
678 }
679
680 String seriesId = indexService.createSeries(metadataJson);
681
682 var mounted = mountSeriesInTobira(seriesId, metadataJson);
683
684 var responseObject = new JSONObject();
685 responseObject.put("id", seriesId);
686 responseObject.put("mounted", mounted);
687
688 return Response.created(URI.create(UrlSupport.concat(serverUrl, "admin-ng/series/", seriesId, "metadata.json")))
689 .entity(responseObject.toString()).build();
690 } catch (IllegalArgumentException e) {
691 return RestUtil.R.badRequest(e.getMessage());
692 } catch (IndexServiceException e) {
693 return RestUtil.R.serverError();
694 }
695 }
696
697 private boolean mountSeriesInTobira(String seriesId, JSONObject params) {
698 var tobira = getTobira();
699 if (!tobira.ready()) {
700 return false;
701 }
702
703 var tobiraParams = params.get("tobira");
704 if (tobiraParams == null) {
705 return false;
706 }
707 if (!(tobiraParams instanceof JSONObject)) {
708 return false;
709 }
710 var tobiraParamsObject = (JSONObject) tobiraParams;
711
712 var metadataCatalogs = (JSONArray) params.get("metadata");
713 var firstCatalog = (JSONObject) metadataCatalogs.get(0);
714 var metadataFields = (List<JSONObject>) firstCatalog.get("fields");
715 var title = metadataFields.stream()
716 .filter(field -> field.get("id").equals("title"))
717 .findAny()
718 .map(field -> field.get("value"))
719 .map(String.class::cast)
720 .get();
721
722 var series = new JSONObject(Map.of(
723 "opencastId", seriesId,
724 "title", title));
725 tobiraParamsObject.put("series", series);
726
727 try {
728 tobira.mount(tobiraParamsObject);
729 } catch (TobiraException e) {
730 return false;
731 }
732
733 return true;
734 }
735
736 @DELETE
737 @Path("{seriesId}")
738 @Produces(MediaType.APPLICATION_JSON)
739 @RestQuery(
740 name = "deleteseries",
741 description = "Delete a series.",
742 returnDescription = "Ok if the series has been deleted.",
743 pathParameters = {
744 @RestParameter(name = "seriesId", isRequired = true, description = "The id of the series to delete.",
745 type = STRING),
746 },
747 responses = {
748 @RestResponse(responseCode = SC_OK, description = "The series has been deleted."),
749 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The series could not be found.")
750 })
751 public Response deleteSeries(@PathParam("seriesId") String id) throws NotFoundException {
752 try {
753 indexService.removeSeries(id);
754 return Response.ok().build();
755 } catch (NotFoundException e) {
756 throw e;
757 } catch (Exception e) {
758 logger.error("Unable to delete the series '{}' due to", id, e);
759 return Response.serverError().build();
760 }
761 }
762
763 @POST
764 @Path("deleteSeries")
765 @Produces(MediaType.APPLICATION_JSON)
766 @RestQuery(
767 name = "deletemultipleseries",
768 description = "Deletes a json list of series by their given ids e.g. [\"Series-1\", \"Series-2\"]",
769 returnDescription = "A JSON object with arrays that show whether a series was deleted, was not found or there "
770 + "was an error deleting it.",
771 responses = {
772 @RestResponse(description = "Series have been deleted", responseCode = HttpServletResponse.SC_OK),
773 @RestResponse(description = "The list of ids could not be parsed into a json list.",
774 responseCode = HttpServletResponse.SC_BAD_REQUEST)
775 })
776 public Response deleteMultipleSeries(String seriesIdsContent) throws NotFoundException {
777 if (StringUtils.isBlank(seriesIdsContent)) {
778 return Response.status(Status.BAD_REQUEST).build();
779 }
780
781 JSONParser parser = new JSONParser();
782 JSONArray seriesIdsArray;
783 try {
784 seriesIdsArray = (JSONArray) parser.parse(seriesIdsContent);
785 } catch (org.json.simple.parser.ParseException e) {
786 logger.error("Unable to parse '{}'", seriesIdsContent, e);
787 return Response.status(Status.BAD_REQUEST).build();
788 } catch (ClassCastException e) {
789 logger.error("Unable to cast '{}' to a JSON array", seriesIdsContent, e);
790 return Response.status(Status.BAD_REQUEST).build();
791 }
792
793 BulkOperationResult result = new BulkOperationResult();
794 for (Object seriesId : seriesIdsArray) {
795 try {
796 indexService.removeSeries(seriesId.toString());
797 result.addOk(seriesId.toString());
798 } catch (NotFoundException e) {
799 result.addNotFound(seriesId.toString());
800 } catch (Exception e) {
801 logger.error("Unable to remove the series '{}'", seriesId.toString(), e);
802 result.addServerError(seriesId.toString());
803 }
804 }
805 return Response.ok(result.toJson()).build();
806 }
807
808 @GET
809 @Produces(MediaType.APPLICATION_JSON)
810 @Path("series.json")
811 @RestQuery(
812 name = "listSeriesAsJson",
813 description = "Returns the series matching the query parameters",
814 returnDescription = "Returns the series search results as JSON",
815 restParameters = {
816 @RestParameter(name = "sortorganizer", isRequired = false, description = "The sort type to apply to the "
817 + "series organizer or organizers either Ascending or Descending.", type = STRING),
818 @RestParameter(name = "sort", description = "The order instructions used to sort the query result. Must be "
819 + "in the form '<field name>:(ASC|DESC)'", isRequired = false, type = STRING),
820 @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They "
821 + "should be formated like that: 'filter1:value1,filter2,value2'", type = STRING),
822 @RestParameter(name = "offset", isRequired = false, description = "The page offset", type = INTEGER,
823 defaultValue = "0"),
824 @RestParameter(name = "limit", isRequired = false, description = "The limit to define the number of "
825 + "returned results (-1 for all)", type = INTEGER, defaultValue = "100")
826 },
827 responses = {
828 @RestResponse(responseCode = SC_OK, description = "The access control list."),
829 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
830 + "perform this action")
831 })
832 public Response getSeries(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
833 @QueryParam("offset") int offset, @QueryParam("limit") int limit)
834 throws UnauthorizedException {
835 try {
836 logger.debug("Requested series list");
837 SeriesSearchQuery query = new SeriesSearchQuery(securityService.getOrganization().getId(),
838 securityService.getUser());
839 Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
840
841 if (offset != 0) {
842 query.withOffset(offset);
843 }
844
845
846 query.withLimit(limit == 0 ? DEFAULT_LIMIT : limit);
847
848 Map<String, String> filters = RestUtils.parseFilter(filter);
849 for (String name : filters.keySet()) {
850 if (SeriesListQuery.FILTER_ACL_NAME.equals(name)) {
851 query.withManagedAcl(filters.get(name));
852 } else if (SeriesListQuery.FILTER_CONTRIBUTORS_NAME.equals(name)) {
853 query.withContributor(filters.get(name));
854 } else if (SeriesListQuery.FILTER_CREATIONDATE_NAME.equals(name)) {
855 try {
856 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(filters.get(name));
857 query.withCreatedFrom(fromAndToCreationRange.getA());
858 query.withCreatedTo(fromAndToCreationRange.getB());
859 } catch (IllegalArgumentException e) {
860 return RestUtil.R.badRequest(e.getMessage());
861 }
862 } else if (SeriesListQuery.FILTER_CREATOR_NAME.equals(name)) {
863 query.withCreator(filters.get(name));
864 } else if (SeriesListQuery.FILTER_TEXT_NAME.equals(name)) {
865 query.withText(filters.get(name));
866 } else if (SeriesListQuery.FILTER_LANGUAGE_NAME.equals(name)) {
867 query.withLanguage(filters.get(name));
868 } else if (SeriesListQuery.FILTER_LICENSE_NAME.equals(name)) {
869 query.withLicense(filters.get(name));
870 } else if (SeriesListQuery.FILTER_ORGANIZERS_NAME.equals(name)) {
871 query.withOrganizer(filters.get(name));
872 } else if (SeriesListQuery.FILTER_SUBJECT_NAME.equals(name)) {
873 query.withSubject(filters.get(name));
874 } else if (SeriesListQuery.FILTER_TITLE_NAME.equals(name)) {
875 query.withTitle(filters.get(name));
876 } else if (SeriesListQuery.FILTER_READ_ACCESS_NAME.equals(name)) {
877 query.withAccessControlEntry(filters.get(name), Permissions.Action.READ);
878 } else if (SeriesListQuery.FILTER_WRITE_ACCESS_NAME.equals(name)) {
879 query.withAccessControlEntry(filters.get(name), Permissions.Action.WRITE);
880 }
881 }
882
883 if (optSort.isPresent()) {
884 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
885 for (SortCriterion criterion : sortCriteria) {
886
887 switch (criterion.getFieldName()) {
888 case SeriesIndexSchema.TITLE:
889 query.sortByTitle(criterion.getOrder());
890 break;
891 case SeriesIndexSchema.CONTRIBUTORS:
892 query.sortByContributors(criterion.getOrder());
893 break;
894 case SeriesIndexSchema.ORGANIZERS:
895 query.sortByOrganizers(criterion.getOrder());
896 break;
897 case SeriesIndexSchema.CREATED_DATE_TIME:
898 query.sortByCreatedDateTime(criterion.getOrder());
899 break;
900 case SeriesIndexSchema.MANAGED_ACL:
901 query.sortByManagedAcl(criterion.getOrder());
902 break;
903 default:
904 logger.info("Unknown filter criteria {}", criterion.getFieldName());
905 return Response.status(SC_BAD_REQUEST).build();
906 }
907 }
908 }
909
910
911 if (onlySeriesWithWriteAccessSeriesTab) {
912 query.withoutActions();
913 query.withAction(Permissions.Action.WRITE);
914 query.withAction(Permissions.Action.READ);
915 }
916
917 logger.trace("Using Query: " + query.toString());
918
919 SearchResult<Series> result = searchIndex.getByQuery(query);
920 if (logger.isDebugEnabled()) {
921 logger.debug("Found {} results in {} ms", result.getDocumentCount(), result.getSearchTime());
922 }
923
924 List<JsonObject> series = new ArrayList<>();
925 for (SearchResultItem<Series> item : result.getItems()) {
926 JsonObject sJson = new JsonObject();
927 Series s = item.getSource();
928
929 sJson.addProperty("id", s.getIdentifier());
930 sJson.addProperty("title", safeString(s.getTitle()));
931 sJson.add("organizers", collectionToJsonArray(s.getOrganizers()));
932 sJson.add("contributors", collectionToJsonArray(s.getContributors()));
933 if (s.getCreator() != null) {
934 sJson.addProperty("createdBy", s.getCreator());
935 }
936 if (s.getCreatedDateTime() != null) {
937 sJson.addProperty("creation_date", toUTC(s.getCreatedDateTime().getTime()));
938 }
939 if (s.getLanguage() != null) {
940 sJson.addProperty("language", s.getLanguage());
941 }
942 if (s.getLicense() != null) {
943 sJson.addProperty("license", s.getLicense());
944 }
945 if (s.getRightsHolder() != null) {
946 sJson.addProperty("rightsHolder", s.getRightsHolder());
947 }
948 if (StringUtils.isNotBlank(s.getManagedAcl())) {
949 sJson.addProperty("managedAcl", s.getManagedAcl());
950 }
951
952 series.add(sJson);
953 }
954
955 logger.debug("Request done");
956
957 return okJsonList(series, offset, limit, result.getHitCount());
958 } catch (Exception e) {
959 logger.warn("Could not perform search query", e);
960 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
961 }
962 }
963
964
965
966
967
968
969
970
971
972
973 public Map<String, String> getUserSeriesByAccess(boolean writeAccess) {
974 SeriesListQuery query = new SeriesListQuery();
975 if (writeAccess) {
976 query.withoutPermissions();
977 query.withReadPermission(true);
978 query.withWritePermission(true);
979 }
980 try {
981 return listProvidersService.getList(SeriesListProvider.PROVIDER_PREFIX, query, true);
982 } catch (ListProviderException e) {
983 logger.warn("Could not perform search query.", e);
984 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
985 }
986 }
987
988 @SuppressWarnings("unchecked")
989 @GET
990 @Produces(MediaType.APPLICATION_JSON)
991 @Path("{id}/properties")
992 @RestQuery(
993 name = "getSeriesProperties",
994 description = "Returns the series properties",
995 returnDescription = "Returns the series properties as JSON",
996 pathParameters = {
997 @RestParameter(name = "id", description = "ID of series", isRequired = true, type = Type.STRING)
998 },
999 responses = {
1000 @RestResponse(responseCode = SC_OK, description = "The access control list."),
1001 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1002 + "perform this action")
1003 })
1004 public Response getSeriesPropertiesAsJson(@PathParam("id") String seriesId) throws UnauthorizedException,
1005 NotFoundException {
1006 if (StringUtils.isBlank(seriesId)) {
1007 logger.warn("Series id parameter is blank '{}'.", seriesId);
1008 return Response.status(BAD_REQUEST).build();
1009 }
1010 try {
1011 Map<String, String> properties = seriesService.getSeriesProperties(seriesId);
1012 JSONArray jsonProperties = new JSONArray();
1013 for (String name : properties.keySet()) {
1014 JSONObject property = new JSONObject();
1015 property.put(name, properties.get(name));
1016 jsonProperties.add(property);
1017 }
1018 return Response.ok(jsonProperties.toString()).build();
1019 } catch (UnauthorizedException e) {
1020 throw e;
1021 } catch (NotFoundException e) {
1022 throw e;
1023 } catch (Exception e) {
1024 logger.warn("Could not perform search query: {}", e.getMessage());
1025 }
1026 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1027 }
1028
1029 @GET
1030 @Produces(MediaType.APPLICATION_JSON)
1031 @Path("{seriesId}/property/{propertyName}.json")
1032 @RestQuery(
1033 name = "getSeriesProperty",
1034 description = "Returns a series property value",
1035 returnDescription = "Returns the series property value",
1036 pathParameters = {
1037 @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING),
1038 @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true,
1039 type = Type.STRING)
1040 },
1041 responses = {
1042 @RestResponse(responseCode = SC_OK, description = "The access control list."),
1043 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1044 + "perform this action")
1045 })
1046 public Response getSeriesProperty(@PathParam("seriesId") String seriesId,
1047 @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException {
1048 if (StringUtils.isBlank(seriesId)) {
1049 logger.warn("Series id parameter is blank '{}'.", seriesId);
1050 return Response.status(BAD_REQUEST).build();
1051 }
1052 if (StringUtils.isBlank(propertyName)) {
1053 logger.warn("Series property name parameter is blank '{}'.", propertyName);
1054 return Response.status(BAD_REQUEST).build();
1055 }
1056 try {
1057 String propertyValue = seriesService.getSeriesProperty(seriesId, propertyName);
1058 return Response.ok(propertyValue).build();
1059 } catch (UnauthorizedException e) {
1060 throw e;
1061 } catch (NotFoundException e) {
1062 throw e;
1063 } catch (Exception e) {
1064 logger.warn("Could not perform search query", e);
1065 }
1066 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1067 }
1068
1069 @POST
1070 @Path("/{seriesId}/property")
1071 @RestQuery(
1072 name = "updateSeriesProperty",
1073 description = "Updates a series property",
1074 returnDescription = "No content.",
1075 restParameters = {
1076 @RestParameter(name = "name", isRequired = true, description = "The property's name", type = TEXT),
1077 @RestParameter(name = "value", isRequired = true, description = "The property's value", type = TEXT)
1078 },
1079 pathParameters = {
1080 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1081 },
1082 responses = {
1083 @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found."),
1084 @RestResponse(responseCode = SC_NO_CONTENT, description = "The access control list has been updated."),
1085 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1086 + "perform this action"),
1087 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required path or form params were missing "
1088 + "in the request.")
1089 })
1090 public Response updateSeriesProperty(@PathParam("seriesId") String seriesId, @FormParam("name") String name,
1091 @FormParam("value") String value) throws UnauthorizedException {
1092 if (StringUtils.isBlank(seriesId)) {
1093 logger.warn("Series id parameter is blank '{}'.", seriesId);
1094 return Response.status(BAD_REQUEST).build();
1095 }
1096 if (StringUtils.isBlank(name)) {
1097 logger.warn("Name parameter is blank '{}'.", name);
1098 return Response.status(BAD_REQUEST).build();
1099 }
1100 if (StringUtils.isBlank(value)) {
1101 logger.warn("Series id parameter is blank '{}'.", value);
1102 return Response.status(BAD_REQUEST).build();
1103 }
1104 try {
1105 seriesService.updateSeriesProperty(seriesId, name, value);
1106 return Response.status(NO_CONTENT).build();
1107 } catch (NotFoundException e) {
1108 return Response.status(NOT_FOUND).build();
1109 } catch (SeriesException e) {
1110 logger.warn("Could not update series property for series {} property {}:{}", seriesId, name, value, e);
1111 }
1112 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1113 }
1114
1115 @DELETE
1116 @Path("{seriesId}/property/{propertyName}")
1117 @RestQuery(
1118 name = "deleteSeriesProperty",
1119 description = "Deletes a series property",
1120 returnDescription = "No Content",
1121 pathParameters = {
1122 @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING),
1123 @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true,
1124 type = Type.STRING)
1125 },
1126 responses = {
1127 @RestResponse(responseCode = SC_NO_CONTENT, description = "The series property has been deleted."),
1128 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or property has not been found."),
1129 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1130 + "perform this action")
1131 })
1132 public Response deleteSeriesProperty(@PathParam("seriesId") String seriesId,
1133 @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException {
1134 if (StringUtils.isBlank(seriesId)) {
1135 logger.warn("Series id parameter is blank '{}'.", seriesId);
1136 return Response.status(BAD_REQUEST).build();
1137 }
1138 if (StringUtils.isBlank(propertyName)) {
1139 logger.warn("Series property name parameter is blank '{}'.", propertyName);
1140 return Response.status(BAD_REQUEST).build();
1141 }
1142 try {
1143 seriesService.deleteSeriesProperty(seriesId, propertyName);
1144 return Response.status(NO_CONTENT).build();
1145 } catch (UnauthorizedException | NotFoundException e) {
1146 throw e;
1147 } catch (Exception e) {
1148 logger.warn("Could not delete series '{}' property '{}' query", seriesId, propertyName, e);
1149 }
1150 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1151 }
1152
1153
1154
1155
1156
1157
1158
1159
1160 private Response getSimpleThemeJsonResponse(Theme theme) {
1161 JsonObject json = new JsonObject();
1162 json.addProperty(Long.toString(theme.getId().get()), theme.getName());
1163 return okJson(json);
1164 }
1165
1166 @GET
1167 @Produces(MediaType.APPLICATION_JSON)
1168 @Path("{seriesId}/theme.json")
1169 @RestQuery(
1170 name = "getSeriesTheme",
1171 description = "Returns the series theme id and name as JSON",
1172 returnDescription = "Returns the series theme name and id as JSON",
1173 pathParameters = {
1174 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1175 },
1176 responses = {
1177 @RestResponse(responseCode = SC_OK, description = "The series theme id and name as JSON."),
1178 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found")
1179 })
1180 public Response getSeriesTheme(@PathParam("seriesId") String seriesId) {
1181 Long themeId;
1182 try {
1183 Optional<Series> series = searchIndex.getSeries(seriesId, securityService.getOrganization(),
1184 securityService.getUser());
1185 if (series.isEmpty()) {
1186 return notFound("Cannot find a series with id {}", seriesId);
1187 }
1188
1189 themeId = series.get().getTheme();
1190 } catch (SearchIndexException e) {
1191 logger.error("Unable to get series {}", seriesId, e);
1192 throw new WebApplicationException(e);
1193 }
1194
1195
1196 if (themeId == null) {
1197 return okJson(new JsonObject());
1198 }
1199
1200 try {
1201 Theme theme = themesServiceDatabase.getTheme(themeId);
1202 return getSimpleThemeJsonResponse(theme);
1203 } catch (NotFoundException e) {
1204 return notFound("Cannot find a theme with id {}", themeId);
1205 } catch (ThemesServiceDatabaseException e) {
1206 logger.error("Unable to get theme {}", themeId, e);
1207 throw new WebApplicationException(e);
1208 }
1209 }
1210
1211 @PUT
1212 @Path("{seriesId}/theme")
1213 @RestQuery(
1214 name = "updateSeriesTheme",
1215 description = "Update the series theme id",
1216 returnDescription = "Returns the id and name of the theme.",
1217 pathParameters = {
1218 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1219 },
1220 restParameters = {
1221 @RestParameter(name = "themeId", isRequired = true, type = RestParameter.Type.INTEGER, description = "The "
1222 + "id of the theme for this series")
1223 },
1224 responses = {
1225 @RestResponse(responseCode = SC_OK, description = "The series theme has been updated and the theme id and "
1226 + "name are returned as JSON."),
1227 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found"),
1228 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1229 + "perform this action")
1230 })
1231 public Response updateSeriesTheme(@PathParam("seriesId") String seriesID, @FormParam("themeId") long themeId)
1232 throws UnauthorizedException, NotFoundException {
1233 try {
1234 Theme theme = themesServiceDatabase.getTheme(themeId);
1235 seriesService.updateSeriesProperty(seriesID, THEME_KEY, Long.toString(themeId));
1236 return getSimpleThemeJsonResponse(theme);
1237 } catch (SeriesException e) {
1238 logger.error("Unable to update series theme {}", themeId, e);
1239 throw new WebApplicationException(e);
1240 } catch (ThemesServiceDatabaseException e) {
1241 logger.error("Unable to get theme {}", themeId, e);
1242 throw new WebApplicationException(e);
1243 }
1244 }
1245
1246 @DELETE
1247 @Path("{seriesId}/theme")
1248 @RestQuery(
1249 name = "deleteSeriesTheme",
1250 description = "Removes the theme from the series",
1251 returnDescription = "Returns no content",
1252 pathParameters = {
1253 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1254 },
1255 responses = {
1256 @RestResponse(responseCode = SC_NO_CONTENT, description = "The series theme has been removed"),
1257 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
1258 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1259 + "perform this action")
1260 })
1261 public Response deleteSeriesTheme(@PathParam("seriesId") String seriesID) throws UnauthorizedException,
1262 NotFoundException {
1263 try {
1264 seriesService.deleteSeriesProperty(seriesID, THEME_KEY);
1265 return Response.noContent().build();
1266 } catch (SeriesException e) {
1267 logger.error("Unable to remove theme from series {}", seriesID, e);
1268 throw new WebApplicationException(e);
1269 }
1270 }
1271
1272 @GET
1273 @Path("{seriesId}/tobira/pages")
1274 @RestQuery(
1275 name = "getSeriesHostPages",
1276 description = "Returns the pages of a connected Tobira instance that contain the given series",
1277 returnDescription = "The Tobira pages that contain the given series",
1278 pathParameters = {
1279 @RestParameter(
1280 name = "seriesId",
1281 isRequired = true,
1282 description = "The series identifier",
1283 type = STRING) },
1284 responses = {
1285 @RestResponse(
1286 responseCode = SC_OK,
1287 description = "The Tobira pages containing the given series"),
1288 @RestResponse(
1289 responseCode = SC_NOT_FOUND,
1290 description = "Tobira doesn't know about the given series"),
1291 @RestResponse(
1292 responseCode = SC_SERVICE_UNAVAILABLE,
1293 description = "Tobira is not configured (correctly)") })
1294 public Response getSeriesHostPages(@PathParam("seriesId") String seriesId) {
1295 var tobira = getTobira();
1296 if (!tobira.ready()) {
1297 return Response.status(Status.SERVICE_UNAVAILABLE)
1298 .entity("Tobira is not configured (correctly)")
1299 .build();
1300 }
1301
1302 try {
1303 var seriesData = tobira.getHostPages(seriesId);
1304 if (seriesData == null) {
1305 throw new WebApplicationException(NOT_FOUND);
1306 }
1307 seriesData.put("baseURL", tobira.getOrigin());
1308 return Response.ok(seriesData.toJSONString()).build();
1309 } catch (TobiraException e) {
1310 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
1311 }
1312 }
1313
1314 @POST
1315 @Path("{seriesId}/tobira/path")
1316 @RestQuery(
1317 name = "updateSeriesTobiraPath",
1318 description = "Updates the path of the given series in a connected Tobira instance",
1319 returnDescription = "Status code",
1320 pathParameters = {
1321 @RestParameter(
1322 name = "seriesId",
1323 isRequired = true,
1324 description = "The series id",
1325 type = STRING) },
1326 restParameters = {
1327 @RestParameter(
1328 name = "pathComponents",
1329 isRequired = true,
1330 description = "List of realms with name and path segment on path to series.",
1331 type = TEXT),
1332 @RestParameter(
1333 name = "currentPath",
1334 isRequired = false,
1335 description = "Path where the series is currently mounted.",
1336 type = STRING),
1337 @RestParameter(
1338 name = "targetPath",
1339 isRequired = true,
1340 description = "Path where the series will be mounted.",
1341 type = STRING) },
1342 responses = {
1343 @RestResponse(
1344 responseCode = SC_OK,
1345 description = "The path of the series has successfully been updated in Tobira."),
1346 @RestResponse(
1347 responseCode = SC_NOT_FOUND,
1348 description = "Tobira doesn't know about the given series"),
1349 @RestResponse(
1350 responseCode = SC_SERVICE_UNAVAILABLE,
1351 description = "Tobira is not configured (correctly)") })
1352 public Response updateSeriesTobiraPath(
1353 @PathParam("seriesId") String seriesId,
1354 @FormParam("pathComponents") String pathComponents,
1355 @FormParam("currentPath") String currentPath,
1356 @FormParam("targetPath") String targetPath
1357 ) throws IOException, InterruptedException {
1358 if (targetPath == null) {
1359 throw new WebApplicationException("target path is missing", BAD_REQUEST);
1360 }
1361
1362 var tobira = getTobira();
1363 if (!tobira.ready()) {
1364 return Response.status(Status.SERVICE_UNAVAILABLE)
1365 .entity("Tobira is not configured (correctly)")
1366 .build();
1367 }
1368
1369 try {
1370 var paths = (List<JSONObject>) new JSONParser().parse(pathComponents);
1371
1372 var mountParams = new JSONObject();
1373 mountParams.put("seriesId", seriesId);
1374 mountParams.put("targetPath", targetPath);
1375
1376 tobira.createRealmLineage(paths);
1377 tobira.addSeriesMountPoint(mountParams);
1378
1379 if (currentPath != null && !currentPath.trim().isEmpty()) {
1380 var unmountParams = new JSONObject();
1381 unmountParams.put("seriesId", seriesId);
1382 unmountParams.put("currentPath", currentPath);
1383 tobira.removeSeriesMountPoint(unmountParams);
1384 }
1385
1386 return ok();
1387 } catch (Exception e) {
1388 return Response.status(Status.INTERNAL_SERVER_ERROR)
1389 .entity("Internal server error: " + e.getMessage())
1390 .build();
1391 }
1392 }
1393
1394 @DELETE
1395 @Path("{seriesId}/tobira/{currentPath}")
1396 @RestQuery(
1397 name = "removeSeriesTobiraPath",
1398 description = "Removes the path of the given series in a connected Tobira instance",
1399 returnDescription = "Status code",
1400 pathParameters = {
1401 @RestParameter(
1402 name = "seriesId",
1403 isRequired = true,
1404 description = "The series id",
1405 type = STRING),
1406 @RestParameter(
1407 name = "currentPath",
1408 isRequired = true,
1409 description = "URL encoded path where the series is currently mounted.",
1410 type = STRING) },
1411 responses = {
1412 @RestResponse(
1413 responseCode = SC_OK,
1414 description = "The path of the series has successfully been removed in Tobira."),
1415 @RestResponse(
1416 responseCode = SC_NOT_FOUND,
1417 description = "Tobira doesn't know about the given series"),
1418 @RestResponse(
1419 responseCode = SC_SERVICE_UNAVAILABLE,
1420 description = "Tobira is not configured (correctly)") })
1421 public Response removeSeriesTobiraPath(
1422 @PathParam("seriesId") String seriesId,
1423 @PathParam("currentPath") String currentPath
1424 ) throws IOException, InterruptedException {
1425 var tobira = getTobira();
1426 if (!tobira.ready()) {
1427 return Response.status(Status.SERVICE_UNAVAILABLE)
1428 .entity("Tobira is not configured (correctly)")
1429 .build();
1430 }
1431
1432 try {
1433 if (currentPath != null && !currentPath.trim().isEmpty()) {
1434 var unmountParams = new JSONObject();
1435 unmountParams.put("seriesId", seriesId);
1436 unmountParams.put("currentPath", currentPath);
1437 tobira.removeSeriesMountPoint(unmountParams);
1438 }
1439
1440 return ok();
1441 } catch (Exception e) {
1442 return Response.status(Status.INTERNAL_SERVER_ERROR)
1443 .entity("Internal server error: " + e.getMessage())
1444 .build();
1445 }
1446 }
1447
1448 @POST
1449 @Path("/{seriesId}/access")
1450 @RestQuery(
1451 name = "applyAclToSeries",
1452 description = "Immediate application of an ACL to a series",
1453 returnDescription = "Status code",
1454 pathParameters = {
1455 @RestParameter(name = "seriesId", isRequired = true, description = "The series ID", type = STRING)
1456 },
1457 restParameters = {
1458 @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING),
1459 @RestParameter(name = "override", isRequired = false, defaultValue = "false", description = "If true the "
1460 + "series ACL will take precedence over any existing episode ACL", type = BOOLEAN)
1461 },
1462 responses = {
1463 @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"),
1464 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"),
1465 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
1466 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error"),
1467 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1468 + "perform this action")
1469 })
1470 public Response applyAclToSeries(@PathParam("seriesId") String seriesId, @FormParam("acl") String acl,
1471 @DefaultValue("false") @FormParam("override") boolean override) throws SearchIndexException {
1472
1473 AccessControlList accessControlList;
1474 try {
1475 accessControlList = AccessControlParser.parseAcl(acl);
1476 } catch (Exception e) {
1477 logger.warn("Unable to parse ACL '{}'", acl);
1478 return badRequest();
1479 }
1480
1481 if (!accessControlList.isValid()) {
1482 logger.debug("POST api/series/{}/access: Invalid series ACL detected", seriesId);
1483 return badRequest();
1484 }
1485
1486 Optional<Series> series = searchIndex.getSeries(seriesId, securityService.getOrganization(),
1487 securityService.getUser());
1488 if (series.isEmpty()) {
1489 return notFound("Cannot find a series with id {}", seriesId);
1490 }
1491
1492 if (hasProcessingEvents(seriesId)) {
1493 logger.warn("Can not update the ACL from series {}. Events being part of the series are currently processed.",
1494 seriesId);
1495 return conflict();
1496 }
1497
1498 try {
1499 seriesService.updateAccessControl(seriesId, accessControlList, override);
1500 return ok();
1501 } catch (NotFoundException e) {
1502 logger.warn("Unable to find series '{}' to apply the ACL.", seriesId);
1503 return notFound();
1504 } catch (UnauthorizedException e) {
1505 return forbidden();
1506 } catch (SeriesException e) {
1507 logger.error("Error applying acl to series {}", seriesId);
1508 return serverError();
1509 }
1510 }
1511
1512
1513
1514
1515
1516
1517
1518
1519 private boolean hasProcessingEvents(String seriesId) {
1520 EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser());
1521 long elementsCount = 0;
1522 query.withSeriesId(seriesId);
1523
1524 try {
1525 query.withWorkflowState(WorkflowInstance.WorkflowState.RUNNING.toString());
1526 SearchResult<Event> events = searchIndex.getByQuery(query);
1527 elementsCount = events.getHitCount();
1528 query.withWorkflowState(WorkflowInstance.WorkflowState.INSTANTIATED.toString());
1529 events = searchIndex.getByQuery(query);
1530 elementsCount += events.getHitCount();
1531 } catch (SearchIndexException e) {
1532 logger.warn("Could not perform search query", e);
1533 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1534 }
1535
1536 return elementsCount > 0;
1537 }
1538
1539 @GET
1540 @Path("{seriesId}/hasEvents.json")
1541 @Produces(MediaType.APPLICATION_JSON)
1542 @RestQuery(
1543 name = "hasEvents",
1544 description = "Check if given series has events",
1545 returnDescription = "true if series has events, otherwise false",
1546 pathParameters = {
1547 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier",
1548 type = Type.STRING)
1549 },
1550 responses = {
1551 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the "
1552 + "request."),
1553 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."),
1554 @RestResponse(responseCode = SC_OK, description = "The access information ")
1555 })
1556 public Response getSeriesEvents(@PathParam("seriesId") String seriesId) throws Exception {
1557 if (StringUtils.isBlank(seriesId)) {
1558 return RestUtil.R.badRequest("Path parameter series ID is missing");
1559 }
1560
1561 long elementsCount = 0;
1562
1563 try {
1564 EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(),
1565 securityService.getUser());
1566 query.withSeriesId(seriesId);
1567 SearchResult<Event> result = searchIndex.getByQuery(query);
1568 elementsCount = result.getHitCount();
1569 } catch (SearchIndexException e) {
1570 logger.warn("Could not perform search query", e);
1571 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1572 }
1573
1574 JSONObject jsonReturnObj = new JSONObject();
1575 jsonReturnObj.put("hasEvents", elementsCount > 0);
1576 return Response.ok(jsonReturnObj.toString()).build();
1577 }
1578
1579 @GET
1580 @Path("configuration.json")
1581 @Produces(MediaType.APPLICATION_JSON)
1582 @RestQuery(
1583 name = "getseriesconfiguration",
1584 description = "Get the series configuration",
1585 returnDescription = "List of configuration keys",
1586 responses = {
1587 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the "
1588 + "request."),
1589 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."),
1590 @RestResponse(responseCode = SC_OK, description = "The access information ")
1591 })
1592 public Response getSeriesOptions() {
1593 JSONObject jsonReturnObj = new JSONObject();
1594 jsonReturnObj.put("deleteSeriesWithEventsAllowed", deleteSeriesWithEventsAllowed);
1595 return Response.ok(jsonReturnObj.toString()).build();
1596 }
1597
1598 public Boolean getOnlySeriesWithWriteAccessSeriesTab() {
1599 return onlySeriesWithWriteAccessSeriesTab;
1600 }
1601
1602 public Boolean getOnlySeriesWithWriteAccessEventsFilter() {
1603 return onlySeriesWithWriteAccessEventsFilter;
1604 }
1605 }