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

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

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

0.0
/exist-core/src/main/java/org/exist/http/servlets/XSLTServlet.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.http.servlets;
50

51
import org.apache.logging.log4j.LogManager;
52
import org.apache.logging.log4j.Logger;
53
import org.exist.EXistException;
54
import org.exist.security.AuthenticationException;
55
import org.exist.security.Subject;
56
import org.exist.security.internal.web.HttpAccount;
57
import org.exist.storage.BrokerPool;
58
import org.exist.storage.DBBroker;
59
import org.exist.storage.serializers.Serializer;
60
import org.exist.storage.serializers.XIncludeFilter;
61
import org.exist.util.serializer.*;
62
import org.exist.xmldb.XmldbURI;
63
import org.exist.xquery.Constants;
64
import org.exist.xquery.XPathException;
65
import org.exist.xquery.value.Item;
66
import org.exist.xquery.value.NodeValue;
67
import org.exist.xquery.value.Type;
68
import org.exist.xquery.value.ValueSequence;
69
import org.exist.xslt.Stylesheet;
70
import org.exist.xslt.TemplatesFactory;
71
import org.exist.xslt.TransformerFactoryAllocator;
72
import org.exist.xslt.XSLTErrorsListener;
73
import org.xml.sax.InputSource;
74
import org.xml.sax.SAXException;
75
import org.xml.sax.SAXParseException;
76
import org.xml.sax.XMLReader;
77

78
import jakarta.servlet.ServletException;
79
import jakarta.servlet.http.HttpServlet;
80
import jakarta.servlet.http.HttpServletRequest;
81
import jakarta.servlet.http.HttpServletResponse;
82
import javax.xml.transform.Transformer;
83
import javax.xml.transform.TransformerException;
84
import javax.xml.transform.sax.SAXResult;
85
import javax.xml.transform.sax.TransformerHandler;
86
import java.io.*;
87
import java.nio.file.Files;
88
import java.nio.file.Path;
89
import java.nio.file.Paths;
90
import java.util.Enumeration;
91
import java.util.Optional;
92
import java.util.Properties;
93
import java.util.zip.GZIPInputStream;
94

95
import static java.nio.charset.StandardCharsets.UTF_8;
96

97
/**
98
 * Elemental servlet for XSLT transformations.
99
 *
100
 * @author Wolfgang
101
 */
