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  
23  package org.opencastproject.metadata.mpeg7;
24  
25  import org.opencastproject.util.XmlSafeParser;
26  
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  import org.xml.sax.Attributes;
30  import org.xml.sax.SAXException;
31  import org.xml.sax.SAXParseException;
32  import org.xml.sax.helpers.DefaultHandler;
33  
34  import java.awt.Rectangle;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.net.URI;
38  import java.text.DecimalFormat;
39  import java.text.DecimalFormatSymbols;
40  import java.text.ParseException;
41  import java.util.Locale;
42  
43  import javax.xml.parsers.ParserConfigurationException;
44  import javax.xml.parsers.SAXParser;
45  import javax.xml.parsers.SAXParserFactory;
46  
47  /**
48   * Parser implementation for mpeg-7 files. Note that this implementation does by far not cover the full mpeg-7 standard
49   * but only deals with those parts relevant to Opencast, mainly temporal decompositions.
50   */
51  public class Mpeg7Parser extends DefaultHandler {
52  
53    /** the logging facility */
54    private static final Logger logger = LoggerFactory.getLogger(Mpeg7Parser.class.getName());
55  
56    /** The current parser state */
57    enum ParserState {
58      Document, MultimediaContent, Segment, VideoText
59    };
60  
61    /**
62     * Number formatter, used to deal with relevance values in a locale
63     * independent way
64     */
65    private static DecimalFormatSymbols standardSymbols = new DecimalFormatSymbols(Locale.US);
66  
67    /** The manifest */
68    private Mpeg7CatalogImpl mpeg7Doc = null;
69  
70    /** The element content */
71    private StringBuffer tagContent = new StringBuffer();
72  
73    /** The multimedia content */
74    private MultimediaContentType multimediaContent = null;
75  
76    /** Current multimedia content type (audio, video, audiovisual) */
77    private MultimediaContentType.Type contentType = null;
78  
79    /** The multimedia content identifier */
80    private String contentId = null;
81  
82    /** The media locator */
83    private MediaLocator mediaLocator = null;
84  
85    /** The content media time point (will usually refer to 0:00:00) */
86    private MediaTimePoint contentTimePoint = null;
87  
88    /** The time point (relative to the content time point) */
89    private MediaTimePoint mediaTimePoint = null;
90  
91    /** The duration */
92    private MediaDuration mediaDuration = null;
93  
94    /** The media time and duration */
95    private MediaTime mediaTime = null;
96  
97    /** The temporal decomposition container */
98    private TemporalDecomposition<?> temporalDecomposition = null;
99  
100   /** The temporal segment */
101   private Segment segment = null;
102 
103   /** The spatio temporal decomposition container */
104   private SpatioTemporalDecomposition spatioTemporalDecomposition = null;
105 
106   /** The text annoation */
107   private TextAnnotation textAnnotation = null;
108 
109   /** The videotext element */
110   private VideoText videoText = null;
111 
112   /** The videotext text */
113   private Textual textual = null;
114 
115   /** The current parser state */
116   private Mpeg7Parser.ParserState state = ParserState.Document;
117 
118   /** Flag to check if this is not just an arbitrary xml document */
119   private boolean isMpeg7 = false;
120 
121   private DecimalFormat floatFormat = new DecimalFormat();
122 
123   /**
124    * Creates a new parser for mpeg-7 files.
125    */
126   public Mpeg7Parser() {
127     floatFormat.setDecimalFormatSymbols(standardSymbols);
128   }
129 
130   public Mpeg7Parser(Mpeg7CatalogImpl catalog) {
131     this.mpeg7Doc = catalog;
132     floatFormat.setDecimalFormatSymbols(standardSymbols);
133   }
134 
135   /**
136    * Parses the mpeg-7 catalog file and returns its object representation.
137    *
138    * @param is
139    *          the input stream containing the catalog
140    * @return the catalog representation
141    * @throws ParserConfigurationException
142    *           if setting up the parser failed
143    * @throws SAXException
144    *           if an error occured while parsing the document
145    * @throws IOException
146    *           if the file cannot be accessed in a proper way
147    * @throws IllegalArgumentException
148    *           if the provided file does not contain mpeg-7 data
149    */
150   public Mpeg7CatalogImpl parse(InputStream is) throws ParserConfigurationException, SAXException, IOException {
151     if (mpeg7Doc == null) {
152       mpeg7Doc = new Mpeg7CatalogImpl();
153     }
154     SAXParserFactory factory = XmlSafeParser.newSAXParserFactory();
155     // REPLAY does not use a DTD here
156     factory.setValidating(false);
157     factory.setNamespaceAware(true);
158     SAXParser parser = factory.newSAXParser();
159     parser.parse(is, this);
160 
161     // Did we parse an mpeg-7 document?
162     if (!isMpeg7) {
163       throw new IllegalArgumentException("Content of input stream is not mpeg-7");
164     }
165     return mpeg7Doc;
166   }
167 
168   /**
169    * Read <code>type</code> attribute from track or catalog element.
170    *
171    * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
172    *      org.xml.sax.Attributes)
173    */
174   @Override
175   public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
176     super.startElement(uri, localName, name, attributes);
177     tagContent = new StringBuffer();
178 
179     // Make sure this is an mpeg-7 catalog
180     // TODO: Improve this test, add namespace awareness
181     if (!isMpeg7 && "Mpeg7".equals(name)) {
182       isMpeg7 = true;
183     }
184 
185     // Handle parser state
186     if ("MultimediaContent".equals(localName)) {
187       state = ParserState.MultimediaContent;
188     }
189 
190     // Content type
191     if ("Audio".equals(localName) || "Video".equals(localName) || "AudioVisual".equals(localName)) {
192       contentType = MultimediaContentType.Type.valueOf(localName);
193       contentId = attributes.getValue("id");
194       if (MultimediaContentType.Type.Audio.equals(contentType)) {
195         multimediaContent = mpeg7Doc.addAudioContent(contentId, mediaTime, mediaLocator);
196       } else if (MultimediaContentType.Type.Video.equals(contentType)) {
197         multimediaContent = mpeg7Doc.addVideoContent(contentId, mediaTime, mediaLocator);
198       } else if (MultimediaContentType.Type.AudioVisual.equals(contentType)) {
199         multimediaContent = mpeg7Doc.addAudioVisualContent(contentId, mediaTime, mediaLocator);
200       }
201     }
202 
203     // Temporal decomposition
204     if ("TemporalDecomposition".equals(localName)) {
205       String hasGap = attributes.getValue("gap");
206       String isOverlapping = attributes.getValue("overlap");
207       String criteria = attributes.getValue("criteria");
208       if (!"temporal".equals(criteria)) {
209         throw new IllegalStateException("Decompositions other than temporal are not supported");
210       }
211       temporalDecomposition = multimediaContent.getTemporalDecomposition();
212       temporalDecomposition.setGap("true".equals(hasGap));
213       temporalDecomposition.setOverlapping("overlap".equals(isOverlapping));
214     }
215 
216     // Segment
217     if ("AudioSegment".equals(localName) || "VideoSegment".equals(localName)
218         || "AudioVisualSegment".equals(localName)) {
219       String segmentId = attributes.getValue("id");
220       segment = temporalDecomposition.createSegment(segmentId);
221       state = ParserState.Segment;
222     }
223 
224     // TextAnnotation
225     if ("TextAnnotation".equals(localName)) {
226       String language = attributes.getValue("xml:lang");
227       float confidence = -1.0f;
228       float relevance = -1.0f;
229       try {
230         confidence = floatFormat.parse(attributes.getValue("confidence")).floatValue();
231       } catch (Exception e) {
232         confidence = -1.0f;
233       }
234       try {
235         relevance = floatFormat.parse(attributes.getValue("relevance")).floatValue();
236       } catch (Exception e) {
237         relevance = -1.0f;
238       }
239       textAnnotation = segment.createTextAnnotation(confidence, relevance, language);
240     }
241 
242     // Spatiotemporal decomposition
243     if ("SpatioTemporalDecomposition".equals(localName)) {
244       String hasGap = attributes.getValue("gap");
245       String isOverlapping = attributes.getValue("overlap");
246       if (!(segment instanceof VideoSegment)) {
247         throw new IllegalStateException("Can't have a spatio temporal decomposition outside of a video segment");
248       }
249       boolean gap = "true".equalsIgnoreCase(attributes.getValue("gap"));
250       boolean overlap = "true".equalsIgnoreCase(attributes.getValue("overlap"));
251       spatioTemporalDecomposition = ((VideoSegment) segment).createSpatioTemporalDecomposition(gap, overlap);
252       spatioTemporalDecomposition.setGap("true".equals(hasGap));
253       spatioTemporalDecomposition.setOverlapping("overlap".equals(isOverlapping));
254     }
255 
256     // Video Text
257     if ("VideoText".equals(localName)) {
258       String id = attributes.getValue("id");
259       videoText = new VideoTextImpl(id);
260       state = ParserState.VideoText;
261     }
262 
263     // Textual
264     if ("Text".equals(localName)) {
265       String language = attributes.getValue("xml:lang");
266       textual = new TextualImpl();
267       textual.setLanguage(language);
268     }
269 
270   }
271 
272   /**
273    * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
274    */
275   @Override
276   public void endElement(String uri, String localName, String name) throws SAXException {
277     super.endElement(uri, localName, name);
278 
279     // Handle parser state
280     if ("MultimediaContent".equals(localName)) {
281       state = ParserState.Document;
282     } else if ("AudioSegment".equals(localName) || "VideoSegment".equals(localName)
283             || "AudioVisualSegment".equals(localName)) {
284       state = ParserState.MultimediaContent;
285     }
286 
287     // Media locator uri
288     if ("MediaUri".equals(localName)) {
289       MediaLocatorImpl locator = new MediaLocatorImpl();
290       URI mediaUri = URI.create(getTagContent());
291       locator.setMediaURI(mediaUri);
292       if (ParserState.MultimediaContent.equals(state)) {
293         multimediaContent.setMediaLocator(locator);
294       }
295     }
296 
297     // Media/Segment time
298     if ("MediaTime".equals(localName)) {
299       if (ParserState.MultimediaContent.equals(state)) {
300         mediaTime = new MediaTimeImpl(mediaTimePoint, mediaDuration);
301         multimediaContent.setMediaTime(mediaTime);
302       } else if (ParserState.Segment.equals(state)) {
303         mediaTime = new MediaTimeImpl(mediaTimePoint, mediaDuration);
304         segment.setMediaTime(mediaTime);
305       } else if (ParserState.VideoText.equals(state)) {
306         SpatioTemporalLocator spatioTemporalLocator = new SpatioTemporalLocatorImpl(mediaTime);
307         videoText.setSpatioTemporalLocator(spatioTemporalLocator);
308       }
309     }
310 
311     // Media/Segment time point
312     if ("MediaTimePoint".equals(localName)) {
313       mediaTimePoint = MediaTimePointImpl.parseTimePoint(getTagContent());
314       if (ParserState.MultimediaContent.equals(state)) {
315         contentTimePoint = mediaTimePoint;
316       }
317     }
318 
319     // Media/Segment time point
320     if ("MediaRelTimePoint".equals(localName)) {
321       MediaRelTimePointImpl tp = MediaRelTimePointImpl.parseTimePoint(getTagContent());
322       mediaTimePoint = tp;
323       if (ParserState.MultimediaContent.equals(state)) {
324         contentTimePoint = tp;
325       } else if (ParserState.Segment.equals(state)) {
326         tp.setReferenceTimePoint(contentTimePoint);
327       }
328     }
329 
330     // Media/Segment duration
331     if ("MediaDuration".equals(localName)) {
332       mediaDuration = MediaDurationImpl.parseDuration(getTagContent());
333     }
334 
335     // Keyword
336     if ("Keyword".equals(localName)) {
337       KeywordAnnotation keyword = new KeywordAnnotationImpl(tagContent.toString());
338       textAnnotation.addKeywordAnnotation(keyword);
339     }
340 
341     // Free text
342     if ("FreeTextAnnotation".equals(localName)) {
343       FreeTextAnnotation freeText = new FreeTextAnnotationImpl(tagContent.toString());
344       textAnnotation.addFreeTextAnnotation(freeText);
345     }
346 
347     // Video Text
348     if ("VideoText".equals(localName)) {
349       spatioTemporalDecomposition.addVideoText(videoText);
350     }
351 
352     // SpatioTemporalLocator
353     if ("SpatioTemporalLocator".equals(localName)) {
354       videoText.setSpatioTemporalLocator(new SpatioTemporalLocatorImpl(mediaTime));
355     }
356 
357     // Videotext text
358     if ("Text".equals(localName)) {
359       textual.setText(tagContent.toString());
360       videoText.setText(textual);
361     }
362 
363     // Videotext bouding box
364     if ("Box".equals(localName)) {
365       String[] coords = tagContent.toString().trim().split(" ");
366       if (coords.length != 4) {
367         throw new IllegalStateException("Box coordinates '" + tagContent + "' is malformatted");
368       }
369       int[] coordsL = new int[4];
370       for (int i = 0; i < 4; i++) {
371         try {
372           coordsL[i] = (int) floatFormat.parse(coords[i]).floatValue();
373         } catch (ParseException e) {
374           throw new SAXException(e);
375         }
376       }
377       videoText.setBoundary(new Rectangle(coordsL[0], coordsL[1], (coordsL[2] - coordsL[0]), coordsL[3] - coordsL[1]));
378     }
379 
380   }
381 
382   /**
383    * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
384    */
385   @Override
386   public void characters(char[] ch, int start, int length) throws SAXException {
387     super.characters(ch, start, length);
388     tagContent.append(ch, start, length);
389   }
390 
391   /**
392    * Returns the element content.
393    *
394    * @return the element content
395    */
396   private String getTagContent() {
397     String str = tagContent.toString().trim();
398     return str;
399   }
400 
401   /**
402    * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
403    */
404   @Override
405   public void error(SAXParseException e) throws SAXException {
406     logger.warn("Error while parsing mpeg-7 catalog: " + e.getMessage());
407     super.error(e);
408   }
409 
410   /**
411    * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException)
412    */
413   @Override
414   public void fatalError(SAXParseException e) throws SAXException {
415     logger.warn("Fatal error while parsing mpeg-7 catalog: " + e.getMessage());
416     super.fatalError(e);
417   }
418 
419   /**
420    * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException)
421    */
422   @Override
423   public void warning(SAXParseException e) throws SAXException {
424     logger.warn("Warning while parsing mpeg-7 catalog: " + e.getMessage());
425     super.warning(e);
426   }
427 
428 }