/**
* Copyright 2011 Unicon (R) Licensed 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://www.osedu.org/licenses/ECL-2.0
*
* 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 net.unicon.kaltura.service;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import net.unicon.kaltura.MediaItem;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.sakaiproject.nakamura.api.doc.ServiceDocumentation;
import org.sakaiproject.nakamura.api.files.FileUploadHandler;
import org.sakaiproject.nakamura.api.files.FilesConstants;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
import org.sakaiproject.nakamura.api.lite.authorizable.AuthorizableManager;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.api.lite.content.Content;
import org.sakaiproject.nakamura.api.lite.content.ContentManager;
import org.sakaiproject.nakamura.api.user.UserConstants;
import org.sakaiproject.nakamura.lite.content.InternalContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.kaltura.client.KalturaApiException;
import com.kaltura.client.KalturaClient;
import com.kaltura.client.KalturaConfiguration;
import com.kaltura.client.enums.KalturaEditorType;
import com.kaltura.client.enums.KalturaMediaType;
import com.kaltura.client.enums.KalturaSessionType;
import com.kaltura.client.services.KalturaBaseEntryService;
import com.kaltura.client.services.KalturaSessionService;
import com.kaltura.client.types.KalturaBaseEntry;
import com.kaltura.client.types.KalturaBaseEntryFilter;
import com.kaltura.client.types.KalturaBaseEntryListResponse;
import com.kaltura.client.types.KalturaFilterPager;
import com.kaltura.client.types.KalturaMediaEntry;
import com.kaltura.client.types.KalturaMixEntry;
/**
* The Kaltura service which handles all the actual processing related to Kaltura
*
* @author Aaron Zeckoski (azeckoski @ unicon.net) (azeckoski @ vt.edu)
*/
@ServiceDocumentation(
name = "Kaltura Service",
description = "Handles all the processing related to the kaltura media integration"
)
@Component(immediate = true, metatype=true)
@Service({KalturaService.class, FileUploadHandler.class, EventHandler.class})
public class KalturaService implements FileUploadHandler, EventHandler {
private static final Logger LOG = LoggerFactory.getLogger(KalturaService.class);
private static final int MAX_ITEMS_FROM_KALTURA = 1000;
private static final int defaultWidgetWidth = 480;
private static final int defaultWidgetHeight = 360;
public static final String KALTURA_MIMETYPE_VIDEO = "kaltura/video";
public static final String KALTURA_MIMETYPE_AUDIO = "kaltura/audio";
public static final String KALTURA_MIMETYPE_IMAGE = "kaltura/image";
@Property(value="Unicon, Inc.")
static final String SERVICE_VENDOR = "service.vendor";
@Property(value="Handles all the processing related to the kaltura media integration")
static final String SERVICE_DESCRIPTION = "service.description";
@Property(value={ KalturaService.TOPIC_CONTENT_UPDATED })
static final String SERVICE_EVENT_TOPICS = "event.topics";
@Property(intValue=111, label="Partner Id")
private static final String KALTURA_PARTNER_ID = "kaltura.partnerid";
@Property(value="setThisToYourKalturaSecret", label="Secret")
private static final String KALTURA_SECRET = "kaltura.secret";
@Property(value="setThisToYourKalturaAdminSecret", label="Admin Secret")
private static final String KALTURA_ADMIN_SECRET = "kaltura.adminsecret";
@Property(value="http://www.kaltura.com", label="Endpoint")
private static final String KALTURA_ENDPOINT = "kaltura.endpoint";
@Property(value="http://cdn.kaltura.com", label="CDN")
private static final String KALTURA_CDN = "kaltura.cdn";
/* DEFAULT set as confirmed by Kaltura (Nir) on 21 Sept 2010 @ 2300
kaltura.player.image - 2162571
kaltura.player.audio - 2158531
kaltura.player.view - 1522202
kaltura.player.edit - 1522362
kaltura.uploader - 5612211
kaltura.editor - 2733871
*/
//@Property(value="2162571", label="Player - Image")
//private static final String KALTURA_PLAYER_IMAGE = "kaltura.player.image";
@Property(value="2158531", label="Player - Audio")
private static final String KALTURA_PLAYER_AUDIO = "kaltura.player.audio";
@Property(value="1522202", label="Player - Video View")
private static final String KALTURA_PLAYER_VIEW = "kaltura.player.view";
//@Property(value="1522362", label="Player - Video Edit")
//private static final String KALTURA_PLAYER_EDIT = "kaltura.player.edit";
//@Property(value="2733871", label="Player - Editor")
//private static final String KALTURA_PLAYER_EDITOR = "kaltura.player.editor";
//@Property(intValue=KalturaService.defaultWidgetWidth, label="Player - Image - Width")
//private static final String KALTURA_PLAYER_IMAGE_WIDTH = "kaltura.player.image.width";
//@Property(intValue=KalturaService.defaultWidgetHeight, label="Player - Image - Height")
//private static final String KALTURA_PLAYER_IMAGE_HEIGHT = "kaltura.player.image.height";
@Property(intValue=KalturaService.defaultWidgetWidth, label="Player - Audio - Width")
private static final String KALTURA_PLAYER_AUDIO_WIDTH = "kaltura.player.audio.width";
@Property(intValue=30, label="Player - Audio - Height")
private static final String KALTURA_PLAYER_AUDIO_HEIGHT = "kaltura.player.audio.height";
@Property(intValue=KalturaService.defaultWidgetWidth, label="Player - Video - Width")
private static final String KALTURA_PLAYER_VIDEO_WIDTH = "kaltura.player.video.width";
@Property(intValue=KalturaService.defaultWidgetHeight, label="Player - Video - Height")
private static final String KALTURA_PLAYER_VIDEO_HEIGHT = "kaltura.player.video.height";
KalturaConfiguration kalturaConfig;
String kalturaCDN = null;
/*
* The kaltura widget ids from config
*/
String kalturaPlayerIdImage = null;
String kalturaPlayerIdAudio = null;
String kalturaPlayerIdView = null;
String kalturaPlayerIdEdit = null;
String kalturaEditorId = null;
/*
* widgets sizes from config
*/
int kalturaPlayerImageWidth = KalturaService.defaultWidgetWidth;
int kalturaPlayerImageHeight = KalturaService.defaultWidgetHeight;
int kalturaPlayerAudioWidth = KalturaService.defaultWidgetWidth;
int kalturaPlayerAudioHeight = 30;
int kalturaPlayerVideoWidth = KalturaService.defaultWidgetWidth;
int kalturaPlayerVideoHeight = KalturaService.defaultWidgetHeight;
// SERVICES
@Reference
Repository repository;
// OSGI INIT CODE
@Activate
protected void activate(Map<?, ?> properties) {
LOG.info("Kaltura: start");
init(properties);
}
@Deactivate
protected void deactivate(Map<?, ?> properties) {
LOG.info("Kaltura: stop");
}
@Modified
protected void modified(Map<?, ?> properties) {
LOG.info("Kaltura: modified config");
init(properties);
}
/**
* Initialize the configuration based on the OSGi config properties for this service
* @param properties map of config settings
*/
protected void init(Map<?, ?> properties) {
// load up the config
int kalturaPartnerId = getConfigurationSetting(KALTURA_PARTNER_ID, -1, properties);
String kalturaSecret = getConfigurationSetting(KALTURA_SECRET, null, properties);
String kalturaAdminSecret = getConfigurationSetting(KALTURA_ADMIN_SECRET, null, properties);
String kalturaEndpoint = getConfigurationSetting(KALTURA_ENDPOINT, null, properties);
this.kalturaCDN = getConfigurationSetting(KALTURA_CDN, null, properties);
// supports customizing the look and feel AND functionality of the kaltura widgets
//this.kalturaPlayerIdImage = getConfigurationSetting(KALTURA_PLAYER_IMAGE, "2162571", properties);
this.kalturaPlayerIdAudio = getConfigurationSetting(KALTURA_PLAYER_AUDIO, "2158531", properties);
this.kalturaPlayerIdView = getConfigurationSetting(KALTURA_PLAYER_VIEW, "1522202", properties);
//this.kalturaPlayerIdEdit = getConfigurationSetting(KALTURA_PLAYER_EDIT, "1522362", properties);
//this.kalturaEditorId = getConfigurationSetting(KALTURA_PLAYER_EDITOR, "2733871", properties);
// allows for config of the sizes of the players
//this.kalturaPlayerImageWidth = getConfigurationSetting(KALTURA_PLAYER_IMAGE_WIDTH, this.kalturaPlayerImageWidth, properties);
//this.kalturaPlayerImageHeight = getConfigurationSetting(KALTURA_PLAYER_IMAGE_HEIGHT, this.kalturaPlayerImageHeight, properties);
this.kalturaPlayerAudioWidth = getConfigurationSetting(KALTURA_PLAYER_AUDIO_WIDTH, this.kalturaPlayerAudioWidth, properties);
this.kalturaPlayerAudioHeight = getConfigurationSetting(KALTURA_PLAYER_AUDIO_HEIGHT, this.kalturaPlayerAudioHeight, properties);
this.kalturaPlayerVideoWidth = getConfigurationSetting(KALTURA_PLAYER_VIDEO_WIDTH, this.kalturaPlayerVideoWidth, properties);
this.kalturaPlayerVideoHeight = getConfigurationSetting(KALTURA_PLAYER_VIDEO_HEIGHT, this.kalturaPlayerVideoHeight, properties);
MediaItem.setDefaultSizes(
this.kalturaPlayerImageWidth, this.kalturaPlayerImageHeight,
this.kalturaPlayerAudioWidth, this.kalturaPlayerAudioHeight,
this.kalturaPlayerVideoWidth, this.kalturaPlayerVideoHeight
);
// create the shared kaltura config
KalturaConfiguration kc = new KalturaConfiguration();
kc.setPartnerId(kalturaPartnerId);
kc.setSecret(kalturaSecret);
kc.setAdminSecret(kalturaAdminSecret);
kc.setEndpoint(kalturaEndpoint);
this.kalturaConfig = kc;
// dump the config
dumpServiceConfigToLog(properties);
// test out that the kc can initialize a session
KalturaClient kalturaClient = makeKalturaClient("admin", KalturaSessionType.ADMIN, 10);
if (kalturaClient == null || kalturaClient.getSessionId() == null) {
throw new RuntimeException("Failed to connect to kaltura server endpoint ("+kc.getEndpoint()+") as admin");
}
kalturaClient = makeKalturaClient("admin", KalturaSessionType.USER, 10);
if (kalturaClient == null || kalturaClient.getSessionId() == null) {
throw new RuntimeException("Failed to connect to kaltura server endpoint ("+kc.getEndpoint()+") as user");
}
LOG.info("Kaltura: Init complete: API version: "+kalturaClient.getApiVersion()+", Connected to endpoint: "+kc.getEndpoint());
}
/**
* Special logging method
* @param properties
*/
private void dumpServiceConfigToLog(Map<?, ?> properties) {
String propsDump="";
if (properties != null && LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
sb.append("\n Properties:\n");
for (Map.Entry<?, ?> entry : properties.entrySet()) {
sb.append(" * ");
sb.append(entry.getKey());
sb.append(" -> ");
sb.append(entry.getValue());
sb.append("\n");
}
propsDump = sb.toString();
}
LOG.info("\nKalturaService Configuration: START ---------\n"
+" partnerId="+this.kalturaConfig.getPartnerId()+"\n"
+" endPoint="+this.kalturaConfig.getEndpoint()+"\n"
+" timeout="+this.kalturaConfig.getTimeout()+"\n"
+" kalturaCDN="+this.kalturaCDN+"\n"
+" kalturaEditorId="+this.kalturaEditorId+"\n"
+" kalturaPlayerIdView="+this.kalturaPlayerIdView+"\n"
+" kalturaPlayerIdEdit="+this.kalturaPlayerIdEdit+"\n"
+" kalturaPlayerIdAudio="+this.kalturaPlayerIdAudio+"\n"
+" kalturaPlayerIdImage="+this.kalturaPlayerIdImage+"\n"
+propsDump
+"KalturaService Configuration: END ---------\n");
}
/**
* Special logging method
* @param properties
* @param name
*/
protected void dumpMapToLog(Map<?, ?> properties, String name) {
String propsDump="";
if (properties != null) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<?, ?> entry : properties.entrySet()) {
sb.append(" * ");
sb.append(entry.getKey());
sb.append(" -> ");
sb.append(entry.getValue());
sb.append("\n");
}
propsDump = sb.toString();
}
LOG.info("\nMap ("+name+"): START ---------\n"
+propsDump
+"Map ("+name+"): END ---------\n");
}
/**
* Special method for handling retrieval of OAE config settings in a typesafe way
* @param <T>
* @param settingName the key for the setting
* @param defaultValue the default value if unset
* @param properties the set of properties to search
* @return the value of the setting (if set) or default value if not
*/
@SuppressWarnings("unchecked")
private <T> T getConfigurationSetting(String settingName, T defaultValue, Map<?,?> properties) {
T returnValue = defaultValue;
Object propValue = properties.get(settingName);
if (defaultValue == null) {
returnValue = (T) PropertiesUtil.toString(propValue, null);
if ("".equals(returnValue)) {
returnValue = null;
}
} else {
if (defaultValue instanceof Number) {
int num = ((Number) defaultValue).intValue();
int value = PropertiesUtil.toInteger(propValue, num);
returnValue = (T) Integer.valueOf(value);
} else if (defaultValue instanceof Boolean) {
boolean bool = ((Boolean) defaultValue).booleanValue();
boolean value = PropertiesUtil.toBoolean(propValue, bool);
returnValue = (T) Boolean.valueOf(value);
} else if (defaultValue instanceof String) {
returnValue = (T) PropertiesUtil.toString(propValue, (String) defaultValue);
}
}
return returnValue;
}
protected static final String TOPIC_CONTENT_UPDATED = "org/sakaiproject/nakamura/lite/content/UPDATED";
private static final String TOPIC_PROPERTY_POOLID = "path";
/* (non-Javadoc)
* @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
*/
public void handleEvent(Event event) {
/* NOTES: Q&A from AZ to Carl and Mark
> 1) What's the constant for
> "org/sakaiproject/nakamura/lite/content/UPDATED"? grep did not find it
> so I am guessing it is probably constructed.
Yep, other spots in the code seem to just hard-code the strings. I'd
normally look in org.sakaiproject.nakamura.api.lite.StoreListener for
such a constant, but I already did and there isn't one :)
> 2) Is that going to be the "topic" for the event? If so, where do I
> get the poolId/path for the content item once I have the event? If
> not, what is that matching?
Yep, org/sakaiproject/nakamura/lite/content/UPDATED is the topic. You
can get the pathId with:
event.getProperty("path")
(again, most places in the code just seem to use "path" instead of a
constant...). You can look at the events getting fired by hitting:
http://localhost:8080/system/console/events
and the ones you're interested in will just have an unadorned path like
"h1o6Hi3ie". In mine I see other events with the same topic but paths
like "/activity/content/h1o6Hi3ie" too, but those aren't relevant of
interest here and can be ignored.
So yeah, it's all a bit hairy :) If that all makes sense I'll try to
amend the doco for FileUploadHandler to emphasise that getting notified
about uploaded files is only half the story.
NOTES:
- this is called 16 times on a new item upload and about 12 times for content updates and 6 times for update version file uploads,
as a result we cannot just update kaltura each time this is called or it will crush the kaltura servers
- I attempted to compare the original and current properties to see if I could identify when they changed but this did not work
as the results in the logs show:
... realUpdate=false, (az-test.mov)=(az-test.mov),(null)=(null)
... realUpdate=false, (aaaaaaaaaa)=(aaaaaaaaaa),(bbbbbb)=(bbbbbb)
- Attempting to use the 'update' property to filter down the number of events - "update".equals(event.getProperty("op"),
this only gets it down to 3 events so still too many to be reasonable
- added in a filter to check if the _versionHistoryId is present, this seems to finally get it down to only 2 updates
*/
boolean updateEvent = "update".equals(event.getProperty("op"));
String poolId = (String) event.getProperty(TOPIC_PROPERTY_POOLID);
if (poolId != null && updateEvent) {
Content content = getContent(poolId);
if (content != null) {
// check for the key
String kalturaEntryId = (String) content.getProperties().get(OAE_CONTENT_NEW_FLAG);
String versionHistoryId = (String) content.getProperties().get(InternalContent.VERSION_HISTORY_ID_FIELD);
boolean realUpdate = versionHistoryId != null && content.getOriginalProperties().containsKey(OAE_CONTENT_NEW_FLAG);
if (kalturaEntryId != null && realUpdate) {
/*
* If it gets to this point it means 3 things are true:
* (1) This is a kaltura content item which has not be updated to kaltura server yet
* (2) This event type operation is an update
* (3) The content item has the version history id set
*/
//dumpMapToLog(content.getProperties(), "contentProperties - "+kalturaEntryId);
LOG.info("Found kaltura content item ("+poolId+") to update during OAE content update with keid ("+kalturaEntryId+")...");
// make the kaltura entry to update it
KalturaBaseEntry kbe = new KalturaBaseEntry();
kbe.id = kalturaEntryId;
int version = getContentVersion(poolId);
kbe.name = makeKalturaTitle(content.getProperties(), version);
kbe.description = (String)content.getProperties().get(FilesConstants.SAKAI_DESCRIPTION); // may be blank
kbe.tags = makeKalturaTags(content.getProperties());
updateKalturaItem(null, kbe);
// remove the flag and update the kaltura updated timestamp
Map<String, Object> props = new HashMap<String, Object>(2);
props.put(OAE_CONTENT_NEW_FLAG, null);
props.put("kaltura-updated", new Date().getTime());
updateContent(poolId, props); // exception if update fails
//dumpMapToLog(newProps, "updatedContentProperties");
LOG.info("Updated OAE content item ("+poolId+") and synced Kaltura item ("+kalturaEntryId+") data");
}
}
}
}
// OAE FILE UPLOAD HANDLER
private static final String OAE_CONTENT_NEW_FLAG = "kaltura-content-new";
private static final String OAE_CONTENT_EXTENSION = "sakai:fileextension";
/*
* NOTE: requires https://github.com/marktriggs/nakamura/tree/fileuploadhandlers for now
*
* Handling requires some SPECIAL work here because of weaknesses in OAE:
* 1) When user uploads a new file, we process the call to handle method and
* put in fake meta-info and then put a marker in the content properties
* 2) When next post comes in a few ms later, the event processor method is called
* which will tell us that the content has been updated, we
* check for the marker and if it is there then we have to do a second call
* over to kaltura to update the 3 values for title, desc, and tags, then
* we clear the marker
* 3) When the user later on updates the content, we have to ignore those
* updates which would trigger calls to that interface as long as the
* marker is not present
* 4) When the user uploads a new version, we have to process that upload via
* the handle method and also update the metadata in one operation
* because in that case the metadata is correct
*
* (non-Javadoc)
* @see org.sakaiproject.nakamura.api.files.FileUploadHandler#handleFile(java.lang.String, java.io.InputStream, java.lang.String, boolean)
*/
public void handleFile(Map<String, Object> arg0, String poolId, InputStream inputStream, String userId, boolean isNew) throws IOException {
// TODO - what is arg0?
Map<String, Object> contentProperties = getContentProperties(poolId);
//dumpMapToLog(contentProperties, "contentProperties");
// check if this is a video file and do nothing if it is not
String mimeType = (String)contentProperties.get(InternalContent.MIMETYPE_FIELD);
String fileExtension = (String)contentProperties.get(OAE_CONTENT_EXTENSION);
String path = (String)contentProperties.get(InternalContent.PATH_FIELD);
String fileName = path+fileExtension;
// NOTE: no handling for images yet
KalturaMediaType mediaType = KalturaMediaType.VIDEO;
boolean isVideo = isFileVideo(fileExtension, mimeType);
boolean isAudio = false;
if (!isVideo) {
isAudio = isFileAudio(fileExtension, mimeType);
if (isAudio) {
mediaType = KalturaMediaType.AUDIO;
}
}
if ( userId != null && UserConstants.ANON_USERID.equals(userId)) {
// only include real users, no anonymous ones
LOG.warn("Anonymous user uploaded a file - it is not being processed into Kaltura: "+fileName);
} else if (!isVideo && !isAudio) {
if (!isAudio) {
LOG.debug("Uploaded file is not an audio file, no processing for Kaltura: "+fileName);
} else {
LOG.debug("Uploaded file is not a video, no processing for Kaltura: "+fileName);
}
} else {
//String fileId = (String)contentProperties.get(InternalContent.UUID_FIELD);
int version = 1;
if (isNew) {
// do something different when this is new
} else {
// do things when this is an update to an existing content item
version = getContentVersion(poolId); // exception if lookup fails
}
String title = makeKalturaTitle(contentProperties, version);
String desc = (String)contentProperties.get(FilesConstants.SAKAI_DESCRIPTION); // may be blank
String tags = makeKalturaTags(contentProperties);
// do processing of the video file
long fileSize = (Long) contentProperties.get(InternalContent.LENGTH_FIELD);
KalturaBaseEntry kbe = uploadItem(userId, fileName, fileSize, inputStream, mediaType, title, desc, tags); // exception if upload fails
if (kbe != null) {
// item upload successful
MediaItem mediaItem = new MediaItem(kbe, userId);
Map<String, Object> props = new HashMap<String, Object>(10);
if (isNew) {
// if this is newly uploaded content then we have to do special handling, store the kaltura entry ID here
props.put(OAE_CONTENT_NEW_FLAG, mediaItem.getKalturaId());
}
props.put("kaltura-updated", new Date().getTime());
props.put("kaltura-id", mediaItem.getKalturaId());
props.put("kaltura-thumbnail", mediaItem.getThumbnail());
props.put("kaltura-download", mediaItem.getDownloadURL());
props.put("kaltura-duration", mediaItem.getDuration()); // probably will be 0
props.put("kaltura-height", mediaItem.getHeight());
props.put("kaltura-width", mediaItem.getWidth());
props.put("kaltura-type", mediaItem.getType());
String kalturaMimeType = KALTURA_MIMETYPE_VIDEO;
if (MediaItem.TYPE_AUDIO.equals(mediaItem.getMediaType())) {
kalturaMimeType = KALTURA_MIMETYPE_AUDIO;
} else if (MediaItem.TYPE_IMAGE.equals(mediaItem.getMediaType())) {
kalturaMimeType = KALTURA_MIMETYPE_IMAGE;
}
props.put(InternalContent.MIMETYPE_FIELD, kalturaMimeType);
LOG.info("Completed upload ("+title+") to Kaltura of file ("+fileName+") of type ("+kalturaMimeType+") and created kalturaEntry ("+mediaItem.getKalturaId()+")");
updateContent(poolId, props); // exception if update fails
// Map<String, Object> newProps = ...
//dumpMapToLog(newProps, "newContentProperties");
} else {
// should we fail here if kaltura does not return a valid KBE? -AZ
}
LOG.info("Kaltura file upload handler complete: "+fileName);
}
}
/**
* Make a title to be sent to Kaltura
* @param contentProperties OAE content properties
* @param version the content version (greater than or equal to 1)
* @return the title to send to kaltura
*/
protected String makeKalturaTitle(Map<String, Object> contentProperties, int version) {
String title = "title";
if (contentProperties.get(FilesConstants.POOLED_CONTENT_FILENAME) != null) {
title = (String) contentProperties.get(FilesConstants.POOLED_CONTENT_FILENAME);
}
if (version < 1) {
version = 1;
}
title += " - "+version;
return title;
}
/**
* Make the tags to be send to kaltura based on OAE content
* @param contentProperties OAE content properties
* @return the tags comma separated string (empty string if there are none)
*/
protected String makeKalturaTags(Map<String, Object> contentProperties) {
String tags = "";
if (contentProperties.get(FilesConstants.SAKAI_TAGS) != null) {
// convert tags array into CSV string
String[] fileTags = (String[]) contentProperties.get(FilesConstants.SAKAI_TAGS);
if (fileTags.length > 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fileTags.length; i++) {
String tag = fileTags[i];
if (i > 0) {
sb.append(",");
}
sb.append(tag);
}
tags = sb.toString();
}
}
return tags;
}
// OAE processing methods
/**
* Find the current version number (same as the number of versions) for this content item
* @param poolId the content pool id
* @return the current version number (defaults to 1)
*/
private int getContentVersion(String poolId) {
// NOTE: InternalContent.VERSION_NUMBER_FIELD is not useful
int version = 1;
try {
Session adminSession = repository.loginAdministrative();
ContentManager cm = adminSession.getContentManager();
// Content content = cm.getVersion(poolId, fileId);
List<String> versions = cm.getVersionHistory(poolId);
version = versions.size();
adminSession.logout();
} catch (Exception e) {
LOG.error("Unable to get versions for pool="+poolId+", defaulting to "+version+": "+e, e);
}
return version;
}
/**
* Retrieve an OAE content item
* @param poolId the unique path/poolId of a content object
* @return the Content object
* @throws RuntimeException if the content object cannot be retrieved
*/
private Content getContent(String poolId) {
Content content = null;
try {
Session adminSession = repository.loginAdministrative();
ContentManager cm = adminSession.getContentManager();
content = cm.get(poolId);
adminSession.logout();
} catch (Exception e) {
LOG.error("Unable to get content by path="+poolId+": "+e, e);
throw new RuntimeException("Unable to get content by path="+poolId+": "+e, e);
}
if (content == null) {
throw new RuntimeException("Unable to get content by path="+poolId+": item not found");
}
return content;
}
/**
* Retrieve the properties for some OAE content
* @param poolId the unique path/poolId of a content object
* @return the Map of content properties
* @throws RuntimeException if the content object cannot be retrieved
*/
private Map<String, Object> getContentProperties(String poolId) {
Content content = getContent(poolId);
return content.getProperties();
}
/**
* Update an OAE content item
* @param poolId the unique path/poolId of a content object
* @param properties the properties to update or delete on this object (props with a NULL value will be removed, all others will be replaced or added)
* @return the complete set of new properties for the content
* @throws RuntimeException if the content object cannot be updated
*/
private Map<String, Object> updateContent(String poolId, Map<?, ?> properties) {
Map<String, Object> props = null;
Content contentItem = getContent(poolId);
//dumpMapToLog(properties, "NEW-properties");
for (Entry<?, ?> entry : properties.entrySet()) {
String key = (String) entry.getKey();
Object val = entry.getValue();
if (val != null) {
contentItem.setProperty(key, val);
} else {
contentItem.removeProperty(key);
}
}
try {
Session adminSession = repository.loginAdministrative();
ContentManager contentManager = adminSession.getContentManager();
contentManager.update(contentItem);
Content content = contentManager.get(poolId);
props = content.getProperties();
adminSession.logout();
LOG.debug("Completed update of content item props ("+poolId+") for Kaltura upload");
} catch (Exception e) {
LOG.error("Unable to update content at path="+poolId+": "+e, e);
throw new RuntimeException("Unable to update content at path="+poolId+": "+e, e);
}
return props;
}
/**
* Determine if a file has video content
* @param fileExtension the file extension (includes the ., e.g. .mov)
* @param mimeType the mimetype from the UI
* @return true if video, false otherwise
*/
protected boolean isFileVideo(String fileExtension, String mimeType) {
boolean video = false;
if (mimeType != null
&& (KALTURA_MIMETYPE_VIDEO.equals(mimeType) || mimeType.startsWith("video/")) ) {
video = true;
} else {
if (fileExtension != null) {
if (fileExtension.equals(".avi") // avi
|| fileExtension.equals(".mpg") // mpeg 2
|| fileExtension.equals(".mpe") // mpeg 2
|| fileExtension.equals(".mpeg") // mpeg 2
|| fileExtension.equals(".mp4") // mpeg 4
|| fileExtension.equals(".m4v") // mpeg 4
|| fileExtension.equals(".mov") // quicktime
|| fileExtension.equals(".qt") // quicktime
|| fileExtension.equals(".asf") // windows media
|| fileExtension.equals(".asx") // windows media
|| fileExtension.equals(".wmv") // windows media
|| fileExtension.equals(".rm") // real video
|| fileExtension.equals(".ogm") // OG media
|| fileExtension.equals(".3gp") // 3gpp
|| fileExtension.equals(".mkv") // matroska
) {
video = true;
}
}
}
return video;
}
/**
* Determine if a file has audio content
* @param fileExtension the file extension (includes the ., e.g. .mov)
* @param mimeType the mimetype from the UI
* @return true if audio, false otherwise
*/
protected boolean isFileAudio(String fileExtension, String mimeType) {
boolean audio = false;
if (mimeType != null
&& (KALTURA_MIMETYPE_AUDIO.equals(mimeType) || mimeType.startsWith("audio/")) ) {
audio = true;
} else {
if (fileExtension != null) {
if (fileExtension.equals(".wav") // wave audio
|| fileExtension.equals(".aif") // aiff
|| fileExtension.equals(".mp3") // mpeg 3
|| fileExtension.equals(".aac") // aac
|| fileExtension.equals(".mid") // midi
|| fileExtension.equals(".mpa") // mpeg 2 audio
|| fileExtension.equals(".wma") // windows media audio
|| fileExtension.equals(".ra") // realaudio
) {
audio = true;
}
}
}
return audio;
}
/**
* Get a user and their data based on the user identifier
* @param userId user id (username)
* @return the User object OR null if not found
*/
protected User getUser(String userId) {
User u = null;
Session adminSession = null;
try {
adminSession = repository.loginAdministrative();
AuthorizableManager authorizableManager = adminSession.getAuthorizableManager();
Authorizable authorizable = authorizableManager.findAuthorizable(userId);
u = (User) authorizable;
adminSession.logout();
} catch (StorageClientException e) {
// nothing to do here
} catch (AccessDeniedException e) {
// nothing to do here
} finally {
if ( adminSession != null ) {
try {
adminSession.logout();
} catch (ClientPoolException e) {
LOG.warn(e.getMessage(), e);
}
}
}
return u;
}
// KALTURA CLIENT
/*
* NOTE: the KalturaClient is not even close to being threadsafe -AZ
*/
ThreadLocal<KalturaClient> kctl = new ThreadLocal<KalturaClient>() {
@Override
protected KalturaClient initialValue() {
return makeKalturaClient();
};
};
/**
* threadsafe method to get a kaltura client
* @return the current kaltura client for this thread
*/
public KalturaClient getKalturaClient() {
return kctl.get();
}
/**
* threadsafe method to get a kaltura client
* @param userKey the user key (normally should be the username)
* @return the current kaltura client for this thread
*/
public KalturaClient getKalturaClient(String userKey) {
if (userKey != null && !"".equals(userKey)) {
KalturaClient kc = makeKalturaClient(userKey, KalturaSessionType.ADMIN, 0);
kctl.set(kc);
}
return kctl.get();
}
/**
* destroys the current kaltura client
*/
public void clearKalturaClient() {
kctl.remove();
}
/**
* NOTE: this method will generate a new kaltura client using all defaults and sakai user,
* make sure you store this into the {@link #kctl} threadlocal if you are generating it using this method
*/
private KalturaClient makeKalturaClient() {
// defaults
String userKey = "anonymous";
KalturaSessionType sessionType = KalturaSessionType.USER;
// NOTE: there is no way to get the user outside of a request in OAE
KalturaClient kc = makeKalturaClient(userKey, sessionType, 0);
return kc;
}
/**
* NOTE: this method will generate a new kaltura client,
* make sure you store this into the {@link #kctl} threadlocal if you are generating it using this method
*/
private KalturaClient makeKalturaClient(String userKey, KalturaSessionType sessionType, int timeoutSecs) {
// client is not threadsafe
if (timeoutSecs <= 0) {
timeoutSecs = 86400; // NOTE set to 24 hours by request of kaltura 60; // default to 60 seconds
}
KalturaClient kalturaClient = new KalturaClient(this.kalturaConfig);
String secret = this.kalturaConfig.getSecret();
if (KalturaSessionType.ADMIN.equals(sessionType)) {
secret = this.kalturaConfig.getAdminSecret();
}
KalturaSessionService sessionService = kalturaClient.getSessionService();
try {
String sessionId = sessionService.start(secret, userKey, sessionType,
this.kalturaConfig.getPartnerId(), timeoutSecs, "edit:*"); // the edit is needed to fix an issue with kaltura servers
kalturaClient.setSessionId(sessionId);
LOG.debug("Created new kaltura client (oid="+kalturaClient.toString()+", tid="+Thread.currentThread().getId()+", ks="+kalturaClient.getSessionId()+")");
} catch (KalturaApiException e) {
//kalturaClient.setSessionId(null); // should we clear this?
LOG.error("Unable to establish a kaltura session ("+kalturaClient.toString()+", "+kalturaClient.getSessionId()+"):: " + e, e);
}
return kalturaClient;
}
// KALTURA METHODS
public KalturaBaseEntry uploadItem(String userId, String fileName, long fileSize, InputStream inputStream,
KalturaMediaType mediaType, String title, String description, String tags) {
if (title == null || "".equals(title)) {
title = fileName;
}
if (mediaType == null) {
mediaType = KalturaMediaType.VIDEO;
}
KalturaMediaEntry kme = null;
KalturaClient kc = getKalturaClient(userId); // force this to be an admin key
if (kc != null) {
try {
String uploadTokenId = kc.getMediaService().upload(inputStream, fileName, fileSize);
//LOG.info("upload token result: "+uploadTokenId);
KalturaMediaEntry mediaEntry = new KalturaMediaEntry();
mediaEntry.mediaType = KalturaMediaType.VIDEO;
mediaEntry.userId = userId;
mediaEntry.name = title;
if (description != null) {
mediaEntry.description = description;
}
if (tags != null) {
mediaEntry.tags = tags;
}
mediaEntry.adminTags = "OAE"; // Should we handle with custom meta fields instead (for 9 July 2011, we will not)?
kme = kc.getMediaService().addFromUploadedFile(mediaEntry, uploadTokenId);
//kme = kc.getBaseEntryService().update(entryId, mediaEntry); // NOTE: updateKalturaItem()
} catch (Exception e) {
LOG.error("Failure uploading item ("+fileName+"): "+e, e);
throw new RuntimeException(e);
}
}
return kme;
}
/**
* @param textFilter a search filter string, null or "" includes all
* @param keids [OPTIONAL] listing of keids to limit the results to
* @param start 0 for all, or >0 start with that item
* @param max 0 for all, or >0 to only return that many
* @return the List of kaltura entries
*/
public List<KalturaBaseEntry> getKalturaItems(String userKey, String textFilter, String[] keids, int start, int max) {
if (start < 0) {
start = 0;
}
if (max <= 0) {
max = MAX_ITEMS_FROM_KALTURA;
}
List<KalturaBaseEntry> items = new ArrayList<KalturaBaseEntry>();
if (textFilter == null) {
textFilter = "";
}
KalturaClient kc = getKalturaClient();
if (kc != null) {
try {
// use base entry service instead to get all -AZ
//KalturaBaseEntry kbe = entryService.get("qqqq");
KalturaBaseEntryService entryService = kc.getBaseEntryService();
KalturaBaseEntryFilter filter = new KalturaBaseEntryFilter();
filter.partnerIdEqual = this.kalturaConfig.getPartnerId();
filter.userIdEqual = userKey;
if (StringUtils.isNotBlank(textFilter)) {
filter.searchTextMatchOr = textFilter; // I think this is what I need but it does not seem to prioritize results?
//filter.nameLike = textFilter;
}
filter.statusIn = "0,1,2"; // KalturaEntryStatus.IMPORT+","+KalturaEntryStatus.PRECONVERT+","+KalturaEntryStatus.READY;
// limit to a set of items as needed
if (keids != null) {
filter.idIn = StringUtils.join(keids, ',');
}
//kmef.orderBy = "title";
KalturaFilterPager pager = new KalturaFilterPager();
pager.pageSize = max;
pager.pageIndex = 0; // NOTE - kaltura does not support a start item in the paging API, only a start page
KalturaBaseEntryListResponse listResponse = entryService.list(filter, pager);
for (KalturaBaseEntry entry : listResponse.objects) {
items.add(entry); // KalturaMediaEntry KalturaMixEntry
}
} catch (KalturaApiException e) {
LOG.error("Unable to get kaltura media items listing using session (oid="+kc.toString()+", tid="+Thread.currentThread().getId()+", ks="+kc.getSessionId()+"):: " + e, e);
}
}
return items;
}
/**
* Retrieve a single KME by the kaltura id
* @param keid the kaltura entry id
* @return the entry OR null if none found
*/
public KalturaBaseEntry getKalturaItem(String userKey, String keid) {
if (keid == null) {
throw new IllegalArgumentException("keid must not be null");
}
KalturaBaseEntry kme = null;
KalturaClient kc = getKalturaClient();
if (kc != null) {
try {
//KalturaMediaService mediaService = kc.getMediaService();
KalturaBaseEntryService entryService = kc.getBaseEntryService();
kme = getKalturaEntry(userKey, keid, entryService);
} catch (KalturaApiException e) {
LOG.error("Unable to get kaltura media item ("+keid+") using session (oid="+kc.toString()+", tid="+Thread.currentThread().getId()+", ks="+kc.getSessionId()+"):: " + e, e);
}
}
return kme;
}
public boolean removeKalturaItem(String userKey, String keid) {
if (keid == null) {
throw new IllegalArgumentException("keid must not be null");
}
boolean removed = false;
KalturaClient kc = getKalturaClient();
if (kc != null) {
try {
KalturaBaseEntryService entryService = kc.getBaseEntryService();
KalturaBaseEntry entry = getKalturaEntry(userKey, keid, entryService);
entryService.delete(entry.id);
removed = true;
} catch (KalturaApiException e) {
LOG.error("Unable to remove kaltura item ("+keid+") using session (oid="+kc.toString()+", tid="+Thread.currentThread().getId()+", ks="+kc.getSessionId()+"):: " + e, e);
removed = false;
}
}
return removed;
}
/**
* Creates a new kaltura mix for the current user/kaltura session from an existing kaltura entry
* @param keid the id of the entry to create this mix from
* @param name OPTIONAL the name for this new mix, null to use the entry name
* @return the new mix item
* @throws IllegalStateException if the mix cannot be created
*/
public KalturaMixEntry createMix(String userKey, String keid, String name) {
if (keid == null) {
throw new IllegalArgumentException("keid must not be null");
}
KalturaMixEntry kmix = null;
KalturaClient kc = getKalturaClient();
if (kc != null) {
try {
KalturaBaseEntry kme = getKalturaItem(userKey, keid);
if (kme == null) {
throw new IllegalArgumentException("Invalid keid ("+keid+"), cannot find entry");
}
KalturaMixEntry mix = new KalturaMixEntry();
mix.name = name != null ? name : kme.name;
mix.editorType = KalturaEditorType.ADVANCED;
kmix = kc.getMixingService().add(mix);
// append existing entry to this mix
kc.getMixingService().appendMediaEntry(kmix.id, kme.id);
// flattening is async, no way to tell if a mix has been flattened?
//kc.getMixingService().requestFlattening(entryId, fileFormat);s
} catch (KalturaApiException e) {
throw new IllegalStateException("Unable to create new mix ("+name+") using session (oid="+kc.toString()+", tid="+Thread.currentThread().getId()+", ks="+kc.getSessionId()+"):: " + e, e);
}
}
return kmix;
}
public KalturaBaseEntry updateKalturaItem(String userKey, KalturaBaseEntry kalturaEntry) {
if (kalturaEntry == null) {
throw new IllegalArgumentException("entry must not be null");
}
String keid = kalturaEntry.id;
if (keid == null) {
throw new IllegalArgumentException("entry keid must not be null");
}
KalturaBaseEntry kbe = null;
KalturaClient kc = getKalturaClient();
if (kc != null) {
try {
KalturaBaseEntryService entryService = kc.getBaseEntryService();
kbe = getKalturaEntry(userKey, keid, entryService);
if (kbe == null) {
throw new IllegalArgumentException("Cannot find KME to update using id ("+keid+")");
}
// integrate the fields we allow to be changed
KalturaBaseEntry fields = new KalturaBaseEntry();
//fields.creditUrl = entry.creditUrl;
//fields.creditUserName = entry.creditUserName;
fields.description = kalturaEntry.description;
fields.name = kalturaEntry.name;
fields.tags = kalturaEntry.tags;
// now update the KME
kbe = entryService.update(keid, fields);
} catch (KalturaApiException e) {
String msg = "Unable to update kaltura media item ("+keid+") using session (oid="+kc.toString()+", tid="+Thread.currentThread().getId()+", ks="+kc.getSessionId()+"):: " + e;
LOG.error(msg, e);
throw new RuntimeException(msg, e);
}
}
return kbe;
}
/**
* Get the KME with a permissions check to make sure the user key matches
* @param keid the kaltura entry id
* @param entryService the katura entry service
* @return the entry
* @throws KalturaApiException if kaltura cannot be accessed
* @throws IllegalArgumentException if the keid cannot be found for this user
*/
private KalturaBaseEntry getKalturaEntry(String userKey, String keid, KalturaBaseEntryService entryService) throws KalturaApiException {
// DO NOT CACHE THIS ONE
KalturaBaseEntry entry = null;
// Cannot use the KMEF because it cannot filter by id correctly -AZ
/*
KalturaBaseEntryFilter kmef = new KalturaBaseEntryFilter();
kmef.partnerIdEqual = this.kalturaConfig.getPartnerId();
kmef.userIdEqual = currentUserName;
kmef.idEqual = keid;
//kmef.orderBy = "title";
KalturaMediaListResponse listResponse = mediaService.list(kmef);
if (listResponse != null && ! listResponse.objects.isEmpty()) {
kme = listResponse.objects.get(0); // just get the first one
}
*/
// have to use - mediaService.get(keid); despite it not even checking if we have access to this - AZ
entry = entryService.get(keid);
if (entry == null) {
// did not find the item by keid so we die
throw new IllegalArgumentException("Cannot find kaltura item ("+keid+") with for user ("+userKey+")");
}
// also do a manual check for security, not so sure about this check though -AZ
if (entry.partnerId != this.kalturaConfig.getPartnerId()) {
throw new SecurityException("KME partnerId ("+entry.partnerId+") does not match current one ("+this.kalturaConfig.getPartnerId()+"), cannot access this KME ("+keid+")");
}
return entry;
}
}