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.metadata.dublincore;
23  
24  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_ACCESS_RIGHTS;
25  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_AVAILABLE;
26  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CONTRIBUTOR;
27  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATED;
28  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATOR;
29  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_DESCRIPTION;
30  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_EXTENT;
31  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
32  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IS_PART_OF;
33  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LANGUAGE;
34  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LICENSE;
35  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_PUBLISHER;
36  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_REPLACES;
37  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_RIGHTS_HOLDER;
38  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SPATIAL;
39  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SUBJECT;
40  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL;
41  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE;
42  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TYPE;
43  import static org.opencastproject.util.data.Collections.head;
44  import static org.opencastproject.util.data.Monadics.mlist;
45  import static org.opencastproject.util.data.Option.option;
46  import static org.opencastproject.util.data.Option.some;
47  
48  import org.opencastproject.mediapackage.Catalog;
49  import org.opencastproject.mediapackage.MediaPackage;
50  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
51  import org.opencastproject.mediapackage.MediaPackageElements;
52  import org.opencastproject.mediapackage.MediaPackageSerializer;
53  import org.opencastproject.metadata.api.MetadataValue;
54  import org.opencastproject.metadata.api.StaticMetadata;
55  import org.opencastproject.metadata.api.StaticMetadataService;
56  import org.opencastproject.metadata.api.util.Interval;
57  import org.opencastproject.util.data.Function;
58  import org.opencastproject.util.data.NonEmptyList;
59  import org.opencastproject.util.data.Option;
60  import org.opencastproject.util.data.Predicate;
61  import org.opencastproject.util.data.functions.Misc;
62  import org.opencastproject.workspace.api.Workspace;
63  
64  import org.apache.commons.io.IOUtils;
65  import org.osgi.service.component.annotations.Activate;
66  import org.osgi.service.component.annotations.Component;
67  import org.osgi.service.component.annotations.Reference;
68  import org.osgi.service.component.annotations.ReferenceCardinality;
69  import org.osgi.service.component.annotations.ReferencePolicy;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  import java.io.InputStream;
74  import java.net.URI;
75  import java.util.Date;
76  import java.util.List;
77  import java.util.Map;
78  
79  /**
80   * This service provides {@link org.opencastproject.metadata.api.StaticMetadata} for a given mediapackage,
81   * based on a contained dublin core catalog describing the episode.
82   */
83  @Component(
84      immediate = true,
85      service = StaticMetadataService.class,
86      property = {
87          "service.description=Static Metadata Service, dublin core based",
88          "metadata.source=dublincore",
89          "priority=1"
90      }
91  )
92  public class StaticMetadataServiceDublinCoreImpl implements StaticMetadataService {
93  
94    private static final Logger logger = LoggerFactory.getLogger(StaticMetadataServiceDublinCoreImpl.class);
95  
96    // Catalog loader function
97    private Function<Catalog, Option<DublinCoreCatalog>> loader = new Function<Catalog, Option<DublinCoreCatalog>>() {
98      @Override
99      public Option<DublinCoreCatalog> apply(Catalog catalog) {
100       return load(catalog);
101     }
102   };
103 
104   protected int priority = 0;
105 
106   protected Workspace workspace = null;
107 
108   protected MediaPackageSerializer serializer = null;
109 
110   @Reference
111   public void setWorkspace(Workspace workspace) {
112     this.workspace = workspace;
113   }
114 
115   @Reference(
116       cardinality = ReferenceCardinality.OPTIONAL,
117       policy = ReferencePolicy.DYNAMIC,
118       target = "(service.pid=org.opencastproject.mediapackage.ChainingMediaPackageSerializer)",
119       unbind = "unsetMediaPackageSerializer"
120   )
121   public void setMediaPackageSerializer(MediaPackageSerializer serializer) {
122     this.serializer = serializer;
123   }
124 
125   public void unsetMediaPackageSerializer(MediaPackageSerializer serializer) {
126     if (this.serializer == serializer) {
127       this.serializer = null;
128     }
129   }
130 
131   @Activate
132   public void activate(@SuppressWarnings("rawtypes") Map properties) {
133     logger.debug("activate()");
134     if (properties != null) {
135       String priorityString = (String) properties.get(PRIORITY_KEY);
136       if (priorityString != null) {
137         try {
138           priority = Integer.parseInt(priorityString);
139         } catch (NumberFormatException e) {
140           logger.warn("Unable to set priority to {}", priorityString);
141           throw e;
142         }
143       }
144     }
145   }
146 
147   /**
148    * {@inheritDoc}
149    *
150    * @see org.opencastproject.metadata.api.MetadataService#getMetadata(org.opencastproject.mediapackage.MediaPackage)
151    */
152   @Override
153   public StaticMetadata getMetadata(final MediaPackage mp) {
154     Catalog[] catalogs = mp.getCatalogs(MediaPackageElements.EPISODE);
155     if (catalogs.length > 0) {
156       return newStaticMetadataFromEpisode(DublinCoreUtil.loadDublinCore(workspace, catalogs[0]));
157     }
158     return null;
159   }
160 
161   private static StaticMetadata newStaticMetadataFromEpisode(DublinCoreCatalog episode) {
162     // Ensure that the mandatory properties are present
163     final Option<String> id = option(episode.getFirst(PROPERTY_IDENTIFIER));
164     final Option<Date> created = option(episode.getFirst(PROPERTY_CREATED)).map(new Function<String, Date>() {
165       @Override
166       public Date apply(String a) {
167         final Date date = EncodingSchemeUtils.decodeDate(a);
168         return date != null ? date : Misc.<Date>chuck(new RuntimeException(a + " does not conform to W3C-DTF encoding scheme."));
169       }
170     });
171     final Option temporalOpt = option(episode.getFirstVal(PROPERTY_TEMPORAL)).map(dc2temporalValueOption());
172     final Option<Date> start;
173     if (episode.getFirst(PROPERTY_TEMPORAL) != null) {
174       DCMIPeriod period = EncodingSchemeUtils
175                   .decodeMandatoryPeriod(episode.getFirst(PROPERTY_TEMPORAL));
176       start = option(period.getStart());
177     } else {
178       start = created;
179     }
180     final Option<String> language = option(episode.getFirst(PROPERTY_LANGUAGE));
181     final Option<Long> extent = head(episode.get(PROPERTY_EXTENT)).map(new Function<DublinCoreValue, Long>() {
182       @Override
183       public Long apply(DublinCoreValue a) {
184         final Long extent = EncodingSchemeUtils.decodeDuration(a);
185         return extent != null ? extent : Misc.<Long>chuck(new RuntimeException(a + " does not conform to ISO8601 encoding scheme for durations."));
186       }
187     });
188     final Option<String> type = option(episode.getFirst(PROPERTY_TYPE));
189 
190     final Option<String> isPartOf = option(episode.getFirst(PROPERTY_IS_PART_OF));
191     final Option<String> replaces = option(episode.getFirst(PROPERTY_REPLACES));
192     final Option<Interval> available = head(episode.get(PROPERTY_AVAILABLE)).flatMap(
193             new Function<DublinCoreValue, Option<Interval>>() {
194               @Override
195               public Option<Interval> apply(DublinCoreValue v) {
196                 final DCMIPeriod p = EncodingSchemeUtils.decodePeriod(v);
197                 return p != null
198                         ? some(Interval.fromValues(p.getStart(), p.getEnd()))
199                         : Misc.<Option<Interval>>chuck(new RuntimeException(v + " does not conform to W3C-DTF encoding scheme for periods"));
200               }
201             });
202     final NonEmptyList<MetadataValue<String>> titles = new NonEmptyList<MetadataValue<String>>(
203             mlist(episode.get(PROPERTY_TITLE)).map(dc2mvString(PROPERTY_TITLE.getLocalName())).value());
204     final List<MetadataValue<String>> subjects =
205             mlist(episode.get(PROPERTY_SUBJECT)).map(dc2mvString(PROPERTY_SUBJECT.getLocalName())).value();
206     final List<MetadataValue<String>> creators =
207             mlist(episode.get(PROPERTY_CREATOR)).map(dc2mvString(PROPERTY_CREATOR.getLocalName())).value();
208     final List<MetadataValue<String>> publishers =
209             mlist(episode.get(PROPERTY_PUBLISHER)).map(dc2mvString(PROPERTY_PUBLISHER.getLocalName())).value();
210     final List<MetadataValue<String>> contributors =
211             mlist(episode.get(PROPERTY_CONTRIBUTOR)).map(dc2mvString(PROPERTY_CONTRIBUTOR.getLocalName())).value();
212     final List<MetadataValue<String>> description =
213             mlist(episode.get(PROPERTY_DESCRIPTION)).map(dc2mvString(PROPERTY_DESCRIPTION.getLocalName())).value();
214     final List<MetadataValue<String>> rightsHolders =
215             mlist(episode.get(PROPERTY_RIGHTS_HOLDER)).map(dc2mvString(PROPERTY_RIGHTS_HOLDER.getLocalName())).value();
216     final List<MetadataValue<String>> spatials =
217             mlist(episode.get(PROPERTY_SPATIAL)).map(dc2mvString(PROPERTY_SPATIAL.getLocalName())).value();
218     final List<MetadataValue<String>> accessRights =
219             mlist(episode.get(PROPERTY_ACCESS_RIGHTS)).map(dc2mvString(PROPERTY_ACCESS_RIGHTS.getLocalName())).value();
220     final List<MetadataValue<String>> licenses =
221             mlist(episode.get(PROPERTY_LICENSE)).map(dc2mvString(PROPERTY_LICENSE.getLocalName())).value();
222 
223     return new StaticMetadata() {
224       @Override
225       public Option<String> getId() {
226         return id;
227       }
228 
229       @Override
230       public Option<Date[]> getTemporalPeriod() {
231         if (temporalOpt.isSome()) {
232           if (temporalOpt.get() instanceof DCMIPeriod) {
233             DCMIPeriod p = (DCMIPeriod) temporalOpt.get();
234             return option(new Date[] { p.getStart(), p.getEnd() });
235           }
236         }
237         return Option.none();
238       }
239 
240       @Override
241       public Option<Date> getTemporalInstant() {
242         if (temporalOpt.isSome()) {
243           if (temporalOpt.get() instanceof Date) {
244             return temporalOpt;
245           }
246         }
247         return Option.none();
248       }
249 
250       @Override
251       public Option<Long> getTemporalDuration() {
252         if (temporalOpt.isSome()) {
253           if (temporalOpt.get() instanceof Long) {
254             return temporalOpt;
255           }
256         }
257         return Option.none();
258       }
259 
260       @Override
261       public Option<Long> getExtent() {
262         return extent;
263       }
264 
265       @Override
266       public Option<String> getLanguage() {
267         return language;
268       }
269 
270       @Override
271       public Option<String> getIsPartOf() {
272         return isPartOf;
273       }
274 
275       @Override
276       public Option<String> getReplaces() {
277         return replaces;
278       }
279 
280       @Override
281       public Option<String> getType() {
282         return type;
283       }
284 
285       @Override
286       public Option<Interval> getAvailable() {
287         return available;
288       }
289 
290       @Override
291       public NonEmptyList<MetadataValue<String>> getTitles() {
292         return titles;
293       }
294 
295       @Override
296       public List<MetadataValue<String>> getSubjects() {
297         return subjects;
298       }
299 
300       @Override
301       public List<MetadataValue<String>> getCreators() {
302         return creators;
303       }
304 
305       @Override
306       public List<MetadataValue<String>> getPublishers() {
307         return publishers;
308       }
309 
310       @Override
311       public List<MetadataValue<String>> getContributors() {
312         return contributors;
313       }
314 
315       @Override
316       public List<MetadataValue<String>> getDescription() {
317         return description;
318       }
319 
320       @Override
321       public List<MetadataValue<String>> getRightsHolders() {
322         return rightsHolders;
323       }
324 
325       @Override
326       public List<MetadataValue<String>> getSpatials() {
327         return spatials;
328       }
329 
330       @Override
331       public List<MetadataValue<String>> getAccessRights() {
332         return accessRights;
333       }
334 
335       @Override
336       public List<MetadataValue<String>> getLicenses() {
337         return licenses;
338       }
339     };
340   }
341 
342   /**
343    *
344    * {@inheritDoc}
345    *
346    * @see org.opencastproject.metadata.api.MetadataService#getPriority()
347    */
348   @Override
349   public int getPriority() {
350     return priority;
351   }
352 
353   /**
354    * Return a function that creates a Option with the value of temporal from a DublinCoreValue.
355    */
356   private static Function<DublinCoreValue, Object> dc2temporalValueOption() {
357     return new Function<DublinCoreValue, Object>() {
358       @Override
359       public Object apply(DublinCoreValue dcv) {
360         Temporal temporal = EncodingSchemeUtils.decodeTemporal(dcv);
361         if (temporal != null) {
362           return temporal.fold(new Temporal.Match<Object>() {
363             @Override
364             public Object period(DCMIPeriod period) {
365               return period;
366             }
367 
368             @Override
369             public Object instant(Date instant) {
370               return instant;
371             }
372 
373             @Override
374             public Object duration(long duration) {
375               return duration;
376             }
377           });
378         }
379         return Misc.<Object>chuck(new RuntimeException(dcv
380                 + " does not conform to ISO8601 encoding scheme for temporal."));
381       }
382     };
383   }
384 
385   /**
386    * Return a function that creates a MetadataValue[String] from a DublinCoreValue setting its name to <code>name</code>.
387    */
388   private static Function<DublinCoreValue, MetadataValue<String>> dc2mvString(final String name) {
389     return new Function<DublinCoreValue, MetadataValue<String>>() {
390       @Override
391       public MetadataValue<String> apply(DublinCoreValue dcv) {
392         return new MetadataValue<String>(dcv.getValue(), name, dcv.getLanguage());
393       }
394     };
395   }
396 
397   private static Predicate<Catalog> flavorPredicate(final MediaPackageElementFlavor flavor) {
398     return new Predicate<Catalog>() {
399       @Override
400       public Boolean apply(Catalog catalog) {
401         return flavor.equals(catalog.getFlavor());
402       }
403     };
404   }
405 
406   private Option<DublinCoreCatalog> load(Catalog catalog) {
407     InputStream in = null;
408     try {
409       URI uri = catalog.getURI();
410       if (serializer != null) uri = serializer.decodeURI(uri);
411       in = workspace.read(uri);
412       return some((DublinCoreCatalog) DublinCores.read(in));
413     } catch (Exception e) {
414       logger.warn("Unable to load metadata from catalog '{}'", catalog);
415       return Option.none();
416     } finally {
417       IOUtils.closeQuietly(in);
418     }
419   }
420 }