source: SRUCQIBridge/src/main/java/eu/clarin/sru/cqibridge/CqiSRUSearchEngine.java @ 2477

Last change on this file since 2477 was 2477, checked in by akislev, 11 years ago

integrated recent changes needed for the scan operation to work

File size: 20.2 KB
Line 
1/**
2 * This software is copyright (c) 2012 by - Institut fuer Deutsche Sprache
3 * (http://www.ids-mannheim.de), Seminar fuer Sprachwissenschaft
4 * (http://www.sfs.uni-tuebingen.de/) This is free software. You can
5 * redistribute it and/or modify it under the terms described in the GNU General
6 * Public License v3 of which you should have received a copy. Otherwise you can
7 * download it from
8 *
9 * http://www.gnu.org/licenses/gpl-3.0.txt
10 *
11 * @copyright Seminar fuer Sprachwissenschaft (http://www.sfs.uni-tuebingen.de/)
12 *
13 * @license http://www.gnu.org/licenses/gpl-3.0.txt GNU General Public License
14 * v3
15 */
16package eu.clarin.sru.cqibridge;
17
18import eu.clarin.cqi.client.CqiClient;
19import eu.clarin.cqi.client.CqiClientException;
20import eu.clarin.cqi.client.CqiResult;
21import eu.clarin.sru.server.*;
22import eu.clarin.sru.server.utils.SRUSearchEngineBase;
23import java.util.Arrays;
24import java.util.Map;
25import java.util.NoSuchElementException;
26import java.util.Vector;
27import java.util.regex.Pattern;
28import javax.xml.stream.XMLStreamException;
29import javax.xml.stream.XMLStreamWriter;
30import org.slf4j.Logger;
31import org.slf4j.LoggerFactory;
32import org.z3950.zing.cql.CQLNode;
33import org.z3950.zing.cql.CQLRelation;
34import org.z3950.zing.cql.CQLTermNode;
35import org.z3950.zing.cql.Modifier;
36
37/**
38 *
39 * @author akislev
40 */
41public class CqiSRUSearchEngine extends SRUSearchEngineBase {
42
43    private static final String PARAM_CQI_SERVER_HOST = "cqi.serverHost";
44    private static final String PARAM_CQI_SERVER_PORT = "cqi.serverPort";
45    private static final String PARAM_CQI_SERVER_USERNAME = "cqi.serverUsername";
46    private static final String PARAM_CQI_SERVER_PASSWORD = "cqi.serverPassword";
47    private static final String PARAM_CQI_DEFAULT_CORPUS = "cqi.defaultCorpus";
48    private static final String PARAM_CQI_DEFAULT_CORPUS_PID = "cqi.defaultCorpusPID";
49    private static final String PARAM_CQI_DEFAULT_CORPUS_REF = "cqi.defaultCorpusRef";
50    private static final String CQI_SUPPORTED_RELATION_CQL_1_1 = "scr";
51    private static final String CQI_SUPPORTED_RELATION_CQL_1_2 = "=";
52    private static final String CQI_SUPPORTED_RELATION_EXACT = "exact";
53    private static final String INDEX_CQL_SERVERCHOICE = "cql.serverChoice";
54    private static final String INDEX_FCS_WORDS = "words";
55    private static final String INDEX_FCS_RESOURCE = "fcs.resource";
56    private static final String FCS_RESOURCE_TERM_WILDCARD = "*";
57    private static final String FCS_RESOURCE_TERM_ANY = "any";
58    private static final String FCS_NS = "http://clarin.eu/fcs/1.0";
59    private static final String FCS_PREFIX = "fcs";
60    private static final String FCS_KWIC_NS = "http://clarin.eu/fcs/1.0/kwic";
61    private static final String FCS_KWIC_PREFIX = "kwic";
62    private static final String CLARIN_FCS_RECORD_SCHEMA = FCS_NS;
63    private static final String X_CLARIN_RESOURCE_INFO = "x-clarin-resource-info";
64    private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+");
65    private static final String WORD_POSITIONAL_ATTRIBUTE = "word";
66    private static final String CONTEXT_STRUCTURAL_ATTRIBUTE = "s";
67    private static final Logger logger =
68            LoggerFactory.getLogger(CqiSRUSearchEngine.class);
69    private static final ResourceInfo[] RESOURCE_INFOS = new ResourceInfo[]{
70        new ResourceInfo("tueba-ddc", -1, false,
71        Arrays.asList("en", "TuebaDDC",
72        "de", "TÃŒbaDDC"),
73        Arrays.asList("en", "TÃŒbingen Treebank of Written German - Diachronic Corpus.",
74        "de", "TÃŒbingen Baumbank des Deutschen - Diachrones Corpus."),
75        Arrays.asList("deu"),
76        Arrays.asList("text", "fcs.words")),};
77    private CqiClient client;
78    private String defaultCorpusName;
79    private String defaultCorpusPID;
80    private String defaultCorpusRef;
81
82    @Override
83    public void init(SRUServerConfig config, Map<String, String> params)
84            throws SRUConfigException {
85        final String serverHost = params.get(PARAM_CQI_SERVER_HOST);
86        if (serverHost == null) {
87            throw new SRUConfigException("parameter \""
88                    + PARAM_CQI_SERVER_HOST + "\" is mandatory");
89        }
90        logger.info("using cqi server host: {}", serverHost);
91        final String serverPortString = params.get(PARAM_CQI_SERVER_PORT);
92        if (serverPortString == null) {
93            throw new SRUConfigException("parameter \""
94                    + PARAM_CQI_SERVER_PORT + "\" is mandatory");
95        }
96        final int serverPort = Integer.parseInt(serverPortString);
97        logger.info("using cqi server port: {}", serverPort);
98        final String username = params.get(PARAM_CQI_SERVER_USERNAME);
99        if (username == null) {
100            throw new SRUConfigException("parameter \""
101                    + PARAM_CQI_SERVER_USERNAME + "\" is mandatory");
102        }
103        final String password = params.get(PARAM_CQI_SERVER_PASSWORD);
104        if (password == null) {
105            throw new SRUConfigException("parameter \""
106                    + PARAM_CQI_SERVER_PASSWORD + "\" is mandatory");
107        }
108        defaultCorpusName = params.get(PARAM_CQI_DEFAULT_CORPUS);
109        if (defaultCorpusName == null) {
110            throw new SRUConfigException("parameter \""
111                    + PARAM_CQI_DEFAULT_CORPUS + "\" is mandatory");
112        }
113        defaultCorpusPID = params.get(PARAM_CQI_DEFAULT_CORPUS_PID);
114        if (defaultCorpusPID == null) {
115            throw new SRUConfigException("parameter \""
116                    + PARAM_CQI_DEFAULT_CORPUS_PID + "\" is mandatory");
117        }
118        defaultCorpusRef = params.get(PARAM_CQI_DEFAULT_CORPUS_REF);
119        if (defaultCorpusRef == null) {
120            throw new SRUConfigException("parameter \""
121                    + PARAM_CQI_DEFAULT_CORPUS_REF + "\" is mandatory");
122        }
123        try {
124            client = new CqiClient(serverHost, serverPort);
125        } catch (CqiClientException ex) {
126            throw new SRUConfigException("can't initialize a cqi client", ex);
127        }
128        try {
129            client.connect(username, password);
130        } catch (CqiClientException ex) {
131            throw new SRUConfigException("can't connect to the cqi server", ex);
132        }
133    }
134
135    @Override
136    public SRUExplainResult explain(SRUServerConfig config,
137            SRURequest request, SRUDiagnosticList diagnostics)
138            throws SRUException {
139        return null;
140    }
141
142    @Override
143    public SRUScanResultSet scan(SRUServerConfig config, SRURequest request,
144            SRUDiagnosticList diagnostics) throws SRUException {
145        /*
146         * handle scan on CLARIN FCS fcs.resource;
147         * otherwise return an empty scan result set ...
148         */
149        final ResourceInfo[] result =
150                translateFcsScanResource(request.getScanClause());
151        final boolean provideResourceInfo = (result != null)
152                && parseBoolean(request.getExtraRequestData(X_CLARIN_RESOURCE_INFO));
153        return new SRUScanResultSet(diagnostics) {
154            private int idx = -1;
155
156            @Override
157            public boolean nextTerm() {
158                return (result != null) && (++idx < result.length);
159            }
160
161            @Override
162            public String getValue() {
163                return result[idx].getCorpusId();
164            }
165
166            @Override
167            public int getNumberOfRecords() {
168                return result[idx].getResourceCount();
169            }
170
171            @Override
172            public String getDisplayTerm() {
173                return null;
174            }
175
176            @Override
177            public SRUScanResultSet.WhereInList getWhereInList() {
178                return null;
179            }
180
181            @Override
182            public boolean hasExtraTermData() {
183                return provideResourceInfo;
184            }
185
186            @Override
187            public void writeExtraTermData(XMLStreamWriter writer)
188                    throws XMLStreamException {
189                if (provideResourceInfo) {
190                    result[idx].writeResourceInfo(writer, null);
191                }
192            }
193        };
194    }
195
196    @Override
197    public SRUSearchResultSet search(SRUServerConfig config,
198            SRURequest request, SRUDiagnosticList diagnostics)
199            throws SRUException {
200        /*
201         * sanity check: make sure we are asked to return stuff in CLARIN FCS
202         * format if a recordSchema is specified.
203         */
204        final String recordSchemaIdentifier =
205                request.getRecordSchemaIdentifier();
206        if ((recordSchemaIdentifier != null)
207                && !recordSchemaIdentifier.equals(CLARIN_FCS_RECORD_SCHEMA)) {
208            throw new SRUException(
209                    SRUConstants.SRU_UNKNOWN_SCHEMA_FOR_RETRIEVAL,
210                    recordSchemaIdentifier, "Record schema \""
211                    + recordSchemaIdentifier
212                    + "\" is not supported by this endpoint.");
213        }
214
215        /*
216         * commence search ...
217         */
218        final CQLNode query = request.getQuery();
219        int startRecord = request.getStartRecord();
220        final int maximumRecords = request.getMaximumRecords();
221
222        final String cqpQuery = translateCQLtoCQP(query);
223        if (startRecord > 0) {
224            startRecord--;
225        } else if (startRecord == -1) {
226            startRecord = 0;
227        }
228        logger.info("running query = \"{}\", offset = {}, limit = {}",
229                new Object[]{cqpQuery, startRecord, maximumRecords});
230        try {
231            final CqiResult result = client.query(defaultCorpusName, cqpQuery, CONTEXT_STRUCTURAL_ATTRIBUTE);
232            if ((result.size() > 0 && !result.absolute(startRecord)) || (result.size() == 0 && startRecord > 0)) {
233                diagnostics.addDiagnostic(SRUConstants.SRU_FIRST_RECORD_POSITION_OUT_OF_RANGE, Integer.toString(startRecord + 1), null);
234            }
235
236            return new SRUSearchResultSet(diagnostics) {
237                private int pos = 0;
238
239                @Override
240                public int getTotalRecordCount() {
241                    return result.size();
242                }
243
244                @Override
245                public int getRecordCount() {
246                    return result.size();
247                }
248
249                @Override
250                public String getRecordSchemaIdentifier() {
251                    return CLARIN_FCS_RECORD_SCHEMA;
252                }
253
254                @Override
255                public String getRecordIdentifier() {
256                    return null;
257                }
258
259                @Override
260                public boolean nextRecord() {
261                    try {
262                        return pos++ < maximumRecords && result.next();
263                    } catch (CqiClientException e) {
264                        throw new NoSuchElementException(e.getMessage());
265                    }
266                }
267
268                @Override
269                public SRUDiagnostic getSurrogateDiagnostic() {
270                    return null;
271                }
272
273                @Override
274                public void writeRecord(XMLStreamWriter writer)
275                        throws XMLStreamException {
276                    final int contextStart = result.getContextStart();
277                    final int contextEnd = result.getContextEnd();
278                    final int matchStart = result.getMatchStart();
279                    final int matchEnd = result.getMatchEnd();
280                    final int relMatchStart = matchStart - contextStart;
281                    final int relMatchEnd = matchEnd - contextStart + 1;
282                    final int relContextEnd = contextEnd - contextStart + 1;
283                    final StringBuilder leftContext = new StringBuilder();
284                    final StringBuilder keyWord = new StringBuilder();
285                    final StringBuilder rightContext = new StringBuilder();
286                    String[] words;
287                    try {
288                        words = result.getValues(WORD_POSITIONAL_ATTRIBUTE, contextStart, contextEnd);
289                    } catch (CqiClientException e) {
290                        throw new XMLStreamException("can't obtain the values of the positional attribute '" + WORD_POSITIONAL_ATTRIBUTE + "'", e);
291                    }
292                    boolean isFirst = true;
293                    for (int i = 0; i < relMatchStart; i++) {
294                        if (isFirst) {
295                            isFirst = false;
296                        } else {
297                            leftContext.append(' ');
298                        }
299                        leftContext.append(words[i]);
300                    }
301                    isFirst = true;
302                    for (int i = relMatchStart; i < relMatchEnd; i++) {
303                        if (isFirst) {
304                            isFirst = false;
305                        } else {
306                            keyWord.append(' ');
307                        }
308                        keyWord.append(words[i]);
309                    }
310                    isFirst = true;
311                    for (int i = relMatchEnd; i < relContextEnd; i++) {
312                        if (isFirst) {
313                            isFirst = false;
314                        } else {
315                            rightContext.append(' ');
316                        }
317                        rightContext.append(words[i]);
318                    }
319                    writer.setPrefix(FCS_PREFIX, FCS_NS);
320                    writer.writeStartElement(FCS_NS, "Resource");
321                    writer.writeNamespace(FCS_PREFIX, FCS_NS);
322                    writer.writeAttribute("pid", defaultCorpusPID);
323                    writer.writeAttribute("ref", defaultCorpusRef);
324                    writer.writeStartElement(FCS_NS, "DataView");
325                    writer.writeAttribute("type", "kwic");
326
327                    writer.setPrefix(FCS_KWIC_PREFIX, FCS_KWIC_NS);
328                    writer.writeStartElement(FCS_KWIC_NS, "kwic");
329                    writer.writeNamespace(FCS_KWIC_PREFIX, FCS_KWIC_NS);
330
331                    writer.writeStartElement(FCS_KWIC_NS, "c");
332                    writer.writeAttribute("type", "left");
333                    writer.writeCharacters(leftContext.toString());
334                    writer.writeEndElement(); // "c" element
335
336                    writer.writeStartElement(FCS_KWIC_NS, "kw");
337                    writer.writeCharacters(keyWord.toString());
338                    writer.writeEndElement(); // "kw" element
339
340                    writer.writeStartElement(FCS_KWIC_NS, "c");
341                    writer.writeAttribute("type", "right");
342                    writer.writeCharacters(rightContext.toString());
343                    writer.writeEndElement(); // "c" element
344
345                    writer.writeEndElement(); // "kwic" element
346
347                    writer.writeEndElement(); // "DataView" element
348                    writer.writeEndElement(); // "Resource" element
349                }
350            };
351        } catch (CqiClientException e) {
352            logger.error("error processing query", e);
353            throw new SRUException(
354                    SRUConstants.SRU_CANNOT_PROCESS_QUERY_REASON_UNKNOWN,
355                    "Error processing query (" + e.getMessage() + ").", e);
356        }
357    }
358
359    private ResourceInfo[] translateFcsScanResource(CQLNode query)
360            throws SRUException {
361        if (query instanceof CQLTermNode) {
362            final CQLTermNode root = (CQLTermNode) query;
363            logger.debug("index = '{}', relation = '{}', term = '{}'",
364                    new Object[]{root.getIndex(),
365                        root.getRelation().getBase(), root.getTerm()});
366
367            String index = root.getIndex();
368            if (!(INDEX_FCS_RESOURCE.equals(index) || INDEX_CQL_SERVERCHOICE.equals(index))) {
369                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_INDEX,
370                        root.getIndex(), "Index \"" + root.getIndex()
371                        + "\" is not supported in scan operation.");
372            }
373
374
375            // only allow "=" relation without any modifiers
376            final CQLRelation relationNode = root.getRelation();
377            String relation = relationNode.getBase();
378            if (!(CQI_SUPPORTED_RELATION_CQL_1_1.equals(relation)
379                    || CQI_SUPPORTED_RELATION_CQL_1_2.equals(relation)
380                    || CQI_SUPPORTED_RELATION_EXACT.equals(relation))) {
381                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_RELATION,
382                        relationNode.getBase(), "Relation \""
383                        + relationNode.getBase()
384                        + "\" is not supported in scan operation.");
385            }
386            Vector<Modifier> modifiers = relationNode.getModifiers();
387            if ((modifiers != null) && !modifiers.isEmpty()) {
388                Modifier modifier = modifiers.get(0);
389                throw new SRUException(
390                        SRUConstants.SRU_UNSUPPORTED_RELATION_MODIFIER,
391                        modifier.getValue(), "Relation modifier \""
392                        + modifier.getValue()
393                        + "\" is not supported in scan operation.");
394            }
395
396            String term = root.getTerm();
397            if ((term == null) || term.isEmpty()) {
398                throw new SRUException(SRUConstants.SRU_EMPTY_TERM_UNSUPPORTED,
399                        "An empty term is not supported in scan operation.");
400            }
401
402            /*
403             * generate result: currently we only have a flat hierarchy, so
404             * return an empty result on any attempt to do a recursive scan ...
405             */
406            if ((INDEX_CQL_SERVERCHOICE.equals(index)
407                    && INDEX_FCS_RESOURCE.equals(term))
408                    || (INDEX_FCS_RESOURCE.equals(index)
409                    && (FCS_RESOURCE_TERM_WILDCARD.equals(term)
410                    || FCS_RESOURCE_TERM_ANY.equalsIgnoreCase(term)))) {
411                return RESOURCE_INFOS;
412            } else {
413                return null;
414            }
415        } else {
416            throw new SRUException(SRUConstants.SRU_QUERY_FEATURE_UNSUPPORTED,
417                    "Scan clause too complex.");
418        }
419    }
420
421    private String translateCQLtoCQP(CQLNode query) throws SRUException {
422        if (query instanceof CQLTermNode) {
423            final CQLTermNode root = (CQLTermNode) query;
424
425            // only allow "cql.serverChoice" and "words" index
426            if (!(INDEX_CQL_SERVERCHOICE.equals(root.getIndex())
427                    || INDEX_FCS_WORDS.equals(root.getIndex()))) {
428                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_INDEX,
429                        root.getIndex(), "Index \"" + root.getIndex()
430                        + "\" is not supported.");
431            }
432
433            // only allow "=" relation without any modifiers
434            final CQLRelation relation = root.getRelation();
435            final String baseRel = relation.getBase();
436            if (!(CQI_SUPPORTED_RELATION_CQL_1_1.equals(baseRel)
437                    || CQI_SUPPORTED_RELATION_CQL_1_2.equals(baseRel)
438                    || CQI_SUPPORTED_RELATION_EXACT.equals(baseRel))) {
439                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_RELATION,
440                        relation.getBase(), "Relation \""
441                        + relation.getBase() + "\" is not supported.");
442            }
443            Vector<Modifier> modifiers = relation.getModifiers();
444            if ((modifiers != null) && !modifiers.isEmpty()) {
445                Modifier modifier = modifiers.get(0);
446                throw new SRUException(
447                        SRUConstants.SRU_UNSUPPORTED_RELATION_MODIFIER,
448                        modifier.getValue(), "Relation modifier \""
449                        + modifier.getValue() + "\" is not supported.");
450            }
451
452            // check term
453            final String term = root.getTerm();
454            if ((term == null) || term.isEmpty()) {
455                throw new SRUException(SRUConstants.SRU_EMPTY_TERM_UNSUPPORTED,
456                        "An empty term is not supported.");
457            }
458            //convert to cqp by inserting quotes around each token
459            return String.format("\"%s\"", SPACE_PATTERN.matcher(term).replaceAll("\" \""));
460
461        }
462        throw new SRUException(SRUConstants.SRU_QUERY_FEATURE_UNSUPPORTED,
463                "Server currently supportes term-only query "
464                + "(CQL conformance level 0).");
465    }
466
467    private boolean parseBoolean(String value) {
468        if (value != null) {
469            return value.endsWith("1") || Boolean.parseBoolean(value);
470        }
471        return false;
472    }
473}
Note: See TracBrowser for help on using the repository browser.