• 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

50.89
/extensions/webdav/src/main/java/org/exist/webdav/MiltonDocument.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.webdav;
50

51
import com.bradmcevoy.http.*;
52
import com.bradmcevoy.http.exceptions.*;
53
import com.bradmcevoy.http.webdav.DefaultUserAgentHelper;
54
import com.bradmcevoy.http.webdav.UserAgentHelper;
55
import org.apache.commons.io.IOUtils;
56
import org.apache.commons.io.output.CountingOutputStream;
57
import org.apache.commons.io.output.NullOutputStream;
58
import org.exist.EXistException;
59
import org.exist.security.PermissionDeniedException;
60
import org.exist.security.Subject;
61
import org.exist.storage.BrokerPool;
62
import org.exist.webdav.ExistResource.Mode;
63
import org.exist.webdav.exceptions.DocumentAlreadyLockedException;
64
import org.exist.webdav.exceptions.DocumentNotLockedException;
65
import org.exist.xmldb.XmldbURI;
66

67
import javax.xml.stream.XMLStreamException;
68
import javax.xml.stream.XMLStreamWriter;
69
import java.io.IOException;
70
import java.io.OutputStream;
71
import java.util.Date;
72
import java.util.Map;
73
import java.util.Properties;
74

75
/**
76
 * Class for representing a document as a Milton WebDAV document.
77
 * See <a href="http://milton.ettrema.com">Milton</a>.
78
 *
79
 * @author Dannes Wessels (dizzzz_at_exist-db.org)
80
 */
81
public class MiltonDocument extends MiltonResource
82
        implements GetableResource, PropFindableResource,
