• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eXist-db / exist / 39745471938

22 Oct 2026 08:07AM UTC coverage: 57.812%. Remained the same
39745471938

push

github

web-flow
Merge pull request #6911 from eXist-db/dependabot/maven/io.github.hakky54-logcaptor-2.13.0

Bump io.github.hakky54:logcaptor from 2.12.7 to 2.13.0

32284 of 61732 branches covered (52.3%)

Branch coverage included in aggregate %.

85034 of 141197 relevant lines covered (60.22%)

2.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.0
/exist-core/src/main/java/org/exist/http/urlrewrite/XQueryURLRewrite.java
1
/*
2
 * eXist-db Open Source Native XML Database
3
 * Copyright (C) 2001 The eXist-db Authors
4
 *
5
 * info@exist-db.org
6
 * http://www.exist-db.org
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU Lesser General Public
10
 * License as published by the Free Software Foundation; either
11
 * version 2.1 of the License, or (at your option) any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 * Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Lesser General Public
19
 * License along with this library; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21
 */
22
package org.exist.http.urlrewrite;
23

24
import jakarta.servlet.annotation.MultipartConfig;
25
import org.apache.logging.log4j.LogManager;
26
import org.apache.logging.log4j.Logger;
27
import org.exist.EXistException;
28
import org.exist.Namespaces;
29
import org.exist.collections.Collection;
30
import org.exist.dom.persistent.BinaryDocument;
31
import org.exist.dom.persistent.DocumentImpl;
32
import org.exist.dom.persistent.LockedDocument;
33
import org.exist.http.Descriptor;
34
import org.exist.http.servlets.Authenticator;
35
import org.exist.http.servlets.BasicAuthenticator;
36
import org.exist.http.servlets.HttpRequestWrapper;
37
import org.exist.http.servlets.HttpResponseWrapper;
38
import org.exist.security.AuthenticationException;
39
import org.exist.security.Permission;
40
import org.exist.security.PermissionDeniedException;
41
import org.exist.security.Subject;
42
import org.exist.security.internal.web.HttpAccount;
43
import org.exist.source.DBSource;
44
import org.exist.source.FileSource;
45
import org.exist.source.Source;
46
import org.exist.source.SourceFactory;
47
import org.exist.storage.BrokerPool;
48
import org.exist.storage.DBBroker;
49
import org.exist.storage.XQueryPool;
50
import org.exist.storage.lock.Lock.LockMode;
51
import org.exist.storage.serializers.Serializer;
52
import org.exist.util.LockException;
53
import org.exist.util.MimeType;
54
import org.exist.util.serializer.XQuerySerializer;
55
import org.exist.xmldb.XmldbURI;
56
import org.exist.xquery.*;
57
import org.exist.xquery.value.Item;
58
import org.exist.xquery.value.NodeValue;
59
import org.exist.xquery.value.Sequence;
60
import org.exist.xquery.value.Type;
61
import org.w3c.dom.Document;
62
import org.w3c.dom.Element;
63
import org.w3c.dom.Node;
64
import org.w3c.dom.NodeList;
65
import org.xml.sax.SAXException;
66
import org.xmldb.api.DatabaseManager;
67
import org.xmldb.api.base.Database;
68

69
import javax.annotation.Nullable;
70
import jakarta.servlet.*;
71
import jakarta.servlet.http.HttpServlet;
72
import jakarta.servlet.http.HttpServletRequest;
73
import jakarta.servlet.http.HttpServletResponse;
74
import javax.xml.transform.OutputKeys;
75
import java.io.*;
76
import java.net.URISyntaxException;
77
import java.nio.file.Files;
78
import java.nio.file.Path;
79
import java.util.*;
80
import java.util.regex.Matcher;
81
import java.util.regex.Pattern;
82

83
import static java.nio.charset.StandardCharsets.UTF_8;
84

85
/**
86
 * A servlet to redirect HTTP requests. Similar to the popular UrlRewriteFilter, but
87
 * based on XQuery.
88
 *
89
 * The request is passed to an XQuery whose return value determines where the request will be
90
 * redirected to. An empty return value means the request will be passed through the filter
91
 * untouched. Otherwise, the query should return a single XML element, which will instruct the filter
92
 * how to further process the request. Details about the format can be found in the main documentation.
93
 *
94
 * The request is forwarded via {@link jakarta.servlet.RequestDispatcher#forward(jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse)}.
95
 * Contrary to HTTP forwarding, there is no additional roundtrip to the client. It all happens on
96
 * the server. The client will not notice the redirect.
97
 *
98
 * Please read the <a href="http://exist-db.org/urlrewrite.html">documentation</a> for further information.
99
 */
