/** * 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.index.service.impl.index.event; import org.opencastproject.index.service.catalog.adapter.MetadataList; import org.opencastproject.index.service.exception.IndexServiceException; import org.opencastproject.index.service.util.RequestUtils; import org.opencastproject.ingest.api.IngestException; import org.opencastproject.ingest.api.IngestService; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElements; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter; import org.opencastproject.metadata.dublincore.MetadataCollection; import org.opencastproject.metadata.dublincore.MetadataField; import org.opencastproject.security.api.AccessControlEntry; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.util.NotFoundException; import com.entwinemedia.fn.data.Opt; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.TimeZone; import javax.servlet.http.HttpServletRequest; public class EventHttpServletRequest { /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(EventHttpServletRequest.class); private static final String ACTION_JSON_KEY = "action"; private static final String ALLOW_JSON_KEY = "allow"; private static final String ID_JSON_KEY = "id"; private static final String METADATA_JSON_KEY = "metadata"; private static final String ROLE_JSON_KEY = "role"; private static final String VALUE_JSON_KEY = "value"; /** A parser for handling JSON documents inside the body of a request. **/ private static final JSONParser parser = new JSONParser(); private Opt<AccessControlList> acl = Opt.none(); private Opt<MediaPackage> mediaPackage = Opt.none(); private Opt<MetadataList> metadataList = Opt.none(); private Opt<JSONObject> processing = Opt.none(); private Opt<JSONObject> source = Opt.none(); public void setAcl(AccessControlList acl) { this.acl = Opt.some(acl); } public void setMediaPackage(MediaPackage mediaPackage) { this.mediaPackage = Opt.some(mediaPackage); } public void setMetadataList(MetadataList metadataList) { this.metadataList = Opt.some(metadataList); } public void setProcessing(JSONObject processing) { this.processing = Opt.some(processing); } public void setSource(JSONObject source) { this.source = Opt.some(source); } public Opt<AccessControlList> getAcl() { return acl; } public Opt<MediaPackage> getMediaPackage() { return mediaPackage; } public Opt<MetadataList> getMetadataList() { return metadataList; } public Opt<JSONObject> getProcessing() { return processing; } public Opt<JSONObject> getSource() { return source; } /** * Create a {@link EventHttpServletRequest} from a {@link HttpServletRequest} to create a new {@link Event}. * * @param request * The multipart request that should result in a new {@link Event} * @param ingestService * The {@link IngestService} to use to ingest {@link Event} media. * @param eventCatalogUIAdapters * The catalog ui adapters to use for getting the event metadata. * @return An {@link EventHttpServletRequest} populated from the request. * @throws IndexServiceException * Thrown if unable to create the event for an internal reason. * @throws IllegalArgumentException * Thrown if the multi part request doesn't have the necessary data. */ public static EventHttpServletRequest createFromHttpServletRequest(HttpServletRequest request, IngestService ingestService, List<EventCatalogUIAdapter> eventCatalogUIAdapters, JSONObject source) throws IndexServiceException { EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest(); eventHttpServletRequest.setSource(source); try { if (ServletFileUpload.isMultipartContent(request)) { eventHttpServletRequest.setMediaPackage(ingestService.createMediaPackage()); if (eventHttpServletRequest.getMediaPackage().isNone()) { throw new IndexServiceException("Unable to create a new mediapackage to store the new event's media."); } for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); String fieldName = item.getFieldName(); if (item.isFormField()) { setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName); } else { ingestFile(ingestService, eventHttpServletRequest, item); } } } else { throw new IllegalArgumentException("No multipart content"); } return eventHttpServletRequest; } catch (Exception e) { throw new IndexServiceException("Unable to parse new event.", e); } } /** * Ingest a file from a multi part request for a new event. * * @param ingestService * The {@link IngestService} to use to ingest the file. * @param eventHttpServletRequest * The {@link EventHttpServletRequest} that has the ingest mediapackage. * @param item * The representation of the file. * @throws MediaPackageException * Thrown if unable to add the track to the mediapackage. * @throws IOException * Thrown if unable to upload the file into the mediapackage. * @throws IngestException * Thrown if unable to ingest the file. */ private static void ingestFile(IngestService ingestService, EventHttpServletRequest eventHttpServletRequest, FileItemStream item) throws MediaPackageException, IOException, IngestException { MediaPackage mp = eventHttpServletRequest.getMediaPackage().get(); if ("presenter".equals(item.getFieldName())) { eventHttpServletRequest.setMediaPackage( ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTER_SOURCE, mp)); } else if ("presentation".equals(item.getFieldName())) { eventHttpServletRequest.setMediaPackage( ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTATION_SOURCE, mp)); } else if ("audio".equals(item.getFieldName())) { eventHttpServletRequest.setMediaPackage(ingestService.addTrack(item.openStream(), item.getName(), new MediaPackageElementFlavor("presenter-audio", "source"), mp)); } else { logger.warn("Unknown field name found {}", item.getFieldName()); } } /** * Set a value for creating a new event from a form field. * * @param eventCatalogUIAdapters * The list of event catalog ui adapters used for loading the metadata for the new event. * @param eventHttpServletRequest * The current details of the request that have been loaded. * @param item * The content of the field. * @param fieldName * The key of the field. * @throws IOException * Thrown if unable to laod the content of the field. * @throws NotFoundException * Thrown if unable to find a metadata catalog or field that matches an input catalog or field. */ private static void setFormField(List<EventCatalogUIAdapter> eventCatalogUIAdapters, EventHttpServletRequest eventHttpServletRequest, FileItemStream item, String fieldName) throws IOException, NotFoundException { if (METADATA_JSON_KEY.equals(fieldName)) { String metadata = Streams.asString(item.openStream()); try { MetadataList metadataList = deserializeMetadataList(metadata, eventCatalogUIAdapters); eventHttpServletRequest.setMetadataList(metadataList); } catch (IllegalArgumentException e) { throw e; } catch (ParseException e) { throw new IllegalArgumentException(String.format("Unable to parse event metadata because: '%s'", e.toString())); } catch (NotFoundException e) { throw e; } } else if ("acl".equals(item.getFieldName())) { String access = Streams.asString(item.openStream()); try { AccessControlList acl = deserializeJsonToAcl(access, true); eventHttpServletRequest.setAcl(acl); } catch (Exception e) { logger.warn("Unable to parse acl {}", access); throw new IllegalArgumentException("Unable to parse acl"); } } else if ("processing".equals(item.getFieldName())) { String processing = Streams.asString(item.openStream()); try { eventHttpServletRequest.setProcessing((JSONObject) parser.parse(processing)); } catch (Exception e) { logger.warn("Unable to parse processing configuration {}", processing); throw new IllegalArgumentException("Unable to parse processing configuration"); } } } /** * Load the details of updating an event. * * @param event * The event to update. * @param request * The multipart request that has the data to load the updated event. * @param eventCatalogUIAdapters * The list of catalog ui adapters to use to load the event metadata. * @return The data for the event update * @throws IllegalArgumentException * Thrown if the request to update the event is malformed. * @throws IndexServiceException * Thrown if something is unable to load the event data. * @throws NotFoundException * Thrown if unable to find a metadata catalog or field that matches an input catalog or field. */ public static EventHttpServletRequest updateFromHttpServletRequest(Event event, HttpServletRequest request, List<EventCatalogUIAdapter> eventCatalogUIAdapters) throws IllegalArgumentException, IndexServiceException, NotFoundException { EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest(); if (ServletFileUpload.isMultipartContent(request)) { try { for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); String fieldName = item.getFieldName(); if (item.isFormField()) { setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName); } } } catch (IOException e) { throw new IndexServiceException("Unable to update event", e); } catch (FileUploadException e) { throw new IndexServiceException("Unable to update event", e); } } else { throw new IllegalArgumentException("No multipart content"); } return eventHttpServletRequest; } /** * De-serialize an JSON into an {@link AccessControlList}. * * @param json * The {@link AccessControlList} to serialize. * @param assumeAllow * Assume that all entries are allows. * @return An {@link AccessControlList} representation of the Json * @throws ParseException */ protected static AccessControlList deserializeJsonToAcl(String json, boolean assumeAllow) throws ParseException { JSONArray aclJson = (JSONArray) parser.parse(json); @SuppressWarnings("unchecked") ListIterator<Object> iterator = aclJson.listIterator(); JSONObject aceJson; List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>(); while (iterator.hasNext()) { aceJson = (JSONObject) iterator.next(); String action = aceJson.get(ACTION_JSON_KEY) != null ? aceJson.get(ACTION_JSON_KEY).toString() : ""; String allow; if (assumeAllow) { allow = "true"; } else { allow = aceJson.get(ALLOW_JSON_KEY) != null ? aceJson.get(ALLOW_JSON_KEY).toString() : ""; } String role = aceJson.get(ROLE_JSON_KEY) != null ? aceJson.get(ROLE_JSON_KEY).toString() : ""; if (StringUtils.trimToNull(action) != null && StringUtils.trimToNull(allow) != null && StringUtils.trimToNull(role) != null) { AccessControlEntry ace = new AccessControlEntry(role, action, Boolean.parseBoolean(allow)); entries.add(ace); } else { throw new IllegalArgumentException(String.format( "One of the access control elements is missing a property. The action was '%s', allow was '%s' and the role was '%s'", action, allow, role)); } } return new AccessControlList(entries); } /** * Change the simplified fields of key values provided to the external api into a {@link MetadataList}. * * @param json * The json string that contains an array of metadata field lists for the different catalogs. * @return A {@link MetadataList} with the fields populated with the values provided. * @throws ParseException * Thrown if unable to parse the json string. * @throws NotFoundException * Thrown if unable to find the catalog or field that the json refers to. */ protected static MetadataList deserializeMetadataList(String json, List<EventCatalogUIAdapter> catalogAdapters) throws ParseException, NotFoundException { MetadataList metadataList = new MetadataList(); JSONArray jsonCatalogs = (JSONArray) parser.parse(json); for (int i = 0; i < jsonCatalogs.size(); i++) { JSONObject catalog = (JSONObject) jsonCatalogs.get(i); if (catalog.get("flavor") == null || StringUtils.isBlank(catalog.get("flavor").toString())) { throw new IllegalArgumentException( "Unable to create new event as no flavor was given for one of the metadata collections"); } String flavorString = catalog.get("flavor").toString(); MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString); MetadataCollection collection = null; EventCatalogUIAdapter adapter = null; for (EventCatalogUIAdapter eventCatalogUIAdapter : catalogAdapters) { if (eventCatalogUIAdapter.getFlavor().equals(flavor)) { adapter = eventCatalogUIAdapter; collection = eventCatalogUIAdapter.getRawFields(); } } if (collection == null) { throw new IllegalArgumentException( String.format("Unable to find an EventCatalogUIAdapter with Flavor '%s'", flavorString)); } String fieldsJson = catalog.get("fields").toString(); if (StringUtils.trimToNull(fieldsJson) != null) { Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson); for (String key : fields.keySet()) { if ("subjects".equals(key)) { // Handle the special case of allowing subjects to be an array. MetadataField<?> field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName()); if (field == null) { throw new NotFoundException(String.format( "Cannot find a metadata field with id 'subject' from Catalog with Flavor '%s'.", flavorString)); } collection.removeField(field); try { JSONArray subjects = (JSONArray) parser.parse(fields.get(key)); collection.addField( MetadataField.copyMetadataFieldWithValue(field, StringUtils.join(subjects.iterator(), ","))); } catch (ParseException e) { throw new IllegalArgumentException( String.format("Unable to parse the 'subjects' metadata array field because: %s", e.toString())); } } else { MetadataField<?> field = collection.getOutputFields().get(key); if (field == null) { throw new NotFoundException(String.format( "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString)); } collection.removeField(field); collection.addField(MetadataField.copyMetadataFieldWithValue(field, fields.get(key))); } } } metadataList.add(adapter, collection); } setStartDateAndTimeIfUnset(metadataList); return metadataList; } /** * Set the start date and time to the current date & time if it hasn't been set through the api call. * * @param metadataList * The metadata list created from the json request to create a new event */ private static void setStartDateAndTimeIfUnset(MetadataList metadataList) { Opt<MetadataCollection> optCommonEventCollection = metadataList .getMetadataByFlavor(MediaPackageElements.EPISODE.toString()); if (optCommonEventCollection.isSome()) { MetadataCollection commonEventCollection = optCommonEventCollection.get(); MetadataField<?> startDate = commonEventCollection.getOutputFields().get("startDate"); if (!startDate.isUpdated()) { SimpleDateFormat utcDateFormat = new SimpleDateFormat(startDate.getPattern().get()); utcDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); String currentDate = utcDateFormat.format(new DateTime(DateTimeZone.UTC).toDate()); commonEventCollection.removeField(startDate); commonEventCollection.addField(MetadataField.copyMetadataFieldWithValue(startDate, currentDate)); } MetadataField<?> startTime = commonEventCollection.getOutputFields().get("startTime"); if (!startTime.isUpdated()) { SimpleDateFormat utcTimeFormat = new SimpleDateFormat(startTime.getPattern().get()); utcTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); String currentTime = utcTimeFormat.format(new DateTime(DateTimeZone.UTC).toDate()); commonEventCollection.removeField(startTime); commonEventCollection.addField(MetadataField.copyMetadataFieldWithValue(startTime, currentTime)); } } } }