View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.composer.impl;
23  
24  import static org.opencastproject.util.ReadinessIndicator.ARTIFACT;
25  
26  import org.opencastproject.composer.api.EncodingProfile;
27  import org.opencastproject.composer.api.EncodingProfile.MediaType;
28  import org.opencastproject.composer.api.EncodingProfileImpl;
29  import org.opencastproject.util.ConfigurationException;
30  import org.opencastproject.util.ReadinessIndicator;
31  
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.felix.fileinstall.ArtifactInstaller;
34  import org.osgi.framework.BundleContext;
35  import org.osgi.service.component.annotations.Activate;
36  import org.osgi.service.component.annotations.Component;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import java.io.File;
41  import java.io.FileInputStream;
42  import java.io.FilenameFilter;
43  import java.io.IOException;
44  import java.io.InputStreamReader;
45  import java.nio.charset.StandardCharsets;
46  import java.util.ArrayList;
47  import java.util.Dictionary;
48  import java.util.HashMap;
49  import java.util.Hashtable;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Properties;
54  import java.util.Set;
55  
56  /**
57   * This manager class tries to read encoding profiles from the classpath.
58   */
59  @Component(
60      property = {
61          "service.description=Encoding Profile Scanner"
62      },
63      immediate = true,
64      service = { EncodingProfileScanner.class, ArtifactInstaller.class }
65  )
66  public class EncodingProfileScanner implements ArtifactInstaller {
67  
68    /** Prefix for encoding profile property keys **/
69    private static final String PROP_PREFIX = "profile.";
70  
71    /* Property names */
72    private static final String PROP_NAME = ".name";
73    private static final String PROP_APPLICABLE = ".input";
74    private static final String PROP_OUTPUT = ".output";
75    private static final String PROP_SUFFIX = ".suffix";
76    private static final String PROP_JOBLOAD = ".jobload";
77  
78    /** OSGi bundle context */
79    private BundleContext bundleCtx = null;
80  
81    /** Sum of profiles files currently installed */
82    private int sumInstalledFiles = 0;
83  
84    /** Sum of profiles files that could not be parsed */
85    private int sumUnparsableFiles = 0;
86  
87    /** The profiles map */
88    private Map<String, EncodingProfile> profiles = new HashMap<String, EncodingProfile>();
89  
90    /** The logging instance */
91    private static final Logger logger = LoggerFactory.getLogger(EncodingProfileScanner.class);
92  
93    /**
94     * Returns the list of profiles.
95     *
96     * @return the profile definitions
97     */
98    public Map<String, EncodingProfile> getProfiles() {
99      return profiles;
100   }
101 
102   /**
103    * OSGi callback on component activation.
104    *
105    * @param ctx
106    *          the bundle context
107    */
108   @Activate
109   void activate(BundleContext ctx) {
110     this.bundleCtx = ctx;
111   }
112 
113   /**
114    * Returns the encoding profile for the given identifier or <code>null</code> if no such profile has been configured.
115    *
116    * @param id
117    *          the profile identifier
118    * @return the profile
119    */
120   public EncodingProfile getProfile(String id) {
121     return profiles.get(id);
122   }
123 
124   /**
125    * Reads the profiles from the given set of properties.
126    *
127    * @param artifact
128    *          the properties file
129    * @return the profiles found in the properties
130    */
131   Map<String, EncodingProfile> loadFromProperties(File artifact) throws IOException {
132     // Format name
133     Properties properties = new Properties();
134     try (InputStreamReader reader = new InputStreamReader(new FileInputStream(artifact), StandardCharsets.UTF_8)) {
135       properties.load(reader);
136     }
137 
138     // Find list of formats in properties
139     List<String> profileNames = new ArrayList<>();
140     for (Object fullKey : properties.keySet()) {
141       String key = fullKey.toString();
142       if (key.startsWith(PROP_PREFIX) && key.endsWith(PROP_NAME)) {
143         int separatorLocation = fullKey.toString().lastIndexOf('.');
144         key = key.substring(PROP_PREFIX.length(), separatorLocation);
145         if (!profileNames.contains(key)) {
146           profileNames.add(key);
147         } else {
148           throw new ConfigurationException("Found duplicate definition for encoding profile '" + key + "'");
149         }
150       }
151     }
152 
153     // Load the formats
154     Map<String, EncodingProfile> profiles = new HashMap<>();
155     for (String profileId : profileNames) {
156       logger.debug("Enabling media format " + profileId);
157       EncodingProfile profile = loadProfile(profileId, properties, artifact);
158       profiles.put(profileId, profile);
159     }
160 
161     return profiles;
162   }
163 
164   /**
165    * Reads the profile from the given properties
166    *
167    * @param profile
168    * @param properties
169    * @param artifact
170    * @return the loaded profile or null if profile
171    * @throws RuntimeException
172    */
173   private EncodingProfile loadProfile(String profile, Properties properties, File artifact)
174           throws ConfigurationException {
175     List<String> defaultProperties = new ArrayList<>(10);
176 
177     String name = getDefaultProperty(profile, PROP_NAME, properties, defaultProperties);
178     if (StringUtils.isBlank(name)) {
179       throw new ConfigurationException("Distribution profile '" + profile + "' is missing a name (" + PROP_NAME + ").");
180     }
181 
182     EncodingProfileImpl df = new EncodingProfileImpl(profile, name, artifact);
183 
184     // Output Type
185     String type = getDefaultProperty(profile, PROP_OUTPUT, properties, defaultProperties);
186     if (StringUtils.isBlank(type)) {
187       throw new ConfigurationException("Output type (" + PROP_OUTPUT + ") of profile '" + profile + "' is missing");
188     }
189     try {
190       df.setOutputType(MediaType.parseString(StringUtils.trimToEmpty(type)));
191     } catch (IllegalArgumentException e) {
192       throw new ConfigurationException("Output type (" + PROP_OUTPUT + ") '" + type + "' of profile '" + profile
193               + "' is unknown");
194     }
195 
196     //Suffixes with tags?
197     List<String> tags = getTags(profile, properties, defaultProperties);
198     if (tags.size() > 0) {
199       for (String tag : tags) {
200         String prop = PROP_SUFFIX + "." + tag;
201         String suffixObj = getDefaultProperty(profile, prop, properties, defaultProperties);
202         df.setSuffix(tag, StringUtils.trim(suffixObj));
203       }
204     } else {
205       // Suffix old stile, without tags
206       String suffixObj = getDefaultProperty(profile, PROP_SUFFIX, properties, defaultProperties);
207       if (StringUtils.isBlank(suffixObj)) {
208         throw new ConfigurationException("Suffix (" + PROP_SUFFIX + ") of profile '" + profile + "' is missing");
209       }
210       df.setSuffix(StringUtils.trim(suffixObj));
211     }
212 
213     // Applicable to the following track categories
214     String applicableObj = getDefaultProperty(profile, PROP_APPLICABLE, properties, defaultProperties);
215     if (StringUtils.isBlank(applicableObj)) {
216       throw new ConfigurationException("Input type (" + PROP_APPLICABLE + ") of profile '" + profile + "' is missing");
217     }
218     df.setApplicableType(MediaType.parseString(StringUtils.trimToEmpty(applicableObj)));
219 
220     String jobLoad = getDefaultProperty(profile, PROP_JOBLOAD, properties, defaultProperties);
221     if (!StringUtils.isBlank(jobLoad)) {
222       df.setJobLoad(Float.valueOf(jobLoad));
223       logger.debug("Setting job load for profile {} to {}", profile, jobLoad);
224     }
225 
226     // Look for extensions
227     String extensionKey = PROP_PREFIX + profile + ".";
228     for (Map.Entry<Object, Object> entry : properties.entrySet()) {
229       String key = entry.getKey().toString();
230       if (key.startsWith(extensionKey) && !defaultProperties.contains(key)) {
231         String k = key.substring(extensionKey.length());
232         String v = StringUtils.trimToEmpty(entry.getValue().toString());
233         df.addExtension(k, v);
234       }
235     }
236 
237     return df;
238   }
239 
240   /**
241    * Returns the default property and registers the property key in the list.
242    *
243    * @param profile
244    *          the profile identifier
245    * @param keySuffix
246    *          the key suffix, like ".name"
247    * @param properties
248    *          the properties
249    * @param list
250    *          the list of default property keys
251    * @return the property value or <code>null</code>
252    */
253   private static String getDefaultProperty(String profile, String keySuffix, Properties properties, List<String> list) {
254     String key = PROP_PREFIX + profile + keySuffix;
255     list.add(key);
256     return StringUtils.trimToNull(properties.getProperty(key));
257   }
258 
259   /**
260    * Get any tags that might follow the PROP_SUFFIX
261    * @param profile
262    *          the profile identifier
263    * @param properties
264    *          the properties
265    * @param list
266    *          the list of default property keys
267    * @return A list of tags for output files
268    */
269 
270   private static List<String> getTags(String profile, Properties properties, List<String> list) {
271     Set<Object> keys = properties.keySet();
272     String key = PROP_PREFIX + profile + PROP_SUFFIX;
273 
274     ArrayList<String> tags = new ArrayList<>();
275     for (Object o : keys) {
276       String k = o.toString();
277       if (k.startsWith(key)) {
278         if (k.substring(key.length()).length() > 0) {
279           list.add(k);
280           tags.add(k.substring(key.length() + 1));
281         }
282       }
283     }
284     return tags;
285   }
286 
287   /**
288    * {@inheritDoc}
289    *
290    * @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File)
291    */
292   @Override
293   public boolean canHandle(File artifact) {
294     return "encoding".equals(artifact.getParentFile().getName()) && artifact.getName().endsWith(".properties");
295   }
296 
297   /**
298    * {@inheritDoc}
299    *
300    * @see org.apache.felix.fileinstall.ArtifactInstaller#install(java.io.File)
301    */
302   @Override
303   public void install(File artifact) throws Exception {
304     logger.info("Registering encoding profiles from {}", artifact);
305     try {
306       Map<String, EncodingProfile> profileMap = loadFromProperties(artifact);
307       for (Map.Entry<String, EncodingProfile> entry : profileMap.entrySet()) {
308         EncodingProfile profile = entry.getValue();
309         logger.info("Installed profile {} (load {})", profile.getIdentifier(), profile.getJobLoad());
310         profiles.put(entry.getKey(), profile);
311       }
312       sumInstalledFiles++;
313     } catch (Exception e) {
314       logger.error("Encoding profiles could not be read from {}: {}", artifact, e.getMessage());
315       sumUnparsableFiles++;
316     }
317 
318     // Determine the number of available profiles
319     String[] filesInDirectory = artifact.getParentFile().list(new FilenameFilter() {
320       public boolean accept(File arg0, String name) {
321         return name.endsWith(".properties");
322       }
323     });
324 
325     // Once all profiles have been loaded, announce readiness
326     if (filesInDirectory.length == (sumInstalledFiles + sumUnparsableFiles)) {
327       Dictionary<String, String> properties = new Hashtable<String, String>();
328       properties.put(ARTIFACT, "encodingprofile");
329       logger.debug("Indicating readiness of encoding profiles");
330       bundleCtx.registerService(ReadinessIndicator.class.getName(), new ReadinessIndicator(), properties);
331 
332       if (filesInDirectory.length == sumInstalledFiles) {
333         logger.info("All {} encoding profiles in {} files installed", profiles.size(), filesInDirectory.length);
334       } else {
335         logger.warn("{} encoding config files installed, {} encoding config files could not be installed",
336                 sumInstalledFiles, sumUnparsableFiles);
337       }
338     } else {
339       logger.debug("{} of {} encoding profile config files installed", sumInstalledFiles, filesInDirectory.length);
340     }
341   }
342 
343   /**
344    * {@inheritDoc}
345    *
346    * @see org.apache.felix.fileinstall.ArtifactInstaller#uninstall(java.io.File)
347    */
348   @Override
349   public void uninstall(File artifact) throws Exception {
350     for (Iterator<EncodingProfile> iter = profiles.values().iterator(); iter.hasNext();) {
351       EncodingProfile profile = iter.next();
352       if (artifact.equals(profile.getSource())) {
353         logger.info("Uninstalling profile {}", profile.getIdentifier());
354         iter.remove();
355       }
356     }
357   }
358 
359   /**
360    * {@inheritDoc}
361    *
362    * @see org.apache.felix.fileinstall.ArtifactInstaller#update(java.io.File)
363    */
364   @Override
365   public void update(File artifact) throws Exception {
366     uninstall(artifact);
367     install(artifact);
368   }
369 
370 }