100
@MultipartConfig
101
public class XQueryURLRewrite extends HttpServlet {
3 ✔
102

103
    private static final Logger LOG = LogManager.getLogger(XQueryURLRewrite.class);
3 ✔
104
    private static final String DRIVER = "org.exist.xmldb.DatabaseImpl";
105
    private static final Pattern NAME_REGEX = Pattern.compile("^.*/([^/]+)$", 0);
4 ✔
106

107
    public static final String XQUERY_CONTROLLER_FILENAME = "controller.xq";
108
    public static final String LEGACY_XQUERY_CONTROLLER_FILENAME = "controller.xql";
109
    public static final XmldbURI XQUERY_CONTROLLER_URI = XmldbURI.create(XQUERY_CONTROLLER_FILENAME);
3 ✔
110
    public static final XmldbURI LEGACY_XQUERY_CONTROLLER_URI = XmldbURI.create(LEGACY_XQUERY_CONTROLLER_FILENAME);
3 ✔
111

112
    public static final String RQ_ATTR = "org.exist.forward";
113
    public static final String RQ_ATTR_REQUEST_URI = "org.exist.forward.request-uri";
114
    public static final String RQ_ATTR_SERVLET_PATH = "org.exist.forward.servlet-path";
115
    public static final String RQ_ATTR_RESULT = "org.exist.forward.result";
116
    public static final String RQ_ATTR_ERROR = "org.exist.forward.error";
1 ✔
117

118
    private ServletConfig config;
119
    private final Map<String, ModelAndView> urlCache = Collections.synchronizedMap(new TreeMap<>());
6 ✔
120
    private Subject defaultUser = null;
3 ✔
121
    private BrokerPool pool;
122
    // path to the query
123
    private String query = null;
3 ✔
124
    private boolean compiledCache = true;
3 ✔
125
    private boolean sendChallenge = true;
3 ✔
126
    private RewriteConfig rewriteConfig;
127
    private Authenticator authenticator;
128

129
    @Override
130
    public void init(final ServletConfig filterConfig) {
131
        // save FilterConfig for later use
132
        this.config = filterConfig;
3 ✔
133

134
        query = filterConfig.getInitParameter("xquery");
5 ✔
135

136
        final String optCompiledCache = filterConfig.getInitParameter("compiled-cache");
4 ✔
137
        if (optCompiledCache != null) {
2 !
138
            compiledCache = "true".equalsIgnoreCase(optCompiledCache);
×
139
        }
140

141
        final String optSendChallenge = filterConfig.getInitParameter("send-challenge");
4 ✔
142
        if (optSendChallenge != null) {
2 ✔
143
            sendChallenge = "true".equalsIgnoreCase(optSendChallenge);
5 ✔
144
        }
145
    }
1 ✔
146

147
    @Override
148
    protected void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
149
        if (rewriteConfig == null) {
3 ✔
150
            configure();
2 ✔
151
            rewriteConfig = new RewriteConfig(this);
6 ✔
152
        }
153

154
        final long start = System.currentTimeMillis();
2 ✔
155

156
        if (LOG.isTraceEnabled()) {
3 !
157
            LOG.trace(request.getRequestURI());
×
158
        }
159

160
        final Descriptor descriptor = Descriptor.getDescriptorSingleton();
2 ✔
161
        if (descriptor != null && descriptor.requestsFiltered()) {
5 !
162
            final String attr = (String) request.getAttribute("XQueryURLRewrite.forwarded");
×
163
            if (attr == null) {
×
164
                //logs the request if specified in the descriptor
165
                descriptor.doLogRequestInReplayLog(request);
×
166

167
                request.setAttribute("XQueryURLRewrite.forwarded", "true");
×
168
            }
169
        }
170

171
        Subject user = defaultUser;
3 ✔
172

173
        Subject requestUser = HttpAccount.getUserFromServletRequest(request);
3 ✔
174
        if (requestUser != null) {
2 !
175
            user = requestUser;
×
176
        } else {
×
177
            // Secondly try basic authentication
178
            final String auth = request.getHeader("Authorization");
4 ✔
179
            if (auth != null && auth.toLowerCase().startsWith("basic ")) {
7 ✔
180
                requestUser = authenticator.authenticate(request, response, sendChallenge);
8 ✔
181
                if (requestUser != null) {
2 ✔
182
                    user = requestUser;
2 ✔
183
                }
184
            }
185
        }
186

187
        try {
188
            configure();
2 ✔
189
            //checkCache(user);
190

191
            final ControllerRequestWrapper modifiedRequest = new ControllerRequestWrapper(request);
5 ✔
192
            final URLRewrite staticRewrite = rewriteConfig.lookup(modifiedRequest);
5 ✔
193
            if (staticRewrite != null && !staticRewrite.isControllerForward()) {
5 !
194
                modifiedRequest.setPaths(staticRewrite.resolve(modifiedRequest), staticRewrite.getPrefix());
7 ✔
195

196
                if (LOG.isTraceEnabled()) {
3 !
197
                    LOG.trace("Forwarding to target: {}", staticRewrite.getTarget());
×
198
                }
199

200
                staticRewrite.doRewrite(modifiedRequest, response);
4 ✔
201

202
            } else {
1 ✔
203
                if (LOG.isTraceEnabled()) {
3 !
204
                    LOG.trace("Processing request URI: {}", request.getRequestURI());
×
205
                }
206

207
                if (staticRewrite != null) {
2 !
208
                    // fix the request URI
209
                    staticRewrite.updateRequest(modifiedRequest);
3 ✔
210
                }
211

212
                // check if the request URI is already in the url cache
213
                ModelAndView modelView = getFromCache(
3 ✔
214
                        request.getHeader("Host") + request.getRequestURI(),
6 ✔
215
                        user);
1 ✔
216

217
                if (LOG.isDebugEnabled()) {
3 !
218
                    LOG.debug("Checked cache for URI: {} original: {}", modifiedRequest.getRequestURI(), request.getRequestURI());
×
219
                }
220

221
                // no: create a new model and view configuration
222
                if (modelView == null) {
2 !
223
                    modelView = new ModelAndView();
4 ✔
224

225
                    // Execute the query
226
                    try (final DBBroker broker = pool.get(Optional.ofNullable(user))) {
10 ✔
227

228
                        modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI());
5 ✔
229

230
                        final Properties outputProperties = new Properties();
4 ✔
231

232
                        outputProperties.setProperty(OutputKeys.INDENT, "yes");
5 ✔
233
                        outputProperties.setProperty(OutputKeys.ENCODING, UTF_8.name());
6 ✔
234
                        outputProperties.setProperty(OutputKeys.MEDIA_TYPE, MimeType.XML_TYPE.getName());
6 ✔
235

236
                        final Sequence result = runQuery(broker, modifiedRequest, response, modelView, staticRewrite, outputProperties);
9 ✔
237

238
                        logResult(broker, result);
4 ✔
239

240
                        if (response.isCommitted()) {
3 !
241
                            return;
×
242
                        }
243

244
                        // process the query result
245
                        if (result.getItemCount() == 1) {
4 ✔
246
                            final Item resource = result.itemAt(0);
4 ✔
247
                            if (!Type.subTypeOf(resource.getType(), Type.NODE)) {
5 !
248
                                throw new ServletException("XQueryURLRewrite: urlrewrite query should return an element!");
×
249
                            }
250
                            Node node = ((NodeValue) resource).getNode();
4 ✔
251
                            if (node.getNodeType() == Node.DOCUMENT_NODE) {
4 !
252
                                node = ((Document) node).getDocumentElement();
×
253
                            }
254
                            if (node.getNodeType() != Node.ELEMENT_NODE) {
4 !
255
                                //throw new ServletException("Redirect XQuery should return an XML element!");
256
                                response(broker, response, outputProperties, result);
×
257
                                return;
×
258
                            }
259
                            Element elem = (Element) node;
3 ✔
260
                            final String ns = elem.getNamespaceURI();
3 ✔
261
                            if (!Namespaces.EXIST_NS.equals(ns)) {
4 ✔
262
                                response(broker, response, outputProperties, result);
6 ✔
263
                                return;
1 ✔
264
                            }
265

266
                            final String nsUri = elem.getNamespaceURI();
3 ✔
267
                            if (Namespaces.EXIST_NS.equals(nsUri) && "dispatch".equals(elem.getLocalName())) {
9 !
268
                                node = elem.getFirstChild();
3 ✔
269
                                while (node != null) {
3 ✔
270
                                    final String nodeNs = node.getNamespaceURI();
3 ✔
271
                                    if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(nodeNs)) {
8 !
272
                                        final Element action = (Element) node;
3 ✔
273
                                        switch (action.getLocalName()) {
7 !
274
                                            case "view" -> parseViews(modifiedRequest, action, modelView);
6 ✔
275
                                            case "error-handler" ->
276
                                                    parseErrorHandlers(modifiedRequest, action, modelView);
×
277
                                            case "cache-control" -> {
278
                                                final String option = action.getAttribute("cache");
4 ✔
279
                                                modelView.setUseCache("yes".equals(option));
6 ✔
280
                                            }
281
                                            case null, default -> {
282
                                                final URLRewrite urw = parseAction(modifiedRequest, action);
5 ✔
283
                                                if (urw != null) {
2 !
284
                                                    modelView.setModel(urw);
3 ✔
285
                                                }
286
                                            }
287
                                        }
288
                                    }
289
                                    node = node.getNextSibling();
3 ✔
290
                                }
291
                                if (modelView.getModel() == null) {
3 ✔
292
                                    modelView.setModel(new PassThrough(config, elem, modifiedRequest));
9 ✔
293
                                }
294
                            } else if (nsUri != null && Namespaces.EXIST_NS.equals(elem.getNamespaceURI()) && "ignore".equals(elem.getLocalName())) {
13 !
295
                                modelView.setModel(new PassThrough(config, elem, modifiedRequest));
9 ✔
296
                                final NodeList nl = elem.getElementsByTagNameNS(Namespaces.EXIST_NS, "cache-control");
5 ✔
297
                                if (nl.getLength() > 0) {
3 !
298
                                    elem = (Element) nl.item(0);
5 ✔
299
                                    final String option = elem.getAttribute("cache");
4 ✔
300
                                    modelView.setUseCache("yes".equals(option));
5 ✔
301
                                }
302
                            } else {
1 ✔
303
                                response(broker, response, outputProperties, result);
×
304
                                return;
×
305
                            }
306
                        } else if (result.getItemCount() > 1) {
4 !
307
                            response(broker, response, outputProperties, result);
×
308
                            return;
×
309
                        }
310

311
                        if (modelView.useCache()) {
3 !
312
                            LOG.debug("Caching request to {}", request.getRequestURI());
×
313
                            urlCache.put(modifiedRequest.getHeader("Host") + request.getRequestURI(), modelView);
×
314
                        }
315
                    }
316

317
                    // store the original request URI to org.exist.forward.request-uri
318
                    modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI());
5 ✔
319
                    modifiedRequest.setAttribute(RQ_ATTR_SERVLET_PATH, getServletPathSafely(request));
5 ✔
320

321
                }