102
public class XSLTServlet extends HttpServlet {
×
103

104
    private static final long serialVersionUID = -7258405385386062151L;
105

106
    private final static String REQ_ATTRIBUTE_PREFIX = "xslt.";
107

108
    private final static String REQ_ATTRIBUTE_STYLESHEET = "xslt.stylesheet";
109
    private final static String REQ_ATTRIBUTE_INPUT = "xslt.input";
110
    private final static String REQ_ATTRIBUTE_OUTPUT = "xslt.output.";
111
    private final static String REQ_ATTRIBUTE_BASE = "xslt.base";
112

113
    private final static Logger LOG = LogManager.getLogger(XSLTServlet.class);
×
114

115
    private final static XSLTErrorsListener<ServletException> errorListener =
×
116
        new XSLTErrorsListener<ServletException>(true, false) {
×
117

118
            @Override
119
            protected void raiseError(final String error, final TransformerException ex) throws ServletException {
120
                throw new ServletException(error, ex);
×
121
            }
122
        };
×
123

124
    private BrokerPool pool;
125

126
    private Boolean caching = null;
×
127

128
    /**
129
     * @return Value of TransformerFactoryAllocator.PROPERTY_CACHING_ATTRIBUTE or TRUE if not present.
130
     */
131
    private boolean isCaching() {
132
        if (caching == null) {
×
133
            final Object property = pool.getConfiguration().getProperty(TransformerFactoryAllocator.PROPERTY_CACHING_ATTRIBUTE);
×
134
            if (property != null) {
×
135
                caching = (Boolean) property;
×
136
            } else {
×
137
                caching = true;
×
138
            }
139
        }
140
        return caching;
×
141
    }
142

143
    @Override
144
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
145

146
        final String uri = (String) request.getAttribute(REQ_ATTRIBUTE_STYLESHEET);
×
147
        if (uri == null) {
×
148
            throw new ServletException("No stylesheet source specified!");
×
149
        }
150

151
        Item inputNode = null;
×
152

153
        final String sourceAttrib = (String) request.getAttribute(REQ_ATTRIBUTE_INPUT);
×
154
        if (sourceAttrib != null) {
×
155

156
            Object sourceObj = request.getAttribute(sourceAttrib);
×
157
            if (sourceObj != null) {
×
158
                if (sourceObj instanceof ValueSequence seq) {
×
159

160
                    if (seq.size() == 1) {
×
161
                        sourceObj = seq.itemAt(0);
×
162
                    }
163
                }
164

165
                if (sourceObj instanceof Item) {
×
166
                    inputNode = (Item) sourceObj;
×
167
                    if (!Type.subTypeOf(inputNode.getType(), Type.NODE)) {
×
168
                        throw new ServletException("Input for XSLT servlet is not a node. Read from attribute " +
×
169
                                sourceAttrib);
×
170
                    }
171

172
                    if (LOG.isDebugEnabled()) {
×
173
                        LOG.debug("Taking XSLT input from request attribute {}", sourceAttrib);
×
174
                    }
175

176
                } else {
×
177
                    throw new ServletException("Input for XSLT servlet is not a node. Read from attribute " +
×
178
                            sourceAttrib);
×
179
                }
180
            }
181
        }
182

183
        try {
184
            pool = BrokerPool.getInstance();
×
185
        } catch (final EXistException e) {
×
186
            throw new ServletException(e.getMessage(), e);
×
187
        }
188

189
        Subject user = pool.getSecurityManager().getGuestSubject();
×
190

191
        Subject requestUser = HttpAccount.getUserFromServletRequest(request);
×
192
        if (requestUser != null) {
×
193
            user = requestUser;
×
194
        }
195

196
        // Retrieve username / password from HTTP request attributes
197
        final String userParam = (String) request.getAttribute("xslt.user");
×
198
        final String passwd = (String) request.getAttribute("xslt.password");
×
199

200
        if (userParam != null) {
×
201
            try {
202
                user = pool.getSecurityManager().authenticate(userParam, passwd);
×
203
            } catch (final AuthenticationException e1) {
×
204
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Wrong password or user");
×
205
                return;
×
206
            }
207
        }
208

209
        final Stylesheet stylesheet = stylesheet(uri, request, response);
×
210
        if (stylesheet == null) {
×
211
            return;
×
212
        }
213

214
        //do the transformation
215
        try (final DBBroker broker = pool.get(Optional.of(user))) {
×
216

217
            final TransformerHandler handler = stylesheet.newTransformerHandler(broker, errorListener);
×
218
            setTransformerParameters(request, handler.getTransformer());
×
219

220
            final Properties properties = handler.getTransformer().getOutputProperties();
×
221
            setOutputProperties(request, properties);
×
222

223
            String encoding = properties.getProperty("encoding");
×
224
            if (encoding == null) {
×
225
                encoding = UTF_8.name();
×
226
            }
227
            response.setCharacterEncoding(encoding);
×
228

229
            final String mediaType = properties.getProperty("media-type");
×
230
            if (mediaType != null) {
×
231
                //check, do mediaType have "charset"
232
                if (!mediaType.contains("charset")) {
×
233
                    response.setContentType(mediaType + "; charset=" + encoding);
×
234
                } else {
×
235
                    response.setContentType(mediaType);
×
236
                }
237
            }
238

239
            final SAXSerializer sax = (SAXSerializer) SerializerPool.getInstance().borrowObject(SAXSerializer.class);
×
240
            final Writer writer = new BufferedWriter(response.getWriter());
×
241
            sax.setOutput(writer, properties);
×
242

243
            final SAXResult result = new SAXResult(sax);
×
244
            handler.setResult(result);
×
245

246
            final Serializer serializer = broker.borrowSerializer();
×
247
            Receiver receiver = new ReceiverToSAX(handler);
×
248
            try {
249
                XIncludeFilter xinclude = new XIncludeFilter(serializer, receiver);
×
250
                receiver = xinclude;
×
251

252
                String baseUri;
253
                final String base = (String) request.getAttribute(REQ_ATTRIBUTE_BASE);
×
254
                if (base != null) {
×
255
                    baseUri = getServletContext().getRealPath(base);
×
256
                } else if (uri.startsWith("xmldb:exist://")) {
×
257
                    baseUri = XmldbURI.xmldbUriFor(uri).getCollectionPath();
×
258
                } else {
×
259
                    baseUri = getCurrentDir(request).toAbsolutePath().toString();
×
260
                }
261
                xinclude.setModuleLoadPath(baseUri);
×
262

263
                serializer.setReceiver(receiver);
×
264
                if (inputNode != null) {
×
265
                    serializer.toSAX((NodeValue) inputNode);
×
266

267
                } else {
×
268
                    final SAXToReceiver saxreceiver = new SAXToReceiver(receiver);
×
269
                    final XMLReader reader = pool.getParserPool().borrowXMLReader();
×
270
                    try {
271
                        reader.setContentHandler(saxreceiver);
×
272

273
                        //Handle gziped input stream
274
                        InputStream stream;
275

276
                        InputStream inStream = new BufferedInputStream(request.getInputStream());
×
277
                        inStream.mark(10);
×
278
                        try {
279
                            stream = new GZIPInputStream(inStream);
×
280
                        } catch (final IOException e) {
×
281
                            inStream.reset();
×
282
                            stream = inStream;
×
283
                        }
284

285
                        reader.parse(new InputSource(stream));
×
286
                    } finally {
×
287
                        pool.getParserPool().returnXMLReader(reader);
×
288
                    }
289
                }
290

291
            } catch (final SAXParseException e) {
×
292
                LOG.error(e.getMessage());
×
293
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
×
294

295
            } catch (final SAXException e) {
×
296
                throw new ServletException("SAX exception while transforming node: " + e.getMessage(), e);
×
297

298
            } finally {
299
                SerializerPool.getInstance().returnObject(sax);
×
300
                broker.returnSerializer(serializer);
×
301
            }
302

303
            writer.flush();
×
304
            response.flushBuffer();
×
305

306
        } catch (final IOException e) {
×
307
            throw new ServletException("IO exception while transforming node: " + e.getMessage(), e);
×
308

309
        } catch (final TransformerException e) {
×
310
            throw new ServletException("Exception while transforming node: " + e.getMessage(), e);
×
311

312
        } catch (final Throwable e) {
×
313
            LOG.error(e);
×
314
            throw new ServletException("An error occurred: " + e.getMessage(), e);
×
315

316
        }
317
    }
