Java tutorial
/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.adminui.endpoint; import static com.entwinemedia.fn.data.json.Jsons.f; import static com.entwinemedia.fn.data.json.Jsons.j; import static com.entwinemedia.fn.data.json.Jsons.jsonArrayFromList; import static com.entwinemedia.fn.data.json.Jsons.v; import static com.entwinemedia.fn.data.json.Jsons.vN; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.Status.NO_CONTENT; import static org.apache.commons.lang.StringUtils.trimToNull; import static org.opencastproject.index.service.util.RestUtils.notFound; import static org.opencastproject.index.service.util.RestUtils.okJson; import static org.opencastproject.index.service.util.RestUtils.okJsonList; import static org.opencastproject.util.Jsons.arr; import static org.opencastproject.util.Jsons.obj; import static org.opencastproject.util.Jsons.p; import static org.opencastproject.util.RestUtil.splitCommaSeparatedParam; import static org.opencastproject.util.RestUtil.R.badRequest; import static org.opencastproject.util.RestUtil.R.conflict; import static org.opencastproject.util.RestUtil.R.notFound; import static org.opencastproject.util.RestUtil.R.ok; import static org.opencastproject.util.RestUtil.R.serverError; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.option; import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN; import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER; import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING; import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT; import org.opencastproject.adminui.impl.index.AdminUISearchIndex; import org.opencastproject.adminui.util.ParticipationUtils; import org.opencastproject.authorization.xacml.manager.api.AclService; import org.opencastproject.authorization.xacml.manager.api.AclServiceException; import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory; import org.opencastproject.authorization.xacml.manager.api.ManagedAcl; import org.opencastproject.authorization.xacml.manager.api.SeriesACLTransition; import org.opencastproject.authorization.xacml.manager.api.TransitionQuery; import org.opencastproject.index.service.api.IndexService; import org.opencastproject.index.service.catalog.adapter.AbstractMetadataCollection; import org.opencastproject.index.service.catalog.adapter.MetadataField; import org.opencastproject.index.service.catalog.adapter.MetadataList; import org.opencastproject.index.service.catalog.adapter.MetadataUtils; import org.opencastproject.index.service.catalog.adapter.series.CommonSeriesCatalogUIAdapter; import org.opencastproject.index.service.catalog.adapter.series.SeriesCatalogUIAdapter; import org.opencastproject.index.service.exception.InternalServerErrorException; import org.opencastproject.index.service.impl.index.event.Event; import org.opencastproject.index.service.impl.index.event.Event.SchedulingStatus; import org.opencastproject.index.service.impl.index.event.EventSearchQuery; import org.opencastproject.index.service.impl.index.series.Series; import org.opencastproject.index.service.impl.index.series.SeriesIndexSchema; import org.opencastproject.index.service.impl.index.series.SeriesSearchQuery; import org.opencastproject.index.service.impl.index.theme.Theme; import org.opencastproject.index.service.impl.index.theme.ThemeSearchQuery; import org.opencastproject.index.service.resources.list.query.SeriesListQuery; import org.opencastproject.index.service.util.AccessInformationUtil; import org.opencastproject.index.service.util.JSONUtils; import org.opencastproject.index.service.util.RestUtils; import org.opencastproject.matterhorn.search.SearchIndexException; import org.opencastproject.matterhorn.search.SearchResult; import org.opencastproject.matterhorn.search.SearchResultItem; import org.opencastproject.matterhorn.search.SortCriterion; import org.opencastproject.metadata.dublincore.DublinCoreCatalogList; import org.opencastproject.pm.api.Course; import org.opencastproject.pm.api.Message; import org.opencastproject.pm.api.Person; import org.opencastproject.pm.api.Recording; import org.opencastproject.pm.api.persistence.ParticipationManagementDatabase; import org.opencastproject.pm.api.persistence.ParticipationManagementDatabase.SortType; import org.opencastproject.pm.api.persistence.ParticipationManagementDatabaseException; import org.opencastproject.pm.api.persistence.RecordingQuery; import org.opencastproject.rest.BulkOperationResult; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlParser; import org.opencastproject.security.api.AclScope; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UnauthorizedException; import org.opencastproject.series.api.SeriesException; import org.opencastproject.series.api.SeriesQuery; import org.opencastproject.series.api.SeriesService; import org.opencastproject.systems.MatterhornConstants; import org.opencastproject.util.DateTimeSupport; import org.opencastproject.util.Jsons; import org.opencastproject.util.Jsons.Val; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.RestUtil; import org.opencastproject.util.UrlSupport; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Monadics; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; import org.opencastproject.util.doc.rest.RestParameter; import org.opencastproject.util.doc.rest.RestParameter.Type; import org.opencastproject.util.doc.rest.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.util.doc.rest.RestService; import org.opencastproject.workflow.api.ConfiguredWorkflowRef; import org.opencastproject.workflow.api.WorkflowInstance; import com.entwinemedia.fn.Fn2; import com.entwinemedia.fn.Stream; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.data.json.JField; import com.entwinemedia.fn.data.json.JValue; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @Path("/") @RestService(name = "SeriesProxyService", title = "UI Series", notes = "These Endpoints deliver informations about the series required for the UI.", abstractText = "This service provides the series data for the UI.") public class SeriesEndpoint { private static final Logger logger = LoggerFactory.getLogger(SeriesEndpoint.class); private static final int CREATED_BY_UI_ORDER = 9; /** Default number of items on page */ private static final int DEFAULT_LIMIT = 100; public static final String THEME_KEY = "theme"; private SeriesService seriesService; private ParticipationManagementDatabase participationManagementDatabase; private SecurityService securityService; private AclServiceFactory aclServiceFactory; private IndexService indexService; private AdminUISearchIndex searchIndex; private final List<SeriesCatalogUIAdapter> seriesCatalogUIAdapters = new ArrayList<SeriesCatalogUIAdapter>(); private SeriesCatalogUIAdapter commonSeriesCatalogUIAdapter; /** Default server URL */ private String serverUrl = "http://localhost:8080"; /** A parser for handling JSON documents inside the body of a request. **/ private final JSONParser parser = new JSONParser(); /** OSGi callback for the series service. */ public void setSeriesService(SeriesService seriesService) { this.seriesService = seriesService; } /** OSGi callback for the search index. */ public void setIndex(AdminUISearchIndex index) { this.searchIndex = index; } public IndexService getIndexService() { return indexService; } /** OSGi DI. */ public void setIndexService(IndexService indexService) { this.indexService = indexService; } /** OSGi callback for the participation management database. */ public void setPersistence(ParticipationManagementDatabase persistence) { this.participationManagementDatabase = persistence; } /** OSGi callback for the security service */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi callback for the acl service factory */ public void setAclServiceFactory(AclServiceFactory aclServiceFactory) { this.aclServiceFactory = aclServiceFactory; } private AclService getAclService() { return aclServiceFactory.serviceFor(securityService.getOrganization()); } /** OSGi callback to add the series dublincore {@link SeriesCatalogUIAdapter} instance. */ public void setCommonSeriesCatalogUIAdapter(CommonSeriesCatalogUIAdapter commonSeriesCatalogUIAdapter) { this.commonSeriesCatalogUIAdapter = commonSeriesCatalogUIAdapter; } /** OSGi callback to add {@link SeriesCatalogUIAdapter} instance. */ public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) { seriesCatalogUIAdapters.add(catalogUIAdapter); } /** OSGi callback to remove {@link SeriesCatalogUIAdapter} instance. */ public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) { seriesCatalogUIAdapters.remove(catalogUIAdapter); } /** * @param organization * The organization to filter the results with. * @return A {@link List} of {@link SeriesCatalogUIAdapter} that provide the metadata to the front end. */ public List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters(String organization) { return Stream.$(seriesCatalogUIAdapters).filter(organizationFilter._2(organization)).toList(); } private static final Fn2<SeriesCatalogUIAdapter, String, Boolean> organizationFilter = new Fn2<SeriesCatalogUIAdapter, String, Boolean>() { @Override public Boolean ap(SeriesCatalogUIAdapter catalogUIAdapter, String organization) { return catalogUIAdapter.getOrganization().equals(organization); } }; protected void activate(ComponentContext cc) { if (cc != null) { String ccServerUrl = cc.getBundleContext().getProperty(MatterhornConstants.SERVER_URL_PROPERTY); logger.debug("Configured server url is {}", ccServerUrl); if (ccServerUrl != null) this.serverUrl = ccServerUrl; } logger.info("Activate series endpoint"); } /** * Get a single series * * @param seriesId * the series id * @return a series or none if not found wrapped in an option * @throws SearchIndexException */ public Opt<Series> getSeries(String seriesId) throws SearchIndexException { SearchResult<Series> result = searchIndex.getByQuery( new SeriesSearchQuery(securityService.getOrganization().getId(), securityService.getUser()) .withIdentifier(seriesId)); if (result.getPageSize() == 0) { logger.debug("Didn't find series with id {}", seriesId); return Opt.<Series>none(); } return Opt.some(result.getItems()[0].getSource()); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("{seriesId}/messages") @RestQuery(name = "getseriesmessages", description = "Returns the series messages as JSON", returnDescription = "Returns the series messages as JSON", pathParameters = { @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING) }, restParameters = { @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the following: DATE OR SENDER. Add '_DESC' to reverse the sort order (e.g. DATE_DESC).", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series messages as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid SORT type, it was not DATE, DATE_DESC SENDER or SENDER_DESC"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response getSeriesMessages(@PathParam("seriesId") String series, @QueryParam("sort") String sort) throws UnauthorizedException, NotFoundException { if (participationManagementDatabase == null) return Response.status(Status.SERVICE_UNAVAILABLE).build(); Option<SortType> sortType = Option.<SortType>none(); sortType = ParticipationUtils.getMessagesSortField(sort); if (StringUtils.isNotBlank(sort) && sortType.isNone()) { return Response.status(SC_BAD_REQUEST).build(); } try { seriesService.getSeries(series); } catch (SeriesException e) { logger.error("Unable to get series {}: {}", series, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } try { List<Message> messagesBySeriesId = participationManagementDatabase.getMessagesBySeriesId(series, sortType); List<Val> jsonArr = new ArrayList<Jsons.Val>(); for (Message m : messagesBySeriesId) { jsonArr.add(m.toJson()); } return Response.ok(arr(jsonArr).toJson()).build(); } catch (ParticipationManagementDatabaseException e) { logger.error("Unable to get messages by series {}: {}", series, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @GET @Path("{seriesId}/access.json") @SuppressWarnings("unchecked") @Produces(MediaType.APPLICATION_JSON) @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) }, reponses = { @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."), @RestResponse(responseCode = SC_NOT_FOUND, description = "If the series has not been found."), @RestResponse(responseCode = SC_OK, description = "The access information ") }) public Response getSeriesAccessInformation(@PathParam("seriesId") String seriesId) throws NotFoundException { if (StringUtils.isBlank(seriesId)) return RestUtil.R.badRequest("Path parameter series ID is missing"); boolean hasProcessingEvents = hasProcessingEvents(seriesId); // Add all available ACLs to the response JSONArray systemAclsJson = new JSONArray(); List<ManagedAcl> acls = getAclService().getAcls(); for (ManagedAcl acl : acls) { systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl)); } final TransitionQuery q = TransitionQuery.query().withId(seriesId).withScope(AclScope.Series); List<SeriesACLTransition> seriesTransistions; JSONArray transitionsJson = new JSONArray(); try { seriesTransistions = getAclService().getTransitions(q).getSeriesTransistions(); for (SeriesACLTransition trans : seriesTransistions) { transitionsJson.add(AccessInformationUtil.serializeSeriesACLTransition(trans)); } } catch (AclServiceException e) { logger.error( "There was an error while trying to get the ACL transitions for serie '{}' from the ACL service: {}", seriesId, e); return RestUtil.R.serverError(); } JSONObject seriesAccessJson = new JSONObject(); try { AccessControlList seriesAccessControl = seriesService.getSeriesAccessControl(seriesId); Option<ManagedAcl> currentAcl = AccessInformationUtil.matchAcls(acls, seriesAccessControl); seriesAccessJson.put("current_acl", currentAcl.isSome() ? currentAcl.get().getId() : 0); seriesAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(seriesAccessControl)); seriesAccessJson.put("acl", AccessControlParser.toJsonSilent(seriesAccessControl)); seriesAccessJson.put("transitions", transitionsJson); seriesAccessJson.put("locked", hasProcessingEvents); } catch (SeriesException e) { logger.error("Unable to get ACL from series {}: {}", seriesId, ExceptionUtils.getStackTrace(e)); return RestUtil.R.serverError(); } JSONObject jsonReturnObj = new JSONObject(); jsonReturnObj.put("system_acls", systemAclsJson); jsonReturnObj.put("series_access", seriesAccessJson); return Response.ok(jsonReturnObj.toString()).build(); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("{seriesId}/metadata.json") @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) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series metadata as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response getSeriesMetadata(@PathParam("seriesId") String series) throws UnauthorizedException, NotFoundException, SearchIndexException { Opt<Series> optSeries = getSeries(series); if (optSeries.isNone()) return notFound("Cannot find a series with id '%s'.", series); MetadataList metadataList = new MetadataList(); List<SeriesCatalogUIAdapter> catalogUIAdapters = new ArrayList<SeriesCatalogUIAdapter>( seriesCatalogUIAdapters); catalogUIAdapters.remove(commonSeriesCatalogUIAdapter); for (SeriesCatalogUIAdapter adapter : catalogUIAdapters) { final Opt<AbstractMetadataCollection> optSeriesMetadata = adapter.getFields(series); if (optSeriesMetadata.isSome()) { metadataList.add(adapter.getFlavor(), adapter.getUITitle(), optSeriesMetadata.get()); } } metadataList.add(commonSeriesCatalogUIAdapter, getSeriesMetadata(optSeries.get())); return okJson(metadataList.toJSON()); } /** * Loads the metadata for the given series * * @param series * the source {@link Series} * @return a {@link AbstractMetadataCollection} instance with all the series metadata */ @SuppressWarnings("unchecked") private AbstractMetadataCollection getSeriesMetadata(Series series) { AbstractMetadataCollection metadata = commonSeriesCatalogUIAdapter.getRawFields(); MetadataField<?> title = metadata.getOutputFields().get("title"); metadata.removeField(title); MetadataField<String> newTitle = MetadataUtils.copyMetadataField(title); newTitle.setValue(series.getTitle()); metadata.addField(newTitle); MetadataField<?> subject = metadata.getOutputFields().get("subject"); metadata.removeField(subject); MetadataField<String> newSubject = MetadataUtils.copyMetadataField(subject); newSubject.setValue(series.getSubject()); metadata.addField(newSubject); MetadataField<?> description = metadata.getOutputFields().get("description"); metadata.removeField(description); MetadataField<String> newDescription = MetadataUtils.copyMetadataField(description); newDescription.setValue(series.getDescription()); metadata.addField(newDescription); MetadataField<?> language = metadata.getOutputFields().get("language"); metadata.removeField(language); MetadataField<String> newLanguage = MetadataUtils.copyMetadataField(language); newLanguage.setValue(series.getLanguage()); metadata.addField(newLanguage); MetadataField<?> rightsHolder = metadata.getOutputFields().get("rightsHolder"); metadata.removeField(rightsHolder); MetadataField<String> newRightsHolder = MetadataUtils.copyMetadataField(rightsHolder); newRightsHolder.setValue(series.getRightsHolder()); metadata.addField(newRightsHolder); MetadataField<?> license = metadata.getOutputFields().get("license"); metadata.removeField(license); MetadataField<String> newLicense = MetadataUtils.copyMetadataField(license); newLicense.setValue(series.getLicense()); metadata.addField(newLicense); MetadataField<?> organizers = metadata.getOutputFields().get("creator"); metadata.removeField(organizers); MetadataField<String> newOrganizers = MetadataUtils.copyMetadataField(organizers); newOrganizers.setValue(StringUtils.join(series.getOrganizers(), ", ")); metadata.addField(newOrganizers); MetadataField<?> contributors = metadata.getOutputFields().get("contributor"); metadata.removeField(contributors); MetadataField<String> newContributors = MetadataUtils.copyMetadataField(contributors); newContributors.setValue(StringUtils.join(series.getContributors(), ", ")); metadata.addField(newContributors); MetadataField<?> publishers = metadata.getOutputFields().get("publisher"); metadata.removeField(publishers); MetadataField<String> newPublishers = MetadataUtils.copyMetadataField(publishers); newPublishers.setValue(StringUtils.join(series.getPublishers(), ", ")); metadata.addField(newPublishers); // Admin UI only field MetadataField<String> createdBy = MetadataField.createTextMetadataField("createdBy", Opt.<String>none(), "EVENTS.SERIES.DETAILS.METADATA.CREATED_BY", true, false, Opt.<Map<String, Object>>none(), Opt.<String>none(), Opt.some(CREATED_BY_UI_ORDER), Opt.<String>none()); createdBy.setValue(series.getCreator()); metadata.addField(createdBy); MetadataField<?> uid = metadata.getOutputFields().get("uid"); metadata.removeField(uid); MetadataField<String> newUID = MetadataUtils.copyMetadataField(uid); newUID.setValue(series.getIdentifier()); metadata.addField(newUID); return metadata; } /** * @return A {@link MetadataList} with all of the available CatalogUIAdapters empty {@link AbstractMetadataCollection} * available */ private MetadataList getMetadatListWithAllSeriesCatalogUIAdapters() { MetadataList metadataList = new MetadataList(); for (SeriesCatalogUIAdapter adapter : getSeriesCatalogUIAdapters( securityService.getOrganization().getId())) { metadataList.add(adapter.getFlavor(), adapter.getUITitle(), adapter.getRawFields()); } return metadataList; } @PUT @Path("{seriesId}/metadata") @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") }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series metadata as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response updateSeriesMetadata(@PathParam("seriesId") String seriesID, @FormParam("metadata") String metadataJSON) throws UnauthorizedException, NotFoundException, SearchIndexException { try { MetadataList metadataList = getIndexService().updateAllSeriesMetadata(seriesID, metadataJSON, searchIndex); return okJson(metadataList.toJSON()); } catch (IllegalArgumentException e) { return RestUtil.R.badRequest(e.getMessage()); } catch (InternalServerErrorException e) { return RestUtil.R.serverError(); } } /** * Checks the list of metadata for updated fields and stores/updates them in the respective metadata catalog. * * @param seriesId * The series identifier * @param metadataList * The metadata list */ private void updateSeriesMetadata(String seriesId, MetadataList metadataList) { for (SeriesCatalogUIAdapter adapter : seriesCatalogUIAdapters) { Opt<AbstractMetadataCollection> metadata = metadataList.getMetadataByFlavor(adapter.getFlavor()); if (metadata.isSome() && metadata.get().isUpdated()) { adapter.storeFields(seriesId, metadata.get()); } } } @GET @Produces(MediaType.APPLICATION_JSON) @Path("series/sendmessage") @RestQuery(name = "getseriesrecordingsandrecipients", description = "Returns the series recordings and recipients as JSON", returnDescription = "Returns the series recordings and recipients as JSON", restParameters = { @RestParameter(name = "seriesIds", isRequired = true, description = "A list of comma separated series IDs", type = STRING), }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series recordings and recipients as JSON."), @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "At least one series id must be set.") }) public Response getSeriesRecordingsAndRecipients(@QueryParam("seriesIds") String seriesIds) { if (participationManagementDatabase == null) return Response.status(Status.SERVICE_UNAVAILABLE).build(); final Monadics.ListMonadic<String> sIds = splitCommaSeparatedParam(option(seriesIds)); if (sIds.value().isEmpty()) return badRequest(); try { List<Recording> recordings = new ArrayList<Recording>(); for (String seriesId : sIds.value()) { Course course = getCourseBySeries(seriesId); if (course == null) continue; recordings.addAll(participationManagementDatabase .findRecordings(RecordingQuery.createWithoutDeleted().withCourse(course))); } List<Val> recipientsArr = mlist(new HashSet<Person>(mlist(recordings).flatMap(getRecipients).value())) .map(JSONUtils.personToJsonVal).value(); List<Val> recordingArr = mlist(recordings).map(JSONUtils.recordingToJsonVal).value(); return Response .ok(obj(p("recordings", arr(recordingArr)), p("recipients", arr(recipientsArr))).toJson()) .build(); } catch (ParticipationManagementDatabaseException e) { logger.error("Unable to get recordings and recipients by series {}: {}", seriesIds, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @GET @Path("new/metadata") @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", reponses = { @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the series metadata tab as JSON") }) public Response getNewMetadata() { MetadataList metadataList = getMetadatListWithAllSeriesCatalogUIAdapters(); Opt<AbstractMetadataCollection> metadataByAdapter = metadataList .getMetadataByAdapter(commonSeriesCatalogUIAdapter); if (metadataByAdapter.isSome()) { AbstractMetadataCollection collection = metadataByAdapter.get(); safelyRemoveField(collection, "uid"); metadataList.add(commonSeriesCatalogUIAdapter, collection); } return okJson(metadataList.toJSON()); } private void safelyRemoveField(AbstractMetadataCollection collection, String fieldName) { MetadataField<?> metadataField = collection.getOutputFields().get(fieldName); if (metadataField != null) { collection.removeField(metadataField); } } @GET @Path("new/themes") @SuppressWarnings("unchecked") @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", reponses = { @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the series themes tab as JSON") }) public Response getNewThemes() { ThemeSearchQuery query = new ThemeSearchQuery(securityService.getOrganization().getId(), securityService.getUser()); SearchResult<Theme> results = null; try { results = searchIndex.getByQuery(query); } catch (SearchIndexException e) { logger.error("The admin UI Search Index was not able to get the themes: {}", ExceptionUtils.getStackTrace(e)); return RestUtil.R.serverError(); } JSONObject themesJson = new JSONObject(); for (SearchResultItem<Theme> item : results.getItems()) { Theme theme = item.getSource(); themesJson.put(theme.getIdentifier(), theme.getName()); } return Response.ok(themesJson.toJSONString()).build(); } @POST @Path("new") @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) }, reponses = { @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Returns the created series id"), @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "he request could not be fulfilled due to the incorrect syntax of the request"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If user doesn't have rights to create the series") }) public Response createNewSeries(@FormParam("metadata") String metadata) throws UnauthorizedException { String seriesId; try { seriesId = getIndexService().createSeries(metadata); return Response.created(getSeriesMetadataUrl(seriesId)).entity(seriesId).build(); } catch (IllegalArgumentException e) { return RestUtil.R.badRequest(e.getMessage()); } catch (InternalServerErrorException e) { return RestUtil.R.serverError(); } } /** * Remove a series. * * @param id * The id of the series to remove. */ private void removeSeries(String id) throws NotFoundException, SeriesException, UnauthorizedException { SeriesQuery seriesQuery = new SeriesQuery(); seriesQuery.setSeriesId(id); DublinCoreCatalogList dublinCoreCatalogList = seriesService.getSeries(seriesQuery); if (dublinCoreCatalogList.size() == 0) { throw new NotFoundException(); } seriesService.deleteSeries(id); } @DELETE @Path("{seriesId}") @Produces(MediaType.APPLICATION_JSON) @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), }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series has been deleted."), @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The series could not be found.") }) public Response deleteSeries(@PathParam("seriesId") String id) throws NotFoundException { try { removeSeries(id); return Response.ok().build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.error("Unable to delete the series '{}' due to: {}", id, ExceptionUtils.getStackTrace(e)); return Response.serverError().build(); } } @POST @Path("deleteSeries") @Produces(MediaType.APPLICATION_JSON) @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.", reponses = { @RestResponse(description = "Series have been deleted", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST) }) public Response deleteMultipleSeries(String seriesIdsContent) throws NotFoundException { if (StringUtils.isBlank(seriesIdsContent)) { return Response.status(Response.Status.BAD_REQUEST).build(); } JSONArray seriesIdsArray; try { seriesIdsArray = (JSONArray) parser.parse(seriesIdsContent); } catch (org.json.simple.parser.ParseException e) { logger.error("Unable to parse '{}' because: {}", seriesIdsContent, ExceptionUtils.getStackTrace(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } catch (ClassCastException e) { logger.error("Unable to cast '{}' to a JSON array because: {}", seriesIdsContent, ExceptionUtils.getMessage(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } BulkOperationResult result = new BulkOperationResult(); for (Object seriesId : seriesIdsArray) { try { removeSeries(seriesId.toString()); result.addOk(seriesId.toString()); } catch (NotFoundException e) { result.addNotFound(seriesId.toString()); } catch (Exception e) { logger.error("Unable to remove the series '{}': {}", seriesId.toString(), ExceptionUtils.getStackTrace(e)); result.addServerError(seriesId.toString()); } } return Response.ok(result.toJson()).build(); } @POST @Path("optOutSeries/{optout}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "optOutSeries", description = "Changes the opt out status of 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' opt out status was updated, was not found or there was an error in changing it.", pathParameters = { @RestParameter(name = "optout", description = "True to opt out the series, false if not.", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Series have been updated", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST) }) public Response optOutMultipleSeries(String seriesIdsContent, @PathParam("optout") boolean optout) throws NotFoundException { if (StringUtils.isBlank(seriesIdsContent)) { return Response.status(Response.Status.BAD_REQUEST).build(); } JSONArray seriesIdsArray; try { seriesIdsArray = (JSONArray) parser.parse(seriesIdsContent); } catch (org.json.simple.parser.ParseException e) { logger.error("Unable to parse '{}' because: {}", seriesIdsContent, ExceptionUtils.getStackTrace(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } catch (ClassCastException e) { logger.error("Unable to cast '{}' to a JSON array because: {}", seriesIdsContent, ExceptionUtils.getStackTrace(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } BulkOperationResult result = new BulkOperationResult(); for (Object seriesId : seriesIdsArray) { try { seriesService.updateOptOutStatus(seriesId.toString(), optout); result.addOk(seriesId.toString()); } catch (NotFoundException e) { result.addNotFound(seriesId.toString()); } catch (Exception e) { logger.error("Unable to remove the series '{}': {}", seriesId.toString(), ExceptionUtils.getStackTrace(e)); result.addServerError(seriesId.toString()); } } return Response.ok(result.toJson()).build(); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("series.json") @RestQuery(name = "listSeriesAsJson", description = "Returns the series matching the query parameters", returnDescription = "Returns the series search results as JSON", restParameters = { @RestParameter(name = "sortorganizer", isRequired = false, description = "The sort type to apply to the series organizer or organizers either Ascending or Descending.", type = STRING), @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), @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2,value2'", type = STRING), @RestParameter(name = "offset", isRequired = false, description = "The page offset", type = INTEGER, defaultValue = "0"), @RestParameter(name = "optedOut", isRequired = false, description = "Whether this series is opted out", type = BOOLEAN), @RestParameter(name = "limit", isRequired = false, description = "Results per page (max 100)", type = INTEGER, defaultValue = "100") }, reponses = { @RestResponse(responseCode = SC_OK, description = "The access control list."), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response getSeries(@QueryParam("filter") String filter, @QueryParam("sort") String sort, @QueryParam("offset") int offset, @QueryParam("limit") int limit, @QueryParam("optedOut") Boolean optedOut) throws UnauthorizedException { try { SeriesSearchQuery query = new SeriesSearchQuery(securityService.getOrganization().getId(), securityService.getUser()); Option<String> optSort = Option.option(trimToNull(sort)); if (offset != 0) { query.withOffset(offset); } // If limit is 0, we set the default limit query.withLimit(limit == 0 ? DEFAULT_LIMIT : limit); if (optedOut != null) query.withOptedOut(optedOut); Map<String, String> filters = RestUtils.parseFilter(filter); for (String name : filters.keySet()) { if (SeriesListQuery.FILTER_ACL_NAME.equals(name)) { query.withManagedAcl(filters.get(name)); } else if (SeriesListQuery.FILTER_CONTRIBUTORS_NAME.equals(name)) { query.withContributor(filters.get(name)); } else if (SeriesListQuery.FILTER_CREATIONDATE_NAME.equals(name)) { try { Tuple<Date, Date> fromAndToCreationRange = RestUtils .getFromAndToDateRange(filters.get(name)); query.withCreatedFrom(fromAndToCreationRange.getA()); query.withCreatedTo(fromAndToCreationRange.getB()); } catch (IllegalArgumentException e) { return RestUtil.R.badRequest(e.getMessage()); } } else if (SeriesListQuery.FILTER_CREATOR_NAME.equals(name)) { query.withCreator(filters.get(name)); } else if (SeriesListQuery.FILTER_TEXT_NAME.equals(name)) { query.withText("*" + filters.get(name) + "*"); } else if (SeriesListQuery.FILTER_LANGUAGE_NAME.equals(name)) { query.withLanguage(filters.get(name)); } else if (SeriesListQuery.FILTER_LICENSE_NAME.equals(name)) { query.withLicense(filters.get(name)); } else if (SeriesListQuery.FILTER_ORGANIZERS_NAME.equals(name)) { query.withOrganizer(filters.get(name)); } else if (SeriesListQuery.FILTER_SUBJECT_NAME.equals(name)) { query.withSubject(filters.get(name)); } else if (SeriesListQuery.FILTER_TITLE_NAME.equals(name)) { query.withTitle(filters.get(name)); } } if (optSort.isSome()) { Set<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get()); for (SortCriterion criterion : sortCriteria) { switch (criterion.getFieldName()) { case SeriesIndexSchema.TITLE: query.sortByTitle(criterion.getOrder()); break; case SeriesIndexSchema.CONTRIBUTORS: query.sortByContributors(criterion.getOrder()); break; case SeriesIndexSchema.CREATOR: query.sortByOrganizers(criterion.getOrder()); break; case SeriesIndexSchema.CREATED_DATE_TIME: query.sortByCreatedDateTime(criterion.getOrder()); break; case SeriesIndexSchema.MANAGED_ACL: query.sortByManagedAcl(criterion.getOrder()); break; default: logger.info("Unknown filter criteria {}", criterion.getFieldName()); return Response.status(SC_BAD_REQUEST).build(); } } } logger.trace("Using Query: " + query.toString()); SearchResult<Series> result = searchIndex.getByQuery(query); List<JValue> series = new ArrayList<JValue>(); for (SearchResultItem<Series> item : result.getItems()) { List<JField> fields = new ArrayList<JField>(); Series s = item.getSource(); String sId = s.getIdentifier(); fields.add(f("id", v(sId))); fields.add(f("optedOut", v(s.isOptedOut()))); fields.add(f("title", vN(s.getTitle()))); fields.add(f("organizers", jsonArrayFromList(s.getOrganizers()))); fields.add(f("contributors", jsonArrayFromList(s.getContributors()))); if (s.getCreator() != null) { fields.add(f("createdBy", v(s.getCreator()))); } if (s.getCreatedDateTime() != null) { fields.add(f("creation_date", vN(DateTimeSupport.toUTC(s.getCreatedDateTime().getTime())))); } if (s.getLanguage() != null) { fields.add(f("language", v(s.getLanguage()))); } if (s.getLicense() != null) { fields.add(f("license", v(s.getLicense()))); } if (s.getRightsHolder() != null) { fields.add(f("rightsHolder", v(s.getRightsHolder()))); } if (StringUtils.isNotBlank(s.getManagedAcl())) { fields.add(f("managedAcl", v(s.getManagedAcl()))); } extendEventsStatusOverview(fields, s); series.add(j(fields)); } return okJsonList(series, offset, limit, result.getHitCount()); } catch (Exception e) { logger.warn("Could not perform search query: {}", ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @SuppressWarnings("unchecked") @GET @Produces(MediaType.APPLICATION_JSON) @Path("{id}/properties") @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) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The access control list."), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response getSeriesPropertiesAsJson(@PathParam("id") String seriesId) throws UnauthorizedException, NotFoundException { if (StringUtils.isBlank(seriesId)) { logger.warn("Series id parameter is blank '{}'.", seriesId); return Response.status(BAD_REQUEST).build(); } try { Map<String, String> properties = seriesService.getSeriesProperties(seriesId); JSONArray jsonProperties = new JSONArray(); for (String name : properties.keySet()) { JSONObject property = new JSONObject(); property.put(name, properties.get(name)); jsonProperties.add(property); } return Response.ok(jsonProperties.toString()).build(); } catch (UnauthorizedException e) { throw e; } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Could not perform search query: {}", e.getMessage()); } throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("{seriesId}/property/{propertyName}.json") @RestQuery(name = "getSeriesProperty", description = "Returns a series property value", returnDescription = "Returns the series property value", pathParameters = { @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING), @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true, type = Type.STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The access control list."), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response getSeriesProperty(@PathParam("seriesId") String seriesId, @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException { if (StringUtils.isBlank(seriesId)) { logger.warn("Series id parameter is blank '{}'.", seriesId); return Response.status(BAD_REQUEST).build(); } if (StringUtils.isBlank(propertyName)) { logger.warn("Series property name parameter is blank '{}'.", propertyName); return Response.status(BAD_REQUEST).build(); } try { String propertyValue = seriesService.getSeriesProperty(seriesId, propertyName); return Response.ok(propertyValue).build(); } catch (UnauthorizedException e) { throw e; } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Could not perform search query: {}", ExceptionUtils.getStackTrace(e)); } throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } @POST @Path("/{seriesId}/property") @RestQuery(name = "updateSeriesProperty", description = "Updates a series property", returnDescription = "No content.", restParameters = { @RestParameter(name = "name", isRequired = true, description = "The property's name", type = TEXT), @RestParameter(name = "value", isRequired = true, description = "The property's value", type = TEXT) }, pathParameters = { @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found."), @RestResponse(responseCode = SC_NO_CONTENT, description = "The access control list has been updated."), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required path or form params were missing in the request.") }) public Response updateSeriesProperty(@PathParam("seriesId") String seriesId, @FormParam("name") String name, @FormParam("value") String value) throws UnauthorizedException { if (StringUtils.isBlank(seriesId)) { logger.warn("Series id parameter is blank '{}'.", seriesId); return Response.status(BAD_REQUEST).build(); } if (StringUtils.isBlank(name)) { logger.warn("Name parameter is blank '{}'.", name); return Response.status(BAD_REQUEST).build(); } if (StringUtils.isBlank(value)) { logger.warn("Series id parameter is blank '{}'.", value); return Response.status(BAD_REQUEST).build(); } try { seriesService.updateSeriesProperty(seriesId, name, value); return Response.status(NO_CONTENT).build(); } catch (NotFoundException e) { return Response.status(NOT_FOUND).build(); } catch (SeriesException e) { logger.warn("Could not update series property for series {} property {}:{} : {}", new Object[] { seriesId, name, value, ExceptionUtils.getStackTrace(e) }); } throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } @DELETE @Path("{seriesId}/property/{propertyName}") @RestQuery(name = "deleteSeriesProperty", description = "Deletes a series property", returnDescription = "No Content", pathParameters = { @RestParameter(name = "seriesId", description = "ID of series", isRequired = true, type = Type.STRING), @RestParameter(name = "propertyName", description = "Name of series property", isRequired = true, type = Type.STRING) }, reponses = { @RestResponse(responseCode = SC_NO_CONTENT, description = "The series property has been deleted."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or property has not been found."), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response deleteSeriesProperty(@PathParam("seriesId") String seriesId, @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException { if (StringUtils.isBlank(seriesId)) { logger.warn("Series id parameter is blank '{}'.", seriesId); return Response.status(BAD_REQUEST).build(); } if (StringUtils.isBlank(propertyName)) { logger.warn("Series property name parameter is blank '{}'.", propertyName); return Response.status(BAD_REQUEST).build(); } try { seriesService.deleteSeriesProperty(seriesId, propertyName); return Response.status(NO_CONTENT).build(); } catch (UnauthorizedException e) { throw e; } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Could not delete series '{}' property '{}' query: {}", new Object[] { seriesId, propertyName, ExceptionUtils.getStackTrace(e) }); } throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } /** * Creates an ok response with the entity being the theme id and name. * * @param theme * The theme to get the id and name from. * @return A {@link Response} with the theme id and name as json contents */ private Response getSimpleThemeJsonResponse(Theme theme) { return okJson(j(f(Long.toString(theme.getIdentifier()), v(theme.getName())))); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("{seriesId}/theme.json") @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) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series theme id and name as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found") }) public Response getSeriesTheme(@PathParam("seriesId") String seriesId) { Long themeId; try { Opt<Series> series = getSeries(seriesId); if (series.isNone()) return notFound("Cannot find a series with id {}", seriesId); themeId = series.get().getTheme(); } catch (SearchIndexException e) { logger.error("Unable to get series {}: {}", seriesId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } // If no theme is set return empty JSON if (themeId == null) return okJson(j()); try { Opt<Theme> themeOpt = getTheme(themeId); if (themeOpt.isNone()) return notFound("Cannot find a theme with id {}", themeId); return getSimpleThemeJsonResponse(themeOpt.get()); } catch (SearchIndexException e) { logger.error("Unable to get theme {}: {}", themeId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @PUT @Path("{seriesId}/theme") @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") }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series theme has been updated and the theme id and name are returned as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series or theme has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response updateSeriesTheme(@PathParam("seriesId") String seriesID, @FormParam("themeId") long themeId) throws UnauthorizedException, NotFoundException { try { Opt<Theme> themeOpt = getTheme(themeId); if (themeOpt.isNone()) return notFound("Cannot find a theme with id {}", themeId); seriesService.updateSeriesProperty(seriesID, THEME_KEY, Long.toString(themeId)); return getSimpleThemeJsonResponse(themeOpt.get()); } catch (SeriesException e) { logger.error("Unable to update series theme {}: {}", themeId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } catch (SearchIndexException e) { logger.error("Unable to get theme {}: {}", themeId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @DELETE @Path("{seriesId}/theme") @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) }, reponses = { @RestResponse(responseCode = SC_NO_CONTENT, description = "The series theme has been removed"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response deleteSeriesTheme(@PathParam("seriesId") String seriesID) throws UnauthorizedException, NotFoundException { try { seriesService.deleteSeriesProperty(seriesID, THEME_KEY); return Response.noContent().build(); } catch (SeriesException e) { logger.error("Unable to remove theme from series {}: {}", seriesID, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @POST @Path("/{seriesId}/access") @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 = { @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING), @RestParameter(name = "override", isRequired = false, defaultValue = "false", description = "If true the series ACL will take precedence over any existing episode ACL", type = BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") }) public Response applyAclToSeries(@PathParam("seriesId") String seriesId, @FormParam("acl") String acl, @DefaultValue("false") @FormParam("override") boolean override) throws SearchIndexException { AccessControlList accessControlList; try { accessControlList = AccessControlParser.parseAcl(acl); } catch (Exception e) { logger.warn("Unable to parse ACL '{}'", acl); return badRequest(); } Opt<Series> series = getSeries(seriesId); if (series.isNone()) return notFound("Cannot find a series with id {}", seriesId); if (hasProcessingEvents(seriesId)) { logger.warn( "Can not update the ACL from series {}. Events being part of the series are currently processed.", seriesId); return conflict(); } try { if (getAclService().applyAclToSeries(seriesId, accessControlList, override, Option.<ConfiguredWorkflowRef>none())) return ok(); else { logger.warn("Unable to find series '{}' to apply the ACL.", seriesId); return notFound(); } } catch (AclServiceException e) { logger.error("Error applying acl to series {}", seriesId); return serverError(); } } /** * Check if the series with the given Id has events being currently processed * * @param seriesId * the series Id * @return true if events being part of the series are currently processed */ private boolean hasProcessingEvents(String seriesId) { EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser()); long elementsCount = 0; query.withSeriesId(seriesId); try { query.withWorkflowState(WorkflowInstance.WorkflowState.RUNNING.toString()); SearchResult<Event> events = searchIndex.getByQuery(query); elementsCount = events.getHitCount(); query.withWorkflowState(WorkflowInstance.WorkflowState.INSTANTIATED.toString()); events = searchIndex.getByQuery(query); elementsCount += events.getHitCount(); } catch (SearchIndexException e) { logger.warn("Could not perform search query: {}", ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } return elementsCount > 0; } private Course getCourseBySeries(String sId) throws ParticipationManagementDatabaseException { try { return participationManagementDatabase.findCourseBySeries(sId); } catch (NotFoundException e) { return null; } } private final Function<Recording, List<Person>> getRecipients = new Function<Recording, List<Person>>() { @Override public List<Person> apply(Recording a) { return a.getStaff(); } }; private void extendEventsStatusOverview(List<JField> fields, Series series) throws SearchIndexException { EventSearchQuery query = new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser()).withoutActions().withSeriesId(series.getIdentifier()); SearchResult<Event> result = searchIndex.getByQuery(query); // collect recording statuses int blacklisted = 0; int optOut = 0; int ready = 0; for (SearchResultItem<Event> item : result.getItems()) { Event event = item.getSource(); if (event.getSchedulingStatus() == null) continue; SchedulingStatus schedulingStatus = SchedulingStatus.valueOf(event.getSchedulingStatus()); if (SchedulingStatus.BLACKLISTED.equals(schedulingStatus)) { blacklisted++; } else if (series.isOptedOut() || SchedulingStatus.OPTED_OUT.equals(schedulingStatus)) { optOut++; } else { ready++; } } fields.add( f("events", j(f("BLACKLISTED", v(blacklisted)), f("OPTED_OUT", v(optOut)), f("READY", v(ready))))); } /** * Get a single theme * * @param id * the theme id * @return a theme or none if not found, wrapped in an option * @throws SearchIndexException */ private Opt<Theme> getTheme(long id) throws SearchIndexException { SearchResult<Theme> result = searchIndex.getByQuery( new ThemeSearchQuery(securityService.getOrganization().getId(), securityService.getUser()) .withIdentifier(id)); if (result.getPageSize() == 0) { logger.debug("Didn't find theme with id {}", id); return Opt.<Theme>none(); } return Opt.some(result.getItems()[0].getSource()); } private URI getSeriesMetadataUrl(String seriesId) { return URI.create(UrlSupport.concat(serverUrl, "admin-ng/series-details", "metadata", seriesId)); } }