322
                if (LOG.isTraceEnabled()) {
3 !
323
                    LOG.trace("URLRewrite took {}ms.", System.currentTimeMillis() - start);
×
324
                }
325
                final HttpServletResponse wrappedResponse =
1 ✔
326
                        new CachingResponseWrapper(response, modelView.hasViews() || modelView.hasErrorHandlers());
13 !
327
                if (modelView.getModel() == null) {
3 ✔
328
                    modelView.setModel(new PassThrough(config, modifiedRequest));
8 ✔
329
                }
330

331
                if (staticRewrite != null) {
2 !
332
                    if (modelView.getModel().doResolve()) {
4 !
333
                        staticRewrite.rewriteRequest(modifiedRequest);
3 ✔
334
                    } else {
1 ✔
335
                        modelView.getModel().setAbsolutePath(modifiedRequest);
×
336
                    }
337
                }
338
                modifiedRequest.allowCaching(!modelView.hasViews());
8 ✔
339
                doRewrite(modelView.getModel(), modifiedRequest, wrappedResponse);
6 ✔
340

341
                final int status = wrappedResponse.getStatus();
3 ✔
342
                if (status == HttpServletResponse.SC_NOT_MODIFIED) {
3 !
343
                    response.flushBuffer();
×
344
                } else if (status < HttpServletResponse.SC_BAD_REQUEST) {
3 ✔
345
                    if (modelView.hasViews()) {
3 ✔
346
                        applyViews(modelView, modelView.views, response, modifiedRequest, wrappedResponse);
8 ✔
347
                    } else {
1 ✔
348
                        ((CachingResponseWrapper) wrappedResponse).flush();
3 ✔
349
                    }
350
                } else {
1 ✔
351
                    // HTTP response code indicates an error
352
                    if (modelView.hasErrorHandlers()) {
3 !
353
                        final byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
×
354
                        if (data != null) {
×
355
                            modifiedRequest.setAttribute(RQ_ATTR_ERROR, new String(data, UTF_8));
×
356
                        }
357
                        applyViews(modelView, modelView.errorHandlers, response, modifiedRequest, wrappedResponse);
×
358
                    } else {
×
359
                        flushError(response, wrappedResponse);
4 ✔
360
                    }
361
                }
362
            }
363
        } catch (final Throwable e) {
1 ✔
364
            LOG.error("Error while processing {}: {}", request.getRequestURI(), e.getMessage(), e);
×
365
            throw new ServletException("An error occurred while processing request to " + request.getRequestURI() + ": "
×
366
                    + e.getMessage(), e);
×
367

368
        }
369
    }
1 ✔
370

371
    /**
372
     * Under Jetty 12's EE10 servlet module, {@link HttpServletRequest#getServletPath()}
373
     * throws {@code IllegalArgumentException} (surfaced to the client as a 400 response)
374
     * when the request URI contains an ambiguous path segment (e.g. a real '/' next to
375
     * an encoded '%2F'), even though {@link HttpServletRequest#getRequestURI()} on the
376
     * very same request remains safe to call. This servlet is always mapped with
377
     * {@code url-pattern} {@code /*} (see web.xml), a path-prefix mapping with an empty
378
     * prefix, so per the Jakarta Servlet spec {@code getServletPath()} is defined to
379
     * always return {@code ""} here regardless of the request URI -- Jetty's own
380
     * ambiguity check is a blanket refusal to answer, not a sign the answer differs.
381
     * Fall back to that known value instead of failing the whole request.
382
     *
383
     * @see <a href="https://github.com/jetty/jetty.project/issues/12346">jetty/jetty.project#12346</a>
384
     */
385
    static String getServletPathSafely(final HttpServletRequest request) {
386
        try {
387
            return request.getServletPath();
3 ✔
388
        } catch (final IllegalArgumentException e) {
1 ✔
389
            LOG.debug("Falling back to \"\" for servlet path of ambiguous request URI {}: {}", request.getRequestURI(), e.getMessage());
7 ✔
390
            return "";
2 ✔
391
        }
392
    }
