• 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

21.79
/exist-core/src/main/java/org/exist/http/Descriptor.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;
50

51
import org.apache.logging.log4j.LogManager;
52
import org.apache.logging.log4j.Logger;
53
import org.exist.Namespaces;
54
import org.exist.dom.memtree.SAXAdapter;
55
import org.exist.util.ConfigurationHelper;
56
import org.exist.util.ExistSAXParserFactory;
57
import org.exist.util.SingleInstanceConfiguration;
58
import org.exist.xquery.Expression;
59
import org.w3c.dom.Document;
60
import org.w3c.dom.Element;
61
import org.w3c.dom.NodeList;
62
import org.xml.sax.*;
63

64
import jakarta.servlet.http.HttpServletRequest;
65
import javax.xml.parsers.ParserConfigurationException;
66
import javax.xml.parsers.SAXParser;
67
import javax.xml.parsers.SAXParserFactory;
68
import java.io.BufferedInputStream;
69
import java.io.BufferedWriter;
70
import java.io.IOException;
71
import java.io.InputStream;
72
import java.nio.file.Files;
73
import java.nio.file.Path;
74
import java.nio.file.Paths;
75
import java.text.SimpleDateFormat;
76
import java.util.Date;
77

78
import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
79

80
/**
81
 * Webapplication Descriptor
82
 *
83
 * Class representation of an XQuery Web Application Descriptor file
84
 * with some helper functions for performing Descriptor related actions
85
 * Uses the Singleton design pattern.
86
 *
87
 * @author <a href="mailto:adam.retter@devon.gov.uk">Adam Retter</a>
88
 * @version 1.71
89
 * @serial 2006-03-19
90
 */
91

92
// TODO: doLogRequestInReplayLog() - add the facility to log HTTP PUT requests, may need changes to HttpServletRequestWrapper
93
// TODO: doLogRequestInReplayLog() - add the facility to log HTTP POST form file uploads, may need changes to HttpServletRequestWrapper
94

