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.resources.list.provider.SeriesListProvider;
72 import org.opencastproject.index.service.resources.list.query.SeriesListQuery;
73 import org.opencastproject.index.service.util.RestUtils;
74 import org.opencastproject.list.api.ListProviderException;
75 import org.opencastproject.list.api.ListProvidersService;
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().getId(),
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 }
877 }
878
879 if (optSort.isPresent()) {
880 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
881 for (SortCriterion criterion : sortCriteria) {
882
883 switch (criterion.getFieldName()) {
884 case SeriesIndexSchema.TITLE:
885 query.sortByTitle(criterion.getOrder());
886 break;
887 case SeriesIndexSchema.CONTRIBUTORS:
888 query.sortByContributors(criterion.getOrder());
889 break;
890 case SeriesIndexSchema.ORGANIZERS:
891 query.sortByOrganizers(criterion.getOrder());
892 break;
893 case SeriesIndexSchema.CREATED_DATE_TIME:
894 query.sortByCreatedDateTime(criterion.getOrder());
895 break;
896 case SeriesIndexSchema.MANAGED_ACL:
897 query.sortByManagedAcl(criterion.getOrder());
898 break;
899 default:
900 logger.info("Unknown filter criteria {}", criterion.getFieldName());
901 return Response.status(SC_BAD_REQUEST).build();
902 }
903 }
904 }
905
906
907 if (onlySeriesWithWriteAccessSeriesTab) {
908 query.withoutActions();
909 query.withAction(Permissions.Action.WRITE);
910 query.withAction(Permissions.Action.READ);
911 }
912
913 logger.trace("Using Query: " + query.toString());
914
915 SearchResult<Series> result = searchIndex.getByQuery(query);
916 if (logger.isDebugEnabled()) {
917 logger.debug("Found {} results in {} ms", result.getDocumentCount(), result.getSearchTime());
918 }
919
920 List<JsonObject> series = new ArrayList<>();
921 for (SearchResultItem<Series> item : result.getItems()) {
922 JsonObject sJson = new JsonObject();
923 Series s = item.getSource();
924
925 sJson.addProperty("id", s.getIdentifier());
926 sJson.addProperty("title", safeString(s.getTitle()));
927 sJson.add("organizers", collectionToJsonArray(s.getOrganizers()));
928 sJson.add("contributors", collectionToJsonArray(s.getContributors()));
929 if (s.getCreator() != null) {
930 sJson.addProperty("createdBy", s.getCreator());
931 }
932 if (s.getCreatedDateTime() != null) {
933 sJson.addProperty("creation_date", toUTC(s.getCreatedDateTime().getTime()));
934 }
935 if (s.getLanguage() != null) {
936 sJson.addProperty("language", s.getLanguage());
937 }
938 if (s.getLicense() != null) {
939 sJson.addProperty("license", s.getLicense());
940 }
941 if (s.getRightsHolder() != null) {
942 sJson.addProperty("rightsHolder", s.getRightsHolder());
943 }
944 if (StringUtils.isNotBlank(s.getManagedAcl())) {
945 sJson.addProperty("managedAcl", s.getManagedAcl());
946 }
947
948 series.add(sJson);
949 }
950
951 logger.debug("Request done");
952
953 return okJsonList(series, offset, limit, result.getHitCount());
954 } catch (Exception e) {
955 logger.warn("Could not perform search query", e);
956 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
957 }
958 }
959
960
961
962
963
964
965
966
967
968
969 public Map<String, String> getUserSeriesByAccess(boolean writeAccess) {
970 SeriesListQuery query = new SeriesListQuery();
971 if (writeAccess) {
972 query.withoutPermissions();
973 query.withReadPermission(true);
974 query.withWritePermission(true);
975 }
976 try {
977 return listProvidersService.getList(SeriesListProvider.PROVIDER_PREFIX, query, true);
978 } catch (ListProviderException e) {
979 logger.warn("Could not perform search query.", e);
980 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
981 }
982 }
983
984 @SuppressWarnings("unchecked")
985 @GET
986 @Produces(MediaType.APPLICATION_JSON)
987 @Path("{id}/properties")
988 @RestQuery(
989 name = "getSeriesProperties",
990 description = "Returns the series properties",
991 returnDescription = "Returns the series properties as JSON",
992 pathParameters = {
993 @RestParameter(name = "id", description = "ID of series", isRequired = true, type = Type.STRING)
994 },
995 responses = {
996 @RestResponse(responseCode = SC_OK, description = "The access control list."),
997 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
998 + "perform this action")
999 })
1000 public Response getSeriesPropertiesAsJson(@PathParam("id") String seriesId) throws UnauthorizedException,
1001 NotFoundException {
1002 if (StringUtils.isBlank(seriesId)) {
1003 logger.warn("Series id parameter is blank '{}'.", seriesId);
1004 return Response.status(BAD_REQUEST).build();
1005 }
1006 try {
1007 Map<String, String> properties = seriesService.getSeriesProperties(seriesId);
1008 JSONArray jsonProperties = new JSONArray();
1009 for (String name : properties.keySet()) {
1010 JSONObject property = new JSONObject();
1011 property.put(name, properties.get(name));
1012 jsonProperties.add(property);
1013 }
1014 return Response.ok(jsonProperties.toString()).build();
1015 } catch (UnauthorizedException e) {
1016 throw e;
1017 } catch (NotFoundException e) {
1018 throw e;
1019 } catch (Exception e) {
1020 logger.warn("Could not perform search query: {}", e.getMessage());
1021 }
1022 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1023 }
1024
1025 @GET
1026 @Produces(MediaType.APPLICATION_JSON)
1027 @Path("{seriesId}/property/{propertyName}.json")
1028 @RestQuery(
1029 name = "getSeriesProperty",
1030 description = "Returns a series property value",
1031 returnDescription = "Returns the series property value",
1032 pathParameters = {
1033 @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING),
1034 @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true,
1035 type = Type.STRING)
1036 },
1037 responses = {
1038 @RestResponse(responseCode = SC_OK, description = "The access control list."),
1039 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1040 + "perform this action")
1041 })
1042 public Response getSeriesProperty(@PathParam("seriesId") String seriesId,
1043 @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException {
1044 if (StringUtils.isBlank(seriesId)) {
1045 logger.warn("Series id parameter is blank '{}'.", seriesId);
1046 return Response.status(BAD_REQUEST).build();
1047 }
1048 if (StringUtils.isBlank(propertyName)) {
1049 logger.warn("Series property name parameter is blank '{}'.", propertyName);
1050 return Response.status(BAD_REQUEST).build();
1051 }
1052 try {
1053 String propertyValue = seriesService.getSeriesProperty(seriesId, propertyName);
1054 return Response.ok(propertyValue).build();
1055 } catch (UnauthorizedException e) {
1056 throw e;
1057 } catch (NotFoundException e) {
1058 throw e;
1059 } catch (Exception e) {
1060 logger.warn("Could not perform search query", e);
1061 }
1062 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1063 }
1064
1065 @POST
1066 @Path("/{seriesId}/property")
1067 @RestQuery(
1068 name = "updateSeriesProperty",
1069 description = "Updates a series property",
1070 returnDescription = "No content.",
1071 restParameters = {
1072 @RestParameter(name = "name", isRequired = true, description = "The property's name", type = TEXT),
1073 @RestParameter(name = "value", isRequired = true, description = "The property's value", type = TEXT)
1074 },
1075 pathParameters = {
1076 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1077 },
1078 responses = {
1079 @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found."),
1080 @RestResponse(responseCode = SC_NO_CONTENT, description = "The access control list has been updated."),
1081 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1082 + "perform this action"),
1083 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required path or form params were missing "
1084 + "in the request.")
1085 })
1086 public Response updateSeriesProperty(@PathParam("seriesId") String seriesId, @FormParam("name") String name,
1087 @FormParam("value") String value) throws UnauthorizedException {
1088 if (StringUtils.isBlank(seriesId)) {
1089 logger.warn("Series id parameter is blank '{}'.", seriesId);
1090 return Response.status(BAD_REQUEST).build();
1091 }
1092 if (StringUtils.isBlank(name)) {
1093 logger.warn("Name parameter is blank '{}'.", name);
1094 return Response.status(BAD_REQUEST).build();
1095 }
1096 if (StringUtils.isBlank(value)) {
1097 logger.warn("Series id parameter is blank '{}'.", value);
1098 return Response.status(BAD_REQUEST).build();
1099 }
1100 try {
1101 seriesService.updateSeriesProperty(seriesId, name, value);
1102 return Response.status(NO_CONTENT).build();
1103 } catch (NotFoundException e) {
1104 return Response.status(NOT_FOUND).build();
1105 } catch (SeriesException e) {
1106 logger.warn("Could not update series property for series {} property {}:{}", seriesId, name, value, e);
1107 }
1108 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1109 }
1110
1111 @DELETE
1112 @Path("{seriesId}/property/{propertyName}")
1113 @RestQuery(
1114 name = "deleteSeriesProperty",
1115 description = "Deletes a series property",
1116 returnDescription = "No Content",
1117 pathParameters = {
1118 @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING),
1119 @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true,
1120 type = Type.STRING)
1121 },
1122 responses = {
1123 @RestResponse(responseCode = SC_NO_CONTENT, description = "The series property has been deleted."),
1124 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or property has not been found."),
1125 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1126 + "perform this action")
1127 })
1128 public Response deleteSeriesProperty(@PathParam("seriesId") String seriesId,
1129 @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException {
1130 if (StringUtils.isBlank(seriesId)) {
1131 logger.warn("Series id parameter is blank '{}'.", seriesId);
1132 return Response.status(BAD_REQUEST).build();
1133 }
1134 if (StringUtils.isBlank(propertyName)) {
1135 logger.warn("Series property name parameter is blank '{}'.", propertyName);
1136 return Response.status(BAD_REQUEST).build();
1137 }
1138 try {
1139 seriesService.deleteSeriesProperty(seriesId, propertyName);
1140 return Response.status(NO_CONTENT).build();
1141 } catch (UnauthorizedException | NotFoundException e) {
1142 throw e;
1143 } catch (Exception e) {
1144 logger.warn("Could not delete series '{}' property '{}' query", seriesId, propertyName, e);
1145 }
1146 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1147 }
1148
1149
1150
1151
1152
1153
1154
1155
1156 private Response getSimpleThemeJsonResponse(Theme theme) {
1157 JsonObject json = new JsonObject();
1158 json.addProperty(Long.toString(theme.getId().get()), theme.getName());
1159 return okJson(json);
1160 }
1161
1162 @GET
1163 @Produces(MediaType.APPLICATION_JSON)
1164 @Path("{seriesId}/theme.json")
1165 @RestQuery(
1166 name = "getSeriesTheme",
1167 description = "Returns the series theme id and name as JSON",
1168 returnDescription = "Returns the series theme name and id as JSON",
1169 pathParameters = {
1170 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1171 },
1172 responses = {
1173 @RestResponse(responseCode = SC_OK, description = "The series theme id and name as JSON."),
1174 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found")
1175 })
1176 public Response getSeriesTheme(@PathParam("seriesId") String seriesId) {
1177 Long themeId;
1178 try {
1179 Optional<Series> series = searchIndex.getSeries(seriesId, securityService.getOrganization().getId(),
1180 securityService.getUser());
1181 if (series.isEmpty()) {
1182 return notFound("Cannot find a series with id {}", seriesId);
1183 }
1184
1185 themeId = series.get().getTheme();
1186 } catch (SearchIndexException e) {
1187 logger.error("Unable to get series {}", seriesId, e);
1188 throw new WebApplicationException(e);
1189 }
1190
1191
1192 if (themeId == null) {
1193 return okJson(new JsonObject());
1194 }
1195
1196 try {
1197 Theme theme = themesServiceDatabase.getTheme(themeId);
1198 return getSimpleThemeJsonResponse(theme);
1199 } catch (NotFoundException e) {
1200 return notFound("Cannot find a theme with id {}", themeId);
1201 } catch (ThemesServiceDatabaseException e) {
1202 logger.error("Unable to get theme {}", themeId, e);
1203 throw new WebApplicationException(e);
1204 }
1205 }
1206
1207 @PUT
1208 @Path("{seriesId}/theme")
1209 @RestQuery(
1210 name = "updateSeriesTheme",
1211 description = "Update the series theme id",
1212 returnDescription = "Returns the id and name of the theme.",
1213 pathParameters = {
1214 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1215 },
1216 restParameters = {
1217 @RestParameter(name = "themeId", isRequired = true, type = RestParameter.Type.INTEGER, description = "The "
1218 + "id of the theme for this series")
1219 },
1220 responses = {
1221 @RestResponse(responseCode = SC_OK, description = "The series theme has been updated and the theme id and "
1222 + "name are returned as JSON."),
1223 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found"),
1224 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1225 + "perform this action")
1226 })
1227 public Response updateSeriesTheme(@PathParam("seriesId") String seriesID, @FormParam("themeId") long themeId)
1228 throws UnauthorizedException, NotFoundException {
1229 try {
1230 Theme theme = themesServiceDatabase.getTheme(themeId);
1231 seriesService.updateSeriesProperty(seriesID, THEME_KEY, Long.toString(themeId));
1232 return getSimpleThemeJsonResponse(theme);
1233 } catch (SeriesException e) {
1234 logger.error("Unable to update series theme {}", themeId, e);
1235 throw new WebApplicationException(e);
1236 } catch (ThemesServiceDatabaseException e) {
1237 logger.error("Unable to get theme {}", themeId, e);
1238 throw new WebApplicationException(e);
1239 }
1240 }
1241
1242 @DELETE
1243 @Path("{seriesId}/theme")
1244 @RestQuery(
1245 name = "deleteSeriesTheme",
1246 description = "Removes the theme from the series",
1247 returnDescription = "Returns no content",
1248 pathParameters = {
1249 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1250 },
1251 responses = {
1252 @RestResponse(responseCode = SC_NO_CONTENT, description = "The series theme has been removed"),
1253 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
1254 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1255 + "perform this action")
1256 })
1257 public Response deleteSeriesTheme(@PathParam("seriesId") String seriesID) throws UnauthorizedException,
1258 NotFoundException {
1259 try {
1260 seriesService.deleteSeriesProperty(seriesID, THEME_KEY);
1261 return Response.noContent().build();
1262 } catch (SeriesException e) {
1263 logger.error("Unable to remove theme from series {}", seriesID, e);
1264 throw new WebApplicationException(e);
1265 }
1266 }
1267
1268 @GET
1269 @Path("{seriesId}/tobira/pages")
1270 @RestQuery(
1271 name = "getSeriesHostPages",
1272 description = "Returns the pages of a connected Tobira instance that contain the given series",
1273 returnDescription = "The Tobira pages that contain the given series",
1274 pathParameters = {
1275 @RestParameter(
1276 name = "seriesId",
1277 isRequired = true,
1278 description = "The series identifier",
1279 type = STRING) },
1280 responses = {
1281 @RestResponse(
1282 responseCode = SC_OK,
1283 description = "The Tobira pages containing the given series"),
1284 @RestResponse(
1285 responseCode = SC_NOT_FOUND,
1286 description = "Tobira doesn't know about the given series"),
1287 @RestResponse(
1288 responseCode = SC_SERVICE_UNAVAILABLE,
1289 description = "Tobira is not configured (correctly)") })
1290 public Response getSeriesHostPages(@PathParam("seriesId") String seriesId) {
1291 var tobira = getTobira();
1292 if (!tobira.ready()) {
1293 return Response.status(Status.SERVICE_UNAVAILABLE)
1294 .entity("Tobira is not configured (correctly)")
1295 .build();
1296 }
1297
1298 try {
1299 var seriesData = tobira.getHostPages(seriesId);
1300 if (seriesData == null) {
1301 throw new WebApplicationException(NOT_FOUND);
1302 }
1303 seriesData.put("baseURL", tobira.getOrigin());
1304 return Response.ok(seriesData.toJSONString()).build();
1305 } catch (TobiraException e) {
1306 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
1307 }
1308 }
1309
1310 @POST
1311 @Path("{seriesId}/tobira/path")
1312 @RestQuery(
1313 name = "updateSeriesTobiraPath",
1314 description = "Updates the path of the given series in a connected Tobira instance",
1315 returnDescription = "Status code",
1316 pathParameters = {
1317 @RestParameter(
1318 name = "seriesId",
1319 isRequired = true,
1320 description = "The series id",
1321 type = STRING) },
1322 restParameters = {
1323 @RestParameter(
1324 name = "pathComponents",
1325 isRequired = true,
1326 description = "List of realms with name and path segment on path to series.",
1327 type = TEXT),
1328 @RestParameter(
1329 name = "currentPath",
1330 isRequired = false,
1331 description = "Path where the series is currently mounted.",
1332 type = STRING),
1333 @RestParameter(
1334 name = "targetPath",
1335 isRequired = true,
1336 description = "Path where the series will be mounted.",
1337 type = STRING) },
1338 responses = {
1339 @RestResponse(
1340 responseCode = SC_OK,
1341 description = "The path of the series has successfully been updated in Tobira."),
1342 @RestResponse(
1343 responseCode = SC_NOT_FOUND,
1344 description = "Tobira doesn't know about the given series"),
1345 @RestResponse(
1346 responseCode = SC_SERVICE_UNAVAILABLE,
1347 description = "Tobira is not configured (correctly)") })
1348 public Response updateSeriesTobiraPath(
1349 @PathParam("seriesId") String seriesId,
1350 @FormParam("pathComponents") String pathComponents,
1351 @FormParam("currentPath") String currentPath,
1352 @FormParam("targetPath") String targetPath
1353 ) throws IOException, InterruptedException {
1354 if (targetPath == null) {
1355 throw new WebApplicationException("target path is missing", BAD_REQUEST);
1356 }
1357
1358 var tobira = getTobira();
1359 if (!tobira.ready()) {
1360 return Response.status(Status.SERVICE_UNAVAILABLE)
1361 .entity("Tobira is not configured (correctly)")
1362 .build();
1363 }
1364
1365 try {
1366 var paths = (List<JSONObject>) new JSONParser().parse(pathComponents);
1367
1368 var mountParams = new JSONObject();
1369 mountParams.put("seriesId", seriesId);
1370 mountParams.put("targetPath", targetPath);
1371
1372 tobira.createRealmLineage(paths);
1373 tobira.addSeriesMountPoint(mountParams);
1374
1375 if (currentPath != null && !currentPath.trim().isEmpty()) {
1376 var unmountParams = new JSONObject();
1377 unmountParams.put("seriesId", seriesId);
1378 unmountParams.put("currentPath", currentPath);
1379 tobira.removeSeriesMountPoint(unmountParams);
1380 }
1381
1382 return ok();
1383 } catch (Exception e) {
1384 return Response.status(Status.INTERNAL_SERVER_ERROR)
1385 .entity("Internal server error: " + e.getMessage())
1386 .build();
1387 }
1388 }
1389
1390 @DELETE
1391 @Path("{seriesId}/tobira/{currentPath}")
1392 @RestQuery(
1393 name = "removeSeriesTobiraPath",
1394 description = "Removes the path of the given series in a connected Tobira instance",
1395 returnDescription = "Status code",
1396 pathParameters = {
1397 @RestParameter(
1398 name = "seriesId",
1399 isRequired = true,
1400 description = "The series id",
1401 type = STRING),
1402 @RestParameter(
1403 name = "currentPath",
1404 isRequired = true,
1405 description = "URL encoded path where the series is currently mounted.",
1406 type = STRING) },
1407 responses = {
1408 @RestResponse(
1409 responseCode = SC_OK,
1410 description = "The path of the series has successfully been removed in Tobira."),
1411 @RestResponse(
1412 responseCode = SC_NOT_FOUND,
1413 description = "Tobira doesn't know about the given series"),
1414 @RestResponse(
1415 responseCode = SC_SERVICE_UNAVAILABLE,
1416 description = "Tobira is not configured (correctly)") })
1417 public Response removeSeriesTobiraPath(
1418 @PathParam("seriesId") String seriesId,
1419 @PathParam("currentPath") String currentPath
1420 ) throws IOException, InterruptedException {
1421 var tobira = getTobira();
1422 if (!tobira.ready()) {
1423 return Response.status(Status.SERVICE_UNAVAILABLE)
1424 .entity("Tobira is not configured (correctly)")
1425 .build();
1426 }
1427
1428 try {
1429 if (currentPath != null && !currentPath.trim().isEmpty()) {
1430 var unmountParams = new JSONObject();
1431 unmountParams.put("seriesId", seriesId);
1432 unmountParams.put("currentPath", currentPath);
1433 tobira.removeSeriesMountPoint(unmountParams);
1434 }
1435
1436 return ok();
1437 } catch (Exception e) {
1438 return Response.status(Status.INTERNAL_SERVER_ERROR)
1439 .entity("Internal server error: " + e.getMessage())
1440 .build();
1441 }
1442 }
1443
1444 @POST
1445 @Path("/{seriesId}/access")
1446 @RestQuery(
1447 name = "applyAclToSeries",
1448 description = "Immediate application of an ACL to a series",
1449 returnDescription = "Status code",
1450 pathParameters = {
1451 @RestParameter(name = "seriesId", isRequired = true, description = "The series ID", type = STRING)
1452 },
1453 restParameters = {
1454 @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING),
1455 @RestParameter(name = "override", isRequired = false, defaultValue = "false", description = "If true the "
1456 + "series ACL will take precedence over any existing episode ACL", type = BOOLEAN)
1457 },
1458 responses = {
1459 @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"),
1460 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"),
1461 @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"),
1462 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error"),
1463 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
1464 + "perform this action")
1465 })
1466 public Response applyAclToSeries(@PathParam("seriesId") String seriesId, @FormParam("acl") String acl,
1467 @DefaultValue("false") @FormParam("override") boolean override) throws SearchIndexException {
1468
1469 AccessControlList accessControlList;
1470 try {
1471 accessControlList = AccessControlParser.parseAcl(acl);
1472 } catch (Exception e) {
1473 logger.warn("Unable to parse ACL '{}'", acl);
1474 return badRequest();
1475 }
1476
1477 if (!accessControlList.isValid()) {
1478 logger.debug("POST api/series/{}/access: Invalid series ACL detected", seriesId);
1479 return badRequest();
1480 }
1481
1482 Optional<Series> series = searchIndex.getSeries(seriesId, securityService.getOrganization().getId(),
1483 securityService.getUser());
1484 if (series.isEmpty()) {
1485 return notFound("Cannot find a series with id {}", seriesId);
1486 }
1487
1488 if (hasProcessingEvents(seriesId)) {
1489 logger.warn("Can not update the ACL from series {}. Events being part of the series are currently processed.",
1490 seriesId);
1491 return conflict();
1492 }
1493
1494 try {
1495 seriesService.updateAccessControl(seriesId, accessControlList, override);
1496 return ok();
1497 } catch (NotFoundException e) {
1498 logger.warn("Unable to find series '{}' to apply the ACL.", seriesId);
1499 return notFound();
1500 } catch (UnauthorizedException e) {
1501 return forbidden();
1502 } catch (SeriesException e) {
1503 logger.error("Error applying acl to series {}", seriesId);
1504 return serverError();
1505 }
1506 }
1507
1508
1509
1510
1511
1512
1513
1514
1515 private boolean hasProcessingEvents(String seriesId) {
1516 EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser());
1517 long elementsCount = 0;
1518 query.withSeriesId(seriesId);
1519
1520 try {
1521 query.withWorkflowState(WorkflowInstance.WorkflowState.RUNNING.toString());
1522 SearchResult<Event> events = searchIndex.getByQuery(query);
1523 elementsCount = events.getHitCount();
1524 query.withWorkflowState(WorkflowInstance.WorkflowState.INSTANTIATED.toString());
1525 events = searchIndex.getByQuery(query);
1526 elementsCount += events.getHitCount();
1527 } catch (SearchIndexException e) {
1528 logger.warn("Could not perform search query", e);
1529 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1530 }
1531
1532 return elementsCount > 0;
1533 }
1534
1535 @GET
1536 @Path("{seriesId}/hasEvents.json")
1537 @Produces(MediaType.APPLICATION_JSON)
1538 @RestQuery(
1539 name = "hasEvents",
1540 description = "Check if given series has events",
1541 returnDescription = "true if series has events, otherwise false",
1542 pathParameters = {
1543 @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier",
1544 type = Type.STRING)
1545 },
1546 responses = {
1547 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the "
1548 + "request."),
1549 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."),
1550 @RestResponse(responseCode = SC_OK, description = "The access information ")
1551 })
1552 public Response getSeriesEvents(@PathParam("seriesId") String seriesId) throws Exception {
1553 if (StringUtils.isBlank(seriesId)) {
1554 return RestUtil.R.badRequest("Path parameter series ID is missing");
1555 }
1556
1557 long elementsCount = 0;
1558
1559 try {
1560 EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(),
1561 securityService.getUser());
1562 query.withSeriesId(seriesId);
1563 SearchResult<Event> result = searchIndex.getByQuery(query);
1564 elementsCount = result.getHitCount();
1565 } catch (SearchIndexException e) {
1566 logger.warn("Could not perform search query", e);
1567 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
1568 }
1569
1570 JSONObject jsonReturnObj = new JSONObject();
1571 jsonReturnObj.put("hasEvents", elementsCount > 0);
1572 return Response.ok(jsonReturnObj.toString()).build();
1573 }
1574
1575 @GET
1576 @Path("configuration.json")
1577 @Produces(MediaType.APPLICATION_JSON)
1578 @RestQuery(
1579 name = "getseriesconfiguration",
1580 description = "Get the series configuration",
1581 returnDescription = "List of configuration keys",
1582 responses = {
1583 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the "
1584 + "request."),
1585 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."),
1586 @RestResponse(responseCode = SC_OK, description = "The access information ")
1587 })
1588 public Response getSeriesOptions() {
1589 JSONObject jsonReturnObj = new JSONObject();
1590 jsonReturnObj.put("deleteSeriesWithEventsAllowed", deleteSeriesWithEventsAllowed);
1591 return Response.ok(jsonReturnObj.toString()).build();
1592 }
1593
1594 public Boolean getOnlySeriesWithWriteAccessSeriesTab() {
1595 return onlySeriesWithWriteAccessSeriesTab;
1596 }
1597
1598 public Boolean getOnlySeriesWithWriteAccessEventsFilter() {
1599 return onlySeriesWithWriteAccessEventsFilter;
1600 }
1601 }