• 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

56.68
/exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBStore.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.xquery.functions.xmldb;
50

51
import java.io.IOException;
52
import java.io.InputStream;
53
import java.io.StringWriter;
54
import java.net.MalformedURLException;
55
import java.net.URI;
56
import java.net.URISyntaxException;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.nio.file.Paths;
60
import java.nio.file.StandardCopyOption;
61
import java.util.Properties;
62

63
import org.apache.logging.log4j.LogManager;
64
import org.apache.logging.log4j.Logger;
65

66
import org.exist.storage.serializers.EXistOutputKeys;
67
import org.exist.util.FileUtils;
68
import org.exist.util.MimeTable;
69
import org.exist.util.MimeType;
70
import org.exist.util.io.TemporaryFileManager;
71
import org.exist.util.serializer.SAXSerializer;
72
import org.exist.xmldb.EXistResource;
73
import org.exist.xquery.Expression;
74
import org.exist.xquery.FunctionSignature;
75
import org.exist.xquery.XPathException;
76
import org.exist.xquery.XQueryContext;
77
import org.exist.xquery.value.AnyURIValue;
78
import org.exist.xquery.value.BinaryValue;
79
import org.exist.xquery.value.FunctionReturnSequenceType;
80
import org.exist.xquery.value.FunctionParameterSequenceType;
81
import org.exist.xquery.value.Item;
82
import org.exist.xquery.value.JavaObjectValue;
83
import org.exist.xquery.value.Sequence;
84
import org.exist.xquery.value.StringValue;
85
import org.exist.xquery.value.Type;
86
import org.xml.sax.ContentHandler;
87
import org.xml.sax.SAXException;
88
import org.xmldb.api.base.Collection;
89
import org.xmldb.api.base.Resource;
90
import org.xmldb.api.base.XMLDBException;
91
import org.xmldb.api.modules.BinaryResource;
92
import org.xmldb.api.modules.XMLResource;
93

94
import static org.exist.util.ByteOrderMark.stripXmlBom;
95
import static org.exist.xquery.FunctionDSL.*;
96
import static org.exist.xquery.XPathException.execAndAddErrorIfMissing;
97
import static org.exist.xquery.functions.xmldb.XMLDBModule.functionSignature;
98
import static org.exist.xquery.functions.xmldb.XMLDBModule.functionSignatures;
99

100
/**
101
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
102
 * @author wolf
103
 */
104
public class XMLDBStore extends XMLDBAbstractCollectionManipulator {
105

106
    private static final Logger LOGGER = LogManager.getLogger(XMLDBStore.class);
1✔
107

108
    private static final FunctionParameterSequenceType FS_PARAM_COLLECTION_URI = param("collection-uri", Type.STRING, "The collection URI");
1✔
109
    private static final FunctionParameterSequenceType FS_PARAM_RESOURCE_NAME = optParam("resource-name", Type.STRING, "The resource name");
1✔
110
    private static final FunctionParameterSequenceType FS_PARAM_CONTENTS = param("contents", Type.ITEM, "The contents");
1✔
111
    private static final FunctionParameterSequenceType FS_PARAM_MIME_TYPE = param("mime-type", Type.STRING,"The mime type");
1✔
112

113
    private static final FunctionReturnSequenceType FS_RETURN_PATH = returnsOpt(Type.STRING, "the path to new resource if sucessfully stored, otherwise the emtpty sequence");
1✔
114

115
    private static final Properties SERIALIZATION_PROPERTIES = new Properties();
1✔
116
    static {
117
        SERIALIZATION_PROPERTIES.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, "no");
1✔
118
    }
119

120
    private static final String FS_STORE_NAME = "store";
121
    static final FunctionSignature[] FS_STORE = functionSignatures(
1✔
122
            FS_STORE_NAME,
1✔
123
            "Stores a new resource into the database. The resource is stored  "
1✔
124
                    + "in the collection $collection-uri with the name $resource-name. "
125
                    + XMLDBModule.COLLECTION_URI
126
                    + " The contents $contents, is either a node, an xs:string, a Java file object or an xs:anyURI. "
127
                    + "A node will be serialized to SAX. It becomes the root node of the new "
128
                    + "document. If $contents is of type xs:anyURI, the resource is loaded "
129
                    + "from that URI. "
130
                    + "Returns the path to the new document if successfully stored, "
131
                    + "otherwise an XPathException is thrown.",
132
            FS_RETURN_PATH,
1✔
133
            arities(
1✔
134
                    arity(FS_PARAM_COLLECTION_URI, FS_PARAM_RESOURCE_NAME, FS_PARAM_CONTENTS),
1✔
135
                    arity(FS_PARAM_COLLECTION_URI, FS_PARAM_RESOURCE_NAME, FS_PARAM_CONTENTS, FS_PARAM_MIME_TYPE)
1✔
136
            )
137
    );
138

139
    private static final String FS_STORE_BINARY_NAME = "store-as-binary";
