source: SRUClient/tags/SRUClient-0.9.4/src/main/java/eu/clarin/sru/client/SRUSimpleClient.java @ 5805

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