source: FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/SimpleEndpointSearchEngineBase.java @ 5546

Last change on this file since 5546 was 5546, checked in by Oliver Schonefeld, 10 years ago
  • support new FCS specification (with some backwards compatibility for old spec)

HEADS UP: not yet ready for release; needs more testing

  • Property svn:eol-style set to native
File size: 24.1 KB
Line 
1package eu.clarin.sru.server.fcs;
2
3import java.net.URI;
4import java.util.Collections;
5import java.util.List;
6import java.util.Map;
7
8import javax.servlet.ServletContext;
9import javax.xml.XMLConstants;
10import javax.xml.stream.XMLStreamException;
11import javax.xml.stream.XMLStreamWriter;
12
13import org.slf4j.Logger;
14import org.slf4j.LoggerFactory;
15import org.z3950.zing.cql.CQLNode;
16import org.z3950.zing.cql.CQLRelation;
17import org.z3950.zing.cql.CQLTermNode;
18import org.z3950.zing.cql.Modifier;
19
20import eu.clarin.sru.server.SRUConfigException;
21import eu.clarin.sru.server.SRUConstants;
22import eu.clarin.sru.server.SRUDiagnosticList;
23import eu.clarin.sru.server.SRUException;
24import eu.clarin.sru.server.SRUExplainResult;
25import eu.clarin.sru.server.SRURequest;
26import eu.clarin.sru.server.SRUScanResultSet;
27import eu.clarin.sru.server.SRUSearchEngine;
28import eu.clarin.sru.server.SRUServerConfig;
29import eu.clarin.sru.server.utils.SRUSearchEngineBase;
30
31
32/**
33 * A base class for implementing a simple search engine to be used as a
34 * CLARIN-FCS endpoint.
35 *
36 */
37public abstract class SimpleEndpointSearchEngineBase extends
38        SRUSearchEngineBase {
39    private static final String X_FCS_ENDPOINT_DESCRIPTION = "x-fcs-endpoint-description";
40    private static final String X_CMD_RESOURCE_INFO = "x-cmd-resource-info";
41    private static final String ED_NS = "http://clarin.eu/fcs/endpoint-description";
42    private static final String ED_PREFIX = "ed";
43    private static final int ED_VERSION = 1;
44    private static final String FCS_RESOURCE_INFO_NS = "http://clarin.eu/fcs/1.0/resource-info";
45    private static final String FCS_SCAN_INDEX_FCS_RESOURCE = "fcs.resource";
46    private static final String FCS_SCAN_INDEX_CQL_SERVERCHOICE = "cql.serverChoice";
47    private static final String FCS_SCAN_SUPPORTED_RELATION_CQL_1_1 = "scr";
48    private static final String FCS_SCAN_SUPPORTED_RELATION_CQL_1_2 = "=";
49    private static final String FCS_SUPPORTED_RELATION_EXACT = "exact";
50    private static final Logger logger =
51            LoggerFactory.getLogger(SimpleEndpointSearchEngineBase.class);
52    protected EndpointDescription endpointDescription;
53
54
55    /**
56     * This method should not be overridden. Perform your custom initialization
57     * in the {@link #doInit(ServletContext, SRUServerConfig, Map)} method
58     * Instead.
59     *
60     * @see #doInit(ServletContext, SRUServerConfig, Map)
61     */
62    @Override
63    public final void init(ServletContext context, SRUServerConfig config,
64            Map<String, String> params) throws SRUConfigException {
65        logger.debug("initializing");
66        super.init(context, config, params);
67
68        logger.debug("initializing search engine implementation");
69        doInit(context, config, params);
70
71        logger.debug("initizalizing endpoint description");
72        this.endpointDescription =
73                createEndpointDescription(context, config, params);
74        if (this.endpointDescription == null) {
75            logger.error("SimpleEndpointSearchEngineBase implementation " +
76                    "error: createEndpointDescription() returned null");
77            throw new SRUConfigException("createEndpointDescription() " +
78                    "returned no valid implementation of an EndpointDescription");
79        }
80    }
81
82
83    /**
84     * This method should not be overridden. Perform you custom cleanup in the
85     * {@link #doDestroy()} method.
86     *
87     * @see #doDestroy()
88     */
89    @Override
90    public final void destroy() {
91        logger.debug("performing cleanup of endpoint description");
92        endpointDescription.destroy();
93        logger.debug("performing cleanup of search engine");
94        doDestroy();
95        super.destroy();
96    }
97
98
99    @Override
100    public final SRUExplainResult explain(SRUServerConfig config,
101            SRURequest request, SRUDiagnosticList diagnostics)
102            throws SRUException {
103
104        final boolean provideEndpointDescription =
105                parseBoolean(request.getExtraRequestData(
106                        X_FCS_ENDPOINT_DESCRIPTION));
107
108        if (provideEndpointDescription) {
109            return new SRUExplainResult(diagnostics) {
110                @Override
111                public boolean hasExtraResponseData() {
112                    return provideEndpointDescription;
113                }
114
115
116                @Override
117                public void writeExtraResponseData(XMLStreamWriter writer)
118                        throws XMLStreamException {
119                    writeEndpointDescription(writer);
120                }
121            };
122        } else {
123            return null;
124        }
125    }
126
127
128    /**
129     * Handle a <em>scan</em> operation. This implementation provides support to
130     * CLARIN FCS resource enumeration. If you want to provide custom scan
131     * behavior for a different index, override the
132     * {@link #doScan(SRUServerConfig, SRURequest, SRUDiagnosticList)} method.
133     *
134     * @see #doScan(SRUServerConfig, SRURequest, SRUDiagnosticList)
135     */
136    @Override
137    public final SRUScanResultSet scan(SRUServerConfig config,
138            SRURequest request, SRUDiagnosticList diagnostics)
139            throws SRUException {
140        /*
141         * Check if we got a scan on fcs.resource. If yes, handle it
142         * accordingly, otherwise delegate to user-provided implementation.
143         */
144        final List<ResourceInfo> result =
145                translateFcsScanResource(request.getScanClause());
146        if (result != null) {
147            /*
148             * Make sure, we honor the maximumTerms limit, of the client
149             * requests it ...
150             */
151            final int maxTerms
152                = ((result.size() > 0) && (request.getMaximumTerms() > 0))
153                ? Math.min(result.size(), request.getMaximumTerms())
154                : result.size();
155
156            /*
157             * Shall we provide extended resource information ... ?
158             */
159            final boolean provideResourceInfo = parseBoolean(
160                    request.getExtraRequestData(X_CMD_RESOURCE_INFO));
161
162            return new SRUScanResultSet(diagnostics) {
163                private int idx = -1;
164
165                @Override
166                public boolean nextTerm() {
167                    return (result != null) && (++idx < maxTerms);
168                }
169
170
171                @Override
172                public String getValue() {
173                    return result.get(idx).getPid();
174                }
175
176
177                @Override
178                public int getNumberOfRecords() {
179                    return -1;
180                }
181
182
183                @Override
184                public String getDisplayTerm() {
185                    return result.get(idx).getTitle("en");
186                }
187
188
189                @Override
190                public WhereInList getWhereInList() {
191                    return null;
192                }
193
194
195                @Override
196                public boolean hasExtraTermData() {
197                    return provideResourceInfo;
198                }
199
200
201                @Override
202                public void writeExtraTermData(XMLStreamWriter writer)
203                        throws XMLStreamException {
204                    if (provideResourceInfo) {
205                        writeLegacyResourceInfo(writer, result.get(idx));
206                    }
207                }
208            };
209        } else {
210            return doScan(config, request, diagnostics);
211        }
212    }
213
214
215    protected abstract EndpointDescription createEndpointDescription(
216            ServletContext context, SRUServerConfig config,
217            Map<String, String> params) throws SRUConfigException;
218
219
220    /**
221     * Initialize the search engine. This initialization should be tailed
222     * towards your environment and needs.
223     *
224     * @param context
225     *            the {@link ServletContext} for the Servlet
226     * @param config
227     *            the {@link SRUServerConfig} object for this search engine
228     * @param params
229     *            additional parameters gathered from the Servlet configuration
230     *            and Servlet context.
231     * @throws SRUConfigException
232     *             if an error occurred
233     */
234    protected abstract void doInit(ServletContext context,
235            SRUServerConfig config, Map<String, String> params)
236            throws SRUConfigException;
237
238
239    /**
240     * Destroy the search engine. Override this method for any cleanup the
241     * search engine needs to perform upon termination.
242     */
243    protected void doDestroy() {
244    }
245
246
247    /**
248     * Handle a <em>explain</em> operation. The default implementation is a
249     * no-op. Override this method, if you want to provide a custom behavior.
250     *
251     * @see SRUSearchEngine#explain(SRUServerConfig, SRURequest,
252     *      SRUDiagnosticList)
253     */
254    protected SRUScanResultSet doScan(SRUServerConfig config,
255            SRURequest request, SRUDiagnosticList diagnostics)
256            throws SRUException {
257        final CQLNode scanClause = request.getScanClause();
258        if (scanClause instanceof CQLTermNode) {
259            final CQLTermNode root = (CQLTermNode) scanClause;
260            final String index = root.getIndex();
261            throw new SRUException(SRUConstants.SRU_UNSUPPORTED_INDEX, index,
262                    "scan operation on index '" + index + "' is not supported");
263        } else {
264            throw new SRUException(SRUConstants.SRU_QUERY_FEATURE_UNSUPPORTED,
265                    "Scan clause too complex.");
266        }
267    }
268
269
270    /**
271     * Convince method for parsing a string to boolean. Values <code>1</code>,
272     * <code>true</code>, <code>yes</code> yield a <em>true</em> boolean value
273     * as a result, all others (including <code>null</code>) a <em>false</em>
274     * boolean value.
275     *
276     * @param value
277     *            the string to parse
278     * @return <code>true</code> if the supplied string was considered something
279     *         representing a <em>true</em> boolean value, <code>false</code>
280     *         otherwise
281     */
282    protected static boolean parseBoolean(String value) {
283        if (value != null) {
284            return value.equals("1") || Boolean.parseBoolean(value);
285        }
286        return false;
287    }
288
289
290    private List<ResourceInfo> translateFcsScanResource(CQLNode scanClause)
291            throws SRUException {
292        if (scanClause instanceof CQLTermNode) {
293            final CQLTermNode root = (CQLTermNode) scanClause;
294            logger.debug("index = '{}', relation = '{}', term = '{}'",
295                    new Object[] { root.getIndex(),
296                            root.getRelation().getBase(), root.getTerm() });
297
298            String index = root.getIndex();
299            if (FCS_SCAN_INDEX_CQL_SERVERCHOICE.equals(index) &&
300                    FCS_SCAN_INDEX_FCS_RESOURCE.equals(root.getTerm())) {
301                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_INDEX,
302                        "scan operation with 'scanClause' with value " +
303                        "'fcs.resource' is deprecated within CLARIN-FCS");
304            }
305            if (!(FCS_SCAN_INDEX_FCS_RESOURCE.equals(index))) {
306                logger.debug("got scan operation on index '{}', bailing ...",
307                        index);
308                return null;
309            }
310
311            logger.warn("scan on 'fcs.resource' for endpoint resource " +
312                    "enumeration is deprecated.");
313
314            // only allow "=" relation without any modifiers
315            final CQLRelation relationNode = root.getRelation();
316            String relation = relationNode.getBase();
317            if (!(FCS_SCAN_SUPPORTED_RELATION_CQL_1_1.equals(relation) ||
318                    FCS_SCAN_SUPPORTED_RELATION_CQL_1_2.equals(relation) ||
319                    FCS_SUPPORTED_RELATION_EXACT.equals(relation))) {
320                throw new SRUException(SRUConstants.SRU_UNSUPPORTED_RELATION,
321                        relationNode.getBase(), "Relation \"" +
322                                relationNode.getBase() +
323                                "\" is not supported in scan operation.");
324            }
325            final List<Modifier> modifiers = relationNode.getModifiers();
326            if ((modifiers != null) && !modifiers.isEmpty()) {
327                Modifier modifier = modifiers.get(0);
328                throw new SRUException(
329                        SRUConstants.SRU_UNSUPPORTED_RELATION_MODIFIER,
330                        modifier.getValue(), "Relation modifier \"" +
331                                modifier.getValue() +
332                                "\" is not supported in scan operation.");
333            }
334
335            final String term = root.getTerm();
336            if ((term == null) || term.isEmpty()) {
337                throw new SRUException(SRUConstants.SRU_EMPTY_TERM_UNSUPPORTED,
338                        "An empty term is not supported in scan operation.");
339            }
340
341            /*
342             * generate result: currently we only have a flat hierarchy, so
343             * return an empty result on any attempt to do a recursive scan ...
344             */
345            List<ResourceInfo> results = null;
346            if ((FCS_SCAN_INDEX_CQL_SERVERCHOICE.equals(index) &&
347                    FCS_SCAN_INDEX_FCS_RESOURCE.equals(term)) ||
348                    (FCS_SCAN_INDEX_FCS_RESOURCE.equals(index))) {
349                results = endpointDescription.getResourceList(term);
350            }
351            if ((results == null) || results.isEmpty()) {
352                return Collections.emptyList();
353            } else {
354                return results;
355            }
356        } else {
357            throw new SRUException(SRUConstants.SRU_QUERY_FEATURE_UNSUPPORTED,
358                    "Scan clause too complex.");
359        }
360    }
361
362
363    private void writeEndpointDescription(XMLStreamWriter writer)
364            throws XMLStreamException {
365        writer.setPrefix(ED_PREFIX, ED_NS);
366        writer.writeStartElement(ED_NS, "EndpointDescription");
367        writer.writeNamespace(ED_PREFIX, ED_NS);
368        writer.writeAttribute("version", Integer.toString(ED_VERSION));
369
370        // capabilities
371        writer.writeStartElement(ED_NS, "Capabilities");
372        for (URI capability : endpointDescription.getCapabilities()) {
373            writer.writeStartElement(ED_NS, "Capability");
374            writer.writeCharacters(capability.toString());
375            writer.writeEndElement(); // "Capability" element
376        }
377        writer.writeEndElement(); // "Capabilities" element
378
379        // supported data views
380        writer.writeStartElement(ED_NS, "SupportedDataViews");
381        for (DataView dataView : endpointDescription.getSupportedDataViews()) {
382            writer.writeStartElement(ED_NS, "SupportedDataView");
383            writer.writeAttribute("id", dataView.getIdentifier());
384            String s;
385            switch (dataView.getDeliveryPolicy()) {
386            case SEND_BY_DEFAULT:
387                s = "send-by-default";
388                break;
389            case NEED_TO_REQUEST:
390                s = "need-to-request";
391                break;
392            default:
393                throw new XMLStreamException(
394                        "invalid value for payload delivery policy: " +
395                                dataView.getDeliveryPolicy());
396            } // switch
397            writer.writeAttribute("delivery-policy", s);
398            writer.writeCharacters(dataView.getMimeType());
399            writer.writeEndElement(); // "SupportedDataView" element
400        }
401        writer.writeEndElement(); // "SupportedDataViews" element
402
403        try {
404            // resources
405            List<ResourceInfo> resources =
406                    endpointDescription.getResourceList(
407                            EndpointDescription.PID_ROOT);
408            writeResourceInfos(writer, resources);
409        } catch (SRUException e) {
410            throw new XMLStreamException("error retriving top-level resources",
411                    e);
412        }
413        writer.writeEndElement(); // "EndpointDescription" element
414    }
415
416
417    private void writeResourceInfos(XMLStreamWriter writer,
418            List<ResourceInfo> resources) throws XMLStreamException {
419        if (resources == null) {
420            throw new NullPointerException("resources == null");
421        }
422        if (!resources.isEmpty()) {
423            writer.writeStartElement(ED_NS, "Resources");
424
425            for (ResourceInfo resource : resources) {
426                writer.writeStartElement(ED_NS, "Resource");
427                writer.writeAttribute("pid", resource.getPid());
428
429                // title
430                final Map<String, String> title = resource.getTitle();
431                for (Map.Entry<String, String> i : title.entrySet()) {
432                    writer.setPrefix(XMLConstants.XML_NS_PREFIX,
433                            XMLConstants.XML_NS_URI);
434                    writer.writeStartElement(ED_NS, "Title");
435                    writer.writeAttribute(XMLConstants.XML_NS_URI, "lang", i.getKey());
436                    writer.writeCharacters(i.getValue());
437                    writer.writeEndElement(); // "title" element
438                }
439
440                // description
441                final Map<String, String> description = resource.getDescription();
442                if (description != null) {
443                    for (Map.Entry<String, String> i : description.entrySet()) {
444                        writer.writeStartElement(ED_NS, "Description");
445                        writer.writeAttribute(XMLConstants.XML_NS_URI, "lang",
446                                i.getKey());
447                        writer.writeCharacters(i.getValue());
448                        writer.writeEndElement(); // "Description" element
449                    }
450                }
451
452                // landing page
453                final String landingPageURI = resource.getLandingPageURI();
454                if (landingPageURI != null) {
455                    writer.writeStartElement(ED_NS, "LandingPageURI");
456                    writer.writeCharacters(landingPageURI);
457                    writer.writeEndElement(); // "LandingPageURI" element
458                }
459
460                // languages
461                final List<String> languages = resource.getLanguages();
462                writer.writeStartElement(ED_NS, "Languages");
463                for (String i : languages) {
464                    writer.writeStartElement(ED_NS, "Language");
465                    writer.writeCharacters(i);
466                    writer.writeEndElement(); // "Language" element
467
468                }
469                writer.writeEndElement(); // "Languages" element
470
471                // available data views
472                StringBuilder sb = new StringBuilder();
473                for (DataView dataview : resource.getAvailableDataViews()) {
474                    if (sb.length() > 0) {
475                        sb.append(" ");
476                    }
477                    sb.append(dataview.getIdentifier());
478                }
479                writer.writeEmptyElement(ED_NS, "AvailableDataViews");
480                writer.writeAttribute("ref", sb.toString());
481
482                // child resources
483                List<ResourceInfo> subs = resource.getSubResources();
484                if ((subs != null) && !subs.isEmpty()) {
485                    writeResourceInfos(writer, subs);
486                }
487
488                writer.writeEndElement(); // "Resource" element
489            }
490            writer.writeEndElement(); // "Resources" element
491        }
492    }
493
494
495    private void writeLegacyResourceInfo(XMLStreamWriter writer,
496            ResourceInfo resourceInfo) throws XMLStreamException {
497        writer.setDefaultNamespace(FCS_RESOURCE_INFO_NS);
498        writer.writeStartElement(FCS_RESOURCE_INFO_NS, "ResourceInfo");
499        writer.writeDefaultNamespace(FCS_RESOURCE_INFO_NS);
500
501        // title
502        final Map<String, String> title = resourceInfo.getTitle();
503        for (Map.Entry<String, String> i : title.entrySet()) {
504            writer.setPrefix(XMLConstants.XML_NS_PREFIX,
505                    XMLConstants.XML_NS_URI);
506            writer.writeStartElement(FCS_RESOURCE_INFO_NS, "Title");
507            writer.writeAttribute(XMLConstants.XML_NS_URI, "lang", i.getKey());
508            writer.writeCharacters(i.getValue());
509            writer.writeEndElement(); // "title" element
510        }
511
512        // description
513        final Map<String, String> description = resourceInfo.getDescription();
514        if (description != null) {
515            for (Map.Entry<String, String> i : description.entrySet()) {
516                writer.writeStartElement(FCS_RESOURCE_INFO_NS, "Description");
517                writer.writeAttribute(XMLConstants.XML_NS_URI, "lang",
518                        i.getKey());
519                writer.writeCharacters(i.getValue());
520                writer.writeEndElement(); // "Description" element
521            }
522        }
523
524        // landing page
525        final String landingPageURI = resourceInfo.getLandingPageURI();
526        if (landingPageURI != null) {
527            writer.writeStartElement(FCS_RESOURCE_INFO_NS, "LandingPageURI");
528            writer.writeCharacters(landingPageURI);
529            writer.writeEndElement(); // "LandingPageURI" element
530        }
531
532        // languages
533        final List<String> languages = resourceInfo.getLanguages();
534        writer.writeStartElement(FCS_RESOURCE_INFO_NS, "Languages");
535        for (String i : languages) {
536            writer.writeStartElement(FCS_RESOURCE_INFO_NS, "Language");
537            writer.writeCharacters(i);
538            writer.writeEndElement(); // "Language" element
539
540        }
541        writer.writeEndElement(); // "Languages" element
542        writer.writeEndElement(); // "ResourceInfo" element
543    }
544
545
546//             final boolean defaultNS = ((prefix == null) || prefix.isEmpty());
547//            if (writeNS) {
548//                 if (defaultNS) {
549//                     writer.setDefaultNamespace(FCS_RESOURCE_INFO_NS);
550//                } else {
551//                    writer.setPrefix(prefix, FCS_RESOURCE_INFO_NS);
552//                }
553//            }
554//            writer.writeStartElement(FCS_RESOURCE_INFO_NS, "ResourceInfo");
555//            if (writeNS) {
556//                if (defaultNS) {
557//                    writer.writeDefaultNamespace(FCS_RESOURCE_INFO_NS);
558//                 } else {
559//                    writer.writeNamespace(prefix, FCS_RESOURCE_INFO_NS);
560//                 }
561//          } )
562//}
563
564//    public static void XwriteResourceInfo(XMLStreamWriter writer, String prefix,
565//            ResourceInfo resourceInfo) throws XMLStreamException {
566//        doWriteResourceInfo(writer, prefix, resourceInfo, true, false);
567//    }
568//
569//
570//    private static void XdoWriteResourceInfo(XMLStreamWriter writer,
571//            String prefix, ResourceInfo resourceInfo, boolean writeNS,
572//            boolean recursive) throws XMLStreamException {
573//
574//        if (writer == null) {
575//            throw new NullPointerException("writer == null");
576//        }
577//        if (resourceInfo == null) {
578//            throw new NullPointerException("resourceInfo == null");
579//        }
580//
581//        final boolean defaultNS = ((prefix == null) || prefix.isEmpty());
582//        if (writeNS) {
583//            if (defaultNS) {
584//                writer.setDefaultNamespace(FCS_RESOURCE_INFO_NS);
585//            } else {
586//                writer.setPrefix(prefix, FCS_RESOURCE_INFO_NS);
587//            }
588//        }
589//        writer.writeStartElement(FCS_RESOURCE_INFO_NS, "Resource");
590//        if (writeNS) {
591//            if (defaultNS) {
592//                writer.writeDefaultNamespace(FCS_RESOURCE_INFO_NS);
593//            } else {
594//                writer.writeNamespace(prefix, FCS_RESOURCE_INFO_NS);
595//            }
596//        }
597//        if (recursive) {
598//            /*
599//             * HACK: only output @pid for recursive (= explain) requests.
600//             * This should be revisited, if we decide to go for the explain
601//             * style enumeration of resources.
602//             */
603//            writer.writeAttribute("pid", resourceInfo.getPid());
604//        }
605//        if (resourceInfo.hasSubResources()) {
606//            writer.writeAttribute("hasSubResources", "true");
607//        }
608//
609//
610//        if (recursive && resourceInfo.hasSubResources()) {
611//            writer.writeStartElement(FCS_RESOURCE_INFO_NS,
612//                    "ResourceInfoCollection");
613//            for (ResourceInfo r : resourceInfo.getSubResources()) {
614//                doWriteResourceInfo(writer, prefix, r, writeNS, recursive);
615//            }
616//            writer.writeEndElement(); // "ResourceCollection" element
617//        }
618//        writer.writeEndElement(); // "ResourceInfo" element
619//    }
620
621} // class SimpleEndpointSearchEngineBase
Note: See TracBrowser for help on using the repository browser.