140
    static final FunctionSignature FS_STORE_BINARY = functionSignature(
1✔
141
            FS_STORE_BINARY_NAME,
1✔
142
            "Stores a new resource into the database. The resource is stored  "
1✔
143
                    + "in the collection $collection-uri with the name $resource-name. "
144
                    + XMLDBModule.COLLECTION_URI
145
                    + " The contents $contents, is either a node, an xs:string, a Java file object or an xs:anyURI. "
146
                    + "A node will be serialized to SAX. It becomes the root node of the new "
147
                    + "document. If $contents is of type xs:anyURI, the resource is loaded "
148
                    + "from that URI. "
149
                    + "Returns the path to the new document if successfully stored, "
150
                    + "otherwise an XPathException is thrown.",
151
            FS_RETURN_PATH,
1✔
152
            FS_PARAM_COLLECTION_URI, FS_PARAM_RESOURCE_NAME, FS_PARAM_CONTENTS
1✔
153
    );
1✔
154

155
    public XMLDBStore(XQueryContext context, FunctionSignature signature) {
156
        super(context, signature);
1✔
157
    }
1✔
158

159
    @Override
160
    public Sequence evalWithCollection(Collection collection, Sequence[] args, Sequence contextSequence) throws XPathException {
161

162
        final Expression expression = this;
1✔
163
        String docName = args[1].isEmpty() ? null : args[1].getStringValue();
1!
164
        if (docName != null && docName.isEmpty()) {
1!
165
            docName = null;
×
166
        } else if (docName != null) {
1!
167
            final String localDocName = docName;
1✔
168
            docName = execAndAddErrorIfMissing(this, () -> new AnyURIValue(expression, localDocName).toXmldbURI().toString());
1✔
169
        }
170

171
        final Item item = args[2].itemAt(0);
1✔
172

173
        // determine the mime type
174
        final boolean storeAsBinary = isCalledAs(FS_STORE_BINARY_NAME);
1✔
175
        MimeType mimeType = null;
1✔
176
        if (getSignature().getArgumentCount() == 4) {
1✔
177
            final String strMimeType = args[3].getStringValue();
1✔
178
            mimeType = MimeTable.getInstance().getContentType(strMimeType);
1✔
179
        }
180

181
        if (mimeType == null && docName != null) {
1!
182
            mimeType = MimeTable.getInstance().getContentTypeFor(docName);
1✔
183
        }
184

185
        if (mimeType == null) {
1✔
186
            mimeType = (storeAsBinary || !Type.subTypeOf(item.getType(), Type.NODE)) ? MimeType.BINARY_TYPE : MimeType.XML_TYPE;
1!
187
        } else if (storeAsBinary) {
1!
188
            mimeType = new MimeType(mimeType.getName(), MimeType.BINARY);
×
189
        }
190

191
        String resourceId = null;
1✔
192
        try {
193
            if (Type.subTypeOf(item.getType(), Type.JAVA_OBJECT)) {
1!
194
                final Object obj = ((JavaObjectValue) item).getObject();
×
195
                if (obj instanceof java.io.File) {
×
196
                    resourceId = loadFromFile(collection, ((java.io.File)obj).toPath(), docName, mimeType);
×
197
                } else if(obj instanceof java.nio.file.Path) {
×
198
                    resourceId = loadFromFile(collection, (Path)obj, docName, mimeType);
×
199
                } else {
×
200
                    LOGGER.error("Passed java object should be either a java.nio.file.Path or java.io.File");
×
201
                    throw new XPathException(this, "Passed java object should be either a java.nio.file.Path or java.io.File");
×
202
                }
203

204
            } else if (Type.subTypeOf(item.getType(), Type.ANY_URI)) {
1✔
205
                try {
206
                    final URI uri = new URI(item.getStringValue());
1✔
207
                    resourceId = loadFromURI(collection, uri, docName, mimeType);
×
208
                } catch (final URISyntaxException e) {
×
209
                    LOGGER.error("Invalid URI: {}", item.getStringValue());
×
210
                    throw new XPathException(this, "Invalid URI: " + item.getStringValue(), e);
×
211
                }
212

213
            } else {
214
                try (Resource resource = getResource(mimeType, collection, docName)) {
1✔
215
                    if (Type.subTypeOf(item.getType(), Type.STRING)) {
1✔
216
                        resource.setContent(stripXmlBom(item.getStringValue()));
1✔
217
                    } else if (item.getType() == Type.BASE64_BINARY) {
1✔
218
                        resource.setContent(((BinaryValue) item).toJavaObject());
1✔
219
                    } else if (Type.subTypeOf(item.getType(), Type.NODE)) {
1!
220
                        if (mimeType.isXMLType()) {
1!
221
                            final ContentHandler handler = ((XMLResource) resource).setContentAsSAX();
1✔
222
                            handler.startDocument();
1✔
223
                            item.toSAX(context.getBroker(), handler, SERIALIZATION_PROPERTIES);
1✔
224
                            handler.endDocument();
1✔
225
                        } else {
1✔
226
                            try (final StringWriter writer = new StringWriter()) {
×
227
                                final SAXSerializer serializer = new SAXSerializer();
×
228
                                serializer.setOutput(writer, null);
×
229
                                item.toSAX(context.getBroker(), serializer, SERIALIZATION_PROPERTIES);
×
230
                                resource.setContent(writer.toString());
×
231
                            } catch (final IOException e) {
×
232
                                LOGGER.error(e.getMessage(), e);
×
233
                            }
234
                        }
235
                    } else {
×
236
                        LOGGER.error("Data should be either a node or a string");
×
237
                        throw new XPathException(this, "Data should be either a node or a string");
×
238
                    }
239
                    ((EXistResource) resource).setMimeType(mimeType.getName());
1✔
240
                    collection.storeResource(resource);
1✔
241
                    resourceId = resource.getId();
1✔
242
                }
243
            }
244

245
        } catch (final XMLDBException e) {
1✔
246
            LOGGER.error(e.getMessage(), e);
1✔
247
            throw new XPathException(this,
1✔
248
                    "XMLDB reported an exception while storing document: " + e.getMessage(), e);
1✔
249

250
        } catch (final SAXException e) {
×
251
            LOGGER.error(e.getMessage());
×
252
            throw new XPathException(this, "SAX reported an exception while storing document", e);
×
253
        }
254

255
        if (resourceId == null) {
1!
256
            return Sequence.EMPTY_SEQUENCE;
×
257

258
        } else {
259
            try {
260
                //TODO : use dedicated function in XmldbURI
261
                return new StringValue(collection.getName() + "/" + resourceId);
1✔
262
            } catch (final XMLDBException e) {
×
263
                LOGGER.error(e.getMessage());
×
264
                throw new XPathException(this, "XMLDB reported an exception while retrieving the stored document", e);
×
265
            }
266
        }
267
    }
