/** * 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.composer.impl; import static org.opencastproject.util.ReadinessIndicator.ARTIFACT; import org.opencastproject.composer.api.EncodingProfile; import org.opencastproject.composer.api.EncodingProfile.MediaType; import org.opencastproject.composer.api.EncodingProfileImpl; import org.opencastproject.util.ConfigurationException; import org.opencastproject.util.MimeType; import org.opencastproject.util.MimeTypes; import org.opencastproject.util.ReadinessIndicator; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.felix.fileinstall.ArtifactInstaller; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; /** * This manager class tries to read encoding profiles from the classpath. */ public class EncodingProfileScanner implements ArtifactInstaller { /** Prefix for encoding profile property keys **/ private static final String PROP_PREFIX = "profile."; /* Property names */ private static final String PROP_NAME = ".name"; private static final String PROP_APPLICABLE = ".input"; private static final String PROP_OUTPUT = ".output"; private static final String PROP_SUFFIX = ".suffix"; private static final String PROP_MIMETYPE = ".mimetype"; private static final String PROP_JOBLOAD = ".jobload"; /** OSGi bundle context */ private BundleContext bundleCtx = null; /** Sum of profiles files currently installed */ private int sumInstalledFiles = 0; /** The profiles map */ private Map<String, EncodingProfile> profiles = new HashMap<String, EncodingProfile>(); /** The logging instance */ private static final Logger logger = LoggerFactory.getLogger(EncodingProfileScanner.class); /** * Returns the list of profiles. * * @return the profile definitions */ public Map<String, EncodingProfile> getProfiles() { return profiles; } /** * OSGi callback on component activation. * * @param ctx * the bundle context */ void activate(BundleContext ctx) { this.bundleCtx = ctx; } /** * Returns the encoding profile for the given identifier or <code>null</code> if no such profile has been configured. * * @param id * the profile identifier * @return the profile */ public EncodingProfile getProfile(String id) { return profiles.get(id); } /** * Returns the list of profiles that are applicable for the given track type. * * @return the profile definitions */ public Map<String, EncodingProfile> getApplicableProfiles(MediaType type) { Map<String, EncodingProfile> result = new HashMap<String, EncodingProfile>(); for (Map.Entry<String, EncodingProfile> entry : profiles.entrySet()) { EncodingProfile profile = entry.getValue(); if (profile.isApplicableTo(type)) { result.put(entry.getKey(), profile); } } return result; } /** * Reads the profiles from the given set of properties. * * @param artifact * the properties file * @return the profiles found in the properties */ Map<String, EncodingProfile> loadFromProperties(File artifact) throws IOException { // Format name FileInputStream in = null; Properties properties = new Properties(); try { in = new FileInputStream(artifact); properties.load(in); } finally { IOUtils.closeQuietly(in); } // Find list of formats in properties List<String> profileNames = new ArrayList<String>(); for (Object fullKey : properties.keySet()) { String key = fullKey.toString(); if (key.startsWith(PROP_PREFIX) && key.endsWith(PROP_NAME)) { int separatorLocation = fullKey.toString().lastIndexOf('.'); key = key.substring(PROP_PREFIX.length(), separatorLocation); if (!profileNames.contains(key)) { profileNames.add(key); } else { throw new ConfigurationException("Found duplicate definition for encoding profile '" + key + "'"); } } } // Load the formats Map<String, EncodingProfile> profiles = new HashMap<String, EncodingProfile>(); for (String profileId : profileNames) { logger.debug("Enabling media format " + profileId); EncodingProfile profile = loadProfile(profileId, properties, artifact); profiles.put(profileId, profile); } return profiles; } /** * Reads the profile from the given properties * * @param profile * @param properties * @param artifact * @return the loaded profile or null if profile * @throws RuntimeException */ private EncodingProfile loadProfile(String profile, Properties properties, File artifact) throws ConfigurationException { String identifier = profile; List<String> defaultProperties = new ArrayList<String>(10); String name = getDefaultProperty(profile, PROP_NAME, properties, defaultProperties); if (name == null || "".equals(name)) throw new ConfigurationException("Distribution profile '" + profile + "' is missing a name (" + PROP_NAME + "). (Check web.xml profiles.)"); EncodingProfileImpl df = new EncodingProfileImpl(identifier, name, artifact); // Output Type String type = getDefaultProperty(profile, PROP_OUTPUT, properties, defaultProperties); if (StringUtils.isBlank(type)) throw new ConfigurationException("Output type (" + PROP_OUTPUT + ") of profile '" + profile + "' is missing"); try { df.setOutputType(MediaType.parseString(StringUtils.trimToEmpty(type))); } catch (IllegalArgumentException e) { throw new ConfigurationException("Output type (" + PROP_OUTPUT + ") '" + type + "' of profile '" + profile + "' is unknwon"); } //Suffixes with tags? List<String> tags = getTags(profile, properties, defaultProperties); if (tags.size() > 0) { for (String tag : tags) { String prop = PROP_SUFFIX + "." + tag; String suffixObj = getDefaultProperty(profile, prop, properties, defaultProperties); df.setSuffix(tag, StringUtils.trim(suffixObj)); } } else { // Suffix old stile, without tags String suffixObj = getDefaultProperty(profile, PROP_SUFFIX, properties, defaultProperties); if (StringUtils.isBlank(suffixObj)) throw new ConfigurationException("Suffix (" + PROP_SUFFIX + ") of profile '" + profile + "' is missing"); df.setSuffix(StringUtils.trim(suffixObj)); } // Mimetype String mimeTypeObj = getDefaultProperty(profile, PROP_MIMETYPE, properties, defaultProperties); if (StringUtils.isNotBlank(mimeTypeObj)) { MimeType mimeType; try { mimeType = MimeTypes.parseMimeType(mimeTypeObj); } catch (Exception e) { throw new ConfigurationException("Mime type (" + PROP_MIMETYPE + ") " + mimeTypeObj + " could not be parsed as a mime type! Expressions are not allowed!"); } df.setMimeType(mimeType.toString()); } // Applicable to the following track categories String applicableObj = getDefaultProperty(profile, PROP_APPLICABLE, properties, defaultProperties); if (StringUtils.isBlank(applicableObj)) throw new ConfigurationException("Input type (" + PROP_APPLICABLE + ") of profile '" + profile + "' is missing"); df.setApplicableType(MediaType.parseString(StringUtils.trimToEmpty(applicableObj))); String jobLoad = getDefaultProperty(profile, PROP_JOBLOAD, properties, defaultProperties); if (!StringUtils.isBlank(jobLoad)) df.setJobLoad(Float.valueOf(jobLoad)); // Look for extensions String extensionKey = PROP_PREFIX + profile + "."; for (Map.Entry<Object, Object> entry : properties.entrySet()) { String key = entry.getKey().toString(); if (key.startsWith(extensionKey) && !defaultProperties.contains(key)) { String k = key.substring(extensionKey.length()); String v = StringUtils.trimToEmpty(entry.getValue().toString()); df.addExtension(k, v); } } return df; } /** * Returns the default property and registers the property key in the list. * * @param profile * the profile identifier * @param keySuffix * the key suffix, like ".name" * @param properties * the properties * @param list * the list of default property keys * @return the property value or <code>null</code> */ private static String getDefaultProperty(String profile, String keySuffix, Properties properties, List<String> list) { StringBuffer buf = new StringBuffer(PROP_PREFIX); buf.append(profile); buf.append(keySuffix); String key = buf.toString(); list.add(key); final String prop = properties.getProperty(key); return prop != null ? prop.trim() : prop; } /** * Get any tags that might follow the PROP_SUFFIX * @param profile * the profile identifier * @param properties * the properties * @param list * the list of default property keys * @return A list of tags for output files */ private static List<String> getTags(String profile, Properties properties, List<String> list) { Set<Object> keys = properties.keySet(); StringBuffer buf = new StringBuffer(PROP_PREFIX); buf.append(profile); buf.append(PROP_SUFFIX); String key = buf.toString(); ArrayList<String> tags = new ArrayList<String>(); for (Object o : keys) { String k = o.toString(); if (k.startsWith(key)) { if (k.substring(key.length()).length() > 0) { list.add(k); tags.add(k.substring(key.length() + 1)); } } } return tags; } /** * {@inheritDoc} * * @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File) */ @Override public boolean canHandle(File artifact) { return "encoding".equals(artifact.getParentFile().getName()) && artifact.getName().endsWith(".properties"); } /** * {@inheritDoc} * * @see org.apache.felix.fileinstall.ArtifactInstaller#install(java.io.File) */ @Override public void install(File artifact) throws Exception { logger.info("Registering encoding profiles from {}", artifact); try { Map<String, EncodingProfile> profileMap = loadFromProperties(artifact); for (Map.Entry<String, EncodingProfile> entry : profileMap.entrySet()) { logger.info("Installed profile {}", entry.getValue().getIdentifier()); profiles.put(entry.getKey(), entry.getValue()); } sumInstalledFiles++; } catch (Exception e) { logger.error("Encoding profiles could not be read from {}: {}", artifact, e.getMessage()); } // Determine the number of available profiles String[] filesInDirectory = artifact.getParentFile().list(new FilenameFilter() { public boolean accept(File arg0, String name) { return name.endsWith(".properties"); } }); // Once all profiles have been loaded, announce readiness if (filesInDirectory.length == sumInstalledFiles) { Dictionary<String, String> properties = new Hashtable<String, String>(); properties.put(ARTIFACT, "encodingprofile"); logger.debug("Indicating readiness of encoding profiles"); bundleCtx.registerService(ReadinessIndicator.class.getName(), new ReadinessIndicator(), properties); logger.info("All {} encoding profiles installed", filesInDirectory.length); } else { logger.debug("{} of {} encoding profiles installed", sumInstalledFiles, filesInDirectory.length); } } /** * {@inheritDoc} * * @see org.apache.felix.fileinstall.ArtifactInstaller#uninstall(java.io.File) */ @Override public void uninstall(File artifact) throws Exception { for (Iterator<EncodingProfile> iter = profiles.values().iterator(); iter.hasNext();) { EncodingProfile profile = iter.next(); if (artifact.equals(profile.getSource())) { logger.info("Uninstalling profile {}", profile.getIdentifier()); iter.remove(); } } } /** * {@inheritDoc} * * @see org.apache.felix.fileinstall.ArtifactInstaller#update(java.io.File) */ @Override public void update(File artifact) throws Exception { uninstall(artifact); install(artifact); } }