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