393

394
    BrokerPool getBrokerPool() {
395
        return pool;
3 ✔
396
    }
397

398
    Subject getDefaultUser() {
399
        return defaultUser;
×
400
    }
401

402
    private void applyViews(final ModelAndView modelView, final List<URLRewrite> views, final HttpServletResponse response, final ControllerRequestWrapper modifiedRequest, final HttpServletResponse currentResponse) throws IOException, ServletException {
403
        HttpServletResponse wrappedResponse = currentResponse;
2 ✔
404
        for (int i = 0; i < views.size(); i++) {
8 ✔
405
            final URLRewrite view = views.get(i);
5 ✔
406

407
            // get data returned from last action
408
            byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
4 ✔
409
            // determine request method to use for calling view
410
            String method = view.getMethod();
3 ✔
411
            if (method == null) {
2 !
412
                method = "POST";    // default is POST
2 ✔
413
            }
414

415
            final ControllerRequestWrapper wrappedReq = new ControllerRequestWrapper(modifiedRequest);
5 ✔
416
            wrappedReq.allowCaching(false);
3 ✔
417
            wrappedReq.setMethod(method);
3 ✔
418
            wrappedReq.setBasePath(modifiedRequest.getBasePath());
4 ✔
419
            wrappedReq.setCharacterEncoding(wrappedResponse.getCharacterEncoding());
4 ✔
420
            wrappedReq.setContentType(wrappedResponse.getContentType());
4 ✔
421

422
            if (data != null) {
2 !
423
                wrappedReq.setData(data);
3 ✔
424
            }
425

426
            wrappedResponse = new CachingResponseWrapper(response, true);
6 ✔
427
            doRewrite(view, wrappedReq, wrappedResponse);
5 ✔
428

429
            // catch errors in the view
430
            final int status = wrappedResponse.getStatus();
3 ✔
431
            if (status >= HttpServletResponse.SC_BAD_REQUEST) {
3 !
432
                if (modelView != null && modelView.hasErrorHandlers()) {
×
433
                    data = ((CachingResponseWrapper) wrappedResponse).getData();
×
434
                    final String msg = data == null ? "" : new String(data, UTF_8);
×
435
                    modifiedRequest.setAttribute(RQ_ATTR_ERROR, msg);
×
436
                    applyViews(null, modelView.errorHandlers, response, modifiedRequest, wrappedResponse);
×
437
                    break;
×
438
                } else {
439
                    flushError(response, wrappedResponse);
×
440
                }
441
                break;
×
442
            } else if (i == views.size() - 1) {
6 !
443
                ((CachingResponseWrapper) wrappedResponse).flush();
3 ✔
444
            }
445
        }
446
    }
1 ✔
447

448
    private void response(final DBBroker broker, final HttpServletResponse response, final Properties outputProperties, final Sequence resultSequence) throws IOException {
449
        final String encoding = outputProperties.getProperty(OutputKeys.ENCODING);
4 ✔
450
        try (final OutputStream os = response.getOutputStream();
7 ✔
451
                final Writer writer = new OutputStreamWriter(os, encoding);
6 ✔
452
                final PrintWriter printWriter = new PrintWriter(writer)) {
5 ✔
453
            if (!response.containsHeader("Content-Type")) {
4 !
454
                String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);
4 ✔
455
                if (mimeType != null) {
2 !
456
                    final int semicolon = mimeType.indexOf(';');
4 ✔
457
                    if (semicolon != Constants.STRING_NOT_FOUND) {
3 !
458
                        mimeType = mimeType.substring(0, semicolon);
×
459
                    }
460
                    response.setContentType(mimeType + "; charset=" + encoding);
5 ✔
461
                }
462
            }
463

464
            try {
465
                final XQuerySerializer serializer = new XQuerySerializer(broker, outputProperties, printWriter);
7 ✔
466
                serializer.serialize(resultSequence);
3 ✔
467
            } catch (final SAXException | XPathException e) {
1 ✔
468
                throw new IOException(e);
×
469
            }
470
            printWriter.flush();
2 ✔
471
        }
472
    }
1 ✔
473

474
    private void flushError(final HttpServletResponse response, final HttpServletResponse wrappedResponse) throws IOException {
475
        if (!response.isCommitted()) {
3 !
476
            response.setStatus(wrappedResponse.getStatus());
×
477
            final byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
×
478
            if (data != null) {
×
479
                response.setContentType(wrappedResponse.getContentType());
×
480
                response.setCharacterEncoding(wrappedResponse.getCharacterEncoding());
×
481
                response.getOutputStream().write(data);
×
482
                response.flushBuffer();
×
483
            }
484
        }
485
    }
1 ✔
486

487
    private ModelAndView getFromCache(final String url, final Subject user) throws EXistException, PermissionDeniedException {
488
        /* Make sure we have a broker *before* we synchronize on urlCache or we may run
489
         * into a deadlock situation (with method checkCache)
490
         */
491
        final ModelAndView model = urlCache.get(url);
6 ✔
492
        if (model == null) {
2 !
493
            return null;
2 ✔
494
        }
495

496
        try (@SuppressWarnings("PMD.UnusedLocalVariable") final DBBroker broker = pool.get(Optional.ofNullable(user))) {
×
497

498
            if (model.getSourceInfo().source instanceof DBSource) {
×
499
                ((DBSource) model.getSourceInfo().source).validate(Permission.EXECUTE);
×
500
            }
501

502
            if (model.getSourceInfo().source.isValid() != Source.Validity.VALID) {
×
503
                urlCache.remove(url);
×
504
                return null;
×
505
            }
506

507
            if (LOG.isDebugEnabled()) {
×
508
                LOG.debug("Using cached entry for {}", url);
×
509
            }
510
            return model;
×
511
        }
512
    }
513

514
    void clearCaches() {
515
        urlCache.clear();
3 ✔
516
    }
1 ✔
517

518
    /**
519
     * Process a rewrite action. Method checks if the target path is mapped
520
     * to another action in controller-config.xml. If yes, replaces the current action
521
     * with the new action.
522
     *
523
     * @param action the URLRewrite action
524
     * @param request the http request
525
     * @param response the http response
526
     */
