source: SRUServer/trunk/src/main/java/eu/clarin/sru/server/SRUXMLStreamWriter.java @ 7269

Last change on this file since 7269 was 7269, checked in by Oliver Schonefeld, 2 years ago
  • Update copyright
  • Property svn:eol-style set to native
File size: 15.7 KB
Line 
1/**
2 * This software is copyright (c) 2011-2022 by
3 *  - Leibniz-Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
4 * This is free software. You can redistribute it
5 * and/or modify it under the terms described in
6 * the GNU General Public License v3 of which you
7 * should have received a copy. Otherwise you can download
8 * it from
9 *
10 *   http://www.gnu.org/licenses/gpl-3.0.txt
11 *
12 * @copyright Leibniz-Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
13 *
14 * @license http://www.gnu.org/licenses/gpl-3.0.txt
15 *  GNU General Public License v3
16 */
17package eu.clarin.sru.server;
18
19import java.io.IOException;
20import java.io.OutputStream;
21import java.io.OutputStreamWriter;
22import java.io.StringReader;
23import java.io.Writer;
24import java.util.ArrayDeque;
25import java.util.Deque;
26
27import javax.xml.namespace.NamespaceContext;
28import javax.xml.parsers.SAXParser;
29import javax.xml.parsers.SAXParserFactory;
30import javax.xml.stream.XMLOutputFactory;
31import javax.xml.stream.XMLStreamException;
32import javax.xml.stream.XMLStreamWriter;
33
34import org.xml.sax.Attributes;
35import org.xml.sax.InputSource;
36import org.xml.sax.SAXException;
37import org.xml.sax.helpers.DefaultHandler;
38import org.z3950.zing.cql.CQLNode;
39
40final class SRUXMLStreamWriter implements XMLStreamWriter {
41    private enum IndentingState {
42        SEEN_NOTHING,
43        SEEN_ELEMENT,
44        SEEN_DATA
45    }
46    private static final SAXParserFactory factory;
47    private final SRURecordXmlEscaping recordEscaping;
48    private final Writer writer;
49    private final XMLStreamWriter xmlwriter;
50    private int indent = -1;
51    private int depth = 0;
52    private Deque<IndentingState> stateStack;
53    private IndentingState state;
54    private boolean writingRecord = false;
55
56
57    SRUXMLStreamWriter(OutputStream stream,
58            XMLOutputFactory factory,
59            SRURecordXmlEscaping recordXmlEscaping,
60            int indent) throws IOException, XMLStreamException {
61        this.recordEscaping = recordXmlEscaping;
62        this.writer = new OutputStreamWriter(stream,
63                SRUServer.RESPONSE_ENCODING) {
64            @Override
65            public void write(int c) throws IOException {
66                if (writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING)) {
67                    /*
68                     * NOTE: need to write single characters here, because
69                     * super.write(String) will call us again, and we would
70                     * create and endless loop here until stack blows up ...
71                     */
72                    switch (c) {
73                    case '<':
74                        super.write('&');
75                        super.write('l');
76                        super.write('t');
77                        super.write(';');
78                        return;
79                    case '>':
80                        super.write('&');
81                        super.write('g');
82                        super.write('t');
83                        super.write(';');
84                        return;
85                    case '&':
86                        super.write('&');
87                        super.write('a');
88                        super.write('m');
89                        super.write('p');
90                        super.write(';');
91                        return;
92                    default:
93                        /* $FALL-THROUGH$ */
94                    }
95                }
96                super.write(c);
97            }
98
99            @Override
100            public void write(char[] c, int off, int len) throws IOException {
101                if (writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING)) {
102                    for (int i = off; i < len; i++) {
103                        this.write(c[i]);
104                    }
105                } else {
106                    super.write(c, off, len);
107                }
108            }
109
110            @Override
111            public void write(String s, int off, int len) throws IOException {
112                if (writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING)) {
113                    for (int i = off; i < len; i++) {
114                        this.write(s.charAt(i));
115                    }
116                } else {
117                    super.write(s, off, len);
118                }
119            }
120        };
121
122        /*
123         * It is believed, that XMLWriterFactories, once configured,
124         * are thread-save ...
125         */
126        this.xmlwriter = factory.createXMLStreamWriter(this.writer);
127
128        if (indent > 0) {
129            this.indent = indent;
130            this.state = IndentingState.SEEN_NOTHING;
131            this.stateStack = new ArrayDeque<IndentingState>(16);
132        }
133    }
134
135
136    @Override
137    public void writeStartElement(String localName) throws XMLStreamException {
138        if (indent > 0) {
139            onStartElement();
140        }
141        xmlwriter.writeStartElement(localName);
142    }
143
144
145    @Override
146    public void writeStartElement(String namespaceURI, String localName)
147            throws XMLStreamException {
148        if (indent > 0) {
149            onStartElement();
150        }
151        xmlwriter.writeStartElement(namespaceURI, localName);
152    }
153
154
155    @Override
156    public void writeStartElement(String prefix, String localName,
157            String namespaceURI) throws XMLStreamException {
158        if (indent > 0) {
159            onStartElement();
160        }
161        xmlwriter.writeStartElement(prefix, localName, namespaceURI);
162    }
163
164
165    @Override
166    public void writeEmptyElement(String namespaceURI, String localName)
167            throws XMLStreamException {
168        if (indent > 0) {
169            onEmptyElement();
170        }
171        xmlwriter.writeEmptyElement(namespaceURI, localName);
172    }
173
174
175    @Override
176    public void writeEmptyElement(String prefix, String localName,
177            String namespaceURI) throws XMLStreamException {
178        if (indent > 0) {
179            onEmptyElement();
180        }
181        xmlwriter.writeEmptyElement(prefix, localName, namespaceURI);
182    }
183
184
185    @Override
186    public void writeEmptyElement(String localName) throws XMLStreamException {
187        if (indent > 0) {
188            onEmptyElement();
189        }
190        xmlwriter.writeEmptyElement(localName);
191    }
192
193
194    @Override
195    public void writeEndElement() throws XMLStreamException {
196        if (indent > 0) {
197            onEndElement();
198        }
199        xmlwriter.writeEndElement();
200    }
201
202
203    @Override
204    public void writeEndDocument() throws XMLStreamException {
205        xmlwriter.writeEndDocument();
206    }
207
208
209    @Override
210    public void close() throws XMLStreamException {
211        xmlwriter.close();
212    }
213
214
215    @Override
216    public void flush() throws XMLStreamException {
217        xmlwriter.flush();
218    }
219
220
221    @Override
222    public void writeAttribute(String localName, String value)
223            throws XMLStreamException {
224        xmlwriter.writeAttribute(localName, value);
225    }
226
227
228    @Override
229    public void writeAttribute(String prefix, String namespaceURI,
230            String localName, String value) throws XMLStreamException {
231        xmlwriter.writeAttribute(prefix, namespaceURI, localName, value);
232    }
233
234
235    @Override
236    public void writeAttribute(String namespaceURI, String localName,
237            String value) throws XMLStreamException {
238        xmlwriter.writeAttribute(namespaceURI, localName, value);
239    }
240
241
242    @Override
243    public void writeNamespace(String prefix, String namespaceURI)
244            throws XMLStreamException {
245        xmlwriter.writeNamespace(prefix, namespaceURI);
246    }
247
248
249    @Override
250    public void writeDefaultNamespace(String namespaceURI)
251            throws XMLStreamException {
252        xmlwriter.writeDefaultNamespace(namespaceURI);
253    }
254
255
256    @Override
257    public void writeComment(String data) throws XMLStreamException {
258        xmlwriter.writeComment(data);
259    }
260
261
262    @Override
263    public void writeProcessingInstruction(String target)
264            throws XMLStreamException {
265        xmlwriter.writeProcessingInstruction(target);
266    }
267
268
269    @Override
270    public void writeProcessingInstruction(String target, String data)
271            throws XMLStreamException {
272        xmlwriter.writeProcessingInstruction(target, data);
273    }
274
275
276    @Override
277    public void writeCData(String data) throws XMLStreamException {
278        if (indent > 0) {
279            state = IndentingState.SEEN_DATA;
280        }
281        xmlwriter.writeCData(data);
282    }
283
284
285    @Override
286    public void writeDTD(String dtd) throws XMLStreamException {
287        xmlwriter.writeDTD(dtd);
288    }
289
290
291    @Override
292    public void writeEntityRef(String name) throws XMLStreamException {
293        xmlwriter.writeEntityRef(name);
294    }
295
296
297    @Override
298    public void writeStartDocument() throws XMLStreamException {
299        xmlwriter.writeStartDocument();
300        if (indent > 0) {
301            xmlwriter.writeCharacters("\n");
302        }
303    }
304
305
306    @Override
307    public void writeStartDocument(String version) throws XMLStreamException {
308        xmlwriter.writeStartDocument(version);
309        if (indent > 0) {
310            xmlwriter.writeCharacters("\n");
311        }
312    }
313
314
315    @Override
316    public void writeStartDocument(String encoding, String version)
317            throws XMLStreamException {
318        xmlwriter.writeStartDocument(encoding, version);
319        if (indent > 0) {
320            xmlwriter.writeCharacters("\n");
321        }
322    }
323
324
325    @Override
326    public void writeCharacters(String text) throws XMLStreamException {
327        if (indent > 0) {
328            state = IndentingState.SEEN_DATA;
329        }
330        xmlwriter.writeCharacters(text);
331    }
332
333
334    @Override
335    public void writeCharacters(char[] text, int start, int len)
336            throws XMLStreamException {
337        if (indent > 0) {
338            state = IndentingState.SEEN_DATA;
339        }
340        xmlwriter.writeCharacters(text, start, len);
341    }
342
343
344    @Override
345    public String getPrefix(String uri) throws XMLStreamException {
346        return xmlwriter.getPrefix(uri);
347    }
348
349
350    @Override
351    public void setPrefix(String prefix, String uri) throws XMLStreamException {
352        xmlwriter.setPrefix(prefix, uri);
353    }
354
355
356    @Override
357    public void setDefaultNamespace(String uri) throws XMLStreamException {
358        xmlwriter.setDefaultNamespace(uri);
359    }
360
361
362    @Override
363    public void setNamespaceContext(NamespaceContext context)
364            throws XMLStreamException {
365        xmlwriter.setNamespaceContext(context);
366    }
367
368
369    @Override
370    public NamespaceContext getNamespaceContext() {
371        return xmlwriter.getNamespaceContext();
372    }
373
374
375    @Override
376    public Object getProperty(String name) throws IllegalArgumentException {
377        return xmlwriter.getProperty(name);
378    }
379
380
381    public Writer getWriter() {
382        return writer;
383    }
384
385
386    public void startRecord() throws XMLStreamException {
387        if (writingRecord) {
388            throw new IllegalStateException("was already writing record");
389        }
390        xmlwriter.flush();
391        /*
392         *  abuse writeCharacters to force writer to close finish
393         *  any pending start or end elements
394         */
395        xmlwriter.writeCharacters("");
396        writingRecord = true;
397    }
398
399
400    public void endRecord() throws XMLStreamException {
401        if (!writingRecord) {
402            throw new IllegalStateException("was not writing record");
403        }
404        /*
405         *  abuse writeCharacters to force writer to close finish
406         *  any pending start or end elements
407         */
408        xmlwriter.writeCharacters("");
409        xmlwriter.flush();
410        writingRecord = false;
411    }
412
413
414    public void writeXCQL(CQLNode query, final boolean searchRetrieveMode)
415            throws XMLStreamException {
416        /*
417         * HACK: Parsing the XCQL to serialize is wasting resources.
418         * Alternative would be to serialize to XCQL from CQLNode, but
419         * I'm not yet enthusiastic on writing the serializer myself.
420         */
421        try {
422            SAXParser parser = factory.newSAXParser();
423            InputSource input =
424                    new InputSource(new StringReader(query.toXCQL()));
425            parser.parse(input, new DefaultHandler() {
426                @Override
427                public void startElement(String uri, String localName,
428                        String qName, Attributes attributes)
429                                throws SAXException {
430                    if (!searchRetrieveMode && qName.equals("searchClause")) {
431                        return;
432                    }
433                    try {
434                        SRUXMLStreamWriter.this.writeStartElement(qName);
435                        for (int i = 0; i < attributes.getLength(); i++) {
436                            SRUXMLStreamWriter.this.writeAttribute(attributes.getQName(i),
437                                    attributes.getValue(i));
438                        }
439                    } catch (XMLStreamException e) {
440                        throw new SAXException(e);
441                    }
442                }
443
444                @Override
445                public void endElement(String uri, String localName,
446                        String qName) throws SAXException {
447                    if (!searchRetrieveMode && qName.equals("searchClause")) {
448                        return;
449                    }
450                    try {
451                        SRUXMLStreamWriter.this.writeEndElement();
452                    } catch (XMLStreamException e) {
453                        throw new SAXException(e);
454                    }
455                }
456
457                @Override
458                public void characters(char[] text, int start, int length)
459                        throws SAXException {
460                    try {
461                        boolean isWhitespaceOnly = true;
462                        for (int i = start; i < start + length; i++) {
463                            if (!Character.isWhitespace(text[i])) {
464                                isWhitespaceOnly = false;
465                                break;
466                            }
467                        }
468                        if (!isWhitespaceOnly) {
469                            SRUXMLStreamWriter.this.writeCharacters(text, start, length);
470                        }
471                    } catch (XMLStreamException e) {
472                        throw new SAXException(e);
473                    }
474                }
475            });
476        } catch (Exception e) {
477            throw new XMLStreamException("cannot write XCQL", e);
478        }
479    }
480
481
482    private void onStartElement() throws XMLStreamException {
483        if (!(writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING))) {
484            stateStack.push(IndentingState.SEEN_ELEMENT);
485            state = IndentingState.SEEN_NOTHING;
486            if (depth > 0) {
487                xmlwriter.writeCharacters("\n");
488            }
489            doIndent();
490            depth++;
491        }
492    }
493
494
495    private void onEndElement() throws XMLStreamException {
496        if (!(writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING))) {
497            depth--;
498            if (state == IndentingState.SEEN_ELEMENT) {
499                xmlwriter.writeCharacters("\n");
500                doIndent();
501            }
502            state = stateStack.pop();
503        }
504    }
505
506
507    private void onEmptyElement() throws XMLStreamException {
508        if (!(writingRecord && (recordEscaping == SRURecordXmlEscaping.STRING))) {
509            state = IndentingState.SEEN_ELEMENT;
510            if (depth > 0) {
511                xmlwriter.writeCharacters("\n");
512            }
513            doIndent();
514        }
515    }
516
517
518    private void doIndent() throws XMLStreamException {
519        if (depth > 0) {
520            for (int i = 0; i < (depth * indent); i++) {
521                xmlwriter.writeCharacters(" ");
522            }
523        }
524    }
525
526
527    static {
528        factory = SAXParserFactory.newInstance();
529        factory.setNamespaceAware(false);
530        factory.setValidating(false);
531        factory.setXIncludeAware(false);
532    }
533
534} // class SRUXMLStreamWriter
Note: See TracBrowser for help on using the repository browser.