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