527
    private void doRewrite(final URLRewrite action, final ControllerRequestWrapper request, final HttpServletResponse response) throws IOException, ServletException {
528
        URLRewrite effectiveAction = action;
2 ✔
529
        ControllerRequestWrapper effectiveRequest = request;
2 ✔
530
        if (effectiveAction.getTarget() != null && !(effectiveAction instanceof Redirect)) {
6 !
531
            final String uri = effectiveAction.resolve(effectiveRequest);
4 ✔
532
            final URLRewrite staticRewrite = rewriteConfig.lookup(uri, effectiveRequest.getServerName(), true, effectiveAction);
9 ✔
533

534
            if (staticRewrite != null) {
2 ✔
535
                staticRewrite.copyFrom(effectiveAction);
3 ✔
536
                effectiveAction = staticRewrite;
2 ✔
537
                final ControllerRequestWrapper modifiedRequest = new ControllerRequestWrapper(effectiveRequest);
5 ✔
538
                modifiedRequest.setPaths(uri, effectiveAction.getPrefix());
5 ✔
539

540
                if (LOG.isTraceEnabled()) {
3 !
541
                    LOG.trace("Forwarding to : {} url: {}", effectiveAction.toString(), effectiveAction.getURI());
×
542
                }
543
                effectiveRequest = modifiedRequest;
2 ✔
544
            }
545
        }
546
        effectiveAction.prepareRequest(effectiveRequest);
3 ✔
547
        effectiveAction.doRewrite(effectiveRequest, response);
4 ✔
548
    }
1 ✔
549

550
    protected ServletConfig getConfig() {
551
        return config;
3 ✔
552
    }
553

554
    private URLRewrite parseAction(final HttpServletRequest request, final Element action) throws ServletException {
555
        final URLRewrite rewrite;
556
        if ("forward".equals(action.getLocalName())) {
5 !
557
            rewrite = new PathForward(config, action, request.getRequestURI());
9 ✔
558
        } else if ("redirect".equals(action.getLocalName())) {
1 !
559
            rewrite = new Redirect(action, request.getRequestURI());
×
560
        } else {
×
561
            rewrite = null;
×
562
        }
563
        return rewrite;
2 ✔
564
    }
565

566
    @SuppressWarnings("PMD.UnusedPrivateMethod") // called from switch expression in service()
567
    private void parseViews(final HttpServletRequest request, final Element view, final ModelAndView modelView) throws ServletException {
568
        Node node = view.getFirstChild();
3 ✔
569
        while (node != null) {
3 ✔
570
            final String ns = node.getNamespaceURI();
3 ✔
571
            if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(ns)) {
8 !
572
                final URLRewrite urw = parseAction(request, (Element) node);
6 ✔
573
                if (urw != null) {
2 !
574
                    modelView.addView(urw);
3 ✔
575
                }
576
            }
577
            node = node.getNextSibling();
3 ✔
578
        }
579
    }
1 ✔
580

581
    @SuppressWarnings("PMD.UnusedPrivateMethod") // called from switch expression in service()
582
    private void parseErrorHandlers(final HttpServletRequest request, final Element view, final ModelAndView modelView) throws ServletException {
583
        Node node = view.getFirstChild();
×
584
        while (node != null) {
×
585
            final String ns = node.getNamespaceURI();
×
586
            if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(ns)) {
×
587
                final URLRewrite urw = parseAction(request, (Element) node);
×
588
                if (urw != null) {
×
589
                    modelView.addErrorHandler(urw);
×
590
                }
591
            }
592
            node = node.getNextSibling();
×
593
        }
594
    }
×
595

596
    private void configure() throws ServletException {
597
        if (pool != null) {
3 ✔
598
            return;
1 ✔
599
        }
600
        try {
601
            final Class<?> driver = Class.forName(DRIVER);
3 ✔
602
            final Database database = (Database) driver.getDeclaredConstructor().newInstance();
9 ✔
603
            database.setProperty("create-database", "true");
4 ✔
604
            DatabaseManager.registerDatabase(database);
2 ✔
605
            if (LOG.isDebugEnabled()) {
3 !
606
                LOG.debug("Initialized database");
×
607
            }
608
        } catch (final Exception e) {
×
609
            final String errorMessage = "Failed to initialize database driver";
×
610
            LOG.error(errorMessage, e);
×
611
            throw new ServletException(errorMessage + ": " + e.getMessage(), e);
×
612
        }
613

614
        try {
615
            pool = BrokerPool.getInstance();
3 ✔
616
        } catch (final EXistException e) {
1 ✔
617
            throw new ServletException("Could not initialize db: " + e.getMessage(), e);
×
618
        }
619

620
        defaultUser = pool.getSecurityManager().getGuestSubject();
6 ✔
621

622
        final String username = config.getInitParameter("user");
5 ✔
623
        if (username != null) {
2 !
624
            final String password = config.getInitParameter("password");
×
625
            try {
626
                final Subject user = pool.getSecurityManager().authenticate(username, password);
×
627
                if (user != null && user.isAuthenticated()) {
×
628
                    defaultUser = user;
×
629
                }
630
            } catch (final AuthenticationException e) {
×
631
                LOG.error("User can not be authenticated ({}), using default user.", username);
×
632
            }
633
        }
634
        authenticator = new BasicAuthenticator(pool);
7 ✔
635
    }
1 ✔
636

637
    private void logResult(final DBBroker broker, final Sequence result) throws SAXException {
638
        if (LOG.isTraceEnabled() && result.getItemCount() > 0) {
3 !
639
            final Serializer serializer = broker.borrowSerializer();
×
640
            try {
641
                final Item item = result.itemAt(0);
×
642
                if (Type.subTypeOf(item.getType(), Type.NODE)) {
×
643
                    LOG.trace(serializer.serialize((NodeValue) item));
×
644
                }
645
            } finally {
×
646
                broker.returnSerializer(serializer);
×
647
            }
648
        }
649
    }
1 ✔
650

651
    @Override
652
    public void destroy() {
653
        config = null;
3 ✔
654
    }
1 ✔
655

656
    private SourceInfo getSourceInfo(final DBBroker broker, final ControllerRequestWrapper request, final URLRewrite staticRewrite) throws ServletException {
657
        final String moduleLoadPath = config.getServletContext().getRealPath("/");
6 ✔
658
        final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget();
5 !
659
        if (basePath == null) {
2 !
660
            return getSource(broker, moduleLoadPath);
×
661
        } else {
662
            return findSource(request, broker, basePath);
6 ✔
663
        }
664
    }
665