95
public class Descriptor implements ErrorHandler {
96
    private static final String SYSTEM_LINE_SEPARATOR = System.getProperty("line.separator");
1✔
97
    //References
98
    private static Descriptor singletonRef;
99
    private final static Logger LOG = LogManager.getLogger(Descriptor.class);        //Logger
1✔
100
    /**
101
     * descriptor file (descriptor.xml)
102
     */
103
    private final static String file = "descriptor.xml";
1✔
104

105
    //Data
106
    private BufferedWriter bufWriteReplayLog = null;    //Should a replay log of requests be created
1✔
107
    private boolean requestsFiltered;
108
    private String allowSourceList[] = null;    //Array of xql files to allow source to be viewed
1✔
109
    private String mapList[][] = null;                    //Array of Mappings
1✔
110

111
    /**
112
     * Descriptor Constructor.
113
     *
114
     * Class has a Singleton design pattern
115
     * to get an instance, call getDescriptorSingleton()
116
     */
117
    private Descriptor() {
1✔
118
        InputStream is = null;
1✔
119
        try {
120
            // First, try to read Descriptor from file. Guess the location if necessary
121
            // from the home folder.
122
            Path f = ConfigurationHelper.lookup(file);
1✔
123
            if (!Files.isReadable(f)) {
1!
124
                f = f.getParent().resolve("etc").resolve(file);
1✔
125
                if (!Files.isReadable(f)) {
1!
126
                    LOG.warn("Giving up unable to read descriptor file from {}", f);
1✔
127
                } else {
1✔
128
                    is = new BufferedInputStream(Files.newInputStream(f));
×
129
                }
130
            } else {
×
131
                is = new BufferedInputStream(Files.newInputStream(f));
×
132
                LOG.info("Reading Descriptor from file {}", f);
×
133
            }
134

135
            if (is == null) {
1!
136
                // otherise, secondly
137
                // try to read the Descriptor from a file within the classpath
138
                is = Descriptor.class.getResourceAsStream(file);
1✔
139
                if (is != null) {
1!
140
                    LOG.info("Reading Descriptor from classloader in {}", this.getClass().getPackage());
×
141
                } else {
×
142
                    LOG.warn("Giving up unable to read descriptor.xml file from classloader in {}", this.getClass().getPackage());
1✔
143
                    return;
1✔
144
                }
145
            }
146

147
            // initialize xml parser
148
            // we use eXist's in-memory DOM implementation to work
149
            // around a bug in Xerces
150
            final SAXParserFactory factory = ExistSAXParserFactory.getSAXParserFactory();
×
151
            factory.setNamespaceAware(true);
×
152

153
            final InputSource src = new InputSource(is);
×
154
            final SAXParser parser = factory.newSAXParser();
×
155
            final XMLReader reader = parser.getXMLReader();
×
156

157
            reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
×
158
            reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
×
159
            reader.setFeature(FEATURE_SECURE_PROCESSING, true);
×
160

161
            final SAXAdapter adapter = new SAXAdapter((Expression) null);
×
162
            reader.setContentHandler(adapter);
×
163
            reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter);
×
164
            reader.parse(src);
×
165

166
            final Document doc = adapter.getDocument();
×
167

168
            //load <xquery-app> attribue settings
169
            if ("true".equals(doc.getDocumentElement().getAttribute("request-replay-log"))) {
×
170
                final Path logFile = Paths.get("request-replay-log.txt");
×
171
                bufWriteReplayLog = Files.newBufferedWriter(logFile);
×
172
                final String attr = doc.getDocumentElement().getAttribute("filtered");
×
173
                if (!attr.isEmpty()) {
×
174
                    requestsFiltered = "true".equals(attr);
×
175
                }
176
            }
177

178
            //load <allow-source> settings
179
            final NodeList allowsourcexqueries = doc.getElementsByTagName("allow-source");
×
180
            if (allowsourcexqueries.getLength() > 0) {
×
181
                configureAllowSourceXQuery((Element) allowsourcexqueries.item(0));
×
182
            }
183

184
            //load <maps> settings
185
            final NodeList maps = doc.getElementsByTagName("maps");
×
186
            if (maps.getLength() > 0) {
×
187
                configureMaps((Element) maps.item(0));
×
188
            }
189
        } catch (final SAXException | IOException | ParserConfigurationException e) {
×
190
            LOG.warn("Error while reading descriptor file: " + file, e);
×
191
            return;
×
192
        } finally {
193
            if (is != null) {
1!
194
                try {
195
                    is.close();
×
196
                } catch (final IOException ioe) {
×
197
                    LOG.warn(ioe);
×
198
                }
199
            }
200
        }
201
    }
×
202

203
    /**
204
     * Returns a refernce to this (Descriptor) Singleton class
205
     *
206
     * @return The Descriptor object reference
207
     */
208
    public static synchronized Descriptor getDescriptorSingleton() {
209
        if (singletonRef == null) {
1✔
210
            singletonRef = new Descriptor();
1✔
211
        }
212

213
        return (singletonRef);
1✔
214
    }
215

216
    /**
217
     * loads {@code allow-source} settings from the descriptor.xml file
218
     *
219
     * @param    allowsourcexqueries    The &lt;allow-source&gt; DOM Element from the descriptor.xml file
220
     */
221
    private void configureAllowSourceXQuery(Element allowsourcexqueries) {
222
        //Get the xquery element(s)
223
        final NodeList nlXQuery = allowsourcexqueries.getElementsByTagName("xquery");
×
224

225
        //Setup the hashmap to hold the xquery elements
226
        allowSourceList = new String[nlXQuery.getLength()];
×
227

228
        Element elem = null; //temporary holds xquery elements
×
229

230
        //Iterate through the xquery elements
231
        for (int i = 0; i < nlXQuery.getLength(); i++) {
×
232
            elem = (Element) nlXQuery.item(i);                //<xquery>
×
233
            String path = elem.getAttribute("path");        //@path
×
234

235
            //must be a path to allow source for
236
            if (path.isEmpty()) {
×
237
                LOG.warn("Error element 'xquery' requires an attribute 'path'");
×
238
                return;
×
239
            }
240
            path = path.replaceAll("\\$\\{WEBAPP_HOME\\}",
×
241
                    SingleInstanceConfiguration.getWebappHome().orElse(Paths.get(".")).toAbsolutePath().toString().replace('\\', '/'));
×
242

243
            //store the path
244
            allowSourceList[i] = path;
×
245
        }
246
    }
