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     SAXParserFactory factory = XmlSafeParser.newSAXParserFactory();
154     // REPLAY does not use a DTD here
155     factory.setValidating(false);
156     factory.setNamespaceAware(true);
157     SAXParser parser = factory.newSAXParser();
158     parser.parse(is, this);
159 
160     // Did we parse an mpeg-7 document?
161     if (!isMpeg7)
162       throw new IllegalArgumentException("Content of input stream is not mpeg-7");
163     return mpeg7Doc;
164   }
165 
166   /**
167    * Read <code>type</code> attribute from track or catalog element.
168    *
169    * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
170    *      org.xml.sax.Attributes)
171    */
172   @Override
173   public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
174     super.startElement(uri, localName, name, attributes);
175     tagContent = new StringBuffer();
176 
177     // Make sure this is an mpeg-7 catalog
178     // TODO: Improve this test, add namespace awareness
179     if (!isMpeg7 && "Mpeg7".equals(name))
180       isMpeg7 = true;
181 
182     // Handle parser state
183     if ("MultimediaContent".equals(localName)) {
184       state = ParserState.MultimediaContent;
185     }
186 
187     // Content type
188     if ("Audio".equals(localName) || "Video".equals(localName) || "AudioVisual".equals(localName)) {
189       contentType = MultimediaContentType.Type.valueOf(localName);
190       contentId = attributes.getValue("id");
191       if (MultimediaContentType.Type.Audio.equals(contentType))
192         multimediaContent = mpeg7Doc.addAudioContent(contentId, mediaTime, mediaLocator);
193       else if (MultimediaContentType.Type.Video.equals(contentType))
194         multimediaContent = mpeg7Doc.addVideoContent(contentId, mediaTime, mediaLocator);
195       else if (MultimediaContentType.Type.AudioVisual.equals(contentType))
196         multimediaContent = mpeg7Doc.addAudioVisualContent(contentId, mediaTime, mediaLocator);
197     }
198 
199     // Temporal decomposition
200     if ("TemporalDecomposition".equals(localName)) {
201       String hasGap = attributes.getValue("gap");
202       String isOverlapping = attributes.getValue("overlap");
203       String criteria = attributes.getValue("criteria");
204       if (!"temporal".equals(criteria))
205         throw new IllegalStateException("Decompositions other than temporal are not supported");
206       temporalDecomposition = multimediaContent.getTemporalDecomposition();
207       temporalDecomposition.setGap("true".equals(hasGap));
208       temporalDecomposition.setOverlapping("overlap".equals(isOverlapping));
209     }
210 
211     // Segment
212     if ("AudioSegment".equals(localName) || "VideoSegment".equals(localName) || "AudioVisualSegment".equals(localName)) {
213       String segmentId = attributes.getValue("id");
214       segment = temporalDecomposition.createSegment(segmentId);
215       state = ParserState.Segment;
216     }
217 
218     // TextAnnotation
219     if ("TextAnnotation".equals(localName)) {
220       String language = attributes.getValue("xml:lang");
221       float confidence = -1.0f;
222       float relevance = -1.0f;
223       try {
224         confidence = floatFormat.parse(attributes.getValue("confidence")).floatValue();
225       } catch (Exception e) {
226         confidence = -1.0f;
227       }
228       try {
229         relevance = floatFormat.parse(attributes.getValue("relevance")).floatValue();
230       } catch (Exception e) {
231         relevance = -1.0f;
232       }
233       textAnnotation = segment.createTextAnnotation(confidence, relevance, language);
234     }
235 
236     // Spatiotemporal decomposition
237     if ("SpatioTemporalDecomposition".equals(localName)) {
238       String hasGap = attributes.getValue("gap");
239       String isOverlapping = attributes.getValue("overlap");
240       if (!(segment instanceof VideoSegment))
241         throw new IllegalStateException("Can't have a spatio temporal decomposition outside of a video segment");
242       boolean gap = "true".equalsIgnoreCase(attributes.getValue("gap"));
243       boolean overlap = "true".equalsIgnoreCase(attributes.getValue("overlap"));
244       spatioTemporalDecomposition = ((VideoSegment) segment).createSpatioTemporalDecomposition(gap, overlap);
245       spatioTemporalDecomposition.setGap("true".equals(hasGap));
246       spatioTemporalDecomposition.setOverlapping("overlap".equals(isOverlapping));
247     }
248 
249     // Video Text
250     if ("VideoText".equals(localName)) {
251       String id = attributes.getValue("id");
252       videoText = new VideoTextImpl(id);
253       state = ParserState.VideoText;
254     }
255 
256     // Textual
257     if ("Text".equals(localName)) {
258       String language = attributes.getValue("xml:lang");
259       textual = new TextualImpl();
260       textual.setLanguage(language);
261     }
262 
263   }
264 
265   /**
266    * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
267    */
268   @Override
269   public void endElement(String uri, String localName, String name) throws SAXException {
270     super.endElement(uri, localName, name);
271 
272     // Handle parser state
273     if ("MultimediaContent".equals(localName))
274       state = ParserState.Document;
275     else if ("AudioSegment".equals(localName) || "VideoSegment".equals(localName)
276             || "AudioVisualSegment".equals(localName))
277       state = ParserState.MultimediaContent;
278 
279     // Media locator uri
280     if ("MediaUri".equals(localName)) {
281       MediaLocatorImpl locator = new MediaLocatorImpl();
282       URI mediaUri = URI.create(getTagContent());
283       locator.setMediaURI(mediaUri);
284       if (ParserState.MultimediaContent.equals(state)) {
285         multimediaContent.setMediaLocator(locator);
286       }
287     }
288 
289     // Media/Segment time
290     if ("MediaTime".equals(localName)) {
291       if (ParserState.MultimediaContent.equals(state)) {
292         mediaTime = new MediaTimeImpl(mediaTimePoint, mediaDuration);
293         multimediaContent.setMediaTime(mediaTime);
294       } else if (ParserState.Segment.equals(state)) {
295         mediaTime = new MediaTimeImpl(mediaTimePoint, mediaDuration);
296         segment.setMediaTime(mediaTime);
297       } else if (ParserState.VideoText.equals(state)) {
298         SpatioTemporalLocator spatioTemporalLocator = new SpatioTemporalLocatorImpl(mediaTime);
299         videoText.setSpatioTemporalLocator(spatioTemporalLocator);
300       }
301     }
302 
303     // Media/Segment time point
304     if ("MediaTimePoint".equals(localName)) {
305       mediaTimePoint = MediaTimePointImpl.parseTimePoint(getTagContent());
306       if (ParserState.MultimediaContent.equals(state)) {
307         contentTimePoint = mediaTimePoint;
308       }
309     }
310 
311     // Media/Segment time point
312     if ("MediaRelTimePoint".equals(localName)) {
313       MediaRelTimePointImpl tp = MediaRelTimePointImpl.parseTimePoint(getTagContent());
314       mediaTimePoint = tp;
315       if (ParserState.MultimediaContent.equals(state))
316         contentTimePoint = tp;
317       else if (ParserState.Segment.equals(state)) {
318         tp.setReferenceTimePoint(contentTimePoint);
319       }
320     }
321 
322     // Media/Segment duration
323     if ("MediaDuration".equals(localName)) {
324       mediaDuration = MediaDurationImpl.parseDuration(getTagContent());
325     }
326 
327     // Keyword
328     if ("Keyword".equals(localName)) {
329       KeywordAnnotation keyword = new KeywordAnnotationImpl(tagContent.toString());
330       textAnnotation.addKeywordAnnotation(keyword);
331     }
332 
333     // Free text
334     if ("FreeTextAnnotation".equals(localName)) {
335       FreeTextAnnotation freeText = new FreeTextAnnotationImpl(tagContent.toString());
336       textAnnotation.addFreeTextAnnotation(freeText);
337     }
338 
339     // Video Text
340     if ("VideoText".equals(localName)) {
341       spatioTemporalDecomposition.addVideoText(videoText);
342     }
343 
344     // SpatioTemporalLocator
345     if ("SpatioTemporalLocator".equals(localName)) {
346       videoText.setSpatioTemporalLocator(new SpatioTemporalLocatorImpl(mediaTime));
347     }
348 
349     // Videotext text
350     if ("Text".equals(localName)) {
351       textual.setText(tagContent.toString());
352       videoText.setText(textual);
353     }
354 
355     // Videotext bouding box
356     if ("Box".equals(localName)) {
357       String[] coords = tagContent.toString().trim().split(" ");
358       if (coords.length != 4)
359         throw new IllegalStateException("Box coordinates '" + tagContent + "' is malformatted");
360       int[] coordsL = new int[4];
361       for (int i = 0; i < 4; i++)
362         try {
363           coordsL[i] = (int) floatFormat.parse(coords[i]).floatValue();
364         } catch (ParseException e) {
365           throw new SAXException(e);
366         }
367       videoText.setBoundary(new Rectangle(coordsL[0], coordsL[1], (coordsL[2] - coordsL[0]), coordsL[3] - coordsL[1]));
368     }
369 
370   }
371 
372   /**
373    * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
374    */
375   @Override
376   public void characters(char[] ch, int start, int length) throws SAXException {
377     super.characters(ch, start, length);
378     tagContent.append(ch, start, length);
379   }
380 
381   /**
382    * Returns the element content.
383    *
384    * @return the element content
385    */
386   private String getTagContent() {
387     String str = tagContent.toString().trim();
388     return str;
389   }
390 
391   /**
392    * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
393    */
394   @Override
395   public void error(SAXParseException e) throws SAXException {
396     logger.warn("Error while parsing mpeg-7 catalog: " + e.getMessage());
397     super.error(e);
398   }
399 
400   /**
401    * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException)
402    */
403   @Override
404   public void fatalError(SAXParseException e) throws SAXException {
405     logger.warn("Fatal error while parsing mpeg-7 catalog: " + e.getMessage());
406     super.fatalError(e);
407   }
408 
409   /**
410    * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException)
411    */
412   @Override
413   public void warning(SAXParseException e) throws SAXException {
414     logger.warn("Warning while parsing mpeg-7 catalog: " + e.getMessage());
415     super.warning(e);
416   }
417 
418 }