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

Last change on this file since 7273 was 7273, checked in by Oliver Schonefeld, 2 years ago
  • update copyright
  • Property svn:eol-style set to native
File size: 23.3 KB
Line 
1/**
2 * This software is copyright (c) 2013-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.fcs;
18
19import java.io.File;
20import java.io.InputStream;
21import java.net.URI;
22import java.util.List;
23import java.util.Map;
24import java.util.Map.Entry;
25
26import javax.servlet.ServletContext;
27import javax.xml.XMLConstants;
28import javax.xml.stream.XMLStreamException;
29import javax.xml.stream.XMLStreamWriter;
30
31import org.slf4j.Logger;
32import org.slf4j.LoggerFactory;
33import org.z3950.zing.cql.CQLNode;
34import org.z3950.zing.cql.CQLTermNode;
35
36import eu.clarin.sru.server.SRUAuthenticationInfoProvider;
37import eu.clarin.sru.server.SRUConfigException;
38import eu.clarin.sru.server.SRUConstants;
39import eu.clarin.sru.server.SRUDiagnosticList;
40import eu.clarin.sru.server.SRUException;
41import eu.clarin.sru.server.SRUExplainResult;
42import eu.clarin.sru.server.SRUQueryParserRegistry;
43import eu.clarin.sru.server.SRURequest;
44import eu.clarin.sru.server.SRUScanResultSet;
45import eu.clarin.sru.server.SRUSearchEngine;
46import eu.clarin.sru.server.SRUServer;
47import eu.clarin.sru.server.SRUServerConfig;
48import eu.clarin.sru.server.fcs.utils.AuthenticationProvider;
49import eu.clarin.sru.server.utils.SRUAuthenticationInfoProviderFactory;
50import eu.clarin.sru.server.utils.SRUSearchEngineBase;
51
52
53/**
54 * A base class for implementing a simple search engine to be used as a
55 * CLARIN-FCS endpoint.
56 *
57 */
58public abstract class SimpleEndpointSearchEngineBase extends
59        SRUSearchEngineBase implements SRUAuthenticationInfoProviderFactory {
60    public static final String FCS_AUTHENTICATION_ENABLE_PARAM =
61            "eu.clarin.sru.server.fcs.authentication.enable";
62    public static final String FCS_AUTHENTICATION_AUDIENCE_PARAM =
63            "eu.clarin.sru.server.fcs.authentication.audience";
64    public static final String FCS_AUTHENTICATION_IGNORE_ISSUEDAT_PARAM =
65            "eu.clarin.sru.server.fcs.authentication.ignoreIssuedAt";
66    public static final String FCS_AUTHENTICATION_ACCEPT_ISSUEDAT_PARAM =
67            "eu.clarin.sru.server.fcs.authentication.acceptIssuedAt";
68    public static final String FCS_AUTHENTICATION_ACCEPT_EXPIRESAT_PARAM =
69            "eu.clarin.sru.server.fcs.authentication.acceptExpiresAt";
70    public static final String FCS_AUTHENTICATION_ACCEPT_NOTBEFORE_PARAM =
71            "eu.clarin.sru.server.fcs.authentication.acceptNotBefore";
72    public static final String FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX =
73            "eu.clarin.sru.server.fcs.authentication.key.";
74    private static final String RESOURCE_URI_PREFIX = "resource:";
75    private static final String X_FCS_ENDPOINT_DESCRIPTION =
76            "x-fcs-endpoint-description";
77    private static final String ED_NS =
78            "http://clarin.eu/fcs/endpoint-description";
79    private static final String ED_PREFIX = "ed";
80    private static final Logger logger =
81            LoggerFactory.getLogger(SimpleEndpointSearchEngineBase.class);
82    protected EndpointDescription endpointDescription;
83
84
85    /**
86     * This method should not be overridden. Perform your custom initialization
87     * in the
88     * {@link #doInit(ServletContext, SRUServerConfig, eu.clarin.sru.server.SRUQueryParserRegistry.Builder, Map)}
89     * method instead.
90     *
91     * @see #doInit(ServletContext, SRUServerConfig,
92     *      eu.clarin.sru.server.SRUQueryParserRegistry.Builder, Map)
93     */
94    @Override
95    public final void init(ServletContext context,
96            SRUServerConfig config,
97            SRUQueryParserRegistry.Builder parserReqistryBuilder,
98            Map<String, String> params) throws SRUConfigException {
99        logger.debug("initializing");
100        super.init(context, config, parserReqistryBuilder, params);
101
102        parserReqistryBuilder.register(new FCSQueryParser());
103
104        logger.debug("initializing search engine implementation");
105        doInit(context, config, parserReqistryBuilder, params);
106
107        logger.debug("initizalizing endpoint description");
108        this.endpointDescription =
109                createEndpointDescription(context, config, params);
110        if (this.endpointDescription == null) {
111            logger.error("SimpleEndpointSearchEngineBase implementation " +
112                    "error: createEndpointDescription() returned null");
113            throw new SRUConfigException("createEndpointDescription() " +
114                    "returned no valid implementation of an EndpointDescription");
115        }
116    }
117
118
119    /**
120     * This method should not be overridden. Perform you custom cleanup in the
121     * {@link #doDestroy()} method.
122     *
123     * @see #doDestroy()
124     */
125    @Override
126    public final void destroy() {
127        logger.debug("performing cleanup of endpoint description");
128        endpointDescription.destroy();
129        logger.debug("performing cleanup of search engine");
130        doDestroy();
131        super.destroy();
132    }
133
134
135    @Override
136    public SRUAuthenticationInfoProvider createAuthenticationInfoProvider(
137            ServletContext context, Map<String, String> params)
138            throws SRUConfigException {
139        String enableAuthentication = params.get(FCS_AUTHENTICATION_ENABLE_PARAM);
140        if (enableAuthentication != null) {
141            if (parseBoolean(enableAuthentication)) {
142                logger.debug("enabling authentication");               
143                AuthenticationProvider.Builder builder =
144                        AuthenticationProvider.Builder.create();
145               
146                String audience = params.get(FCS_AUTHENTICATION_AUDIENCE_PARAM);
147                if (audience != null) {
148                    String[] values = audience.split("\\s*,\\s*");
149                    if (values != null) {
150                        for (String value : values) {
151                            logger.debug("adding audience: {}", value);
152                            builder.withAudience(value);
153                        }
154                    } else {
155                        logger.debug("adding audience: {}", audience);
156                        builder.withAudience(audience);
157                    }
158                }
159               
160                boolean ignoreIssuedAt = parseBoolean(
161                        params.get(FCS_AUTHENTICATION_IGNORE_ISSUEDAT_PARAM));
162                if (ignoreIssuedAt) {
163                    logger.debug("will not verify 'iat' claim");
164                    builder.withIgnoreIssuedAt();
165                } else {
166                    long issuedAtLeeway = parseLong(
167                            params.get(FCS_AUTHENTICATION_ACCEPT_ISSUEDAT_PARAM), -1);
168                    if (issuedAtLeeway > 0) {
169                        logger.debug("allowing {} seconds leeway for 'iat' claim", issuedAtLeeway);
170                        builder.withIssuedAt(issuedAtLeeway);
171                    }
172                }
173                long expiresAtLeeway = parseLong(
174                        params.get(FCS_AUTHENTICATION_ACCEPT_EXPIRESAT_PARAM), -1);
175                if (expiresAtLeeway > 0) {
176                    logger.debug("allowing {} seconds leeway for 'exp' claim", expiresAtLeeway);
177                    builder.withExpiresAt(expiresAtLeeway);
178                }
179               
180                long notBeforeLeeway = parseLong(
181                        params.get(FCS_AUTHENTICATION_ACCEPT_NOTBEFORE_PARAM), -1);
182                if (notBeforeLeeway > 0) {
183                    logger.debug("allowing {} seconds leeway for 'nbf' claim", expiresAtLeeway);
184                    builder.withNotBefore(notBeforeLeeway);
185                }
186
187                // load keys
188                for (Entry<String, String> entry : params.entrySet()) {
189                    if (entry.getKey().startsWith(FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX)) {
190                        String keyId = entry.getKey().substring(FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX.length()).trim();
191                        if (keyId.isEmpty()) {
192                            throw new SRUConfigException("init-parameter: '" + entry.getKey() + "' is invalid: keyId is empty!");
193                        }
194                        String keyFileName = entry.getValue();
195                        logger.debug("keyId = {}, keyFile = {}", keyId, keyFileName);
196                        if (keyFileName.regionMatches(0, RESOURCE_URI_PREFIX, 0, RESOURCE_URI_PREFIX.length())) {
197                            String path = keyFileName.substring(RESOURCE_URI_PREFIX.length());
198                            logger.debug("loading key '{}' from resource '{}'", keyId, keyFileName);
199                            InputStream in = context.getResourceAsStream(path);
200                            builder.withPublicKey(keyId, in);
201                        } else {
202                            logger.debug("loading key '{}' from file '{}'", keyId, keyFileName);
203                            builder.withPublicKey(keyId, new File(keyFileName));
204                        }
205                    }
206                }
207                AuthenticationProvider authenticationProvider = builder.build();
208                if (authenticationProvider.getKeyCount() == 0) {
209                    logger.warn("No keys configured, all well-formed tokens will be accepted. Make sure, youn know what you are doing!");
210                }
211                return authenticationProvider;
212            } else {
213                logger.debug("explictly disable authentication");
214            }
215        }
216        return null;
217    }
218
219
220    @Override
221    public final SRUExplainResult explain(SRUServerConfig config,
222            SRURequest request, SRUDiagnosticList diagnostics)
223            throws SRUException {
224
225        final boolean provideEndpointDescription =
226                parseBoolean(request.getExtraRequestData(
227                        X_FCS_ENDPOINT_DESCRIPTION));
228
229        if (provideEndpointDescription) {
230            return new SRUExplainResult(diagnostics) {
231                @Override
232                public boolean hasExtraResponseData() {
233                    return provideEndpointDescription;
234                }
235
236
237                @Override
238                public void writeExtraResponseData(XMLStreamWriter writer)
239                        throws XMLStreamException {
240                    writeEndpointDescription(writer);
241                }
242            };
243        } else {
244            return null;
245        }
246    }
247
248
249    /**
250     * Handle a <em>scan</em> operation. This implementation provides support to
251     * CLARIN FCS resource enumeration. If you want to provide custom scan
252     * behavior for a different index, override the
253     * {@link #doScan(SRUServerConfig, SRURequest, SRUDiagnosticList)} method.
254     *
255     * @see #doScan(SRUServerConfig, SRURequest, SRUDiagnosticList)
256     */
257    @Override
258    public final SRUScanResultSet scan(SRUServerConfig config,
259            SRURequest request, SRUDiagnosticList diagnostics)
260            throws SRUException {
261        return doScan(config, request, diagnostics);
262    }
263
264
265    protected abstract EndpointDescription createEndpointDescription(
266            ServletContext context, SRUServerConfig config,
267            Map<String, String> params) throws SRUConfigException;
268
269
270    /**
271     * Initialize the search engine. This initialization should be tailed
272     * towards your environment and needs.
273     *
274     * @param context
275     *            the {@link ServletContext} for the Servlet
276     * @param config
277     *            the {@link SRUServerConfig} object for this search engine
278     * @param queryParsersBuilder
279     *            the {@link SRUQueryParserRegistry.Builder} object to be used
280     *            for this search engine. Use to register additional query
281     *            parsers with the {@link SRUServer}.
282     * @param params
283     *            additional parameters gathered from the Servlet configuration
284     *            and Servlet context.
285     * @throws SRUConfigException
286     *             if an error occurred
287     */
288    protected abstract void doInit(ServletContext context,
289            SRUServerConfig config,
290            SRUQueryParserRegistry.Builder queryParsersBuilder,
291            Map<String, String> params) throws SRUConfigException;
292
293
294    /**
295     * Destroy the search engine. Override this method for any cleanup the
296     * search engine needs to perform upon termination.
297     */
298    protected void doDestroy() {
299    }
300
301
302    /**
303     * Handle a <em>scan</em> operation. The default implementation is a no-op.
304     * Override this method, if you want to provide a custom behavior.
305     *
306     * @param config
307     *            the <code>SRUEndpointConfig</code> object that contains the
308     *            endpoint configuration
309     * @param request
310     *            the <code>SRURequest</code> object that contains the request
311     *            made to the endpoint
312     * @param diagnostics
313     *            the <code>SRUDiagnosticList</code> object for storing
314     *            non-fatal diagnostics
315     * @return a <code>SRUScanResultSet</code> object or <code>null</code> if
316     *         this operation is not supported by this search engine
317     * @throws SRUException
318     *             if an fatal error occurred
319     * @see SRUSearchEngine#scan(SRUServerConfig, SRURequest, SRUDiagnosticList)
320     * @deprecated override
321     *             {@link #scan(SRUServerConfig, SRURequest, SRUDiagnosticList)}
322     */
323    @Deprecated
324    protected SRUScanResultSet doScan(SRUServerConfig config,
325            SRURequest request, SRUDiagnosticList diagnostics)
326            throws SRUException {
327        final CQLNode scanClause = request.getScanClause();
328        if (scanClause instanceof CQLTermNode) {
329            final CQLTermNode root = (CQLTermNode) scanClause;
330            final String index = root.getIndex();
331            throw new SRUException(SRUConstants.SRU_UNSUPPORTED_INDEX, index,
332                    "scan operation on index '" + index + "' is not supported");
333        } else {
334            throw new SRUException(SRUConstants.SRU_QUERY_FEATURE_UNSUPPORTED,
335                    "Scan clause too complex.");
336        }
337    }
338
339
340    /**
341     * Convince method for parsing a string to boolean. Values <code>1</code>,
342     * <code>true</code>, <code>yes</code> yield a <em>true</em> boolean value
343     * as a result, all others (including <code>null</code>) a <em>false</em>
344     * boolean value.
345     *
346     * @param value
347     *            the string to parse
348     * @return <code>true</code> if the supplied string was considered something
349     *         representing a <em>true</em> boolean value, <code>false</code>
350     *         otherwise
351     */
352    protected static boolean parseBoolean(String value) {
353        if (value != null) {
354            return value.equals("1") || Boolean.parseBoolean(value);
355        }
356        return false;
357    }
358
359
360    private long parseLong(String value, long defaultValue) throws SRUConfigException {
361        if (value != null) {
362            try {
363                return Long.parseLong(value);
364            } catch (NumberFormatException e) {
365                throw new SRUConfigException("invalid long value");
366            }
367        }
368        return defaultValue;
369    }
370   
371   
372    private void writeEndpointDescription(XMLStreamWriter writer)
373            throws XMLStreamException {
374        writer.setPrefix(ED_PREFIX, ED_NS);
375        writer.writeStartElement(ED_NS, "EndpointDescription");
376        writer.writeNamespace(ED_PREFIX, ED_NS);
377        writer.writeAttribute("version",
378                Integer.toString(endpointDescription.getVersion()));
379
380        // Capabilities
381        writer.writeStartElement(ED_NS, "Capabilities");
382        for (URI capability : endpointDescription.getCapabilities()) {
383            writer.writeStartElement(ED_NS, "Capability");
384            writer.writeCharacters(capability.toString());
385            writer.writeEndElement(); // "Capability" element
386        }
387        writer.writeEndElement(); // "Capabilities" element
388
389        // SupportedDataViews
390        writer.writeStartElement(ED_NS, "SupportedDataViews");
391        for (DataView dataView : endpointDescription.getSupportedDataViews()) {
392            writer.writeStartElement(ED_NS, "SupportedDataView");
393            writer.writeAttribute("id", dataView.getIdentifier());
394            String s;
395            switch (dataView.getDeliveryPolicy()) {
396            case SEND_BY_DEFAULT:
397                s = "send-by-default";
398                break;
399            case NEED_TO_REQUEST:
400                s = "need-to-request";
401                break;
402            default:
403                throw new XMLStreamException(
404                        "invalid value for payload delivery policy: " +
405                                dataView.getDeliveryPolicy());
406            } // switch
407            writer.writeAttribute("delivery-policy", s);
408            writer.writeCharacters(dataView.getMimeType());
409            writer.writeEndElement(); // "SupportedDataView" element
410        }
411        writer.writeEndElement(); // "SupportedDataViews" element
412
413        if (endpointDescription.isVersion(EndpointDescription.VERSION_2)) {
414            // SupportedLayers
415            final List<Layer> layers = endpointDescription.getSupportedLayers();
416            if (layers != null) {
417                writer.writeStartElement(ED_NS, "SupportedLayers");
418                for (Layer layer : layers) {
419                    writer.writeStartElement(ED_NS, "SupportedLayer");
420                    writer.writeAttribute("id", layer.getId());
421                    writer.writeAttribute("result-id",
422                            layer.getResultId().toString());
423                    if (layer.getContentEncoding() ==
424                            Layer.ContentEncoding.EMPTY) {
425                        writer.writeAttribute("type", "empty");
426                    }
427                    if (layer.getQualifier() != null) {
428                        writer.writeAttribute("qualifier",
429                                layer.getQualifier());
430                    }
431                    if (layer.getAltValueInfo() != null) {
432                        writer.writeAttribute("alt-value-info",
433                                layer.getAltValueInfo());
434                        if (layer.getAltValueInfoURI() != null) {
435                            writer.writeAttribute("alt-value-info-uri",
436                                    layer.getAltValueInfoURI().toString());
437                        }
438                    }
439                    writer.writeCharacters(layer.getType());
440                    writer.writeEndElement(); // "SupportedLayer" element
441                }
442                writer.writeEndElement(); // "SupportedLayers" element
443            }
444        }
445
446        // Resources
447        try {
448            List<ResourceInfo> resources =
449                    endpointDescription.getResourceList(
450                            EndpointDescription.PID_ROOT);
451            writeResourceInfos(writer, resources);
452        } catch (SRUException e) {
453            throw new XMLStreamException(
454                    "error retriving top-level resources", e);
455        }
456        writer.writeEndElement(); // "EndpointDescription" element
457    }
458
459
460    private void writeResourceInfos(XMLStreamWriter writer,
461            List<ResourceInfo> resources) throws XMLStreamException {
462        if (resources == null) {
463            throw new NullPointerException("resources == null");
464        }
465        if (!resources.isEmpty()) {
466            writer.writeStartElement(ED_NS, "Resources");
467
468            for (ResourceInfo resource : resources) {
469                writer.writeStartElement(ED_NS, "Resource");
470                writer.writeAttribute("pid", resource.getPid());
471
472                // title
473                final Map<String, String> title = resource.getTitle();
474                for (Map.Entry<String, String> i : title.entrySet()) {
475                    writer.setPrefix(XMLConstants.XML_NS_PREFIX,
476                            XMLConstants.XML_NS_URI);
477                    writer.writeStartElement(ED_NS, "Title");
478                    writer.writeAttribute(XMLConstants.XML_NS_URI, "lang", i.getKey());
479                    writer.writeCharacters(i.getValue());
480                    writer.writeEndElement(); // "title" element
481                }
482
483                // description
484                final Map<String, String> description = resource.getDescription();
485                if (description != null) {
486                    for (Map.Entry<String, String> i : description.entrySet()) {
487                        writer.writeStartElement(ED_NS, "Description");
488                        writer.writeAttribute(XMLConstants.XML_NS_URI, "lang",
489                                i.getKey());
490                        writer.writeCharacters(i.getValue());
491                        writer.writeEndElement(); // "Description" element
492                    }
493                }
494
495                // landing page
496                final String landingPageURI = resource.getLandingPageURI();
497                if (landingPageURI != null) {
498                    writer.writeStartElement(ED_NS, "LandingPageURI");
499                    writer.writeCharacters(landingPageURI);
500                    writer.writeEndElement(); // "LandingPageURI" element
501                }
502
503                // languages
504                final List<String> languages = resource.getLanguages();
505                writer.writeStartElement(ED_NS, "Languages");
506                for (String i : languages) {
507                    writer.writeStartElement(ED_NS, "Language");
508                    writer.writeCharacters(i);
509                    writer.writeEndElement(); // "Language" element
510
511                }
512                writer.writeEndElement(); // "Languages" element
513
514                // available data views
515                StringBuilder sb = new StringBuilder();
516                for (DataView dataview : resource.getAvailableDataViews()) {
517                    if (sb.length() > 0) {
518                        sb.append(" ");
519                    }
520                    sb.append(dataview.getIdentifier());
521                }
522                writer.writeEmptyElement(ED_NS, "AvailableDataViews");
523                writer.writeAttribute("ref", sb.toString());
524
525                if (endpointDescription.isVersion(
526                        EndpointDescription.VERSION_2)) {
527                    final List<Layer> layers = resource.getAvailableLayers();
528                    if (layers != null) {
529                        sb = new StringBuilder();
530                        for (Layer layer : resource.getAvailableLayers()) {
531                            if (sb.length() > 0) {
532                                sb.append(" ");
533                            }
534                            sb.append(layer.getId());
535                        }
536                        writer.writeEmptyElement(ED_NS, "AvailableLayers");
537                        writer.writeAttribute("ref", sb.toString());
538                    }
539                }
540
541                // child resources
542                List<ResourceInfo> subs = resource.getSubResources();
543                if ((subs != null) && !subs.isEmpty()) {
544                    writeResourceInfos(writer, subs);
545                }
546
547                writer.writeEndElement(); // "Resource" element
548            }
549            writer.writeEndElement(); // "Resources" element
550        }
551    }
552
553} // class SimpleEndpointSearchEngineBase
Note: See TracBrowser for help on using the repository browser.