×
247

248
    /**
249
     * loads &lt;maps&gt; settings from the descriptor.xml file
250
     *
251
     * @param    maps    The &lt;maps&gt; DOM Element from the descriptor.xml file
252
     */
253
    private void configureMaps(Element maps) {
254
        //TODO: add pattern support for mappings, as an alternative to path - deliriumsky
255

256
        //Get the map element(s)
257
        final NodeList nlMap = maps.getElementsByTagName("map");
×
258

259
        //Setup the hashmap to hold the map elements
260
        mapList = new String[nlMap.getLength()][2];
×
261

262
        Element elem = null; //temporary holds map elements
×
263

264
        //Iterate through the map elements
265
        for (int i = 0; i < nlMap.getLength(); i++) {
×
266
            elem = (Element) nlMap.item(i);                    //<map>
×
267
            String path = elem.getAttribute("path");        //@path
×
268
            //String pattern = elem.getAttribute("pattern");//@pattern
269
            String view = elem.getAttribute("view");        //@view
×
270

271
            //must be a path or a pattern to map from
272
            if (path.isEmpty() /*&& pattern == null*/) {
×
273
                LOG.warn("Error element 'map' requires an attribute 'path' or an attribute 'pattern'");
×
274
                return;
×
275
            }
276
            path = path.replaceAll("\\$\\{WEBAPP_HOME\\}",
×
277
                    SingleInstanceConfiguration.getWebappHome().orElse(Paths.get(".")).toAbsolutePath().toString().replace('\\', '/'));
×
278

279
            //must be a view to map to
280
            if (view.isEmpty()) {
×
281
                LOG.warn("Error element 'map' requires an attribute 'view'");
×
282
                return;
×
283
            }
284
            view = view.replaceAll("\\$\\{WEBAPP_HOME\\}",
×
285
                    SingleInstanceConfiguration.getWebappHome().orElse(Paths.get(".")).toAbsolutePath().toString().replace('\\', '/'));
×
286

287
            //store what to map from
288
           /* if(path != null)
289
            {*/
290
            //store the path
291
            mapList[i][0] = path;
×
292
            /*}
293
            else
294
            {
295
                    //store the pattern
296
                    mapList[i][0] = pattern;
297
            }*/
298

299
            //store what to map to
300
            mapList[i][1] = view;
×
301
        }
302
    }
×
303

304
    /**
305
     * Determines whether it is permissible to show the source of an XQuery.
306
     * Takes a path such as that from RESTServer.doGet() as an argument,
307
     * if it finds a matching allowsourcexquery path in the descriptor then it returns true else it returns false
308
     *
309
     * @param path The path of the XQuery (e.g. /db/MyCollection/query.xql)
310
     * @return The boolean value true or false indicating whether it is permissible to show the source
311
     */
312
    public boolean allowSource(String path) {
313
        if (allowSourceList != null) {
×
314
            //Iterate through the xqueries that source viewing is allowed for
315
            for (String s : allowSourceList) {
×
316
                // DWES: this helps a lot. quickfix not the final solution
317
                path = path.replace('\\', '/');
×
318

319
                //does the path match the <allow-source><xquery path=""/></allow-source> path
320
                if ((s.equals(path)) || (path.contains(s))) {
×
321
                    //yes, return true
322
                    return (true);
×
323
                }
324
            }
325
        }
326
        return (false);
×
327
    }
328

329
    /**
330
     * Map's one XQuery or Collection path to another
331
     * Takes a path such as that from RESTServer.doGet() as an argument,
332
     * if it finds a matching map path then it returns the map view else it returns the passed in path
333
     *
334
     * @param path The path of the XQuery or Collection (e.g. /db/MyCollection/query.xql or /db/MyCollection) to map from
335
     * @return The path of the XQuery or Collection (e.g. /db/MyCollection/query.xql or /db/MyCollection) to map to
336
     */
