source: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescriptionParser.java @ 7280

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