268

269
    private Resource getResource(MimeType mimeType, Collection collection, String docName) throws XMLDBException {
270
        if (mimeType.isXMLType()) {
1✔
271
            return collection.createResource(docName, XMLResource.class);
1✔
272
        } else {
273
            return collection.createResource(docName, BinaryResource.class);
1✔
274
        }
275
    }
276

277
    private String loadFromURI(final Collection collection, final URI uri, final String docName, final MimeType mimeType)
278
            throws XPathException {
279
        String resourceId;
280
        if ("file".equals(uri.getScheme())) {
1!
281
            final String path = uri.getPath();
×
282
            if (path == null) {
×
283
                throw new XPathException(this, "Cannot read from URI: " + uri.toASCIIString());
×
284
            }
285
            final Path file = Paths.get(path);
×
286
            if (!Files.isReadable(file)) {
×
287
                throw new XPathException(this, "Cannot read path: " + path);
×
288
            }
289
            resourceId = loadFromFile(collection, file, docName, mimeType);
×
290

291
        } else {
×
292
            final TemporaryFileManager temporaryFileManager = TemporaryFileManager.getInstance();
1✔
293
            Path temp = null;
1✔
294
            try {
295
                temp = temporaryFileManager.getTemporaryFile();
1✔
296
                try (final InputStream is = uri.toURL().openStream()) {
1✔
297
                    Files.copy(is, temp, StandardCopyOption.REPLACE_EXISTING);
×
298
                    resourceId = loadFromFile(collection, temp, docName, mimeType);
×
299
                } finally {
300
                    if(temp != null) {
1!
301
                        temporaryFileManager.returnTemporaryFile(temp);
1✔
302
                    }
303
                }
304
            } catch (final MalformedURLException e) {
×
305
                throw new XPathException(this, "Malformed URL: " + uri.toString(), e);
×
306

307
            } catch (final IOException e) {
1✔
308
                throw new XPathException(this, "IOException while reading from URL: " + uri.toString(), e);
1✔
309
            }
310
        }
311
        return resourceId;
×
312
    }
313

314
    private String loadFromFile(final Collection collection, final Path file, String docName, final MimeType mimeType)
315
            throws XPathException {
316
        if (!Files.isDirectory(file)) {
×
317
            if (docName == null) {
×
318
                docName = FileUtils.fileName(file);
×
319
            }
320

321
            try (final Resource resource = getResource(mimeType, collection, docName)){
×
322
                ((EXistResource) resource).setMimeType(mimeType.getName());
×
323
                resource.setContent(file);
×
324
                collection.storeResource(resource);
×
325
                return resource.getId();
×
326
            } catch (final XMLDBException e) {
×
327
                throw new XPathException(this, "Could not store file " + file.toAbsolutePath()
×
328
                        + ": " + e.getMessage(), e);
×
329
            }
330

331
        } else {
332
            throw new XPathException(this, file.toAbsolutePath() + " does not point to a file");
×
333
        }
334
    }
335
}
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