83
        DeletableResource, LockableResource, MoveableResource, CopyableResource {
84

85
    public static final String PROPFIND_METHOD_XML_SIZE = "org.exist.webdav.PROPFIND_METHOD_XML_SIZE";
86
    public static final String GET_METHOD_XML_SIZE = "org.exist.webdav.GET_METHOD_XML_SIZE";
87
    private static SIZE_METHOD propfindSizeMethod = null;
1✔
88
    private static SIZE_METHOD getSizeMethod = null;
1✔
89

90
    private static UserAgentHelper userAgentHelper = null;
1✔
91
    private final ExistDocument existDocument;
92

93
    // Only for PROPFIND the estimate size for an XML document must be shown
94
    private boolean isPropFind = false;
1✔
95

96
    /**
97
     * Constructor of representation of a Document in the Milton framework, without subject information.
98
     * To be called by the resource factory.
99
     *
100
     * @param configuration any configuration properties.
101
     * @param host       FQ host name including port number.
102
     * @param uri        Path on server indicating path of resource
103
     * @param brokerPool Handle to Exist database.
104
     */
105
    public MiltonDocument(final Properties configuration, String host, XmldbURI uri, BrokerPool brokerPool) {
106
        this(configuration, host, uri, brokerPool, null);
1✔
107
    }
1✔
108

109
    /**
110
     * Constructor of representation of a Document in the Milton framework, with subject information.
111
     * To be called by the resource factory.
112
     *
113
     * @param configuration any configuration properties.
114
     * @param host    FQ host name including port number.
115
     * @param uri     Path on server indicating path of resource.
116
     * @param subject An Exist operation is performed with  User. Can be NULL.
117
     * @param pool    Handle to Exist database.
118
     */
119
    public MiltonDocument(final Properties configuration, String host, XmldbURI uri, BrokerPool pool, Subject subject) {
120
        super(configuration);
1✔
121

122
        if (userAgentHelper == null) {
1✔
123
            userAgentHelper = new DefaultUserAgentHelper();
1✔
124
        }
125

126
        if (LOG.isTraceEnabled()) {
1!
127
            LOG.trace("DOCUMENT:{}", uri.toString());
×
128
        }
129

130
        resourceXmldbUri = uri;
1✔
131
        brokerPool = pool;
1✔
132
        this.host = host;
1✔
133

134
        existDocument = new ExistDocument(configuration, uri, brokerPool);
1✔
135

136
        // store simpler type
137
        existResource = existDocument;
1✔
138

139
        if (subject != null) {
1✔
140
            existDocument.setUser(subject);
1✔
141
            existDocument.initMetadata();
1✔
142
        }
143

144
        // PROPFIND method
145
        if (propfindSizeMethod == null) {
1✔
146
            LOG.info("Try to obtain {} from System Property", PROPFIND_METHOD_XML_SIZE);
1✔
147
            String systemProp = System.getProperty(PROPFIND_METHOD_XML_SIZE);
1✔
148
            propfindSizeMethod = getSizeMethod(systemProp);
1✔
149
        }
150

151
        if (propfindSizeMethod == null) {
1!
152
            LOG.info("Alternatively try to obtain {} from properties file", PROPFIND_METHOD_XML_SIZE);
×
153
            String fileProp = configuration.getProperty(PROPFIND_METHOD_XML_SIZE);
×
154
            propfindSizeMethod = getSizeMethod(fileProp);
×
155
        }
156

157
        if (propfindSizeMethod == null) {
1!
158
            LOG.info("Use default value {}", SIZE_METHOD.APPROXIMATE);
×
159
            propfindSizeMethod = SIZE_METHOD.APPROXIMATE;
×
160
        }
161

162
        // GET method
163
        if (getSizeMethod == null) {
1✔
164
            LOG.info("Try to obtain {} from System Property", GET_METHOD_XML_SIZE);
1✔
165
            String systemProp = System.getProperty(GET_METHOD_XML_SIZE);
1✔
166
            getSizeMethod = getSizeMethod(systemProp);
1✔
167
        }
168

169
        if (getSizeMethod == null) {
1✔
170
            LOG.info("Alternatively try to obtain {} from properties file", GET_METHOD_XML_SIZE);
1✔
171
            String fileProp = configuration.getProperty(GET_METHOD_XML_SIZE);
1✔
172
            getSizeMethod = getSizeMethod(fileProp);
1✔
173
        }
174

175
        if (getSizeMethod == null) {
1✔
176
            LOG.info("Use default value {}", SIZE_METHOD.NULL);
1✔
177
            getSizeMethod = SIZE_METHOD.NULL;
1✔
178
        }
179

180
    }
1✔
181

182
    /**
183
     * Determine what size methodology shall be applied.
184
     *
185
     * @param value Properties value
186
     * @return Corresponding SIZE_METHOD, or else NULL.
187
     */
188
    SIZE_METHOD getSizeMethod(String value) {
189
        if (value == null || value.strip().isEmpty()) {
1!
190
            return null;
1✔
191
        }
192

193
        try {
194
            final SIZE_METHOD sizeMethod = SIZE_METHOD.valueOf(value.toUpperCase());
1✔
195
            LOG.info("Found value {}", sizeMethod);
1✔
196
            return sizeMethod;
1✔
197
        } catch (IllegalArgumentException ex) {
×
198
            LOG.debug(ex.getMessage());
×
199
            return null;
×
200
        }
201
    }
202

203
    /**
204
     * Set to TRUE if getContentLength is used for PROPFIND.
205
     *
206
     * @param isPropFind Set to TRUE if request is PropFind request.
207
     */
208
    public void setIsPropFind(boolean isPropFind) {
209
        this.isPropFind = isPropFind;
1✔
210
    }
1✔
211

212
    @Override
213
    public void sendContent(OutputStream out, Range range, Map<String, String> params, String contentType)
214
            throws IOException, NotAuthorizedException {
215
        try {
216
            if (LOG.isDebugEnabled()) {
1!
217
                LOG.debug("Serializing from database");
×
218
            }
219
            existDocument.stream(out);
1✔
220

221
        } catch (PermissionDeniedException e) {
×
222
            if (LOG.isDebugEnabled()) {
×
223
                LOG.debug(e.getMessage());
×
224
            }
225
            throw new NotAuthorizedException(this);
×
226
        } finally {
227
            IOUtils.closeQuietly(out);
1✔
228
        }
229
    }
1✔
230

231
    /* ================
232
     * GettableResource
233
     * ================ */
234

235
    @Override
236
    public Long getMaxAgeSeconds(Auth auth) {
237
        return null;
1✔
238
    }
239

240
    @Override
241
    public String getContentType(String accepts) {
242
        return existDocument.getMimeType();
1✔
243
    }
244

245
    @Override
246
    public Long getContentLength() {
247

248
        /*
249
            ## Due to the way eXist-db stores XML, the exact size of an XML document when
250
            ## it is serialized (e.g., sent to a WebDAV client) may vary depending upon
251
            ## serialization parameters.
252
            ##
253
            ## For performance reasons, eXist by default only reports an approximate file size
254
            ## for XML documents. (eXist reports accurate sizes for binary documents,
255
            ## which aren't subject to serialization parameters.)
256
            ##
257
            ## The approximate size is a good indication of the size of document
258
            ## but some WebDAV clients, in particular the macOS Finder version, can
259
            ## not deal with this estimate, resulting in incomplete or overcomplete
260
            ## documents.
261
            ##
262
            ## To address these various possibilities, two system variables can be set
263
            ## to change the way the size is calculated.
264
            ##
265
            ## Supported values are APPROXIMATE, EXACT, NULL
266
            ##
267
            ## PROPFIND:
268
            ## Unfortunately both NULL and APPROXIMATE do not work for
269
            ## macOS Finder. The default behaviour for the Finder 'user-agent' is
270
            ## exact, for the others it is approximate.
271
            ##
272
            ## GET:
273
            ## The NULL value seems to be working well for macOS too.
274
            ##
275
            ## The system properties are:
276
            ## -Dorg.exist.webdav.PROPFIND_METHOD_XML_SIZE=..  (used for listing documents in collection)
277
            ## -Dorg.exist.webdav.GET_METHOD_XML_SIZE=...      (used during download of one document)
278
            ##
279
            ## Supported values are:
280
            ## NULL         - document sizes are NOT reported
281
            ## EXACT        - document sizes are reported using document pre-serialization [Slow]
282
            ## APPROXIMATE  - document sizes are reported as (pagesize * number of pages)
283
            ##
284
            ## Depending on the WebDAV client needs, one or both properties can be set.
285
            #
286
            # org.exist.webdav.PROPFIND_METHOD_XML_SIZE=APPROXIMATE
287
            # org.exist.webdav.GET_METHOD_XML_SIZE=NULL
288
        */
289

290
        Long size = null;
1✔
291

292
        // MacOsX has a bad reputation
293
        boolean isMacFinder = userAgentHelper.isMacFinder(HttpManager.request().getUserAgentHeader());
1✔
294

295
        if (existDocument.isXmlDocument()) {
1✔
296
            // XML document, exact size is not (directly) known)
297
            if (isMacFinder || SIZE_METHOD.EXACT == propfindSizeMethod) {
1!
298

299
                // Returns the exact size, default behaviour for Finder,
300
                // or when set by a system property
301

302
                if (LOG.isDebugEnabled()) {
1!
303
                    LOG.debug("Serializing XML to /dev/null to determine size ({}) MacFinder={}", resourceXmldbUri, isMacFinder);
×
304
                }
305

306
                // Stream document to '/dev/null' and count bytes
307
                try (final CountingOutputStream counter = new CountingOutputStream(NullOutputStream.NULL_OUTPUT_STREAM)) {
1✔
308
                    existDocument.stream(counter);
1✔
309
                    size = counter.getByteCount();
1✔
310
                } catch (Exception ex) {
×
311
                    LOG.error(ex);
×
312
                }
1✔
313

314
            } else if (SIZE_METHOD.NULL == propfindSizeMethod) {
×
315

316
                // Returns size unknown. This is not supported
317
                // by MacOsX finder
318

319
                size = null;
×
320

321
            } else {
322
                // Returns the estimated document size. This is the
323
                // default value, but not suitable for MacOsX Finder.
324
                size = existDocument.getContentLength();
×
325
            }
326
        } else {
327
            // Non XML document, actual size is known
328
            size = existDocument.getContentLength();
1✔
329
        }
330

331
        if (LOG.isDebugEnabled()) {
1!
332
            LOG.debug("Size={} ({})", size, resourceXmldbUri);
×
333
        }
334
        return size;
1✔
335

336
    }
337

338
    @Override
339
    public Date getCreateDate() {
340
        Date createDate = null;
1✔
341

342
        Long time = existDocument.getCreationTime();
1✔
343
        if (time != null) {
1!
344
            createDate = new Date(time);
1✔
345
        }
346

347
        return createDate;
1✔
348
    }
349

350
    /* ====================
351
     * PropFindableResource
352
     * ==================== */
353

354
    @Override
355
    public void delete() throws NotAuthorizedException, ConflictException, BadRequestException {
356
        existDocument.delete();
1✔
357
    }
1✔
358

359
    /* =================
360
     * DeletableResource
361
     * ================= */
362

363
    @Override
364
    public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
365
            throws NotAuthorizedException, PreConditionFailedException, LockedException {
366

367
        org.exist.dom.persistent.LockToken inputToken = convertToken(timeout, lockInfo);
×
368

369
        if (LOG.isDebugEnabled()) {
×
370
            LOG.debug("Lock: {}", resourceXmldbUri);
×
371
        }
372

373
        LockResult lr = null;
×
374
        try {
375
            org.exist.dom.persistent.LockToken existLT = existDocument.lock(inputToken);
×
376

377
            // Process result
378
            LockToken mltonLT = convertToken(existLT);
×
379
            lr = LockResult.success(mltonLT);
×
380

381
        } catch (PermissionDeniedException ex) {
×
382
            LOG.debug(ex.getMessage());
×
383
            throw new NotAuthorizedException(this);
×
384

385
        } catch (DocumentAlreadyLockedException ex) {
×
386
            // set result iso throw new LockedException(this);
387
            LOG.debug(ex.getMessage());
×
388
            lr = LockResult.failed(LockResult.FailureReason.ALREADY_LOCKED);
×
389

390
        } catch (EXistException ex) {
×
391
            LOG.debug(ex.getMessage());
×
392
            lr = LockResult.failed(LockResult.FailureReason.PRECONDITION_FAILED);
×
393

394
        }
×
395
        return lr;
×
396
    }
397

398
    
399
    /* ================
400
     * LockableResource
401
     * ================ */
402

403
    @Override
404
    public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {
405

406
        if (LOG.isDebugEnabled()) {
×
407
            LOG.debug("Refresh: {} token={}", resourceXmldbUri, token);
×
408
        }
409

410
        LockResult lr = null;
×
411
        try {
412
            org.exist.dom.persistent.LockToken existLT = existDocument.refreshLock(token);
×
413

414
            // Process result
415
            LockToken mltonLT = convertToken(existLT);
×
416
            lr = LockResult.success(mltonLT);
×
417

418
        } catch (PermissionDeniedException ex) {
×
419
            LOG.debug(ex.getMessage());
×
420
            throw new NotAuthorizedException(this);
×
421

422
        } catch (DocumentNotLockedException | EXistException ex) {
×
423
            LOG.debug(ex.getMessage());
×
424
            lr = LockResult.failed(LockResult.FailureReason.PRECONDITION_FAILED);
×
425

426
        } catch (DocumentAlreadyLockedException ex) {
×
427
            //throw new LockedException(this);
428
            LOG.debug(ex.getMessage());
×
429
            lr = LockResult.failed(LockResult.FailureReason.ALREADY_LOCKED);
×
430

431
        }
×
432
        return lr;
×
433
    }
434

435
    @Override
436
    public void unlock(String tokenId) throws NotAuthorizedException, PreConditionFailedException {
437

438
        if (LOG.isDebugEnabled()) {
×
439
            LOG.debug("Unlock: {}", resourceXmldbUri);
×
440
        }
441

442
        try {
443
            existDocument.unlock();
×
444
        } catch (PermissionDeniedException ex) {
×
445
            LOG.debug(ex.getMessage());
×
446
            throw new NotAuthorizedException(this);
×
447

448
        } catch (DocumentNotLockedException | EXistException ex) {
×
449
            LOG.debug(ex.getMessage());
×
450
            throw new PreConditionFailedException(this);
×
451

452
        }
×
453
    }
×
454

455
    @Override
456
    public LockToken getCurrentLock() {
457

458
        if (LOG.isDebugEnabled()) {
1!
459
            LOG.debug("getCurrentLock: {}", resourceXmldbUri);
×
460
        }
461

462
        org.exist.dom.persistent.LockToken existLT = existDocument.getCurrentLock();
1✔
463

464
        if (existLT == null) {
1!
465
            LOG.debug("No database lock token.");
1✔
466
            return null;
1✔
467
        }
468

469
        // Construct Lock Info
470
        LockToken miltonLT = convertToken(existLT);
×
471

472
        // Return values in Milton object
473
        return miltonLT;
×
474
    }
475

476
    @Override
477
    public void moveTo(CollectionResource rDest, String newName) throws ConflictException {
478

479
        if (LOG.isDebugEnabled()) {
1!
480
            LOG.debug("moveTo: {} newName={}", resourceXmldbUri, newName);
×
481
        }
482

483
        XmldbURI destCollection = ((MiltonCollection) rDest).getXmldbUri();
1✔
484
        try {
485
            existDocument.resourceCopyMove(destCollection, newName, Mode.MOVE);
1✔
486

487
        } catch (EXistException ex) {
×
488
            throw new ConflictException(this, "Move '" + getXmldbUri() + "' to '" + destCollection.append(newName) + "' failed: " + ex.getMessage());
×
489
        }
1✔
490
    }
1✔
491

492

493
    /* ================
494
     * MoveableResource
495
     * ================ */
496

497
    @Override
498
    public void copyTo(CollectionResource rDest, String newName) {
499

500
        if (LOG.isDebugEnabled()) {
1!
501
            LOG.debug("copyTo: {} newName={}", resourceXmldbUri, newName);
×
502
        }
503

504
        XmldbURI destCollection = ((MiltonCollection) rDest).getXmldbUri();
1✔
505
        try {
506
            existDocument.resourceCopyMove(destCollection, newName, Mode.COPY);
1✔
507

508
        } catch (EXistException ex) {
×
509
            // unable to throw new ConflictException(this);
510
            LOG.error(ex.getMessage());
×
511
        }
1✔
512
    }
1✔
513

514

515
    /* ================
516
     * CopyableResource
517
     * ================ */
518

519
    /**
520
     * Serialize document properties
521
     *
522
     * @param writer STAX writer
523
     * @throws XMLStreamException Thrown when writing data failed
524
     */
525
    public void writeXML(XMLStreamWriter writer) throws XMLStreamException {
526
        writer.writeStartElement("exist", "document", "http://exist.sourceforge.net/NS/exist");
×
527
        writer.writeAttribute("name", resourceXmldbUri.lastSegment().toString());
×
528
        writer.writeAttribute("created", getXmlDateTime(existDocument.getCreationTime()));
×
529
        writer.writeAttribute("last-modified", getXmlDateTime(existDocument.getLastModified()));
×
530
        writer.writeAttribute("owner", existDocument.getOwnerUser());
×
531
        writer.writeAttribute("group", existDocument.getOwnerGroup());
×
532
        writer.writeAttribute("permissions", "" + existDocument.getPermissions().toString());
×
533
        writer.writeAttribute("size", "" + existDocument.getContentLength());
×
534
        writer.writeEndElement();
×
535
    }
×
536

537

538
    /* ================
539
     * StAX serializer
540
     * ================ */
541

542
    private enum SIZE_METHOD {NULL, EXACT, APPROXIMATE}
1✔
543
}
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