337
    public String mapPath(String path) {
338
        if (mapList == null) //has a list of mappings been specified?
1!
339
        {
340
            return (path);
1✔
341
        }
342

343
        //Iterate through the mappings
344
        for (String[] strings : mapList) {
×
345
            //does the path or the path/ match the map path
346
            if (strings[0].equals(path) || (strings[0] + "/").equals(path)) {
×
347
                //return the view
348
                return (strings[1]);
×
349
            }
350
        }
351

352
        //no match return the original path
353
        return (path);
×
354
    }
355

356
    public boolean requestsFiltered() {
357
        return requestsFiltered;
1✔
358
    }
359

360
    /**
361
     * Determines whether it is permissible to Log Requests.
362
     *
363
     * Enabled by descriptor.xml &lt;xquery-app request-replay-log="true"&gt;
364
     *
365
     * @return The boolean value true or false indicating whether it is permissible to Log Requests
366
     */
367
    public boolean allowRequestLogging() {
368
        return bufWriteReplayLog != null;
1!
369
    }
370

371
    /**
372
     * Logs HTTP Request's in a log file suitable for replaying to eXist later
373
     * Takes a HttpServletRequest or a HttpServletRequestWrapper as an argument for logging.
374
     *
375
     * Enabled by descriptor.xml &lt;xquery-app request-replay-log="true"&gt;
376
     *
377
     * @param request The HttpServletRequest to log.
378
     *                For Simple HTTP POST Requests - EXistServlet/XQueryServlet - POST parameters (e.g. form data) will only be logged if a HttpServletRequestWrapper is used instead of HttpServletRequest! POST Uploaded files are not yet supported!
379
     *                For XML-RPC Requests - RpcServlet - HttpServletRequestWrapper must be used, otherwise the content of the Request will be lost!
380
     *                For Cocoon Requests  -
381
     */
382
    public synchronized void doLogRequestInReplayLog(HttpServletRequest request) {
383
        //Only log if set by the user in descriptor.xml <xquery-app request-replay-log="true">
384
        if (bufWriteReplayLog == null) {
1!
385
            return;
1✔
386
        }
387

388
        //Log the Request
389
        try {
390
            //Store the date and time
391
            bufWriteReplayLog.write("Date: ");
×
392
            final SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
×
393
            bufWriteReplayLog.write(formatter.format(new Date()));
×
394

395
            bufWriteReplayLog.write(SYSTEM_LINE_SEPARATOR);
×
396

397
            //Store the request string excluding the first line
398
            final String requestAsString = request.toString();
×
399
            bufWriteReplayLog.write(requestAsString.substring(requestAsString.indexOf(SYSTEM_LINE_SEPARATOR) + 1));
×
400

401
            //End of record indicator
402
            bufWriteReplayLog.write(SYSTEM_LINE_SEPARATOR);
×
403

404
            //flush the buffer to file
405
            bufWriteReplayLog.flush();
×
406
        } catch (final IOException ioe) {
×
407
            LOG.warn("Could not write request replay log: {}", ioe.getMessage(), ioe);
×
408
            return;
×
409
        }
410
    }
×
411

412
    /**
413
     * Thows a CloneNotSupportedException as this class uses a Singleton design pattern
414
     *
415
     * @return Will never return anything!
416
     */
417
    public Object clone() throws CloneNotSupportedException {
418
        //Class is a Singleton, dont allow cloning
419
        throw new CloneNotSupportedException();
×
420
    }
421

422
    /**
423
     * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
424
     */
425
    @Override
426
    public void error(SAXParseException exception) throws SAXException {
427
        LOG.error("Error occurred while reading descriptor file [line: {}]:{}", exception.getLineNumber(), exception.getMessage(), exception);
×
428
    }
×
429

430
    /**
431
     * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
432
     */
433
    @Override
434
    public void fatalError(SAXParseException exception) throws SAXException {
435
        LOG.error("Error occurred while reading descriptor file [line: {}]:{}", exception.getLineNumber(), exception.getMessage(), exception);
×
436
    }
×
437

438
    /**
439
     * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
440
     */
441
    @Override
442
    public void warning(SAXParseException exception) throws SAXException {
443
        LOG.error("error occurred while reading descriptor file [line: {}]:{}", exception.getLineNumber(), exception.getMessage(), exception);
×
444
    }
×
445

446
}
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