666
    private Sequence runQuery(final DBBroker broker, final ControllerRequestWrapper request, final HttpServletResponse response, final ModelAndView model, final URLRewrite staticRewrite, final Properties outputProperties) throws ServletException, XPathException, PermissionDeniedException {
667
        // Try to find the XQuery
668
        final SourceInfo sourceInfo = getSourceInfo(broker, request, staticRewrite);
6 ✔
669
        if (sourceInfo == null) {
2 ✔
670
            return Sequence.EMPTY_SEQUENCE; // no controller found
2 ✔
671
        }
672

673
        final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget();
5 !
674

675
        final XQuery xquery = broker.getBrokerPool().getXQueryService();
4 ✔
676
        final XQueryPool xqyPool = broker.getBrokerPool().getXQueryPool();
4 ✔
677

678
        CompiledXQuery compiled = null;
2 ✔
679
        if (compiledCache) {
3 !
680
            compiled = xqyPool.borrowCompiledXQuery(broker, sourceInfo.source);
6 ✔
681
        }
682
        final XQueryContext queryContext;
683
        if (compiled == null) {
2 ✔
684
            queryContext = new XQueryContext(broker.getBrokerPool());
6 ✔
685
        } else {
1 ✔
686
            queryContext = compiled.getContext();
3 ✔
687
            queryContext.prepareForReuse();
2 ✔
688
        }
689

690
        // Find correct module load path
691
        queryContext.setModuleLoadPath(sourceInfo.moduleLoadPath);
4 ✔
692
        declareVariables(queryContext, sourceInfo, staticRewrite, basePath, request, response);
8 ✔
693
        if (compiled == null) {
2 ✔
694
            try {
695
                compiled = xquery.compile(queryContext, sourceInfo.source);
6 ✔
696
            } catch (final IOException e) {
1 ✔
697
                throw new ServletException("Failed to read query from " + query, e);
×
698
            }
699
        }
700
        model.setSourceInfo(sourceInfo);
3 ✔
701

702
        try {
703
            return xquery.execute(broker, compiled, null, outputProperties);
9 ✔
704
        } finally {
705
            queryContext.runCleanupTasks();
2 ✔
706
            xqyPool.returnCompiledXQuery(sourceInfo.source, compiled);
5 ✔
707
        }
708
    }
709

710
    String adjustPathForSourceLookup(final String basePath, final String path) {
711
        if (LOG.isTraceEnabled()) {
3 !
712
            LOG.trace("request path={}", path);
×
713
        }
714

715
        String adjustedPath = path;
2 ✔
716
        if (basePath.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX) && adjustedPath.startsWith(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""))) {
11 ✔
717
            adjustedPath = adjustedPath.replace(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""), "");
8 ✔
718

719
        } else if (adjustedPath.startsWith("/db/")) {
5 ✔
720
            adjustedPath = adjustedPath.substring(4);
4 ✔
721
        }
722

723
        if (adjustedPath.startsWith("/")) {
4 ✔
724
            adjustedPath = adjustedPath.substring(1);
4 ✔
725
        }
726

727
        if (LOG.isTraceEnabled()) {
3 !
728
            LOG.trace("adjusted request path={}", adjustedPath);
×
729
        }
730
        return adjustedPath;
2 ✔
731
    }
732

733
    @SuppressWarnings("PMD.UnusedPrivateMethod") // called indirectly from getSourceInfo()
734
    private SourceInfo findSource(final HttpServletRequest request, final DBBroker broker, final String basePath) {
735
        if (LOG.isTraceEnabled()) {
3 !
736
            LOG.trace("basePath={}", basePath);
×
737
        }
738

739
        final String requestURI = request.getRequestURI();
3 ✔
740
        String path = requestURI.substring(request.getContextPath().length());
6 ✔
741
        path = adjustPathForSourceLookup(basePath, path);
5 ✔
742
        final String[] components = path.split("/");
4 ✔
743

744
        if (basePath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
4 ✔
745
            if (LOG.isTraceEnabled()) {
3 !
746
                LOG.trace("Looking for " + XQUERY_CONTROLLER_FILENAME + " in the database, starting from: {}", basePath);
×
747
            }
748
            return findSourceFromDb(broker, basePath, path, components);
7 ✔
749
        } else {
750
            if (LOG.isTraceEnabled()) {
3 !
751
                LOG.trace("Looking for " + XQUERY_CONTROLLER_FILENAME + " in the filesystem, starting from: {}", basePath);
×
752
            }
753
            return findSourceFromFs(basePath, components);
5 ✔
754
        }
755
    }
756

757
    private @Nullable
758
    SourceInfo findSourceFromDb(final DBBroker broker, final String basePath, final String path, final String[] components) {
759
        LockedDocument lockedControllerDoc = null;
2 ✔
760
        try {
761
            final XmldbURI locationUri = XmldbURI.xmldbUriFor(basePath);
3 ✔
762
            XmldbURI resourceUri = locationUri;
2 ✔
763
            for(final String component : components) {
16 ✔
764
                resourceUri = resourceUri.append(component);
4 ✔
765
            }
766

767
            lockedControllerDoc = findDbControllerXql(broker, locationUri, resourceUri);
6 ✔
768

769
            if (lockedControllerDoc == null) {
2 ✔
770
                LOG.warn("XQueryURLRewrite controller could not be found for path: {}", path);
4 ✔
771
                return null;
2 ✔
772
            }
773

774
            final DocumentImpl controllerDoc = lockedControllerDoc.getDocument();
3 ✔
775

776
            if (LOG.isTraceEnabled()) {
3 !
777
                LOG.trace("Found controller file: {}", controllerDoc.getURI());
×
778
            }
779

780
            if (controllerDoc.getResourceType() != DocumentImpl.BINARY_FILE ||
4 !
781
                    !"application/xquery".equals(controllerDoc.getMimeType())) {
5 !
782
                LOG.warn("XQuery resource: {} is not an XQuery or declares a wrong mime-type", query);
×
783
                return null;
×
784
            }
785

786
            final String controllerPath = controllerDoc.getCollection().getURI().getRawCollectionPath();
5 ✔
787
            return new SourceInfo(new DBSource(broker.getBrokerPool(), (BinaryDocument) controllerDoc, true), "xmldb:exist://" + controllerPath, controllerPath.substring(locationUri.getCollectionPath().length()));
21 ✔
788

789
        } catch (final URISyntaxException e) {
×
790
            LOG.warn("Bad URI for base path: {}", e.getMessage(), e);
×
791
            return null;
×
792
        } finally {
793
            if (lockedControllerDoc != null) {
2 ✔
794
                lockedControllerDoc.close();
2 ✔
795
            }
796
        }
797
    }
798

