/** * 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.caption.impl; import static org.opencastproject.util.MimeType.mimeType; import org.opencastproject.caption.api.Caption; import org.opencastproject.caption.api.CaptionConverter; import org.opencastproject.caption.api.CaptionConverterException; import org.opencastproject.caption.api.CaptionService; import org.opencastproject.caption.api.UnsupportedCaptionFormatException; import org.opencastproject.job.api.AbstractJobProducer; import org.opencastproject.job.api.Job; import org.opencastproject.mediapackage.Catalog; import org.opencastproject.mediapackage.MediaPackageElementBuilder; import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UserDirectoryService; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.util.IoSupport; import org.opencastproject.util.LoadUtil; import org.opencastproject.util.NotFoundException; import org.opencastproject.workspace.api.Workspace; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.Dictionary; import java.util.HashMap; import java.util.List; import javax.activation.FileTypeMap; /** * Implementation of {@link CaptionService}. Uses {@link ComponentContext} to get all registered * {@link CaptionConverter}s. Converters are searched based on <code>caption.format</code> property. If there is no * match for specified input or output format {@link UnsupportedCaptionFormatException} is thrown. * */ public class CaptionServiceImpl extends AbstractJobProducer implements CaptionService, ManagedService { /** * Creates a new caption service. */ public CaptionServiceImpl() { super(JOB_TYPE); } /** Logging utility */ private static final Logger logger = LoggerFactory.getLogger(CaptionServiceImpl.class); /** List of available operations on jobs */ private enum Operation { Convert, ConvertWithLanguage }; /** The collection name */ public static final String COLLECTION = "captions"; /** The load introduced on the system by creating a caption job */ public static final float DEFAULT_CAPTION_JOB_LOAD = 0.1f; /** The key to look for in the service configuration file to override the {@link DEFAULT_CAPTION_JOB_LOAD} */ public static final String CAPTION_JOB_LOAD_KEY = "job.load.caption"; /** The load introduced on the system by creating a caption job */ private float captionJobLoad = DEFAULT_CAPTION_JOB_LOAD; /** Reference to workspace */ protected Workspace workspace; /** Reference to remote service manager */ protected ServiceRegistry serviceRegistry; /** The security service */ protected SecurityService securityService = null; /** The user directory service */ protected UserDirectoryService userDirectoryService = null; /** The organization directory service */ protected OrganizationDirectoryService organizationDirectoryService = null; /** Component context needed for retrieving Converter Engines */ protected ComponentContext componentContext = null; /** * Activate this service implementation via the OSGI service component runtime. * * @param componentContext * the component context */ @Override public void activate(ComponentContext componentContext) { super.activate(componentContext); this.componentContext = componentContext; } /** * {@inheritDoc} * * @see org.opencastproject.caption.api.CaptionService#convert(org.opencastproject.mediapackage.Catalog, * java.lang.String, java.lang.String) */ @Override public Job convert(Catalog input, String inputFormat, String outputFormat) throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException { if (input == null) throw new IllegalArgumentException("Input catalog can't be null"); if (StringUtils.isBlank(inputFormat)) throw new IllegalArgumentException("Input format is null"); if (StringUtils.isBlank(outputFormat)) throw new IllegalArgumentException("Output format is null"); try { return serviceRegistry.createJob(JOB_TYPE, Operation.Convert.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat), captionJobLoad); } catch (ServiceRegistryException e) { throw new CaptionConverterException("Unable to create a job", e); } } /** * {@inheritDoc} * * @see org.opencastproject.caption.api.CaptionService#convert(org.opencastproject.mediapackage.Catalog, * java.lang.String, java.lang.String, java.lang.String) */ @Override public Job convert(Catalog input, String inputFormat, String outputFormat, String language) throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException { if (input == null) throw new IllegalArgumentException("Input catalog can't be null"); if (StringUtils.isBlank(inputFormat)) throw new IllegalArgumentException("Input format is null"); if (StringUtils.isBlank(outputFormat)) throw new IllegalArgumentException("Output format is null"); if (StringUtils.isBlank(language)) throw new IllegalArgumentException("Language format is null"); try { return serviceRegistry.createJob(JOB_TYPE, Operation.ConvertWithLanguage.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat, language), captionJobLoad); } catch (ServiceRegistryException e) { throw new CaptionConverterException("Unable to create a job", e); } } /** * Converts the captions and returns them in a new catalog. * * @return the converted catalog */ protected Catalog convert(Job job, Catalog input, String inputFormat, String outputFormat, String language) throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException { try { // check parameters if (input == null) throw new IllegalArgumentException("Input catalog can't be null"); if (StringUtils.isBlank(inputFormat)) throw new IllegalArgumentException("Input format is null"); if (StringUtils.isBlank(outputFormat)) throw new IllegalArgumentException("Output format is null"); // get input file File captionsFile; try { captionsFile = workspace.get(input.getURI()); } catch (NotFoundException e) { throw new CaptionConverterException("Requested media package element " + input + " could not be found."); } catch (IOException e) { throw new CaptionConverterException("Requested media package element " + input + "could not be accessed."); } logger.debug("Atempting to convert from {} to {}...", inputFormat, outputFormat); List<Caption> collection = null; try { collection = importCaptions(captionsFile, inputFormat, language); logger.debug("Parsing to collection succeeded."); } catch (UnsupportedCaptionFormatException e) { throw new UnsupportedCaptionFormatException(inputFormat); } catch (CaptionConverterException e) { throw e; } URI exported; try { exported = exportCaptions(collection, job.getId() + "." + FilenameUtils.getExtension(captionsFile.getAbsolutePath()), outputFormat, language); logger.debug("Exporting captions succeeding."); } catch (UnsupportedCaptionFormatException e) { throw new UnsupportedCaptionFormatException(outputFormat); } catch (IOException e) { throw new CaptionConverterException("Could not export caption collection.", e); } // create catalog and set properties MediaPackageElementBuilder elementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder(); Catalog catalog = (Catalog) elementBuilder.elementFromURI(exported, Catalog.TYPE, new MediaPackageElementFlavor( "captions", outputFormat)); String[] mimetype = FileTypeMap.getDefaultFileTypeMap().getContentType(exported.getPath()).split("/"); catalog.setMimeType(mimeType(mimetype[0], mimetype[1])); catalog.addTag("lang:" + language); return catalog; } catch (Exception e) { logger.warn("Error converting captions in " + input, e); if (e instanceof CaptionConverterException) { throw (CaptionConverterException) e; } else if (e instanceof UnsupportedCaptionFormatException) { throw (UnsupportedCaptionFormatException) e; } else { throw new CaptionConverterException(e); } } } /** * * {@inheritDoc} * */ @Override public String[] getLanguageList(Catalog input, String format) throws UnsupportedCaptionFormatException, CaptionConverterException { if (format == null) { throw new UnsupportedCaptionFormatException("<null>"); } CaptionConverter converter = getCaptionConverter(format); if (converter == null) { throw new UnsupportedCaptionFormatException(format); } File captions; try { captions = workspace.get(input.getURI()); } catch (NotFoundException e) { throw new CaptionConverterException("Requested media package element " + input + " could not be found."); } catch (IOException e) { throw new CaptionConverterException("Requested media package element " + input + "could not be accessed."); } FileInputStream stream = null; String[] languageList; try { stream = new FileInputStream(captions); languageList = converter.getLanguageList(stream); } catch (FileNotFoundException e) { throw new CaptionConverterException("Requested file " + captions + "could not be found."); } finally { IoSupport.closeQuietly(stream); } return languageList == null ? new String[0] : languageList; } /** * Returns all registered CaptionFormats. */ protected HashMap<String, CaptionConverter> getAvailableCaptionConverters() { HashMap<String, CaptionConverter> captionConverters = new HashMap<String, CaptionConverter>(); ServiceReference[] refs = null; try { refs = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(), null); } catch (InvalidSyntaxException e) { // should not happen since it is called with null argument } if (refs != null) { for (ServiceReference ref : refs) { CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref); String format = (String) ref.getProperty("caption.format"); if (captionConverters.containsKey(format)) { logger.warn("Caption converter with format {} has already been registered. Ignoring second definition.", format); } else { captionConverters.put((String) ref.getProperty("caption.format"), converter); } } } return captionConverters; } /** * Returns specific {@link CaptionConverter}. Registry is searched based on formatName, so in order for * {@link CaptionConverter} to be found, it has to have <code>caption.format</code> property set with * {@link CaptionConverter} format. If none is found, null is returned, if more than one is found then the first * reference is returned. * * @param formatName * name of the caption format * @return {@link CaptionConverter} or null if none is found */ protected CaptionConverter getCaptionConverter(String formatName) { ServiceReference[] ref = null; try { ref = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(), "(caption.format=" + formatName + ")"); } catch (InvalidSyntaxException e) { throw new RuntimeException(e); } if (ref == null) { logger.warn("No caption format available for {}.", formatName); return null; } if (ref.length > 1) logger.warn("Multiple references for caption format {}! Returning first service reference.", formatName); CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref[0]); return converter; } /** * Imports captions using registered converter engine and specified language. * * @param input * file containing captions * @param inputFormat * format of imported captions * @param language * (optional) captions' language * @return {@link List} of parsed captions * @throws UnsupportedCaptionFormatException * if there is no registered engine for given format * @throws IllegalCaptionFormatException * if parser encounters exception */ private List<Caption> importCaptions(File input, String inputFormat, String language) throws UnsupportedCaptionFormatException, CaptionConverterException { // get input format CaptionConverter converter = getCaptionConverter(inputFormat); if (converter == null) { logger.error("No available caption format found for {}.", inputFormat); throw new UnsupportedCaptionFormatException(inputFormat); } FileInputStream fileStream = null; try { fileStream = new FileInputStream(input); List<Caption> collection = converter.importCaption(fileStream, language); return collection; } catch (FileNotFoundException e) { throw new CaptionConverterException("Could not locate file " + input); } finally { IOUtils.closeQuietly(fileStream); } } /** * Exports captions {@link List} to specified format. Extension is added to exported file name. Throws * {@link UnsupportedCaptionFormatException} if format is not supported. * * @param captions * {@link {@link List} to be exported * @param outputName * name under which exported captions will be stored * @param outputFormat * format of exported collection * @param language * (optional) captions' language * @throws UnsupportedCaptionFormatException * if there is no registered engine for given format * @return location of converted captions * @throws IOException * if exception occurs while writing to output stream */ private URI exportCaptions(List<Caption> captions, String outputName, String outputFormat, String language) throws UnsupportedCaptionFormatException, IOException { CaptionConverter converter = getCaptionConverter(outputFormat); if (converter == null) { logger.error("No available caption format found for {}.", outputFormat); throw new UnsupportedCaptionFormatException(outputFormat); } // TODO instead of first writing it all in memory, write it directly to disk ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { converter.exportCaption(outputStream, captions, language); } catch (IOException e) { // since we're writing to memory, this should not happen } ByteArrayInputStream in = new ByteArrayInputStream(outputStream.toByteArray()); return workspace.putInCollection(COLLECTION, outputName + "." + converter.getExtension(), in); } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#process(Job) */ @Override protected String process(Job job) throws Exception { Operation op = null; String operation = job.getOperation(); List<String> arguments = job.getArguments(); try { op = Operation.valueOf(operation); Catalog catalog = (Catalog) MediaPackageElementParser.getFromXml(arguments.get(0)); String inputFormat = arguments.get(1); String outputFormat = arguments.get(2); Catalog resultingCatalog = null; switch (op) { case Convert: resultingCatalog = convert(job, catalog, inputFormat, outputFormat, null); return MediaPackageElementParser.getAsXml(resultingCatalog); case ConvertWithLanguage: String language = arguments.get(3); resultingCatalog = convert(job, catalog, inputFormat, outputFormat, language); return MediaPackageElementParser.getAsXml(resultingCatalog); default: throw new IllegalStateException("Don't know how to handle operation '" + operation + "'"); } } catch (IllegalArgumentException e) { throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e); } catch (IndexOutOfBoundsException e) { throw new ServiceRegistryException("This argument list for operation '" + op + "' does not meet expectations", e); } catch (Exception e) { throw new ServiceRegistryException("Error handling operation '" + op + "'", e); } } /** * Setter for workspace via declarative activation */ protected void setWorkspace(Workspace workspace) { this.workspace = workspace; } /** * Setter for remote service manager via declarative activation */ protected void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } /** * Callback for setting the security service. * * @param securityService * the securityService to set */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** * Callback for setting the user directory service. * * @param userDirectoryService * the userDirectoryService to set */ public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } /** * Sets a reference to the organization directory service. * * @param organizationDirectory * the organization directory */ public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) { this.organizationDirectoryService = organizationDirectory; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService() */ @Override protected SecurityService getSecurityService() { return securityService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService() */ @Override protected OrganizationDirectoryService getOrganizationDirectoryService() { return organizationDirectoryService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService() */ @Override protected UserDirectoryService getUserDirectoryService() { return userDirectoryService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry() */ @Override protected ServiceRegistry getServiceRegistry() { return serviceRegistry; } @Override public void updated(@SuppressWarnings("rawtypes") Dictionary properties) throws ConfigurationException { captionJobLoad = LoadUtil.getConfiguredLoadValue(properties, CAPTION_JOB_LOAD_KEY, DEFAULT_CAPTION_JOB_LOAD, serviceRegistry); } }