×
318

319
    /*
320
     * Please add comments to this method. make assumption clear. These might not be valid.
321
     */
322
    private Stylesheet stylesheet(String stylesheet, HttpServletRequest request, HttpServletResponse response)
323
            throws IOException {
324

325
        // Check if stylesheet contains an URI. If not, try to resolve from file system
326
        if (stylesheet.indexOf(':') == Constants.STRING_NOT_FOUND) {
×
327
            // replace double slash
328
            stylesheet = stylesheet.replaceAll("//", "/");
×
329
            Path f = Paths.get(stylesheet).normalize();
×
330
            if (Files.isReadable(f)) {
×
331
                // Found file, get URI
332
                stylesheet = f.toUri().toASCIIString();
×
333

334
            } else {
×
335
                // if the stylesheet path is absolute, it must be resolved relative to the webapp root
336
                // f.isAbsolute is problematic on windows.
337
                if (stylesheet.startsWith("/")) {
×
338

339
                    final String url = getServletContext().getRealPath(stylesheet);
×
340
                    if (url == null) {
×
341
                        response.sendError(HttpServletResponse.SC_NOT_FOUND,
×
342
                                "Stylesheet not found (URL: " + stylesheet + ")");
×
343
                        return null;
×
344
                    }
345

346
                    f = Paths.get(url);
×
347
                    stylesheet = f.toUri().toASCIIString();
×
348

349
                } else {
×
350
                    // relative path is relative to the current working directory
351
                    f = getCurrentDir(request).resolve(stylesheet);
×
352
                    stylesheet = f.toUri().toASCIIString();
×
353
                }
354

355
                if (!Files.isReadable(f)) {
×
356
                    response.sendError(HttpServletResponse.SC_NOT_FOUND,
×
357
                            "Stylesheet not found (URL: " + stylesheet + ")");
×
358
                    return null;
×
359
                }
360
            }
361
        }
362

363
        return TemplatesFactory.stylesheet(stylesheet, "", isCaching());
×
364
    }
