source: SRUClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescriptionParser.java @ 6917

Last change on this file since 6917 was 6917, checked in by Oliver Schonefeld, 8 years ago
  • enhance endpoint description parser
  • Property svn:eol-style set to native
File size: 23.4 KB
Line 
1/**
2 * This software is copyright (c) 2012-2014 by
3 *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
4 * This is free software. You can redistribute it
5 * and/or modify it under the terms described in
6 * the GNU General Public License v3 of which you
7 * should have received a copy. Otherwise you can download
8 * it from
9 *
10 *   http://www.gnu.org/licenses/gpl-3.0.txt
11 *
12 * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
13 *
14 * @license http://www.gnu.org/licenses/gpl-3.0.txt
15 *  GNU General Public License v3
16 */
17package eu.clarin.sru.client.fcs;
18
19import java.net.URI;
20import java.net.URISyntaxException;
21import java.util.ArrayList;
22import java.util.HashMap;
23import java.util.List;
24import java.util.Map;
25
26import javax.xml.XMLConstants;
27import javax.xml.namespace.QName;
28import javax.xml.stream.XMLStreamException;
29import javax.xml.stream.XMLStreamReader;
30
31import org.slf4j.Logger;
32import org.slf4j.LoggerFactory;
33
34import eu.clarin.sru.client.SRUClientException;
35import eu.clarin.sru.client.SRUExtraResponseData;
36import eu.clarin.sru.client.SRUExtraResponseDataParser;
37import eu.clarin.sru.client.XmlStreamReaderUtils;
38import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.DataView;
39import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.DataView.DeliveryPolicy;
40import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.Layer;
41import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.Layer.ContentEncoding;
42import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.ResourceInfo;
43
44
45/**
46 * An extra response data parser for parsing CLARIN-FCS endpoint descriptions.
47 */
48public class ClarinFCSEndpointDescriptionParser implements
49        SRUExtraResponseDataParser {
50    /**
51     * constant for infinite resource enumeration parsing depth
52     */
53    public static final int INFINITE_MAX_DEPTH = -1;
54    /**
55     * constant for default parsing resource enumeration parsing depth
56     */
57    public static final int DEFAULT_MAX_DEPTH  = INFINITE_MAX_DEPTH;
58    private static final Logger logger =
59            LoggerFactory.getLogger(ClarinFCSClientBuilder.class);
60    private static final String ED_NS_URI =
61            "http://clarin.eu/fcs/endpoint-description";
62    private static final QName ED_ROOT_ELEMENT =
63            new QName(ED_NS_URI, "EndpointDescription");
64    private static final int VERSION_1 = 1;
65    private static final int VERSION_2 = 2;
66    private static final String CAPABILITY_PREFIX =
67            "http://clarin.eu/fcs/capability/";
68    private static final URI CAPABILITY_BASIC_SEARCH =
69            URI.create("http://clarin.eu/fcs/capability/basic-search");
70    private static final URI CAPABILITY_ADVANCED_SEARCH =
71            URI.create("http://clarin.eu/fcs/capability/advanced-search");
72    private static final String MIMETYPE_HITS_DATAVIEW =
73            "application/x-clarin-fcs-hits+xml";
74    private final int maxDepth;
75
76
77    /**
78     * Constructor. By default, the parser will parse the endpoint resource
79     * enumeration to an infinite depth.
80     */
81    public ClarinFCSEndpointDescriptionParser() {
82        this(DEFAULT_MAX_DEPTH);
83    }
84
85
86    /**
87     * Constructor.
88     *
89     * @param maxDepth
90     *            maximum depth for parsing the endpoint resource enumeration.
91     * @throws IllegalArgumentException
92     *             if an argument is illegal
93     */
94    public ClarinFCSEndpointDescriptionParser(int maxDepth) {
95        if (maxDepth < -1) {
96            throw new IllegalArgumentException("maxDepth < -1");
97        }
98        this.maxDepth = maxDepth;
99    }
100
101
102    @Override
103    public boolean supports(QName name) {
104        return ED_ROOT_ELEMENT.equals(name);
105    }
106
107
108    @Override
109    public SRUExtraResponseData parse(XMLStreamReader reader)
110            throws XMLStreamException, SRUClientException {
111        final int version = parseVersion(reader);
112        if ((version != VERSION_1) && (version != VERSION_2)) {
113            throw new SRUClientException("Attribute 'version' of " +
114                    "element '<EndpointDescription>' must be of value '1' or '2'");
115        }
116        reader.next(); // consume start tag
117
118        // Capabilities
119        List<URI> capabilities = null;
120        XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "Capabilities", true);
121        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
122                "Capability", (capabilities == null))) {
123            final String s = XmlStreamReaderUtils.readString(reader, true);
124            try {
125                if (!s.startsWith(CAPABILITY_PREFIX)) {
126                    throw new XMLStreamException("Capabilites must start " +
127                            "with prefix '" + CAPABILITY_PREFIX +
128                            "' (offending value = '" + s +"')",
129                            reader.getLocation());
130                }
131                final URI uri = new URI(s);
132                if (capabilities == null) {
133                    capabilities = new ArrayList<URI>();
134                }
135                capabilities.add(uri);
136            } catch (URISyntaxException e) {
137                throw new XMLStreamException("Capabilities must be encoded " +
138                        "as URIs (offending value = '" + s + "')",
139                        reader.getLocation(), e);
140            }
141            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Capability");
142        } // while
143        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Capabilities");
144
145        if (capabilities == null) {
146            throw new SRUClientException("Endpoint must support at " +
147                    "least one capability!");
148        }
149        final boolean hasBasicSearch =
150                (capabilities.indexOf(CAPABILITY_BASIC_SEARCH) != -1);
151        final boolean hasAdvancedSearch =
152                (capabilities.indexOf(CAPABILITY_ADVANCED_SEARCH) != -1);
153        if (!hasBasicSearch) {
154            throw new SRUClientException("Endpoint must support " +
155                    "'basic-search' (" + CAPABILITY_BASIC_SEARCH +
156                    ") to conform to CLARIN-FCS specification");
157        }
158
159        // SupportedDataViews
160        List<DataView> supportedDataViews = null;
161        XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
162                "SupportedDataViews", true);
163        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
164                "SupportedDataView", (supportedDataViews == null), true)) {
165            final String id = XmlStreamReaderUtils.readAttributeValue(
166                    reader, null, "id", true);
167            if ((id.indexOf(' ') != -1) || (id.indexOf(',') != -1) ||
168                    (id.indexOf(';') != -1)) {
169                throw new XMLStreamException("Value of attribute 'id' on " +
170                        "element '<SupportedDataView>' may not contain the " +
171                        "characters ',' (comma) or ';' (semicolon) " +
172                        "or ' ' (space)", reader.getLocation());
173            }
174            final DeliveryPolicy policy = parsePolicy(reader);
175            reader.next(); // consume start tag
176
177            final String type = XmlStreamReaderUtils.readString(reader, true);
178            // do some sanity checks ...
179            if (supportedDataViews != null) {
180                for (DataView dataView : supportedDataViews) {
181                    if (dataView.getIdentifier().equals(id)) {
182                        throw new XMLStreamException("Supported data view " +
183                                "with identifier '" + id +
184                                "' was already declared", reader.getLocation());
185                    }
186                    if (dataView.getMimeType().equals(type)) {
187                        throw new XMLStreamException("Supported data view " +
188                                "with MIME type '" + type +
189                                "' was already declared", reader.getLocation());
190                    }
191                }
192            } else {
193                supportedDataViews = new ArrayList<DataView>();
194            }
195            supportedDataViews.add(new DataView(id, type, policy));
196            XmlStreamReaderUtils.readEnd(reader,
197                    ED_NS_URI, "SupportedDataView");
198        } // while
199        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI,
200                "SupportedDataViews", true);
201        boolean found = false;
202        if (supportedDataViews != null) {
203            for (DataView dataView : supportedDataViews) {
204                if (MIMETYPE_HITS_DATAVIEW.equals(dataView.getMimeType())) {
205                    found = true;
206                    break;
207                }
208            }
209        }
210        if (!found) {
211            throw new SRUClientException("Endpoint must support " +
212                    "generic hits dataview (expected MIME type '" +
213                    MIMETYPE_HITS_DATAVIEW +
214                    "') to conform to CLARIN-FCS specification");
215        }
216
217        // SupportedLayers
218        List<Layer> supportedLayers = null;
219        if (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
220                "SupportedLayers", false)) {
221            while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
222                    "SupportedLayer", (supportedLayers == null), true)) {
223                final String id = XmlStreamReaderUtils.readAttributeValue(reader, null, "id");
224                if ((id.indexOf(' ') != -1) || (id.indexOf(',') != -1) ||
225                        (id.indexOf(';') != -1)) {
226                    throw new XMLStreamException("Value of attribute 'id' on " +
227                            "element '<SupportedLayer>' may not contain the " +
228                            "characters ',' (comma) or ';' (semicolon) " +
229                            "or ' ' (space)", reader.getLocation());
230                }
231                URI resultId = null;
232                final String s1 =
233                        XmlStreamReaderUtils.readAttributeValue(reader,
234                                null, "result-id");
235                try {
236                    resultId = new URI(s1);
237                } catch (URISyntaxException e) {
238                    throw new XMLStreamException("'result-id' must be encoded " +
239                            "as URIs (offending value = '" + s1 + "')",
240                            reader.getLocation(), e);
241                }
242                final ContentEncoding encoding = parseContentEncoding(reader);
243                String qualifier = XmlStreamReaderUtils.readAttributeValue(reader, null, "qualifier", false);
244                String altValueInfo = XmlStreamReaderUtils.readAttributeValue(reader, null, "alt-value-info", false);
245                URI altValueInfoURI = null;
246                final String s2 =
247                        XmlStreamReaderUtils.readAttributeValue(reader, null,
248                                "alt-value-info-uri", false);
249                if (s2 != null) {
250                    try {
251                        altValueInfoURI = new URI(s2);
252                    } catch (URISyntaxException e) {
253                        throw new XMLStreamException("'alt-value-info-uri' must be encoded " +
254                                "as URIs (offending value = '" + s2 + "')",
255                                reader.getLocation(), e);
256                    }
257                }
258                reader.next(); // consume element
259                final String layer = XmlStreamReaderUtils.readString(reader, true);
260                logger.debug("layer: id={}, resultId={}, layer={}, encoding={}, qualifier={}, alt-value-info={}, alt-value-info-uri={}",
261                        id, resultId, layer, encoding, qualifier, altValueInfo, altValueInfoURI);
262
263                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "SupportedLayer");
264                if (supportedLayers == null) {
265                    supportedLayers = new ArrayList<Layer>();
266                }
267                supportedLayers.add(new Layer(id, resultId, layer,
268                        encoding, qualifier, altValueInfo,
269                        altValueInfoURI));
270            } // while
271            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI,
272                    "SupportedLayers", true);
273        }
274        if (hasAdvancedSearch && (supportedLayers == null)) {
275            throw new SRUClientException("Endpoint must declare " +
276                    "all supported layers (<SupportedLayers>) if they " +
277                    "provide the 'advanced-search' (" +
278                    CAPABILITY_ADVANCED_SEARCH + ") capability");
279        }
280        if (!hasAdvancedSearch && (supportedLayers != null)) {
281            // XXX: hard error?!
282            logger.warn("Endpoint superflously declared supported " +
283                    "layers (<SupportedLayers> without providing the " +
284                    "'advanced-search' (" + CAPABILITY_ADVANCED_SEARCH + ") capability");
285        }
286
287        // Resources
288        final List<ResourceInfo> resources =
289                parseResources(reader, 0, maxDepth,
290                        supportedDataViews, supportedLayers);
291
292        // skip over extensions
293        while (!XmlStreamReaderUtils.peekEnd(reader,
294                ED_NS_URI, "EndpointDescription")) {
295            if (reader.isStartElement()) {
296                final String namespaceURI = reader.getNamespaceURI();
297                final String localName    = reader.getLocalName();
298                logger.debug("skipping over extension with element {{}}{}",
299                        namespaceURI, localName);
300                XmlStreamReaderUtils.skipTag(reader, namespaceURI, localName);
301            }
302        }
303
304        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "EndpointDescription");
305
306        return new ClarinFCSEndpointDescription(version, capabilities,
307                supportedDataViews, supportedLayers, resources);
308    }
309
310
311    /**
312     * Get the maximum resource enumeration parsing depth. The first level is
313     * indicate by the value <code>0</code>.
314     *
315     * @return the default resource parsing depth or <code>-1</code> for
316     *         infinite.
317     */
318    public int getMaximumResourcePArsingDepth() {
319        return maxDepth;
320    }
321
322
323    private static List<ResourceInfo> parseResources(XMLStreamReader reader,
324            int depth, int maxDepth, List<DataView> supportedDataviews,
325            List<Layer> supportedLayers) throws XMLStreamException {
326        List<ResourceInfo> resources = null;
327
328        XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "Resources", true);
329        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
330                "Resource", (resources == null), true)) {
331            final String pid = XmlStreamReaderUtils.readAttributeValue(reader,
332                    null, "pid", true);
333            reader.next(); // consume start tag
334
335            logger.debug("pid = {}", pid);
336
337            final Map<String, String> title =
338                    parseI18String(reader, "Title", true);
339            logger.debug("title: {}", title);
340
341            final Map<String, String> description =
342                    parseI18String(reader, "Description", false);
343            logger.debug("description: {}", description);
344
345            final String landingPageURI =
346                    XmlStreamReaderUtils.readContent(reader, ED_NS_URI,
347                            "LandingPageURI", false);
348            logger.debug("landingPageURI: {}", landingPageURI);
349
350            List<String> languages = null;
351            XmlStreamReaderUtils.readStart(reader,
352                    ED_NS_URI, "Languages", true);
353            while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
354                    "Language", (languages == null))) {
355                final String language =
356                        XmlStreamReaderUtils.readString(reader, true);
357                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Language");
358                if (languages == null) {
359                    languages = new ArrayList<String>();
360                } else {
361                    for (String l : languages) {
362                        if (l.equals(language)) {
363                            throw new XMLStreamException("language '" +
364                                    language + "' was already defined " +
365                                    "in '<Language>'", reader.getLocation());
366                        }
367                    } // for
368                }
369                languages.add(language);
370            } // while
371            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Languages", true);
372            logger.debug("languages: {}", languages);
373
374            // AvailableDataViews
375            XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "AvailableDataViews", true, true);
376            final String dvs = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref", true);
377            reader.next(); // consume start tag
378            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "AvailableDataViews");
379            List<DataView> dataviews = null;
380            for (String dv : dvs.split("\\s+")) {
381                boolean found = false;
382                for (DataView dataview : supportedDataviews) {
383                    if (dataview.getIdentifier().equals(dv)) {
384                        found = true;
385                        if (dataviews == null) {
386                            dataviews = new ArrayList<DataView>();
387                        }
388                        dataviews.add(dataview);
389                        break;
390                    }
391                } // for
392                if (!found) {
393                    throw new XMLStreamException("DataView with id '" + dv +
394                            "' was not declared in <SupportedDataViews>",
395                            reader.getLocation());
396                }
397            } // for
398            logger.debug("DataViews: {}", dataviews);
399
400            // AvailableLayers
401            List<Layer> layers = null;
402            if (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
403                    "AvailableLayers", false, true)) {
404                final String ls =
405                        XmlStreamReaderUtils.readAttributeValue(reader,
406                                null, "ref", true);
407                reader.next(); // consume start tag
408                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "AvailableLayers");
409                for (String l : ls.split("\\s+")) {
410                    boolean found = false;
411                    for (Layer layer : supportedLayers) {
412                        if (layer.getIdentifier().equals(l)) {
413                            found = true;
414                            if (layers == null) {
415                                layers = new ArrayList<Layer>();
416                            }
417                            layers.add(layer);
418                            break;
419                        }
420                    } // for
421                    if (!found) {
422                        throw new XMLStreamException("Layer with id '" + l +
423                                "' was not declared in <SupportedLayers>",
424                                reader.getLocation());
425                    }
426                } // for
427                logger.debug("Layers: {}", layers);
428            }
429
430
431            List<ResourceInfo> subResources = null;
432            if (XmlStreamReaderUtils.peekStart(reader,
433                    ED_NS_URI, "Resources")) {
434                final int nextDepth = depth + 1;
435                if ((maxDepth == INFINITE_MAX_DEPTH) ||
436                        (nextDepth < maxDepth)) {
437                    subResources = parseResources(reader, nextDepth,
438                            maxDepth, supportedDataviews, supportedLayers);
439                } else {
440                    XmlStreamReaderUtils.skipTag(reader, ED_NS_URI,
441                            "Resources", true);
442                }
443            }
444
445            while (!XmlStreamReaderUtils.peekEnd(reader,
446                    ED_NS_URI, "Resource")) {
447                if (reader.isStartElement()) {
448                    final String namespaceURI = reader.getNamespaceURI();
449                    final String localName    = reader.getLocalName();
450                    logger.debug("skipping over extension with element " +
451                            "{{}}{} (resource)", namespaceURI, localName);
452                    XmlStreamReaderUtils.skipTag(reader,
453                            namespaceURI, localName);
454                }
455            } // while
456
457            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Resource");
458
459            if (resources == null) {
460                resources = new ArrayList<ResourceInfo>();
461            }
462            resources.add(new ResourceInfo(pid, title, description,
463                    landingPageURI, languages, dataviews, layers,
464                    subResources));
465        } // while
466        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Resources");
467
468        return resources;
469    }
470
471
472    private static Map<String, String> parseI18String(XMLStreamReader reader,
473            String localName, boolean required) throws XMLStreamException {
474        Map<String, String> result = null;
475        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI, localName,
476                ((result == null) && required), true)) {
477            final String lang = XmlStreamReaderUtils.readAttributeValue(reader,
478                    XMLConstants.XML_NS_URI, "lang", true);
479            reader.next(); // skip start tag
480            final String content = XmlStreamReaderUtils.readString(reader, true);
481            if (result == null) {
482                result = new HashMap<String, String>();
483            }
484            if (result.containsKey(lang)) {
485                throw new XMLStreamException("language '" + lang +
486                        "' already defined for element '<" + localName + ">'",
487                        reader.getLocation());
488            } else {
489                result.put(lang, content);
490            }
491            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, localName);
492        } // while
493        return result;
494    }
495
496
497    private static int parseVersion(XMLStreamReader reader)
498            throws XMLStreamException {
499        try {
500            final String s = XmlStreamReaderUtils.readAttributeValue(
501                    reader, null, "version", true);
502            return Integer.parseInt(s);
503        } catch (NumberFormatException e) {
504            throw new XMLStreamException("Attribute 'version' is not a number",
505                    reader.getLocation(), e);
506        }
507    }
508
509
510    private static DeliveryPolicy parsePolicy(XMLStreamReader reader)
511            throws XMLStreamException {
512        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
513                null, "delivery-policy", true);
514        if ("send-by-default".equals(s)) {
515            return DeliveryPolicy.SEND_BY_DEFAULT;
516        } else if ("need-to-request".equals(s)) {
517            return DeliveryPolicy.NEED_TO_REQUEST;
518        } else {
519            throw new XMLStreamException("Unexpected value '" + s +
520                    "' for attribute 'delivery-policy' on " +
521                    "element '<SupportedDataView>'", reader.getLocation());
522        }
523    }
524
525
526    private static ContentEncoding parseContentEncoding(XMLStreamReader reader)
527            throws XMLStreamException {
528        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
529                null, "encoding", false);
530        if (s != null) {
531            if ("value".equals(s)) {
532                return ContentEncoding.VALUE;
533            } else if ("need-to-request".equals(s)) {
534                return ContentEncoding.EMPTY;
535            } else {
536                throw new XMLStreamException("Unexpected value '" + s +
537                                "' for attribute 'encoding' on " +
538                                "element '<SupportedLayer>'",
539                        reader.getLocation());
540            }
541        } else {
542            return null;
543        }
544    }
545
546} // class ClarinFCSEndpointDescriptionParser
Note: See TracBrowser for help on using the repository browser.