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))
160         .map(dc2temporalValueOption());
161     final Optional<Date> start;
162     if (episode.getFirst(PROPERTY_TEMPORAL) != null) {
163       DCMIPeriod period = EncodingSchemeUtils
164                   .decodeMandatoryPeriod(episode.getFirst(PROPERTY_TEMPORAL));
165       start = Optional.ofNullable(period.getStart());
166     } else {
167       start = created;
168     }
169     final Optional<String> language = Optional.ofNullable(episode.getFirst(PROPERTY_LANGUAGE));
170     final Optional<Long> extent = head(episode.get(PROPERTY_EXTENT))
171         .map(a -> {
172           Long duration = EncodingSchemeUtils.decodeDuration(a);
173           if (duration == null) {
174             throw new RuntimeException(a + " does not conform to ISO8601 encoding scheme for durations.");
175           }
176           return duration;
177         });
178     final Optional<String> type = Optional.ofNullable(episode.getFirst(PROPERTY_TYPE));
179 
180     final Optional<String> isPartOf = Optional.ofNullable(episode.getFirst(PROPERTY_IS_PART_OF));
181     final Optional<String> replaces = Optional.ofNullable(episode.getFirst(PROPERTY_REPLACES));
182     final Optional<Interval> available = head(episode.get(PROPERTY_AVAILABLE))
183         .flatMap(v -> {
184           DCMIPeriod p = EncodingSchemeUtils.decodePeriod(v);
185           if (p == null) {
186             throw new RuntimeException(v + " does not conform to W3C-DTF encoding scheme for periods");
187           }
188           return Optional.of(Interval.fromValues(p.getStart(), p.getEnd()));
189         });
190     final NonEmptyList<MetadataValue<String>> titles = new NonEmptyList<>(
191         episode.get(PROPERTY_TITLE).stream()
192             .map(dc2mvString(PROPERTY_TITLE.getLocalName()))
193             .collect(Collectors.toList())
194     );
195     final List<MetadataValue<String>> subjects = episode.get(PROPERTY_SUBJECT).stream()
196             .map(dc2mvString(PROPERTY_SUBJECT.getLocalName()))
197             .collect(Collectors.toList());
198     final List<MetadataValue<String>> creators = episode.get(PROPERTY_CREATOR).stream()
199             .map(dc2mvString(PROPERTY_CREATOR.getLocalName()))
200             .collect(Collectors.toList());
201     final List<MetadataValue<String>> publishers = episode.get(PROPERTY_PUBLISHER).stream()
202             .map(dc2mvString(PROPERTY_PUBLISHER.getLocalName()))
203             .collect(Collectors.toList());
204     final List<MetadataValue<String>> contributors = episode.get(PROPERTY_CONTRIBUTOR).stream()
205             .map(dc2mvString(PROPERTY_CONTRIBUTOR.getLocalName()))
206             .collect(Collectors.toList());
207     final List<MetadataValue<String>> description = episode.get(PROPERTY_DESCRIPTION).stream()
208             .map(dc2mvString(PROPERTY_DESCRIPTION.getLocalName()))
209             .collect(Collectors.toList());
210     final List<MetadataValue<String>> rightsHolders = episode.get(PROPERTY_RIGHTS_HOLDER).stream()
211             .map(dc2mvString(PROPERTY_RIGHTS_HOLDER.getLocalName()))
212             .collect(Collectors.toList());
213     final List<MetadataValue<String>> spatials = episode.get(PROPERTY_SPATIAL).stream()
214             .map(dc2mvString(PROPERTY_SPATIAL.getLocalName()))
215             .collect(Collectors.toList());
216     final List<MetadataValue<String>> accessRights = episode.get(PROPERTY_ACCESS_RIGHTS).stream()
217             .map(dc2mvString(PROPERTY_ACCESS_RIGHTS.getLocalName()))
218             .collect(Collectors.toList());
219     final List<MetadataValue<String>> licenses = episode.get(PROPERTY_LICENSE).stream()
220             .map(dc2mvString(PROPERTY_LICENSE.getLocalName()))
221             .collect(Collectors.toList());
222 
223     return new StaticMetadata() {
224       @Override
225       public Optional<String> getId() {
226         return id;
227       }
228 
229       @Override
230       public Optional<Date[]> getTemporalPeriod() {
231         if (temporalOpt.isPresent()) {
232           if (temporalOpt.get() instanceof DCMIPeriod) {
233             DCMIPeriod p = (DCMIPeriod) temporalOpt.get();
234             return Optional.ofNullable(new Date[] { p.getStart(), p.getEnd() });
235           }
236         }
237         return Optional.empty();
238       }
239 
240       @Override
241       public Optional<Date> getTemporalInstant() {
242         if (temporalOpt.isPresent()) {
243           if (temporalOpt.get() instanceof Date) {
244             return temporalOpt;
245           }
246         }
247         return Optional.empty();
248       }
249 
250       @Override
251       public Optional<Long> getTemporalDuration() {
252         if (temporalOpt.isPresent()) {
253           if (temporalOpt.get() instanceof Long) {
254             return temporalOpt;
255           }
256         }
257         return Optional.empty();
258       }
259 
260       @Override
261       public Optional<Long> getExtent() {
262         return extent;
263       }
264 
265       @Override
266       public Optional<String> getLanguage() {
267         return language;
268       }
269 
270       @Override
271       public Optional<String> getIsPartOf() {
272         return isPartOf;
273       }
274 
275       @Override
276       public Optional<String> getReplaces() {
277         return replaces;
278       }
279 
280       @Override
281       public Optional<String> getType() {
282         return type;
283       }
284 
285       @Override
286       public Optional<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 java.util.function.Function<DublinCoreValue, Object> dc2temporalValueOption() {
357     return dcv -> {
358       Temporal temporal = EncodingSchemeUtils.decodeTemporal(dcv);
359       if (temporal == null) {
360         throw new RuntimeException(dcv
361             + " does not conform to ISO8601 encoding scheme for temporal.");
362       }
363       return temporal.fold(new Temporal.Match<Object>() {
364         @Override
365         public Object period(DCMIPeriod period) {
366           return period;
367         }
368 
369         @Override
370         public Object instant(Date instant) {
371           return instant;
372         }
373 
374         @Override
375         public Object duration(long duration) {
376           return duration;
377         }
378       });
379     };
380   }
381 
382   /**
383    * Return a function that creates a MetadataValue[String] from a DublinCoreValue setting its name to
384    * <code>name</code>.
385    */
386   private static Function<DublinCoreValue, MetadataValue<String>> dc2mvString(final String name) {
387     return dcv -> new MetadataValue<>(dcv.getValue(), name, dcv.getLanguage());
388   }
389 
390   private Optional<DublinCoreCatalog> load(Catalog catalog) {
391     InputStream in = null;
392     try {
393       URI uri = catalog.getURI();
394       if (serializer != null) {
395         uri = serializer.decodeURI(uri);
396       }
397       in = workspace.read(uri);
398       return Optional.of((DublinCoreCatalog) DublinCores.read(in));
399     } catch (Exception e) {
400       logger.warn("Unable to load metadata from catalog '{}'", catalog);
401       return Optional.empty();
402     } finally {
403       IOUtils.closeQuietly(in);
404     }
405   }
406 }