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     try {
189       df.setOutputType(MediaType.parseString(StringUtils.trimToEmpty(type)));
190     } catch (IllegalArgumentException e) {
191       throw new ConfigurationException("Output type (" + PROP_OUTPUT + ") '" + type + "' of profile '" + profile
192               + "' is unknown");
193     }
194 
195     //Suffixes with tags?
196     List<String> tags = getTags(profile, properties, defaultProperties);
197     if (tags.size() > 0) {
198       for (String tag : tags) {
199         String prop = PROP_SUFFIX + "." + tag;
200         String suffixObj = getDefaultProperty(profile, prop, properties, defaultProperties);
201         df.setSuffix(tag, StringUtils.trim(suffixObj));
202       }
203     } else {
204       // Suffix old stile, without tags
205       String suffixObj = getDefaultProperty(profile, PROP_SUFFIX, properties, defaultProperties);
206       if (StringUtils.isBlank(suffixObj))
207         throw new ConfigurationException("Suffix (" + PROP_SUFFIX + ") of profile '" + profile + "' is missing");
208       df.setSuffix(StringUtils.trim(suffixObj));
209     }
210 
211     // Applicable to the following track categories
212     String applicableObj = getDefaultProperty(profile, PROP_APPLICABLE, properties, defaultProperties);
213     if (StringUtils.isBlank(applicableObj))
214       throw new ConfigurationException("Input type (" + PROP_APPLICABLE + ") of profile '" + profile + "' is missing");
215     df.setApplicableType(MediaType.parseString(StringUtils.trimToEmpty(applicableObj)));
216 
217     String jobLoad = getDefaultProperty(profile, PROP_JOBLOAD, properties, defaultProperties);
218     if (!StringUtils.isBlank(jobLoad)) {
219       df.setJobLoad(Float.valueOf(jobLoad));
220       logger.debug("Setting job load for profile {} to {}", profile, jobLoad);
221     }
222 
223     // Look for extensions
224     String extensionKey = PROP_PREFIX + profile + ".";
225     for (Map.Entry<Object, Object> entry : properties.entrySet()) {
226       String key = entry.getKey().toString();
227       if (key.startsWith(extensionKey) && !defaultProperties.contains(key)) {
228         String k = key.substring(extensionKey.length());
229         String v = StringUtils.trimToEmpty(entry.getValue().toString());
230         df.addExtension(k, v);
231       }
232     }
233 
234     return df;
235   }
236 
237   /**
238    * Returns the default property and registers the property key in the list.
239    *
240    * @param profile
241    *          the profile identifier
242    * @param keySuffix
243    *          the key suffix, like ".name"
244    * @param properties
245    *          the properties
246    * @param list
247    *          the list of default property keys
248    * @return the property value or <code>null</code>
249    */
250   private static String getDefaultProperty(String profile, String keySuffix, Properties properties, List<String> list) {
251     String key = PROP_PREFIX + profile + keySuffix;
252     list.add(key);
253     return StringUtils.trimToNull(properties.getProperty(key));
254   }
255 
256   /**
257    * Get any tags that might follow the PROP_SUFFIX
258    * @param profile
259    *          the profile identifier
260    * @param properties
261    *          the properties
262    * @param list
263    *          the list of default property keys
264    * @return A list of tags for output files
265    */
266 
267   private static List<String> getTags(String profile, Properties properties, List<String> list) {
268     Set<Object> keys = properties.keySet();
269     String key = PROP_PREFIX + profile + PROP_SUFFIX;
270 
271     ArrayList<String> tags = new ArrayList<>();
272     for (Object o : keys) {
273       String k = o.toString();
274       if (k.startsWith(key)) {
275         if (k.substring(key.length()).length() > 0) {
276           list.add(k);
277           tags.add(k.substring(key.length() + 1));
278         }
279       }
280     }
281     return tags;
282   }
283 
284   /**
285    * {@inheritDoc}
286    *
287    * @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File)
288    */
289   @Override
290   public boolean canHandle(File artifact) {
291     return "encoding".equals(artifact.getParentFile().getName()) && artifact.getName().endsWith(".properties");
292   }
293 
294   /**
295    * {@inheritDoc}
296    *
297    * @see org.apache.felix.fileinstall.ArtifactInstaller#install(java.io.File)
298    */
299   @Override
300   public void install(File artifact) throws Exception {
301     logger.info("Registering encoding profiles from {}", artifact);
302     try {
303       Map<String, EncodingProfile> profileMap = loadFromProperties(artifact);
304       for (Map.Entry<String, EncodingProfile> entry : profileMap.entrySet()) {
305         EncodingProfile profile = entry.getValue();
306         logger.info("Installed profile {} (load {})", profile.getIdentifier(), profile.getJobLoad());
307         profiles.put(entry.getKey(), profile);
308       }
309       sumInstalledFiles++;
310     } catch (Exception e) {
311       logger.error("Encoding profiles could not be read from {}: {}", artifact, e.getMessage());
312       sumUnparsableFiles++;
313     }
314 
315     // Determine the number of available profiles
316     String[] filesInDirectory = artifact.getParentFile().list(new FilenameFilter() {
317       public boolean accept(File arg0, String name) {
318         return name.endsWith(".properties");
319       }
320     });
321 
322     // Once all profiles have been loaded, announce readiness
323     if (filesInDirectory.length == (sumInstalledFiles + sumUnparsableFiles)) {
324       Dictionary<String, String> properties = new Hashtable<String, String>();
325       properties.put(ARTIFACT, "encodingprofile");
326       logger.debug("Indicating readiness of encoding profiles");
327       bundleCtx.registerService(ReadinessIndicator.class.getName(), new ReadinessIndicator(), properties);
328 
329       if (filesInDirectory.length == sumInstalledFiles) {
330         logger.info("All {} encoding profiles installed", filesInDirectory.length);
331       } else {
332         logger.warn("{} encoding profile(s) installed, {} encoding profile(s) could not be installed",
333                 sumInstalledFiles, sumUnparsableFiles);
334       }
335     } else {
336       logger.debug("{} of {} encoding profiles installed", sumInstalledFiles, filesInDirectory.length);
337     }
338   }
339 
340   /**
341    * {@inheritDoc}
342    *
343    * @see org.apache.felix.fileinstall.ArtifactInstaller#uninstall(java.io.File)
344    */
345   @Override
346   public void uninstall(File artifact) throws Exception {
347     for (Iterator<EncodingProfile> iter = profiles.values().iterator(); iter.hasNext();) {
348       EncodingProfile profile = iter.next();
349       if (artifact.equals(profile.getSource())) {
350         logger.info("Uninstalling profile {}", profile.getIdentifier());
351         iter.remove();
352       }
353     }
354   }
355 
356   /**
357    * {@inheritDoc}
358    *
359    * @see org.apache.felix.fileinstall.ArtifactInstaller#update(java.io.File)
360    */
361   @Override
362   public void update(File artifact) throws Exception {
363     uninstall(artifact);
364     install(artifact);
365   }
366 
367 }