365

366
    /*
367
     * Please explain what this method is about. Write about assumptions / input.
368
     */
369
    private Path getCurrentDir(HttpServletRequest request) {
370
        String path = request.getPathTranslated();
×
371
        if (path == null) {
×
372
            path = request.getRequestURI().substring(request.getContextPath().length());
×
373
            final int p = path.lastIndexOf('/');
×
374
            if (p != Constants.STRING_NOT_FOUND) {
×
375
                path = path.substring(0, p);
×
376
            }
377
            path = getServletContext().getRealPath(path);
×
378
        }
379

380
        final Path file = Paths.get(path).normalize();
×
381
        if (Files.isDirectory(file)) {
×
382
            return file;
×
383
        } else {
384
            return file.getParent();
×
385
        }
386
    }
387

388
    /**
389
     * Copy "xslt." attributes from HTTP request to transformer. Does not copy 'input', 'output'
390
     * and 'styleheet' attributes.
391
     */
392
    private void setTransformerParameters(HttpServletRequest request, Transformer transformer) throws XPathException {
393

394
        for (final Enumeration<String> e = request.getAttributeNames(); e.hasMoreElements(); ) {
×
395

396
            final String name = e.nextElement();
×
397
            if (name.startsWith(REQ_ATTRIBUTE_PREFIX) &&
×
398
                    !(name.startsWith(REQ_ATTRIBUTE_OUTPUT) || REQ_ATTRIBUTE_INPUT.equals(name)
×
399
                            || REQ_ATTRIBUTE_STYLESHEET.equals(name))) {
×
400
                Object value = request.getAttribute(name);
×
401
                if (value instanceof NodeValue nv) {
×
402
                    if (nv.getImplementationType() == NodeValue.IN_MEMORY_NODE) {
×
403
                        value = nv.toMemNodeSet();
×
404
                    }
405
                }
406
                transformer.setParameter(name, value);
×
407
                transformer.setParameter(name.substring(REQ_ATTRIBUTE_PREFIX.length()), value);
×
408
            }
409
        }
410
    }
×
411

412
    /**
413
     * Copies 'output' attributes to properties object.
414
     */
415
    private void setOutputProperties(HttpServletRequest request, Properties properties) {
416
        for (final Enumeration<String> e = request.getAttributeNames(); e.hasMoreElements(); ) {
×
417
            final String name = e.nextElement();
×
418
            if (name.startsWith(REQ_ATTRIBUTE_OUTPUT)) {
×
419
                final Object value = request.getAttribute(name);
×
420
                if (value != null) {
×
421
                    properties.setProperty(name.substring(REQ_ATTRIBUTE_OUTPUT.length()), value.toString());
×
422
                }
423
            }
424
        }
425
    }
×
426
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc