source: FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/utils/SimpleEndpointDescriptionParser.java @ 5547

Last change on this file since 5547 was 5547, checked in by Oliver Schonefeld, 10 years ago
  • remove obsolete code
  • Property svn:eol-style set to native
File size: 20.7 KB
Line 
1package eu.clarin.sru.server.fcs.utils;
2
3import java.io.IOException;
4import java.net.URI;
5import java.net.URISyntaxException;
6import java.net.URL;
7import java.util.ArrayList;
8import java.util.HashMap;
9import java.util.HashSet;
10import java.util.Iterator;
11import java.util.List;
12import java.util.Map;
13import java.util.Set;
14
15import javax.xml.XMLConstants;
16import javax.xml.namespace.NamespaceContext;
17import javax.xml.parsers.DocumentBuilder;
18import javax.xml.parsers.DocumentBuilderFactory;
19import javax.xml.parsers.ParserConfigurationException;
20import javax.xml.xpath.XPath;
21import javax.xml.xpath.XPathConstants;
22import javax.xml.xpath.XPathExpression;
23import javax.xml.xpath.XPathExpressionException;
24import javax.xml.xpath.XPathFactory;
25
26import org.slf4j.Logger;
27import org.slf4j.LoggerFactory;
28import org.w3c.dom.Document;
29import org.w3c.dom.Element;
30import org.w3c.dom.Node;
31import org.w3c.dom.NodeList;
32import org.xml.sax.SAXException;
33
34import eu.clarin.sru.server.SRUConfigException;
35import eu.clarin.sru.server.fcs.DataView;
36import eu.clarin.sru.server.fcs.DataView.DeliveryPolicy;
37import eu.clarin.sru.server.fcs.EndpointDescription;
38import eu.clarin.sru.server.fcs.ResourceInfo;
39
40
41/**
42 * A parser, that parses an XML file and produces a endpoint description with
43 * static list of resource info records. The XML file has the same format as the
44 * result format defined for endpoint description of the CLARIN-FCS
45 * specification. The {@link #parse(URL)} returns a
46 * {@link SimpleEndpointDescription} instance.
47 *
48 * @see EndpointDescription
49 * @see SimpleEndpointDescription
50 */
51public class SimpleEndpointDescriptionParser {
52    private static final String NS =
53            "http://clarin.eu/fcs/endpoint-description";
54    private static final String NS_LEGACY =
55            "http://clarin.eu/fcs/1.0/resource-info";
56    private static final String CAP_BASIC_SEARCH =
57            "http://clarin.eu/fcs/capability/basic-search";
58    private static final String LANG_EN = "en";
59    private static final String POLICY_SEND_DEFAULT = "send-by-default";
60    private static final String POLICY_NEED_REQUEST = "need-to-request";
61    private static final Logger logger =
62            LoggerFactory.getLogger(SimpleEndpointDescriptionParser.class);
63
64
65    /**
66     * Parse an XML file and return a static list of resource info records.
67     *
68     * @param url
69     *            the URI pointing to the file to be parsed
70     * @return an {@link EndpointDescription} instance
71     * @throws SRUConfigException
72     *             if an error occurred
73     */
74    public static EndpointDescription parse(URL url) throws SRUConfigException {
75        if (url == null) {
76            throw new NullPointerException("url == null");
77        }
78
79        logger.debug("parsing endpoint description from: {}", url);
80
81        try {
82            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
83            dbf.setNamespaceAware(true);
84            dbf.setCoalescing(true);
85            DocumentBuilder db = dbf.newDocumentBuilder();
86            Document doc = db.parse(url.openStream());
87
88            /*
89             * Detect for deprecated resource-info catalog files and bail, if necessary
90             */
91            checkLegacyMode(doc, url);
92
93            /*
94             * Parse on and create endpoint description ...
95             */
96            return parseEndpointDescription(doc);
97        } catch (ParserConfigurationException e) {
98            throw new SRUConfigException("internal error", e);
99        } catch (SAXException e) {
100            throw new SRUConfigException("parsing error", e);
101        } catch (IOException e) {
102            throw new SRUConfigException("error reading file", e);
103        } catch (XPathExpressionException e) {
104            throw new SRUConfigException("internal error", e);
105        }
106    }
107
108
109    private static EndpointDescription parseEndpointDescription(Document doc)
110            throws SRUConfigException, XPathExpressionException {
111        XPathFactory factory = XPathFactory.newInstance();
112        XPath xpath = factory.newXPath();
113
114        xpath.setNamespaceContext(new NamespaceContext() {
115            @Override
116            public Iterator<?> getPrefixes(String namespaceURI) {
117                throw new UnsupportedOperationException();
118            }
119
120            @Override
121            public String getPrefix(String namespaceURI) {
122                throw new UnsupportedOperationException();
123            }
124
125            @Override
126            public String getNamespaceURI(String prefix) {
127                if (prefix == null) {
128                    throw new NullPointerException("prefix == null");
129                }
130                if (prefix.equals("ed")) {
131                    return NS;
132                } else if (prefix.equals(XMLConstants.XML_NS_PREFIX)) {
133                    return XMLConstants.XML_NS_URI;
134                } else {
135                    return XMLConstants.NULL_NS_URI;
136                }
137            }
138        });
139
140        // capabilities
141        List<URI> capabilities = new ArrayList<URI>();
142        XPathExpression exp1 =
143                xpath.compile("//ed:Capabilities/ed:Capability");
144        NodeList list1 = (NodeList) exp1.evaluate(doc, XPathConstants.NODESET);
145        if ((list1 != null) && (list1.getLength() > 0)) {
146            logger.debug("parsing capabilities");
147            for (int i = 0; i < list1.getLength(); i++) {
148                String s = list1.item(i).getTextContent().trim();
149                try {
150                    URI uri = new URI(s);
151                    if (capabilities.contains(uri)) {
152                        logger.warn("ignoring duplicate capability " +
153                                "entry for '{}'", uri);
154                    }
155                    capabilities.add(uri);
156                } catch (URISyntaxException e) {
157                    throw new SRUConfigException("capability is not encoded " +
158                            "as proper URI: " + s);
159                }
160            }
161        } else {
162            logger.warn("No capabilities where defined in " +
163                    "endpoint configuration");
164        }
165        URI cap = URI.create(CAP_BASIC_SEARCH);
166        if (!capabilities.contains(cap)) {
167            logger.warn("capability '{}' was not defined in endpoint " +
168                    "description; added it to meet specification. Please " +
169                    "update your endpoint description!", CAP_BASIC_SEARCH);
170            capabilities.add(cap);
171        }
172        logger.debug("CAPS:'{}'", capabilities);
173
174        // supported data views
175        List<DataView> supportedDataViews = new ArrayList<DataView>();
176        XPathExpression exp2 =
177                xpath.compile("//ed:SupportedDataViews/ed:SupportedDataView");
178        NodeList list2 = (NodeList) exp2.evaluate(doc, XPathConstants.NODESET);
179        if ((list2 != null) && (list2.getLength() > 0)) {
180            logger.debug("parsing supported data views");
181            for (int i = 0; i < list2.getLength(); i++) {
182                Element item = (Element) list2.item(i);
183                String id = getAttribute(item, "id");
184                if (id == null) {
185                    throw new SRUConfigException("Element <SupportedDataView> "
186                            + "must carry a proper 'id' attribute");
187                }
188                String p = getAttribute(item, "delivery-policy");
189                if (p == null) {
190                    throw new SRUConfigException("Element <SupportedDataView> "
191                            + "must carry a 'delivery-policy' attribute");
192                }
193                DeliveryPolicy policy = null;
194                if (POLICY_SEND_DEFAULT.equals(p)) {
195                    policy = DeliveryPolicy.SEND_BY_DEFAULT;
196                } else if (POLICY_NEED_REQUEST.equals(p)) {
197                    policy = DeliveryPolicy.NEED_TO_REQUEST;
198                } else {
199                    throw new SRUConfigException("Invalid value '" + p +
200                            "' for attribute 'delivery-policy' on element " +
201                            "<SupportedDataView>");
202                }
203                String mimeType = item.getTextContent();
204                if (mimeType != null) {
205                    mimeType = mimeType.trim();
206                    if (mimeType.isEmpty()) {
207                        mimeType = null;
208                    }
209                }
210                if (mimeType == null) {
211                    throw new SRUConfigException("Element <SupportedDataView> "
212                            + "must contain a MIME-type as content");
213                }
214                // check for duplicate entries ...
215                for (DataView dataView : supportedDataViews) {
216                    if (id.equals(dataView.getIdentifier())) {
217                        throw new SRUConfigException(
218                                "A <SupportedDataView> with " + "the id '" +
219                                        id + "' is already defined!");
220                    }
221                    if (mimeType.equals(dataView.getMimeType())) {
222                        throw new SRUConfigException(
223                                "A <SupportedDataView> with " +
224                                        "the MIME-type '" + mimeType +
225                                        "' is already defined!");
226                    }
227                }
228                supportedDataViews.add(new DataView(id, mimeType, policy));
229            }
230        } else {
231            logger.error("Endpoint configuration contains no valid " +
232                    "information about supported data views");
233            throw new SRUConfigException("Endpoint configuration contains " +
234                    "no valid information about supported data views");
235        }
236
237        logger.debug("DV: {}", supportedDataViews);
238
239
240        // resources
241        XPathExpression x3 =
242                xpath.compile("/ed:EndpointDescription/ed:Resources/ed:Resource");
243        NodeList l3 = (NodeList) x3.evaluate(doc, XPathConstants.NODESET);
244        final Set<String> ids = new HashSet<String>();
245        List<ResourceInfo> resources =
246                parseRessources(xpath, l3, ids, supportedDataViews);
247        if ((resources == null) || resources.isEmpty()) {
248            throw new SRUConfigException("No resources where " +
249                    "defined in endpoint description");
250        }
251
252        return new SimpleEndpointDescription(capabilities,
253                supportedDataViews,
254                resources,
255                false);
256    }
257
258
259    private static List<ResourceInfo> parseRessources(XPath xpath,
260            NodeList nodes, Set<String> ids, List<DataView> supportedDataViews)
261            throws SRUConfigException, XPathExpressionException {
262      List<ResourceInfo> ris = null;
263      for (int k = 0; k < nodes.getLength(); k++) {
264          final Element node                = (Element) nodes.item(k);
265          String pid                        = null;
266          Map<String, String> titles        = null;
267          Map<String, String> descrs        = null;
268          String link                       = null;
269          List<String> langs                = null;
270          List<DataView> availableDataViews = null;
271          List<ResourceInfo> sub            = null;
272
273          pid = getAttribute(node, "pid");
274          if (pid == null) {
275              throw new SRUConfigException("Element <ResourceInfo> " +
276                      "must carry a proper 'pid' attribute");
277          }
278          if (ids.contains(pid)) {
279              throw new SRUConfigException("Another element <Resource> " +
280                      "with pid '" + pid + "' already exists");
281          }
282          ids.add(pid);
283
284          XPathExpression x1 = xpath.compile("ed:Title");
285          NodeList l1 = (NodeList) x1.evaluate(node, XPathConstants.NODESET);
286          if ((l1 != null) && (l1.getLength() > 0)) {
287              for (int i = 0; i < l1.getLength(); i++) {
288                  final Element n = (Element) l1.item(i);
289
290                  final String lang = getLangAttribute(n);
291                  if (lang == null) {
292                      throw new SRUConfigException("Element <Title> must " +
293                              "carry a proper 'xml:lang' attribute");
294                  }
295
296                  final String title = cleanString(n.getTextContent());
297                  if (title == null) {
298                      throw new SRUConfigException("Element <Title> must " +
299                              "carry a non-empty 'xml:lang' attribute");
300                  }
301
302                  if (titles == null) {
303                      titles = new HashMap<String, String>();
304                  }
305                  if (titles.containsKey(lang)) {
306                      logger.warn("title with language '{}' already exists",
307                              lang);
308                  } else {
309                      logger.debug("title: '{}' '{}'", lang, title);
310                      titles.put(lang, title);
311                  }
312              }
313              if ((titles != null) && !titles.containsKey(LANG_EN)) {
314                  throw new SRUConfigException(
315                          "A <Title> with language 'en' is mandatory");
316              }
317          }
318
319          XPathExpression x2 = xpath.compile("ed:Description");
320          NodeList l2 = (NodeList) x2.evaluate(node, XPathConstants.NODESET);
321          if ((l2 != null) && (l2.getLength() > 0)) {
322              for (int i = 0; i < l2.getLength(); i++) {
323                  Element n = (Element) l2.item(i);
324
325                  String lang = getLangAttribute(n);
326                  if (lang == null) {
327                      throw new SRUConfigException("Element <Description> " +
328                              "must carry a proper 'xml:lang' attribute");
329
330                  }
331                  String desc = cleanString(n.getTextContent());
332
333                  if (descrs == null) {
334                      descrs = new HashMap<String, String>();
335                  }
336
337                  if (descrs.containsKey(lang)) {
338                      logger.warn("description with language '{}' "
339                              + "already exists", lang);
340                  } else {
341                      logger.debug("description: '{}' '{}'", lang, desc);
342                      descrs.put(lang, desc);
343                  }
344              }
345              if ((descrs != null) && !descrs.containsKey(LANG_EN)) {
346                  throw new SRUConfigException(
347                          "A <Description> with language 'en' is mandatory");
348              }
349          }
350
351          XPathExpression x3 = xpath.compile("ed:LandingPageURI");
352          NodeList l3 = (NodeList) x3.evaluate(node, XPathConstants.NODESET);
353          if ((l3 != null) && (l3.getLength() > 0)) {
354              for (int i = 0; i < l3.getLength(); i++) {
355                  Element n = (Element) l3.item(i);
356                  link = cleanString(n.getTextContent());
357              }
358          }
359
360          XPathExpression x4 = xpath.compile("ed:Languages/ed:Language");
361          NodeList l4 = (NodeList) x4.evaluate(node, XPathConstants.NODESET);
362          if ((l4 != null) && (l4.getLength() > 0)) {
363              for (int i = 0; i < l4.getLength(); i++) {
364                  Element n = (Element) l4.item(i);
365
366                  String s = n.getTextContent();
367                  if (s != null) {
368                      s = s.trim();
369                      if (s.isEmpty()) {
370                          s = null;
371                      }
372                  }
373
374                  /*
375                   * enforce three letter codes
376                   */
377                  if ((s == null) || (s.length() != 3)) {
378                      throw new SRUConfigException("Element <Language> " +
379                              "must use ISO-632-3 three letter " +
380                              "language codes");
381                  }
382
383                  if (langs == null) {
384                      langs = new ArrayList<String>();
385                  }
386                  langs.add(s);
387              }
388          }
389
390          XPathExpression x5 = xpath.compile("ed:AvailableDataViews");
391          Node n = (Node) x5.evaluate(node, XPathConstants.NODE);
392          if ((n != null) && (n instanceof Element)) {
393              String ref = getAttribute((Element) n, "ref");
394              if (ref == null) {
395                  throw new SRUConfigException("Element <AvailableDataViews> " +
396                          "must carry a 'ref' attribute");
397              }
398              String[] refs = ref.split("\\s+");
399              if ((refs == null) || (refs.length < 1)) {
400                  throw new SRUConfigException("Attribute 'ref' on element " +
401                          "<AvailableDataViews> must contain a whitespace " +
402                          "seperated list of data view references");
403              }
404
405
406              for (int i = 0; i < refs.length; i++) {
407                  DataView dataview = null;
408                  for (DataView dv : supportedDataViews) {
409                      if (refs[i].equals(dv.getIdentifier())) {
410                          dataview = dv;
411                          break;
412                      }
413                  }
414                  if (dataview != null) {
415                      if (availableDataViews == null) {
416                          availableDataViews = new ArrayList<DataView>();
417                      }
418                      availableDataViews.add(dataview);
419                  } else {
420                      throw new SRUConfigException("A data view with " +
421                              "identifier '" + refs[i] + "' was not defined " +
422                              "in <SupportedDataViews>");
423                  }
424              }
425          } else {
426              throw new SRUConfigException(
427                      "missing element <ed:AvailableDataViews>");
428          }
429          if (availableDataViews == null) {
430              throw new SRUConfigException("No available data views where " +
431                      "defined for resource with PID '" + pid + "'");
432          }
433
434          XPathExpression x6 = xpath.compile("ed:Resources/ed:Resource");
435          NodeList l6 = (NodeList) x6.evaluate(node, XPathConstants.NODESET);
436          if ((l6 != null) && (l6.getLength() > 0)) {
437              sub = parseRessources(xpath, l6, ids, supportedDataViews);
438          }
439
440          if (ris == null) {
441              ris = new ArrayList<ResourceInfo>();
442          }
443          ris.add(new ResourceInfo(pid,
444                  titles,
445                  descrs,
446                  link,
447                  langs,
448                  availableDataViews,
449                  sub));
450      }
451      return ris;
452    }
453
454
455    private static String getAttribute(Element el, String localName) {
456        String lang = el.getAttribute(localName);
457        if (lang != null) {
458            lang = lang.trim();
459            if (!lang.isEmpty()) {
460                return lang;
461            }
462        }
463        return null;
464    }
465
466
467    private static String getLangAttribute(Element el) {
468        String lang = el.getAttributeNS(XMLConstants.XML_NS_URI, "lang");
469        if (lang != null) {
470            lang = lang.trim();
471            if (!lang.isEmpty()) {
472                return lang;
473            }
474        }
475        return null;
476    }
477
478
479    private static String cleanString(String s) {
480        if (s != null) {
481            s = s.trim();
482            if (!s.isEmpty()) {
483                StringBuilder sb = new StringBuilder();
484                for (String z : s.split("\\s*\\n+\\s*")) {
485                    z = z.trim();
486                    if (!z.isEmpty()) {
487                        if (sb.length() > 0) {
488                            sb.append(' ');
489                        }
490                        sb.append(z);
491                    }
492                }
493                if (sb.length() > 0) {
494                    return sb.toString();
495                }
496            }
497        }
498        return null;
499    }
500
501
502    private static void checkLegacyMode(Document doc, URL url)
503            throws SRUConfigException {
504        Element root = doc.getDocumentElement();
505        if (root != null) {
506            String ns = root.getNamespaceURI();
507            if (ns != null) {
508                if (ns.equals(NS_LEGACY)) {
509                    logger.error("Detected out-dated " +
510                            "resource info catalog file '" + url +
511                            "'. Please update to the " +
512                            "current version");
513                    throw new SRUConfigException("unsupport file format: " + ns);
514                } else if (!ns.equals(NS)) {
515                    logger.error("Detected unsupported resource info " +
516                            "catalog file '" + url + "' with namespace '" + ns + '"');
517                    throw new SRUConfigException("unsupport file format: " + ns);
518                }
519            } else {
520                throw new SRUConfigException("No namespace URI was detected " +
521                        "for resource info catalog file '" + url +"'!");
522            }
523        } else {
524            throw new SRUConfigException("Error retrieving root element");
525        }
526    }
527
528} // class SimpleResourceInfoInventoryParser
Note: See TracBrowser for help on using the repository browser.