799
    /**
800
     * Finds a `controller.xq` (or legacy `controller.xql`)
801
     * file within a Collection hierarchy.
802
     * Most specific collections are considered first.
803
     *
804
     * For example, given the collectionUri `/db/apps`
805
     * and the resourceUri /db/apps/myapp/data, the
806
     * order or search will be:
807
     *
808
     * /db/apps/myapp/data/controller.xq
809
     * /db/apps/myapp/controller.xq
810
     * /db/apps/controller.xq
811
     *
812
     * @param broker         The database broker
813
     * @param collectionUri  The root collection URI, below which we should not descend
814
     * @param resourceUri The path to the most specific document or collection for which we should find a controller
815
     * @return The most relevant controller.xq document (with a READ_LOCK), or null if it could not be found.
816
     */
817
    //@tailrec
818
    private @Nullable
819
    LockedDocument findDbControllerXql(final DBBroker broker, final XmldbURI collectionUri, final XmldbURI resourceUri) {
820
        if (collectionUri.compareTo(resourceUri) > 0) {
4 ✔
821
            return null;
2 ✔
822
        }
823

824
        try (final Collection collection = broker.openCollection(resourceUri, LockMode.READ_LOCK)) {
9 ✔
825
            if (collection != null) {
2 ✔
826
                LockedDocument lockedDoc = collection.getDocumentWithLock(broker, XQUERY_CONTROLLER_URI, LockMode.READ_LOCK);
6 ✔
827

828
                if (lockedDoc == null) {
2 ✔
829
                    lockedDoc = collection.getDocumentWithLock(broker, LEGACY_XQUERY_CONTROLLER_URI, LockMode.READ_LOCK);
6 ✔
830
                }
831

832
                if (lockedDoc != null) {
2 ✔
833
                    // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
834
                    // collection lock will be released by the try-with-resources before the locked document is returned by this function
835
                    return lockedDoc;
2 ✔
836
                }
837

838

839
            }
840
        } catch (final PermissionDeniedException e) {
×
841
            if (LOG.isDebugEnabled()) {
×
842
                LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: {}", e.getMessage(), e);
×
843
            }
844
            return null;
×
845
        } catch (final LockException e) {
×
846
            if (LOG.isDebugEnabled()) {
×
847
                LOG.debug("LockException while scanning for XQueryURLRewrite controllers: {}", e.getMessage(), e);
×
848
            }
849
            return null;
×
850
        }
851

852
        if(resourceUri.numSegments() == 2) {
4 ✔
853
            return null;
2 ✔
854
        }
855
        final XmldbURI subResourceUri = resourceUri.removeLastSegment();
3 ✔
856

857
        return findDbControllerXql(broker, collectionUri, subResourceUri);
6 ✔
858
    }
859

860
    // package-private rather than private so it is directly testable without reflection
861
    SourceInfo findSourceFromFs(final String basePath, final String[] components) {
862
        final String realPath = config.getServletContext().getRealPath(basePath);
6 ✔
863
        // the Servlet API permits getRealPath() to return null when the container cannot map
864
        // basePath to a location on disk; there is no filesystem controller to find.
865
        if (realPath == null) {
2 ✔
866
            LOG.warn("Base path for XQueryURLRewrite does not point to a directory");
3 ✔
867
            return null;
2 ✔
868
        }
869
        final Path baseDir = Path.of(realPath);
5 ✔
870
        if (!Files.isDirectory(baseDir)) {
5 !
871
            LOG.warn("Base path for XQueryURLRewrite does not point to a directory");
×
872
            return null;
×
873
        }
874

875
        Path controllerFile = null;
2 ✔
876
        Path subDir = baseDir;
2 ✔
877
        for (final String component : components) {
16 !
878
            if (!component.isEmpty()) {
3 !
879
                subDir = subDir.resolve(component);
4 ✔
880
                if (Files.isDirectory(subDir)) {
5 ✔
881
                    Path cf = subDir.resolve(XQUERY_CONTROLLER_FILENAME);
4 ✔
882

883
                    if (!Files.isReadable(cf)) {
3 !
884
                        cf = subDir.resolve(LEGACY_XQUERY_CONTROLLER_FILENAME);
4 ✔
885
                    }
886

887
                    if (Files.isReadable(cf)) {
3 !
888
                        controllerFile = cf;
2 ✔
889
                    }
890
                } else {
891
                    break;
892
                }
893
            }
894
        }
895

896
        if (controllerFile == null) {
2 !
897
            Path cf = baseDir.resolve(XQUERY_CONTROLLER_FILENAME);
×
898

899
            if (!Files.isReadable(cf)) {
×
900
                cf = subDir.resolve(LEGACY_XQUERY_CONTROLLER_FILENAME);
×
901
            }
902

903
            if (Files.isReadable(cf)) {
×
904
                controllerFile = cf;
×
905
            }
906
        }
907

908
        if (controllerFile == null) {
2 !
909
            LOG.warn("XQueryURLRewrite controller could not be found");
×
910
            return null;
×
911
        }
912

913
        if (LOG.isTraceEnabled()) {
3 !
914
            LOG.trace("Found controller file: {}", controllerFile.toAbsolutePath());
×
915
        }
916

917
        final String parentPath = controllerFile.getParent().toAbsolutePath().toString();
5 ✔
918
        String controllerPath = parentPath.substring(baseDir.toAbsolutePath().toString().length());
7 ✔
919
        // replace windows path separators
920
        controllerPath = controllerPath.replace('\\', '/');
5 ✔
921
        return new SourceInfo(new FileSource(controllerFile, true), parentPath, controllerPath);
11 ✔
922
    }
923

924
    private SourceInfo getSource(final DBBroker broker, final String moduleLoadPath) throws ServletException {
925
        final SourceInfo sourceInfo;
926
        if (query.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
×
927
            // Is the module source stored in the database?
928
            try {
929
                final XmldbURI locationUri = XmldbURI.xmldbUriFor(query);
×
930

931
                try (final LockedDocument lockedSourceDoc = broker.getXMLResource(locationUri.toCollectionPathURI(), LockMode.READ_LOCK)) {
×
932
                    if (lockedSourceDoc == null) {
×
933
                        throw new ServletException("XQuery resource: " + query + " not found in database");
×
934
                    }
935

936
                    final DocumentImpl sourceDoc = lockedSourceDoc.getDocument();
×
937
                    if (sourceDoc.getResourceType() != DocumentImpl.BINARY_FILE ||
×
938
                            !"application/xquery".equals(sourceDoc.getMimeType())) {
×
939
                        throw new ServletException("XQuery resource: " + query + " is not an XQuery or " +
×
940
                                "declares a wrong mime-type");
941
                    }
942
                    sourceInfo = new SourceInfo(new DBSource(broker.getBrokerPool(), (BinaryDocument) sourceDoc, true),
×
943
                            locationUri.toString());
×
944
                } catch (final PermissionDeniedException e) {
×
945
                    throw new ServletException("permission denied to read module source from " + query);
×
946
                }
947
            } catch (final URISyntaxException e) {
×
948
                throw new ServletException(e.getMessage(), e);
×
949
            }
950
        } else {
951
            try {
952
                sourceInfo = new SourceInfo(SourceFactory.getSource(broker, moduleLoadPath, query, true), moduleLoadPath);
×
953
            } catch (final IOException e) {
×
954
                throw new ServletException("IO error while reading XQuery source: " + query);
×
955
            } catch (final PermissionDeniedException e) {
×
956
                throw new ServletException("Permission denied while reading XQuery source: " + query);
×
957
            }
958
        }
959
        return sourceInfo;
×
960
    }
