source: SRUClient/trunk/src/main/java/eu/clarin/sru/client/SRUSimpleClient.java @ 7191

Last change on this file since 7191 was 7191, checked in by Oliver Schonefeld, 6 years ago
  • add feature to allow for using a customized HTTP client and optionally a HTTP client context
  • Property svn:eol-style set to native
File size: 66.7 KB
Line 
1/**
2 * This software is copyright (c) 2012-2016 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;
18
19import java.io.ByteArrayInputStream;
20import java.io.IOException;
21import java.io.InputStream;
22import java.net.URI;
23import java.net.UnknownHostException;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.List;
27import java.util.Map;
28import java.util.concurrent.TimeUnit;
29
30import javax.xml.stream.XMLStreamException;
31import javax.xml.stream.XMLStreamReader;
32
33import org.apache.http.HttpEntity;
34import org.apache.http.HttpStatus;
35import org.apache.http.StatusLine;
36import org.apache.http.client.ClientProtocolException;
37import org.apache.http.client.config.CookieSpecs;
38import org.apache.http.client.config.RequestConfig;
39import org.apache.http.client.methods.CloseableHttpResponse;
40import org.apache.http.client.methods.HttpGet;
41import org.apache.http.config.SocketConfig;
42import org.apache.http.impl.NoConnectionReuseStrategy;
43import org.apache.http.impl.client.CloseableHttpClient;
44import org.apache.http.impl.client.HttpClients;
45import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
46import org.apache.http.protocol.HttpContext;
47import org.slf4j.Logger;
48import org.slf4j.LoggerFactory;
49
50
51/**
52 * A simple client to perform SRU operations using callbacks. The application
53 * must provide the appropriate callbacks to receive the results of the
54 * operations.
55 * <p>
56 * This client is reusable but not thread-safe: the application may reuse a
57 * client object, but it may not be concurrently shared between multiple
58 * threads.
59 * </p>
60 * <p>
61 * This class is modeled after Java's SAX-API.
62 * </p>
63 *
64 * @see SRUExplainHandler
65 * @see SRUScanHandler
66 * @see SRUSearchRetrieveHandler
67 * @see SRUDefaultHandlerAdapter
68 */
69public class SRUSimpleClient {
70    private static final String USER_AGENT = "SRU-Client/1.0.0";
71    /** default version the client will use, if not otherwise specified */
72    private static final String SRU_DIAGNOSTIC_RECORD_SCHEMA =
73            "info:srw/schema/1/diagnostics-v1.1";
74    private static final String VERSION_1_1 = "1.1";
75    private static final String VERSION_1_2 = "1.2";
76    private static final String VERSION_2_0 = "2.0";
77    private static final String RECORD_PACKING_PACKED = "packed";
78    private static final String RECORD_PACKING_UNPACKED = "unpacked";
79    private static final String RECORD_ESCAPING_XML = "xml";
80    private static final String RECORD_ESCAPING_STRING = "string";
81    private static final Logger logger =
82            LoggerFactory.getLogger(SRUSimpleClient.class);
83    private final SRUVersion defaultVersion;
84    private final Map<String, SRURecordDataParser> parsers;
85    private final CloseableHttpClient httpClient;
86    private final HttpContext httpContext;
87    private final XmlStreamReaderProxy proxy = new XmlStreamReaderProxy();
88    private final SRUExplainRecordDataParser explainRecordParser =
89            new SRUExplainRecordDataParser();
90
91
92    /**
93     * Constructor.
94     *
95     * @param config
96     *            the configuration to be used for this client.
97     * @throws NullPointerException
98     *             if argument <code>config</code> is <code>null</code>
99     * @throws IllegalArgumentException
100     *             if an error occurred while registering record data parsers
101     * @see SRUClientConfig
102     */
103    public SRUSimpleClient(final SRUClientConfig config) {
104        if (config == null) {
105            throw new NullPointerException("config == null");
106        }
107        this.defaultVersion = config.getDefaultVersion();
108
109        // Initialize parsers lookup table ...
110        final List<SRURecordDataParser> list = config.getRecordDataParsers();
111        if ((list == null) || list.isEmpty()) {
112            throw new IllegalArgumentException(
113                    "no record data parsers registered");
114        }
115        this.parsers = new HashMap<String, SRURecordDataParser>();
116        for (SRURecordDataParser parser : list) {
117            final String recordSchema = parser.getRecordSchema();
118            if (!parsers.containsKey(recordSchema)) {
119                parsers.put(recordSchema, parser);
120            } else {
121                throw new IllegalArgumentException(
122                        "record data parser already registered: " +
123                                recordSchema);
124            }
125        }
126
127        final CloseableHttpClient client = config.getCustomizedHttpClient();
128        if (client != null) {
129            // use customized http client
130            this.httpClient = client;
131            this.httpContext = config.getHttpClientContext();
132        } else {
133            // create HTTP client
134            httpClient = createHttpClient(config.getConnectTimeout(),
135                    config.getSocketTimeout());
136            httpContext = null;
137        }
138    }
139
140
141    /**
142     * Perform a <em>explain</em> operation.
143     *
144     * @param request
145     *            an instance of a {@link SRUExplainRequest} object
146     * @param handler
147     *            an instance of {@link SRUExplainHandler} to receive callbacks
148     *            when processing the result of this request
149     * @throws SRUClientException
150     *             if an unrecoverable error occurred
151     * @throws NullPointerException
152     *             if any required argument is <code>null</code>
153     * @see SRUExplainRequest
154     * @see SRUExplainHandler
155     */
156    public void explain(SRUExplainRequest request, SRUExplainHandler handler)
157            throws SRUClientException {
158        if (request == null) {
159            throw new NullPointerException("request == null");
160        }
161        if (handler == null) {
162            throw new NullPointerException("handler == null");
163        }
164        logger.debug("performing explain request");
165
166        final long ts_start = System.nanoTime();
167
168        // create URI and perform request
169        final URI uri = request.makeURI(defaultVersion);
170        CloseableHttpResponse response = executeRequest(uri);
171        InputStream stream             = null;
172        SRUXMLStreamReader reader      = null;
173        try {
174            final HttpEntity entity = response.getEntity();
175            if (entity == null) {
176                throw new SRUClientException("cannot get entity");
177            }
178
179            stream = entity.getContent();
180
181            final long ts_parsing = System.nanoTime();
182            reader = createReader(stream, true);
183            parseExplainResponse(reader, request, handler);
184            final long ts_end = System.nanoTime();
185
186            final long millisTotal =
187                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
188            final long millisNetwork =
189                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
190            final long millisProcessing =
191                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
192            logger.debug("{} byte(s) in {} milli(s) " +
193                    "({} milli(s) network / {} milli(s) processing)",
194                    reader.getByteCount(), millisTotal, millisNetwork,
195                    millisProcessing);
196            handler.onRequestStatistics((int) reader.getByteCount(),
197                    millisTotal, millisNetwork, millisProcessing);
198        } catch (IllegalStateException e) {
199            throw new SRUClientException("error reading response", e);
200        } catch (IOException e) {
201            throw new SRUClientException("error reading response", e);
202        } catch (XMLStreamException e) {
203            throw new SRUClientException("error reading response", e);
204        } finally {
205            if (reader != null) {
206                try {
207                    reader.close();
208                } catch (XMLStreamException e) {
209                    /* IGNORE */
210                }
211            }
212            if (stream != null) {
213                try {
214                    stream.close();
215                } catch (IOException e) {
216                    /* IGNORE */
217                }
218            }
219
220            /* make sure to release allocated resources */
221            try {
222                response.close();
223            } catch (IOException e) {
224                /* IGNORE */
225            }
226        }
227    }
228
229
230    /**
231     * Perform a <em>scan</em> operation.
232     *
233     * @param request
234     *            an instance of a {@link SRUScanRequest} object
235     * @param handler
236     *            an instance of {@link SRUScanHandler} to receive callbacks
237     *            when processing the result of this request
238     * @throws SRUClientException
239     *             if an unrecoverable error occurred
240     * @throws NullPointerException
241     *             if any required argument is <code>null</code>
242     * @see SRUScanRequest
243     * @see SRUScanHandler
244     */
245    public void scan(SRUScanRequest request, SRUScanHandler handler)
246            throws SRUClientException {
247        if (request == null) {
248            throw new NullPointerException("request == null");
249        }
250        if (handler == null) {
251            throw new NullPointerException("handler == null");
252        }
253        logger.debug("performing scan request: scanClause = {}",
254                request.getScanClause());
255
256        final long ts_start = System.nanoTime();
257
258        // create URI and perform request
259        final URI uri = request.makeURI(defaultVersion);
260        CloseableHttpResponse response = executeRequest(uri);
261        InputStream stream             = null;
262        SRUXMLStreamReader reader      = null;
263        try {
264            final HttpEntity entity = response.getEntity();
265            if (entity == null) {
266                throw new SRUClientException("cannot get entity");
267            }
268            stream = entity.getContent();
269
270            final long ts_parsing = System.nanoTime();
271            reader = createReader(stream, true);
272            parseScanResponse(reader, request, handler);
273            final long ts_end = System.nanoTime();
274
275            final long millisTotal =
276                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
277            final long millisNetwork =
278                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
279            final long millisProcessing =
280                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
281            logger.debug("{} byte(s) in {} milli(s) " +
282                    "({} milli(s) network / {} milli(s) processing)",
283                    reader.getByteCount(), millisTotal, millisNetwork,
284                    millisProcessing);
285            handler.onRequestStatistics((int) reader.getByteCount(),
286                    millisTotal, millisNetwork, millisProcessing);
287        } catch (IllegalStateException e) {
288            throw new SRUClientException("error reading response", e);
289        } catch (IOException e) {
290            throw new SRUClientException("error reading response", e);
291        } catch (XMLStreamException e) {
292            throw new SRUClientException("error reading response", e);
293        } finally {
294            if (reader != null) {
295                try {
296                    reader.close();
297                } catch (XMLStreamException e) {
298                    /* IGNORE */
299                }
300            }
301            if (stream != null) {
302                try {
303                    stream.close();
304                } catch (IOException e) {
305                    /* IGNORE */
306                }
307            }
308
309            /* make sure to release allocated resources */
310            try {
311                response.close();
312            } catch (IOException e) {
313                /* IGNORE */
314            }
315        }
316    }
317
318
319    /**
320     * Perform a <em>searchRetrieve</em> operation.
321     *
322     * @param request
323     *            an instance of a {@link SRUSearchRetrieveRequest} object
324     * @param handler
325     *            an instance of {@link SRUSearchRetrieveHandler} to receive
326     *            callbacks when processing the result of this request
327     * @throws SRUClientException
328     *             if an unrecoverable error occurred
329     * @throws NullPointerException
330     *             if any required argument is <code>null</code>
331     * @see SRUSearchRetrieveRequest
332     * @see SRUSearchRetrieveHandler
333     */
334    public void searchRetrieve(SRUSearchRetrieveRequest request,
335            SRUSearchRetrieveHandler handler) throws SRUClientException {
336        if (request == null) {
337            throw new NullPointerException("request == null");
338        }
339        if (handler == null) {
340            throw new NullPointerException("handler == null");
341        }
342        logger.debug("performing searchRetrieve request: query = {}",
343                request.getQuery());
344
345        final long ts_start = System.nanoTime();
346
347        // create URI and perform request
348        final URI uri = request.makeURI(defaultVersion);
349        CloseableHttpResponse response = executeRequest(uri);
350        InputStream stream             = null;
351        SRUXMLStreamReader reader      = null;
352        try {
353            final HttpEntity entity = response.getEntity();
354            if (entity == null) {
355                throw new SRUClientException("cannot get entity");
356            }
357
358            stream = entity.getContent();
359
360            final long ts_parsing = System.nanoTime();
361            reader = createReader(stream, true);
362            parseSearchRetrieveResponse(reader, request, handler);
363            final long ts_end = System.nanoTime();
364
365            final long millisTotal =
366                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
367            final long millisNetwork =
368                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
369            final long millisProcessing =
370                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
371            logger.debug("{} byte(s) in {} milli(s) " +
372                    "({} milli(s) network / {} milli(s) processing)",
373                    reader.getByteCount(), millisTotal, millisNetwork,
374                    millisProcessing);
375            handler.onRequestStatistics((int) reader.getByteCount(),
376                    millisTotal, millisNetwork, millisProcessing);
377        } catch (IllegalStateException e) {
378            throw new SRUClientException("error reading response", e);
379        } catch (IOException e) {
380            throw new SRUClientException("error reading response", e);
381        } catch (XMLStreamException e) {
382            throw new SRUClientException("error reading response", e);
383        } finally {
384            if (reader != null) {
385                try {
386                    reader.close();
387                } catch (XMLStreamException e) {
388                    /* IGNORE */
389                }
390            }
391            if (stream != null) {
392                try {
393                    stream.close();
394                } catch (IOException e) {
395                    /* IGNORE */
396                }
397            }
398
399            /* make sure to release allocated resources */
400            try {
401                response.close();
402            } catch (IOException e) {
403                /* IGNORE */
404            }
405        }
406    }
407
408
409    private CloseableHttpResponse executeRequest(URI uri)
410            throws SRUClientException {
411        CloseableHttpResponse response = null;
412        boolean forceClose             = true;
413        try {
414            logger.debug("submitting HTTP request: {}", uri.toString());
415            try {
416                HttpGet request = new HttpGet(uri);
417                response = httpClient.execute(request, httpContext);
418                StatusLine status = response.getStatusLine();
419                if (status.getStatusCode() != HttpStatus.SC_OK) {
420                    if (status.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
421                        throw new SRUClientException("not found: " + uri);
422                    } else {
423                        throw new SRUClientException("unexpected status: " +
424                                status.getStatusCode());
425                    }
426                }
427                forceClose = false;
428                return response;
429            } catch (ClientProtocolException e) {
430                throw new SRUClientException("client protocol exception", e);
431            } catch (UnknownHostException e) {
432                throw new SRUClientException(
433                        "unknown host: " + uri.getHost(), e);
434            } catch (IOException e) {
435                String msg = null;
436                if ((e.getMessage() != null) && !e.getMessage().isEmpty()) {
437                    msg = e.getMessage();
438                }
439                throw new SRUClientException(msg != null
440                        ? msg
441                        : "input/output error", e);
442            }
443        } catch (SRUClientException e) {
444            /*
445             * if an error occurred, make sure we are freeing up the resources
446             * we've used
447             */
448            if (forceClose && (response != null)) {
449                try {
450                    response.close();
451                } catch (IOException ex) {
452                    /* IGNORE */
453                }
454            }
455            throw e;
456        }
457    }
458
459
460    private void parseExplainResponse(final SRUXMLStreamReader reader,
461            final SRUExplainRequest request, final SRUExplainHandler handler)
462            throws SRUClientException {
463        logger.debug("parsing 'explain' response (mode = {})",
464                (request.isStrictMode() ? "strict" : "non-strict"));
465
466        /* detect response namespaces */
467        final SRUNamespaces ns =
468                detectNamespace(reader, request.getRequestedVersion());
469
470        /*
471         * Eventually, SRUClient should always parse explain record data.
472         * However, for now, make caller explicitly ask for it.
473         */
474        final boolean parse = request.isParseRecordDataEnabled();
475        if (!parse) {
476            logger.debug("parsing of explain record data skipped");
477        }
478        doParseExplainResponse(reader, ns, request, handler, parse);
479    }
480
481
482    private void doParseExplainResponse(SRUXMLStreamReader reader,
483            SRUNamespaces ns, SRUAbstractRequest request,
484            SRUExplainHandler handler, boolean parseRecordData)
485                    throws SRUClientException {
486        try {
487            final boolean strictMode = request.isStrictMode();
488
489            // explainResponse
490            reader.readStart(ns.sruNS(), "explainResponse", true);
491
492            // explainResponse/version
493            final SRUVersion version = parseVersion(reader, ns.sruNS());
494            logger.debug("version = {}, requested = {}", version,
495                    request.getRequestedVersion());
496
497            // explainResponse/record
498            reader.readStart(ns.sruNS(), "record", true);
499            if (parseRecordData) {
500                handler.onStartRecords(-1, null, -1);
501
502                /*
503                 * common error: recordEscaping / recordPacking (SRU 1.2) in
504                 * wrong order
505                 */
506                SRURecordXmlEscaping recordXmlEscaping = null;
507                if (!strictMode && reader.peekStart(ns.sruNS(), "recordPacking")) {
508                    recordXmlEscaping = parseRecordXmlEscaping(reader, ns,
509                            request.getRequestedVersion(), false);
510                    if (recordXmlEscaping != null) {
511                        logger.error("element <recordPacking> must apperear " +
512                                "after element <recordSchema> within " +
513                                "element <record>");
514                    }
515                }
516
517                String schema =
518                        reader.readContent(ns.sruNS(), "recordSchema", true);
519
520                // (SRU 2.0) recordPacking (optional)
521                // XXX: what to do with it?
522                SRURecordPacking recordPacking = null;
523                if (version.isVersion(SRUVersion.VERSION_2_0)) {
524                    recordPacking = parseRecordPacking(reader,
525                            ns.sruNS(), strictMode);
526                }
527
528                // recordXmlEscaping (used to be recordPacking in SRU 1.0/1.2)
529                if (recordXmlEscaping == null) {
530                    recordXmlEscaping = parseRecordXmlEscaping(reader, ns,
531                            request.getRequestedVersion(), strictMode);
532                }
533                logger.debug("schema = {}, escaping = {}, packing = {}",
534                        schema, recordXmlEscaping, recordPacking);
535
536                // explainResponse/record/recordData
537                reader.readStart(ns.sruNS(), "recordData", true);
538                reader.consumeWhitespace();
539
540                SRURecordData recordData = null;
541                SRUXMLStreamReader recordReader = null;
542
543                if (recordXmlEscaping == SRURecordXmlEscaping.STRING) {
544                    /*
545                     * read content into temporary buffer and then use a new XML
546                     * reader to parse record data
547                     */
548                    final String data = reader.readString(true);
549                    InputStream in = new ByteArrayInputStream(data.getBytes());
550                    // FIXME: namespace context?
551                    recordReader = createReader(in, false);
552                } else {
553                    recordReader = reader;
554                }
555
556                try {
557                    proxy.reset(recordReader);
558                    /*
559                     * Try to parse explain record data with explain record
560                     * parser. It will throw an exception, if it cannot handle
561                     * the data.
562                     */
563                    recordData = explainRecordParser.parse(proxy,
564                            version, strictMode, schema);
565                } catch (XMLStreamException e) {
566                    throw new SRUClientException(
567                            "error parsing explain record", e);
568                } finally {
569                    /*
570                     * make sure, we're deallocate the record reader in case of
571                     * string record packing
572                     */
573                    if (recordXmlEscaping == SRURecordXmlEscaping.STRING) {
574                        recordReader.closeCompletly();
575                    }
576                }
577                if (recordData == null) {
578                    throw new SRUClientException("no record data could " +
579                            "be extracted from the response");
580                }
581                reader.consumeWhitespace();
582                reader.readEnd(ns.sruNS(), "recordData", true);
583
584                if (version.isVersion(SRUVersion.VERSION_1_2,
585                        SRUVersion.VERSION_2_0)) {
586                    reader.readContent(ns.sruNS(), "recordIdentifier", false);
587                }
588                reader.readContent(ns.sruNS(), "recordPosition", false, -1);
589
590                // notify handler
591                handler.onRecord(null, -1, recordData);
592
593                if (reader.readStart(ns.sruNS(), "extraRecordData", false)) {
594                    reader.consumeWhitespace();
595                    proxy.reset(reader);
596                    try {
597                        logger.debug("parsing extra response data");
598                        handler.onExtraRecordData(null, -1, proxy);
599                    } catch (XMLStreamException e) {
600                        throw new SRUClientException("handler "
601                                + "triggered error while parsing "
602                                + "'extraRecordData'", e);
603                    }
604                    reader.consumeWhitespace();
605                    reader.readEnd(ns.sruNS(), "extraRecordData", true);
606                }
607
608                handler.onFinishRecords(-1);
609
610                reader.readEnd(ns.sruNS(), "record");
611            } else {
612                /*
613                 * do not really parse record and skip everything
614                 * until <record> end tag
615                 */
616                reader.readEnd(ns.sruNS(), "record", true);
617            }
618
619            // explainResponse/echoedExplainRequest
620            if (reader.readStart(ns.sruNS(), "echoedExplainRequest", false)) {
621                reader.readEnd(ns.sruNS(), "echoedExplainRequest", true);
622            }
623
624            /*
625             * common error: echoedExplainRequest in default namespace
626             */
627            if (reader.readStart("", "echoedExplainRequest", false)) {
628                logger.error("Element 'echoedExplainRequest' must be in SRU " +
629                        "namespace, but endpoint put it into default namespace");
630                if (strictMode) {
631                    throw new SRUClientException("Element " +
632                            "'echoedExplainRequest' must be in SRU namespace,"+
633                            " but endpoint put it into default namespace");
634                }
635                reader.readEnd("", "echoedExplainRequest", true);
636            }
637
638            // explainResponse/diagnostics
639            final List<SRUDiagnostic> diagnostics =
640                    parseDiagnostics(reader, ns, ns.scanNS(), strictMode);
641            if (diagnostics != null) {
642                handler.onDiagnostics(diagnostics);
643            }
644
645            // explainResponse/extraResponseData
646            if (reader.readStart(ns.sruNS(), "extraResponseData", false)) {
647                reader.consumeWhitespace();
648                proxy.reset(reader);
649                try {
650                    logger.debug("parsing extra response data");
651                    handler.onExtraResponseData(proxy);
652                } catch (XMLStreamException e) {
653                    throw new SRUClientException("handler triggered "
654                            + "error while parsing 'extraResponseData'", e);
655                }
656                reader.consumeWhitespace();
657                reader.readEnd(ns.sruNS(), "extraResponseData", true);
658            }
659
660            reader.readEnd(ns.sruNS(), "explainResponse");
661        } catch (XMLStreamException e) {
662            throw new SRUClientException(e.getMessage(), e);
663        }
664    }
665
666
667    private void parseScanResponse(final SRUXMLStreamReader reader,
668            final SRUScanRequest request, final SRUScanHandler handler)
669            throws SRUClientException {
670        try {
671            /* detect response namespaces */
672            final SRUNamespaces ns =
673                    detectNamespace(reader, request.getRequestedVersion());
674
675            /*
676             * if the endpoint cannot determine the operation, it should create
677             * a explain response.
678             */
679            if (reader.peekStart(ns.sruNS(), "explainResponse")) {
680                doParseExplainResponse(reader, ns, request, new SRUExplainHandler() {
681                    @Override
682                    public void onRequestStatistics(int bytes, long millisTotal,
683                            long millisNetwork, long millisParsing) {
684                    }
685
686
687                    @Override
688                    public void onExtraResponseData(XMLStreamReader reader)
689                            throws XMLStreamException, SRUClientException {
690                    }
691
692
693                    @Override
694                    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
695                            throws SRUClientException {
696                        handler.onDiagnostics(diagnostics);
697                    }
698
699
700                    @Override
701                    public void onStartRecords(int numberOfRecords,
702                            String resultSetId, int resultSetIdleTime)
703                            throws SRUClientException {
704                    }
705
706
707                    @Override
708                    public void onFinishRecords(int nextRecordPosition)
709                            throws SRUClientException {
710                    }
711
712
713                    @Override
714                    public void onRecord(String identifier, int position,
715                            SRURecordData data) throws SRUClientException {
716                    }
717
718
719                    @Override
720                    public void onExtraRecordData(String identifier,
721                            int position, XMLStreamReader reader)
722                            throws XMLStreamException, SRUClientException {
723                    }
724                }, false);
725            } else {
726                final boolean strictMode = request.isStrictMode();
727
728                logger.debug("parsing 'scan' response (mode = {})",
729                        (strictMode ? "strict" : "non-strict"));
730
731                // scanResponse
732                reader.readStart(ns.scanNS(), "scanResponse", true);
733
734                // scanResponse/version
735                final SRUVersion version = parseVersion(reader, ns.scanNS());
736                logger.debug("version = {}, requested = {}", version,
737                        request.getRequestedVersion());
738
739                // scanResponse/terms
740                if (reader.readStart(ns.scanNS(), "terms", false)) {
741                    boolean first = true;
742                    while (reader.readStart(ns.scanNS(), "term", first)) {
743                        if (first) {
744                            first = false;
745                            handler.onStartTerms();
746                        }
747
748                        // scanResponse/terms/value
749                        String value =
750                                reader.readContent(ns.scanNS(), "value", true);
751
752                        // scanResponse/terms/numberOfRecords
753                        int numberOfRecords = reader.readContent(ns.scanNS(),
754                                "numberOfRecords", false, -1);
755
756                        // scanResponse/terms/displayTerm
757                        String displayTerm = reader.readContent(ns.scanNS(),
758                                "displayTerm", false);
759
760                        // scanResponse/terms/whereInList
761                        String s = reader.readContent(ns.scanNS(),
762                                "whereInList", false);
763                        SRUWhereInList whereInList = null;
764                        if (s != null) {
765                            if ("first".equals(s)) {
766                                whereInList = SRUWhereInList.FIRST;
767                            } else if ("last".equals(s)) {
768                                whereInList = SRUWhereInList.LAST;
769                            } else if ("only".equals(s)) {
770                                whereInList = SRUWhereInList.ONLY;
771                            } else if ("inner".equals(s)) {
772                                whereInList = SRUWhereInList.INNER;
773                            } else {
774                                throw new SRUClientException(
775                                        "invalid value for 'whereInList': " + s);
776                            }
777                        }
778                        logger.debug("value = {}, numberOfRecords = {}, " +
779                                "displayTerm = {}, whereInList = {}", value,
780                                numberOfRecords, displayTerm, whereInList);
781                        handler.onTerm(value, numberOfRecords, displayTerm,
782                                whereInList);
783
784                        // scanResponse/terms/extraTermData
785                        if (reader.readStart(ns.scanNS(), "extraTermData", first)) {
786                            reader.consumeWhitespace();
787                            proxy.reset(reader);
788                            try {
789                                handler.onExtraTermData(value, proxy);
790                            } catch (XMLStreamException e) {
791                                throw new SRUClientException("handler "
792                                        + "triggered error while parsing "
793                                        + "'extraTermData'", e);
794                            }
795                            reader.consumeWhitespace();
796                            reader.readEnd(ns.scanNS(), "extraTermData", true);
797                        }
798                        reader.readEnd(ns.scanNS(), "term", true);
799
800                    } // while
801                    reader.readEnd(ns.scanNS(), "terms");
802                    handler.onFinishTerms();
803                }
804
805                // scanResponse/echoedScanRequest
806                if (reader.readStart(ns.scanNS(), "echoedScanRequest", false)) {
807                    reader.readEnd(ns.scanNS(), "echoedScanRequest", true);
808                }
809
810                /*
811                 * common error: echoedScanRequest in default namespace
812                 */
813                if (reader.readStart("", "echoedScanRequest", false)) {
814                    logger.error("Element 'echoedScanRequest' must be in SRU namespace, but endpoint put it into default namespace");
815                    if (strictMode) {
816                        throw new SRUClientException("Element 'echoedScanRequest' must be in SRU namespace, but endpoint put it into default namespace");
817                    }
818                    reader.readEnd("", "echoedScanRequest", true);
819                }
820
821                // scanResponse/diagnostics
822                final List<SRUDiagnostic> diagnostics =
823                        parseDiagnostics(reader, ns, ns.scanNS(), strictMode);
824                if (diagnostics != null) {
825                    handler.onDiagnostics(diagnostics);
826                }
827
828                // scanResponse/extraResponseData
829                if (reader.readStart(ns.scanNS(), "extraResponseData", false)) {
830                    reader.consumeWhitespace();
831                    proxy.reset(reader);
832                    try {
833                        logger.debug("parsing extra response data");
834                        handler.onExtraResponseData(proxy);
835                    } catch (XMLStreamException e) {
836                        throw new SRUClientException("handler triggered "
837                                + "error while parsing 'extraResponseData'", e);
838                    }
839                    reader.consumeWhitespace();
840                    reader.readEnd(ns.scanNS(), "extraResponseData", true);
841                }
842
843                reader.readEnd(ns.scanNS(), "scanResponse");
844            }
845        } catch (XMLStreamException e) {
846            throw new SRUClientException(e.getMessage(), e);
847        }
848    }
849
850
851    private void parseSearchRetrieveResponse(final SRUXMLStreamReader reader,
852            final SRUSearchRetrieveRequest request,
853            final SRUSearchRetrieveHandler handler) throws SRUClientException {
854        try {
855            /* detect response namespaces */
856            final SRUNamespaces ns =
857                    detectNamespace(reader, request.getRequestedVersion());
858
859            /*
860             * if the endpoint cannot determine the operation, it should create
861             * a explain response.
862             */
863            if (reader.peekStart(ns.sruNS(), "explainResponse")) {
864                doParseExplainResponse(reader, ns, request, new SRUExplainHandler() {
865                    @Override
866                    public void onRequestStatistics(int bytes, long millisTotal,
867                            long millisNetwork, long millisParsing) {
868                    }
869
870
871                    @Override
872                    public void onExtraResponseData(XMLStreamReader reader)
873                            throws XMLStreamException, SRUClientException {
874                    }
875
876
877                    @Override
878                    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
879                            throws SRUClientException {
880                        handler.onDiagnostics(diagnostics);
881                    }
882
883
884                    @Override
885                    public void onStartRecords(int numberOfRecords,
886                            String resultSetId, int resultSetIdleTime)
887                            throws SRUClientException {
888                    }
889
890
891                    @Override
892                    public void onFinishRecords(int nextRecordPosition)
893                            throws SRUClientException {
894                    }
895
896
897                    @Override
898                    public void onRecord(String identifier, int position,
899                            SRURecordData data) throws SRUClientException {
900                    }
901
902
903                    @Override
904                    public void onExtraRecordData(String identifier,
905                            int position, XMLStreamReader reader)
906                            throws XMLStreamException, SRUClientException {
907                    }
908                }, false);
909            } else {
910                final boolean strictMode = request.isStrictMode();
911
912                logger.debug("parsing 'searchRetrieve' response (mode = {})",
913                        (strictMode ? "strict" : "non-strict"));
914
915                // searchRetrieveResponse
916                reader.readStart(ns.sruNS(), "searchRetrieveResponse", true);
917
918                // searchRetrieveResponse/version
919                final SRUVersion version = parseVersion(reader, ns.sruNS());
920                logger.debug("version = {}, requested = {}", version,
921                        request.getRequestedVersion());
922
923                // searchRetrieveResponse/numberOfRecords
924                int numberOfRecords = reader.readContent(ns.sruNS(),
925                        "numberOfRecords", true, -1);
926
927                // searchRetrieveResponse/resultSetId
928                String resultSetId = reader.readContent(ns.sruNS(),
929                        "resultSetId", false);
930
931                // searchRetrieveResponse/resultSetIdleTime
932                int resultSetIdleTime = reader.readContent(ns.sruNS(),
933                        "resultSetIdleTime", false, -1);
934
935                logger.debug("numberOfRecords = {}, resultSetId = {}, " +
936                        "resultSetIdleTime = {}", numberOfRecords,
937                        resultSetId, resultSetIdleTime);
938
939                // searchRetrieveResponse/results
940                if (numberOfRecords > 0) {
941                    /*
942                     * some endpoints set numberOfRecords but do not serialize
943                     * any records, e.g if requested record schema is not
944                     * supported
945                     */
946                    int recordCount = 0;
947                    if (reader.readStart(ns.sruNS(), "records", false)) {
948                        // searchRetrieveResponse/records/record
949                        boolean first = true;
950                        while (reader.readStart(ns.sruNS(), "record", first)) {
951                            if (first) {
952                                first = false;
953                                handler.onStartRecords(numberOfRecords,
954                                        resultSetId, resultSetIdleTime);
955                            }
956
957                            /*
958                             * common error: recordEscaping / recordPacking
959                             * (SRU 1.2) in wrong order
960                             */
961                            SRURecordXmlEscaping recordXmlEscaping = null;
962                            if (!strictMode &&
963                                    reader.peekStart(ns.sruNS(), "recordPacking")) {
964                                recordXmlEscaping =
965                                        parseRecordXmlEscaping(reader, ns,
966                                                version, false);
967                                if (recordXmlEscaping != null) {
968                                    logger.error("element <recordPacking> " +
969                                            "must appear after element " +
970                                            "<recordSchema> within " +
971                                            "element <record>");
972                                }
973                            }
974
975                            String schema = reader.readContent(ns.sruNS(),
976                                    "recordSchema", true);
977
978                            // (SRU 2.0) recordPacking (optional)
979                            // XXX: what to do with it?
980                            SRURecordPacking recordPacking = null;
981                            if (version.isVersion(SRUVersion.VERSION_2_0)) {
982                                recordPacking = parseRecordPacking(reader,
983                                        ns.sruNS(), strictMode);
984                            }
985
986                            if (recordXmlEscaping == null) {
987                                recordXmlEscaping =
988                                        parseRecordXmlEscaping(reader, ns,
989                                                version, strictMode);
990                            }
991
992                            logger.debug("schema = {}, escaping = {}, " +
993                                    "packing = {}, requested escaping = {}, " +
994                                    "requested packing = {}",
995                                    schema, recordXmlEscaping, recordPacking,
996                                    request.getRecordXmlEscaping(),
997                                    request.getRecordPacking());
998
999                            if ((request.getRecordXmlEscaping() != null) &&
1000                                    (recordXmlEscaping != request.getRecordXmlEscaping())) {
1001                                final SRURecordXmlEscaping p =
1002                                        request.getRecordXmlEscaping();
1003                                logger.error("requested '{}' record XML escaping, " +
1004                                        "but server responded with '{}' " +
1005                                        "record XML escaping",
1006                                        p.getStringValue(),
1007                                        recordXmlEscaping.getStringValue());
1008                                if (strictMode) {
1009                                    throw new SRUClientException("requested '" +
1010                                            p.getStringValue() +
1011                                            "' record packing, but server " +
1012                                            "responded with '" +
1013                                            recordXmlEscaping.getStringValue() +
1014                                            "' record packing");
1015                                }
1016                            }
1017
1018                            // searchRetrieveResponse/record/recordData
1019                            reader.readStart(ns.sruNS(), "recordData", true);
1020                            reader.consumeWhitespace();
1021
1022                            SRURecordData recordData = null;
1023                            SRUDiagnostic surrogate = null;
1024                            SRUXMLStreamReader recordReader = null;
1025
1026                            if (recordXmlEscaping == SRURecordXmlEscaping.STRING) {
1027                                /*
1028                                 * read content into temporary buffer and then
1029                                 * use a new XML reader to parse record data
1030                                 */
1031                                final String data = reader.readString(true);
1032                                InputStream in = new ByteArrayInputStream(
1033                                        data.getBytes());
1034                                // FIXME: namespace context?
1035                                recordReader = createReader(in, false);
1036                            } else {
1037                                recordReader = reader;
1038                            }
1039
1040                            if (SRU_DIAGNOSTIC_RECORD_SCHEMA.equals(schema)) {
1041                                surrogate = parseDiagnostic(recordReader, ns,
1042                                        true, strictMode);
1043                            } else {
1044                                SRURecordDataParser parser = findParser(schema);
1045                                if (parser != null) {
1046                                    try {
1047                                        proxy.reset(recordReader);
1048                                        recordData = parser.parse(proxy);
1049                                    } catch (XMLStreamException e) {
1050                                        throw new SRUClientException(
1051                                                "error parsing record", e);
1052                                    } finally {
1053                                        /*
1054                                         * make sure, we deallocate the record
1055                                         * reader in case of string record
1056                                         * packing
1057                                         */
1058                                        if (recordXmlEscaping == SRURecordXmlEscaping.STRING) {
1059                                            recordReader.closeCompletly();
1060                                        }
1061                                    }
1062                                    if (recordData == null) {
1063                                        logger.debug("record parser did not parse "
1064                                                + "record correctly and returned "
1065                                                + "null; injecting client side "
1066                                                + "surrogate diagnostic");
1067                                        surrogate = new SRUDiagnostic(
1068                                                SRUClientDiagnostics.DIAG_RECORD_PARSER_NULL,
1069                                                null,
1070                                                "Record parser for schema '" +
1071                                                        schema +
1072                                                        "' did not " +
1073                                                        "parse record correctly " +
1074                                                        "and errornously " +
1075                                                        "returned null.");
1076                                    }
1077                                } else {
1078                                    /*
1079                                     * no record parser found, inject a
1080                                     * surrogate diagnostic
1081                                     */
1082                                    logger.debug(
1083                                            "no record data parser found "
1084                                                    + "for schema '{}'; injecting client "
1085                                                    + "side surrogate diagnostic",
1086                                            schema);
1087                                    surrogate = new SRUDiagnostic(
1088                                            SRUClientDiagnostics.DIAG_NO_RECORD_PARSER,
1089                                            schema,
1090                                            "No record data parser for schema '" +
1091                                                    schema + "' found.");
1092                                }
1093                            }
1094
1095                            reader.consumeWhitespace();
1096                            reader.readEnd(ns.sruNS(), "recordData", true);
1097
1098                            String identifier = null;
1099                            if (version.isVersion(SRUVersion.VERSION_1_2,
1100                                    SRUVersion.VERSION_2_0)) {
1101                                identifier = reader.readContent(ns.sruNS(),
1102                                        "recordIdentifier", false);
1103                            }
1104
1105                            int position = reader.readContent(ns.sruNS(),
1106                                    "recordPosition", false, -1);
1107
1108                            logger.debug("recordIdentifier = {}, " +
1109                                    "recordPosition = {}",
1110                                    identifier, position);
1111
1112                            // notify handler
1113                            if (surrogate != null) {
1114                                handler.onSurrogateRecord(identifier,
1115                                        position, surrogate);
1116                            } else {
1117                                if (recordData != null) {
1118                                    handler.onRecord(identifier,
1119                                            position, recordData);
1120                                }
1121                            }
1122
1123                            if (reader.readStart(ns.sruNS(),
1124                                    "extraRecordData", false)) {
1125                                reader.consumeWhitespace();
1126                                proxy.reset(reader);
1127                                try {
1128                                    handler.onExtraRecordData(identifier,
1129                                            position, proxy);
1130                                } catch (XMLStreamException e) {
1131                                    throw new SRUClientException("handler " +
1132                                            "triggered error while parsing " +
1133                                            "'extraRecordData'", e);
1134                                }
1135                                reader.consumeWhitespace();
1136                                reader.readEnd(ns.sruNS(), "extraRecordData", true);
1137                            }
1138
1139                            reader.readEnd(ns.sruNS(), "record");
1140                            recordCount++;
1141                        } // while
1142                        reader.readEnd(ns.sruNS(), "records");
1143                    }
1144                    if (recordCount == 0) {
1145                        logger.error("endpoint declared {} results, but response contained no <record> elements (behavior may violate SRU specification)", numberOfRecords);
1146                    } else if ((request.getMaximumRecords() != -1) && (recordCount > request.getMaximumRecords())) {
1147                        logger.error("endpoint did not honour 'maximumRecords' request parameter and responded with {} records instead of a maximum of {}", recordCount, request.getMaximumRecords());
1148                    }
1149                } else {
1150                    /*
1151                     * provide a better error format, if endpoints responds with
1152                     * an empty <records> element
1153                     */
1154                    if (reader.readStart(ns.sruNS(), "records", false)) {
1155                        int bad = 0;
1156                        while (reader.readStart(ns.sruNS(), "record", false)) {
1157                            bad++;
1158                            reader.readEnd(ns.sruNS(), "record", true);
1159                        }
1160                        reader.readEnd(ns.sruNS(), "records", true);
1161                        if (bad == 0) {
1162                            logger.error("endpoint declared 0 results, but " +
1163                                    "response contained an empty 'records' " +
1164                                    "element");
1165                            if (strictMode) {
1166                                throw new SRUClientException(
1167                                        "endpoint declared 0 results, but " +
1168                                        "response contained an empty " +
1169                                        "'records' element (behavior " +
1170                                        "violates SRU specification)");
1171                            }
1172                        } else {
1173                            logger.error("endpoint declared 0 results, but " +
1174                                    "response contained " + bad +
1175                                    " record(s)");
1176                            if (strictMode) {
1177                                throw new SRUClientException(
1178                                            "endpoint declared 0 results, " +
1179                                            "but response containted " + bad +
1180                                            " records (behavior may violate " +
1181                                            "SRU specification)");
1182                            }
1183                        }
1184                    }
1185                }
1186
1187                int nextRecordPosition = reader.readContent(ns.sruNS(),
1188                        "nextRecordPosition", false, -1);
1189                logger.debug("nextRecordPosition = {}", nextRecordPosition);
1190                handler.onFinishRecords(nextRecordPosition);
1191
1192                // searchRetrieveResponse/echoedSearchRetrieveResponse
1193                if (reader.readStart(ns.sruNS(),
1194                        "echoedSearchRetrieveRequest", false)) {
1195                    reader.readEnd(ns.sruNS(), "echoedSearchRetrieveRequest", true);
1196                }
1197
1198                /*
1199                 * common error: echoedSearchRetrieveRequest in
1200                 * default namespace
1201                 */
1202                if (reader.readStart("", "echoedSearchRetrieveRequest", false)) {
1203                    logger.error("Element 'echoedSearchRetrieveRequest' " +
1204                            "must be in SRU namespace, but endpoint put it " +
1205                            "into default namespace");
1206                    if (strictMode) {
1207                        throw new SRUClientException(
1208                                "Element 'echoedSearchRetrieveRequest' must " +
1209                                "be in SRU namespace, but endpoint put it " +
1210                                "into default namespace");
1211                    }
1212                    reader.readEnd("", "echoedSearchRetrieveRequest", true);
1213                }
1214
1215                // searchRetrieveResponse/diagnostics
1216                final List<SRUDiagnostic> diagnostics =
1217                        parseDiagnostics(reader, ns, ns.sruNS(), strictMode);
1218                if (diagnostics != null) {
1219                    handler.onDiagnostics(diagnostics);
1220                }
1221
1222                // explainResponse/extraResponseData
1223                if (reader.readStart(ns.sruNS(), "extraResponseData", false)) {
1224                    reader.consumeWhitespace();
1225                    proxy.reset(reader);
1226                    try {
1227                        handler.onExtraResponseData(proxy);
1228                    } catch (XMLStreamException e) {
1229                        throw new SRUClientException("handler triggered "
1230                                + "error while parsing 'extraResponseData'", e);
1231                    }
1232                    reader.consumeWhitespace();
1233                    reader.readEnd(ns.sruNS(), "extraResponseData", true);
1234                }
1235
1236                if (version.isVersion(SRUVersion.VERSION_2_0)) {
1237                    // SRU (2.0) arbitrary stuff
1238                    // SRU (2.0) resultSetTTL (replaces resultSetIdleTime)
1239                    // SRU (2.0) resultCountPrecision
1240                    if (reader.readStart(ns.sruNS(), "resultCountPrecision", false)) {
1241                        reader.readEnd(ns.sruNS(), "resultCountPrecision", true);
1242                    }
1243                    // SRU (2.0) facetedResults
1244                    // SRU (2.0) searchResultAnalysis
1245                }
1246                reader.readEnd(ns.sruNS(), "searchRetrieveResponse");
1247            }
1248        } catch (XMLStreamException e) {
1249            throw new SRUClientException(e.getMessage(), e);
1250        }
1251    }
1252
1253
1254    private static SRUVersion parseVersion(SRUXMLStreamReader reader,
1255            String envelopNs) throws XMLStreamException, SRUClientException {
1256        final String v = reader.readContent(envelopNs, "version", true);
1257        if (VERSION_1_1.equals(v)) {
1258            return SRUVersion.VERSION_1_1;
1259        } else if (VERSION_1_2.equals(v)) {
1260            return SRUVersion.VERSION_1_2;
1261        } else if (VERSION_2_0.equals(v)) {
1262            return SRUVersion.VERSION_2_0;
1263        } else {
1264            throw new SRUClientException("invalid value '" + v +
1265                    "' for version (valid values are: '" + VERSION_1_1 +
1266                    "' and '" + VERSION_1_2 + "')");
1267        }
1268    }
1269
1270
1271    private static List<SRUDiagnostic> parseDiagnostics(
1272            SRUXMLStreamReader reader, SRUNamespaces ns, String responseNs, boolean strictMode)
1273            throws XMLStreamException, SRUClientException {
1274        if (reader.readStart(responseNs, "diagnostics", false)) {
1275            List<SRUDiagnostic> diagnostics = null;
1276
1277            SRUDiagnostic diagnostic = null;
1278            while ((diagnostic = parseDiagnostic(reader, ns,
1279                    (diagnostics == null), strictMode)) != null) {
1280                if (diagnostics == null) {
1281                    diagnostics = new ArrayList<SRUDiagnostic>();
1282                }
1283                diagnostics.add(diagnostic);
1284            } // while
1285            reader.readEnd(responseNs, "diagnostics");
1286            return diagnostics;
1287        } else {
1288            return null;
1289        }
1290    }
1291
1292
1293    private static SRUDiagnostic parseDiagnostic(SRUXMLStreamReader reader,
1294            SRUNamespaces ns, boolean required, boolean strictMode) throws XMLStreamException,
1295            SRUClientException {
1296        if (reader.readStart(ns.diagnosticNS(), "diagnostic", required)) {
1297
1298            // diagnostic/uri
1299            String uri = reader.readContent(ns.diagnosticNS(), "uri", true);
1300
1301            String details = null;
1302            String message = null;
1303            if (strictMode) {
1304                // diagnostic/details
1305                details = reader.readContent(ns.diagnosticNS(), "details",
1306                        false, true);
1307
1308                // diagnostic/message
1309                message = reader.readContent(ns.diagnosticNS(), "message",
1310                        false, true);
1311            } else {
1312                /*
1313                 * common error: diagnostic/details and diagnostic/message may
1314                 * appear in any order
1315                 */
1316                if (reader.peekStart(ns.diagnosticNS(), "details")) {
1317                    details = reader.readContent(ns.diagnosticNS(), "details",
1318                            false, false);
1319                    message = reader.readContent(ns.diagnosticNS(), "message",
1320                            false, false);
1321                } else {
1322                    message = reader.readContent(ns.diagnosticNS(), "message",
1323                            false, false);
1324                    details = reader.readContent(ns.diagnosticNS(), "details",
1325                            false, false);
1326                    if ((message != null) && (details != null)) {
1327                        logger.error("element <message> and element " +
1328                                "<details> within element <diagnostic> " +
1329                                "appeared in wrong order");
1330                    }
1331                }
1332            }
1333
1334            if ((details != null) && details.isEmpty()) {
1335                details = null;
1336                logger.debug("omitting empty element <details> " +
1337                        "within element <diagnostic>");
1338            }
1339            if ((message != null) && message.isEmpty()) {
1340                message = null;
1341                logger.debug("omitting empty element <message> " +
1342                        "within element <diagnostic>");
1343            }
1344
1345            reader.readEnd(ns.diagnosticNS(), "diagnostic");
1346
1347            logger.debug("diagnostic: uri={}, detail={}, message={}",
1348                    uri, details, message);
1349            return new SRUDiagnostic(uri, details, message);
1350        } else {
1351            return null;
1352        }
1353    }
1354
1355
1356    private static SRURecordPacking parseRecordPacking(
1357            SRUXMLStreamReader reader, String envelopNs, boolean strictMode)
1358                    throws XMLStreamException, SRUClientException {
1359        final String s = reader.readContent(envelopNs, "recordPacking", false);
1360        if (s != null) {
1361            if (RECORD_PACKING_PACKED.equals(s)) {
1362                return SRURecordPacking.PACKED;
1363            } else if (RECORD_PACKING_UNPACKED.equals(s)) {
1364                return SRURecordPacking.UNPACKED;
1365            } else if (!strictMode && RECORD_PACKING_PACKED.equalsIgnoreCase(s)) {
1366                logger.error("invalid value '{}' for '<recordPacking>', should be '{}'",
1367                             s, RECORD_PACKING_PACKED);
1368                return SRURecordPacking.PACKED;
1369            } else if (!strictMode && RECORD_PACKING_UNPACKED.equalsIgnoreCase(s)) {
1370                logger.error("invalid value '{}' for '<recordPacking>', should be '{}'",
1371                             s, RECORD_PACKING_UNPACKED);
1372                return SRURecordPacking.UNPACKED;
1373            } else {
1374                throw new SRUClientException("invalid value '" + s +
1375                        "' for '<recordPacking>' (valid values are: '" +
1376                        RECORD_PACKING_PACKED + "' and '" + RECORD_PACKING_UNPACKED +
1377                        "')");
1378            }
1379        }
1380        return null;
1381    }
1382
1383
1384    private static SRURecordXmlEscaping parseRecordXmlEscaping(
1385            SRUXMLStreamReader reader, SRUNamespaces ns, SRUVersion version, boolean strictMode)
1386            throws XMLStreamException, SRUClientException {
1387
1388        final String name = version.isVersion(SRUVersion.VERSION_2_0)
1389                          ? "recordXMLEscaping" : "recordPacking";
1390        final String s = reader.readContent(ns.sruNS(), name, true);
1391
1392        if (RECORD_ESCAPING_XML.equals(s)) {
1393            return SRURecordXmlEscaping.XML;
1394        } else if (RECORD_ESCAPING_STRING.equals(s)) {
1395            return SRURecordXmlEscaping.STRING;
1396        } else if (!strictMode && RECORD_ESCAPING_XML.equalsIgnoreCase(s)) {
1397            logger.error("invalid value '{}' for '<{}>', should be '{}'",
1398                         s, name, RECORD_ESCAPING_XML);
1399            return SRURecordXmlEscaping.XML;
1400        } else if (!strictMode && RECORD_ESCAPING_STRING.equalsIgnoreCase(s)) {
1401            logger.error("invalid value '{}' for '<{}>', should be '{}'",
1402                         s, name, RECORD_ESCAPING_STRING);
1403            return SRURecordXmlEscaping.STRING;
1404        } else {
1405            throw new SRUClientException("invalid value '" + s +
1406                    "' for '<" + name + ">' (valid values are: '" +
1407                    RECORD_ESCAPING_XML + "' and '" + RECORD_ESCAPING_STRING +
1408                    "')");
1409        }
1410    }
1411
1412
1413    private SRURecordDataParser findParser(String schema) {
1414        SRURecordDataParser parser = parsers.get(schema);
1415        if (parser == null) {
1416            parser = parsers.get(SRUClientConstants.RECORD_DATA_PARSER_SCHEMA_ANY);
1417        }
1418        return parser;
1419    }
1420
1421
1422    private static SRUXMLStreamReader createReader(InputStream in, boolean wrap)
1423            throws XMLStreamException {
1424        return new SRUXMLStreamReader(in, wrap);
1425    }
1426
1427
1428    private static CloseableHttpClient createHttpClient(int connectTimeout,
1429            int socketTimeout) {
1430        final PoolingHttpClientConnectionManager manager =
1431                new PoolingHttpClientConnectionManager();
1432        manager.setDefaultMaxPerRoute(8);
1433        manager.setMaxTotal(128);
1434
1435        final SocketConfig socketConfig = SocketConfig.custom()
1436                .setSoReuseAddress(true)
1437                .setSoLinger(0)
1438                .build();
1439
1440        final RequestConfig requestConfig = RequestConfig.custom()
1441                .setAuthenticationEnabled(false)
1442                .setRedirectsEnabled(true)
1443                .setMaxRedirects(4)
1444                .setCircularRedirectsAllowed(false)
1445                .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
1446                .setConnectTimeout(connectTimeout)
1447                .setSocketTimeout(socketTimeout)
1448                .setConnectionRequestTimeout(0) /* infinite */
1449                .build();
1450
1451        return HttpClients.custom()
1452                .setUserAgent(USER_AGENT)
1453                .setConnectionManager(manager)
1454                .setDefaultSocketConfig(socketConfig)
1455                .setDefaultRequestConfig(requestConfig)
1456                .setConnectionReuseStrategy(new NoConnectionReuseStrategy())
1457                .build();
1458    }
1459
1460
1461    private static SRUNamespaces detectNamespace(XMLStreamReader reader,
1462            SRUVersion requestedVersion)
1463                    throws SRUClientException {
1464        try {
1465            // skip to first start tag
1466            reader.nextTag();
1467
1468            final String namespaceURI = reader.getNamespaceURI();
1469            logger.debug("found namespace URI '{}', requested version = {}",
1470                    namespaceURI, requestedVersion);
1471
1472            SRUNamespaces namespace;
1473            if (NAMESPACES_LEGACY_LOC.matchNamespace(namespaceURI)) {
1474                namespace = NAMESPACES_LEGACY_LOC;
1475            } else if (NAMESPACES_OASIS.matchNamespace(namespaceURI)) {
1476                namespace = NAMESPACES_OASIS;
1477            } else {
1478                throw new SRUClientException(
1479                        "invalid namespace '" + reader.getNamespaceURI() + "'");
1480            }
1481
1482            if (requestedVersion != null) {
1483                if (!namespace.compatibleWithVersion(requestedVersion)) {
1484                    throw new SRUClientException("Server did not honour " +
1485                            "requested SRU version and responded with " +
1486                            "different version (requested version was " +
1487                            requestedVersion + ")");
1488                }
1489            }
1490            return namespace;
1491        } catch (XMLStreamException e) {
1492            throw new SRUClientException("error detecting namespace", e);
1493        }
1494    }
1495
1496
1497    private interface SRUNamespaces {
1498        public boolean compatibleWithVersion(SRUVersion version);
1499
1500        public String sruNS();
1501
1502        public String scanNS();
1503
1504        public String diagnosticNS();
1505
1506        public boolean matchNamespace(String namespaceURI);
1507
1508    } // interface SRUNamespace
1509
1510
1511    private static final SRUNamespaces NAMESPACES_LEGACY_LOC = new SRUNamespaces() {
1512        private static final String SRU_NS =
1513                "http://www.loc.gov/zing/srw/";
1514        private static final String SRU_DIAGNOSIC_NS =
1515                "http://www.loc.gov/zing/srw/diagnostic/";
1516
1517
1518        @Override
1519        public boolean compatibleWithVersion(SRUVersion version) {
1520            return SRUVersion.VERSION_1_1.equals(version) ||
1521                    SRUVersion.VERSION_1_2.equals(version);
1522        }
1523
1524
1525        @Override
1526        public String sruNS() {
1527            return SRU_NS;
1528        }
1529
1530
1531        @Override
1532        public String scanNS() {
1533            return SRU_NS;
1534        }
1535
1536
1537        @Override
1538        public String diagnosticNS() {
1539            return SRU_DIAGNOSIC_NS;
1540        }
1541
1542
1543        @Override
1544        public boolean matchNamespace(String namespaceURI) {
1545            return SRU_NS.equals(namespaceURI);
1546        }
1547    };
1548
1549
1550    private static final SRUNamespaces NAMESPACES_OASIS = new SRUNamespaces() {
1551        private static final String SRU_NS =
1552                "http://docs.oasis-open.org/ns/search-ws/sruResponse";
1553        private static final String SRU_SCAN_NS =
1554                "http://docs.oasis-open.org/ns/search-ws/scan";
1555        private static final String SRU_DIAGNOSIC_NS =
1556                "http://docs.oasis-open.org/ns/search-ws/diagnostic";
1557
1558
1559        @Override
1560        public boolean compatibleWithVersion(SRUVersion version) {
1561            return SRUVersion.VERSION_2_0.equals(version);
1562        }
1563
1564
1565        @Override
1566        public String sruNS() {
1567            return SRU_NS;
1568        }
1569
1570
1571        @Override
1572        public String scanNS() {
1573            return SRU_SCAN_NS;
1574
1575        }
1576
1577
1578        @Override
1579        public String diagnosticNS() {
1580            return SRU_DIAGNOSIC_NS;
1581        }
1582
1583
1584        @Override
1585        public boolean matchNamespace(String namespaceURI) {
1586            return SRU_NS.equals(namespaceURI) || SRU_SCAN_NS.equals(namespaceURI);
1587        }
1588    };
1589
1590} // class SRUSimpleClient
Note: See TracBrowser for help on using the repository browser.