961

962
    private void declareVariables(final XQueryContext context, final SourceInfo sourceInfo, final URLRewrite staticRewrite, final String basePath, final ControllerRequestWrapper request, final HttpServletResponse response) throws XPathException {
963
        // parseMultipart=true so that multipart/form-data uploads (including file parts)
964
        // are exposed to controllers and RESTXQ resource functions for every HTTP method,
965
        // not only POST. See https://github.com/eXist-db/exist/issues/6580 and
966
        // https://github.com/eXist-db/exist/issues/6578
967
        final HttpRequestWrapper reqw = new HttpRequestWrapper(request, UTF_8.name(), UTF_8.name(), true);
10 ✔
968
        final HttpResponseWrapper respw = new HttpResponseWrapper(response);
5 ✔
969
        // context.declareNamespace(RequestModule.PREFIX,
970
        // RequestModule.NAMESPACE_URI);
971
        context.setHttpContext(new XQueryContext.HttpContext(reqw, respw));
7 ✔
972

973
        context.declareVariable("exist:controller", sourceInfo.controllerPath);
6 ✔
974
        request.setAttribute("$exist:controller", sourceInfo.controllerPath);
5 ✔
975
        context.declareVariable("exist:root", basePath);
5 ✔
976
        request.setAttribute("$exist:root", basePath);
4 ✔
977
        context.declareVariable("exist:context", request.getContextPath());
6 ✔
978
        request.setAttribute("$exist:context", request.getContextPath());
5 ✔
979
        final String prefix = staticRewrite == null ? null : staticRewrite.getPrefix();
5 !
980
        context.declareVariable("exist:prefix", prefix == null ? "" : prefix);
9 ✔
981
        request.setAttribute("$exist:prefix", prefix == null ? "" : prefix);
8 ✔
982
        String path;
983
        if (!sourceInfo.controllerPath.isEmpty() && !"/".equals(sourceInfo.controllerPath)) {
9 !
984
            path = request.getInContextPath().substring(sourceInfo.controllerPath.length());
7 ✔
985
        } else {
1 ✔
986
            path = request.getInContextPath();
×
987
        }
988
        final int p = path.lastIndexOf(';');
4 ✔
989
        if (p != Constants.STRING_NOT_FOUND) {
3 !
990
            path = path.substring(0, p);
×
991
        }
992
        context.declareVariable("exist:path", path);
5 ✔
993
        request.setAttribute("$exist:path", path);
4 ✔
994

995
        String resource = "";
2 ✔
996
        final Matcher nameMatcher = NAME_REGEX.matcher(path);
4 ✔
997
        if (nameMatcher.matches()) {
3 !
998
            resource = nameMatcher.group(1);
4 ✔
999
        }
1000
        context.declareVariable("exist:resource", resource);
5 ✔
1001
        request.setAttribute("$exist:resource", resource);
4 ✔
1002

1003
        if (LOG.isDebugEnabled()) {
3 !
1004
            LOG.debug("\nexist:path = {}\nexist:resource = {}\nexist:controller = {}", path, resource, sourceInfo.controllerPath);
×
1005
        }
1006
    }
1 ✔
1007

1008
    private static class ModelAndView {
3 ✔
1009
        private URLRewrite rewrite = null;
3 ✔
1010
        private final List<URLRewrite> views = new LinkedList<>();
5 ✔
1011
        private List<URLRewrite> errorHandlers = null;
3 ✔
1012
        private boolean useCache = false;
3 ✔
1013
        private SourceInfo sourceInfo = null;
3 ✔
1014

1015
        public void setSourceInfo(final SourceInfo sourceInfo) {
1016
            this.sourceInfo = sourceInfo;
3 ✔
1017
        }
1 ✔
1018

1019
        public SourceInfo getSourceInfo() {
1020
            return sourceInfo;
×
1021
        }
1022

1023
        public void setModel(final URLRewrite model) {
1024
            this.rewrite = model;
3 ✔
1025
        }
1 ✔
1026

1027
        public URLRewrite getModel() {
1028
            return rewrite;
3 ✔
1029
        }
1030

1031
        public void addErrorHandler(final URLRewrite handler) {
1032
            if (errorHandlers == null) {
×
1033
                errorHandlers = new LinkedList<>();
×
1034
            }
1035
            errorHandlers.add(handler);
×
1036
        }
×
1037

1038
        public void addView(URLRewrite view) {
1039
            views.add(view);
5 ✔
1040
        }
1 ✔
1041

1042
        public boolean hasViews() {
1043
            return !views.isEmpty();
8 ✔
1044
        }
1045

1046
        public boolean hasErrorHandlers() {
1047
            return errorHandlers != null && !errorHandlers.isEmpty();
5 !
1048
        }
1049

1050
        public boolean useCache() {
1051
            return useCache;
3 ✔
1052
        }
1053

1054
        public void setUseCache(final boolean useCache) {
1055
            this.useCache = useCache;
3 ✔
1056
        }
1 ✔
1057
    }
1058

1059
    private static class SourceInfo {
1060
        final Source source;
1061
        final String moduleLoadPath;
1062
        final String controllerPath;
1063

1064
        private SourceInfo(final Source source, final String moduleLoadPath) {
1065
            this(source, moduleLoadPath, "");
×
1066
        }
×
1067

1068
        private SourceInfo(final Source source, final String moduleLoadPath, final String controllerPath) {
2 ✔
1069
            this.source = source;
3 ✔
1070
            this.moduleLoadPath = moduleLoadPath;
3 ✔
1071
            this.controllerPath = controllerPath;
3 ✔
1072
        }
1 ✔
1073
    }
1074

1075
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc