Skip to content

Commit 8aeb813

Browse files
Merge branch 'main' into xmlns-parsing
2 parents 8665f67 + 807b3e1 commit 8aeb813

File tree

12 files changed

+415
-29
lines changed

12 files changed

+415
-29
lines changed

basex-core/src/main/java/org/basex/query/QueryParser.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ private boolean defaultNamespaceDecl(final boolean fixed) throws QueryException
539539
final boolean elem = wsConsumeWs(ELEMENT);
540540
if(!elem && !wsConsumeWs(FUNCTION)) return false;
541541
wsCheck(NAMESPACE);
542-
final byte[] uri = uriLiteral();
542+
final byte[] uri = elem ? uriLiteral(ANY_URI) : uriLiteral();
543543
if(eq(XML_URI, uri)) throw error(BINDXMLURI_X_X, uri, XML);
544544
if(eq(XMLNS_URI, uri)) throw error(BINDXMLURI_X_X, uri, XMLNS);
545545

@@ -697,7 +697,6 @@ private void schemaImport() throws QueryException {
697697
}
698698
final byte[] uri = uriLiteral();
699699
if(prefix != null && uri.length == 0) throw error(NSEMPTY);
700-
if(!Uri.get(uri).isValid()) throw error(INVURI_X, uri);
701700
addLocations(new TokenList());
702701
throw error(IMPLSCHEMA);
703702
}
@@ -715,7 +714,6 @@ private void moduleImport() throws QueryException {
715714

716715
final byte[] uri = uriLiteral();
717716
if(uri.length == 0) throw error(NSMODURI);
718-
if(!Uri.get(uri).isValid()) throw error(INVURI_X, uri);
719717
if(sc.imports.contains(token(uri))) throw error(DUPLMODULE_X, uri);
720718
sc.imports.add(uri);
721719

@@ -754,8 +752,7 @@ private boolean addLocations(final TokenList list) throws QueryException {
754752
if(add) {
755753
do {
756754
final byte[] uri = uriLiteral();
757-
if(!Uri.get(uri).isValid() || IO.get(string(uri)) instanceof IOContent)
758-
throw error(INVURI_X, uri);
755+
if(IO.get(string(uri)) instanceof IOContent) throw error(INVURI_X, uri);
759756
list.add(uri);
760757
} while(wsConsume(","));
761758
}
@@ -2087,7 +2084,7 @@ private void validate() throws QueryException {
20872084
final int p = pos;
20882085
if(!wsConsumeWs(VALIDATE)) return;
20892086

2090-
if(consume(TYPE)) eQName(sc.elemNS, QNAME_X);
2087+
if(consume(TYPE)) eQName(eq(sc.elemNS, ANY_URI) ? XS_URI : sc.elemNS, QNAME_X);
20912088
consume(STRICT);
20922089
consume(LAX);
20932090
skipWs();
@@ -2459,12 +2456,14 @@ private ExprInfo simpleNodeTest(final Kind kind, final boolean all) throws Query
24592456
} else {
24602457
NameTest.Scope scope = NameTest.Scope.FULL;
24612458
if(!name.hasPrefix()) {
2459+
pos = p;
24622460
if(consume(":*")) {
24632461
// name test: prefix:*
24642462
name = new QNm(concat(name.string(), cpToken(':')));
24652463
scope = NameTest.Scope.URI;
24662464
} else if(!eqName) {
2467-
scope = NameTest.Scope.FLEXIBLE;
2465+
scope = kind == Kind.ELEMENT && eq(sc.elemNS, ANY_URI) ? NameTest.Scope.LOCAL
2466+
: NameTest.Scope.FLEXIBLE;
24682467
}
24692468
}
24702469
// name test: prefix:name, name, Q{uri}name
@@ -2875,11 +2874,15 @@ private byte[] stringLiteral() throws QueryException {
28752874

28762875
/**
28772876
* Parses the "URILiteral" rule.
2877+
* @param special tokens that are allowed though not valid URI literals
28782878
* @return query expression
28792879
* @throws QueryException query exception
28802880
*/
2881-
private byte[] uriLiteral() throws QueryException {
2882-
return normalize(stringLiteral());
2881+
private byte[] uriLiteral(final byte[]... special) throws QueryException {
2882+
final byte[] uri = normalize(stringLiteral());
2883+
for(final byte[] sp : special) if(eq(uri, sp)) return uri;
2884+
if(!Uri.get(uri).isValid()) throw error(INVURI_X, uri);
2885+
return uri;
28832886
}
28842887

28852888
/**
@@ -2895,6 +2898,7 @@ private byte[] bracedURILiteral() throws QueryException {
28952898
entity(token);
28962899
}
28972900
final byte[] ns = normalize(token.toArray());
2901+
if(!Uri.get(ns).isValid()) throw error(INVURI_X, ns);
28982902
if(eq(ns, XMLNS_URI)) {
28992903
pos = p;
29002904
throw error(ILLEGALEQNAME_X, ns);
@@ -3388,7 +3392,7 @@ private Expr compConstructor() throws QueryException {
33883392
* @throws QueryException query exception
33893393
*/
33903394
private Expr compElement() throws QueryException {
3391-
final Expr name = compName(NOELEMNAME, true, sc.elemNS);
3395+
final Expr name = compName(NOELEMNAME, true, eq(sc.elemNS, ANY_URI) ? null : sc.elemNS);
33923396
if(name == null) return null;
33933397
skipWs();
33943398
return current('{') ? new CElem(info(), true, name, new Atts(), enclosedExpr()) : null;
@@ -3493,7 +3497,7 @@ private SeqType castTarget() throws QueryException {
34933497
if(wsConsume("(")) {
34943498
type = choiceItemType().type;
34953499
} else {
3496-
final QNm name = eQName(sc.elemNS, TYPEINVALID);
3500+
final QNm name = eQName(eq(sc.elemNS, ANY_URI) ? XS_URI : sc.elemNS, TYPEINVALID);
34973501
if(!name.hasURI() && eq(name.local(), token(ENUM))) {
34983502
if(!wsConsume("(")) throw error(WHICHCAST_X, BasicType.similar(name));
34993503
type = enumerationType();
@@ -3582,8 +3586,8 @@ private SeqType itemType() throws QueryException {
35823586
}
35833587
}
35843588
} else {
3585-
// attach default element namespace
3586-
if(!name.hasURI()) name.uri(sc.elemNS);
3589+
// attach default element namespace, or schema namespace if default element namespace is ##any
3590+
if(!name.hasURI()) name.uri(eq(sc.elemNS, ANY_URI) ? XS_URI : sc.elemNS);
35873591
// basic type
35883592
type = BasicType.get(name, false);
35893593
// declared type
@@ -4831,7 +4835,7 @@ private void resolveQNm(final QNm name, final byte[] elemNS, final InputInfo inf
48314835
if(name.hasPrefix()) {
48324836
name.uri(qc.ns.resolve(name.prefix(), sc));
48334837
if(!name.hasURI()) throw error(NOURI_X, info, name.prefix());
4834-
} else if(elemNS != null) {
4838+
} else if(elemNS != null && !Token.eq(elemNS, QueryText.ANY_URI)) {
48354839
name.uri(elemNS);
48364840
}
48374841
}

basex-core/src/main/java/org/basex/query/QueryText.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ public interface QueryText {
340340
/** BaseX URI. */ byte[] XQUERY_URI = token(BXMODULES_URL + "xquery");
341341
/** BaseX URI. */ byte[] XSLT_URI = token(BXMODULES_URL + "xslt");
342342

343+
/** "Any" URI. */ byte[] ANY_URI = token("##any");
344+
343345
// QUERY PLAN ===================================================================================
344346

345347
/** Query Info. */ String OP = "op";

basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreate.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.Map.*;
1111
import java.util.zip.*;
1212

13-
import org.basex.io.out.*;
1413
import org.basex.query.*;
1514
import org.basex.query.expr.*;
1615
import org.basex.query.iter.*;
@@ -29,9 +28,12 @@
2928
public class ArchiveCreate extends ArchiveFn {
3029
@Override
3130
public Item item(final QueryContext qc, final InputInfo ii) throws QueryException {
32-
final ArrayOutput ao = new ArrayOutput();
33-
create(ao, qc);
34-
return B64.get(ao.finish());
31+
try(SpillOutput so = new SpillOutput(qc)) {
32+
create(so, qc);
33+
return so.finish(ARCHIVE_ERROR_X);
34+
} catch(final IOException ex) {
35+
throw ARCHIVE_ERROR_X.get(info, ex);
36+
}
3537
}
3638

3739
/**
@@ -40,7 +42,7 @@ public Item item(final QueryContext qc, final InputInfo ii) throws QueryExceptio
4042
* @param qc query context
4143
* @throws QueryException query exception
4244
*/
43-
public final void create(final OutputStream os, final QueryContext qc) throws QueryException {
45+
public void create(final OutputStream os, final QueryContext qc) throws QueryException {
4446
final Map<String, Entry<Item, Item>> files = toFiles(arg(0), arg(1), qc);
4547
final CreateOptions options = toOptions(arg(2), new CreateOptions(), qc);
4648
create(files, options, os, qc);

basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreateFrom.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@
77
import java.util.AbstractMap.*;
88

99
import org.basex.io.*;
10-
import org.basex.io.out.*;
1110
import org.basex.query.*;
1211
import org.basex.query.value.*;
1312
import org.basex.query.value.item.*;
1413
import org.basex.query.value.seq.*;
15-
import org.basex.util.*;
1614
import org.basex.util.list.*;
1715

1816
/**
@@ -23,7 +21,7 @@
2321
*/
2422
public final class ArchiveCreateFrom extends ArchiveCreate {
2523
@Override
26-
public B64 item(final QueryContext qc, final InputInfo ii) throws QueryException {
24+
public void create(final OutputStream os, final QueryContext qc) throws QueryException {
2725
final IOFile root = new IOFile(toPath(arg(0), qc));
2826
final CreateFromOptions options = toOptions(arg(1), new CreateFromOptions(), qc);
2927
Value entries = arg(2).value(qc);
@@ -47,20 +45,18 @@ public B64 item(final QueryContext qc, final InputInfo ii) throws QueryException
4745
final int level = level(options);
4846
final String format = options.get(CreateOptions.FORMAT).toLowerCase(Locale.ENGLISH);
4947
final String dir = rootDir && root.parent() != null ? root.name() + '/' : "";
50-
final ArrayOutput ao = new ArrayOutput();
51-
try(ArchiveOut out = ArchiveOut.get(format, info, ao)) {
48+
try(ArchiveOut out = ArchiveOut.get(format, info, os)) {
5249
out.level(level);
5350
try {
5451
for(final Item item : entries) {
5552
final IOFile file = new IOFile(root, toString(item, qc));
5653
if(!file.exists()) throw FILE_NOT_FOUND_X.get(info, file);
5754
if(file.isDir()) throw FILE_IS_DIR_X.get(info, file);
58-
add(new SimpleEntry<>(item, B64.get(file.read())), out, level, dir, qc);
55+
add(new SimpleEntry<>(item, new B64Lazy(file, FILE_NOT_FOUND_X)), out, level, dir, qc);
5956
}
6057
} catch(final IOException ex) {
6158
throw ARCHIVE_ERROR_X.get(info, ex);
6259
}
6360
}
64-
return B64.get(ao.finish());
6561
}
6662
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package org.basex.query.func.archive;
2+
3+
import java.io.*;
4+
import java.util.*;
5+
6+
import org.basex.io.*;
7+
import org.basex.query.QueryContext;
8+
import org.basex.query.QueryError;
9+
import org.basex.query.value.item.*;
10+
import org.basex.util.*;
11+
12+
/**
13+
* Spill output stream.
14+
*
15+
* This class provides an output stream that buffers data in memory, then spills transparently to a
16+
* temporary file if the data exceeds the maximum array size or a given threshold.
17+
* The result can be retrieved as a binary item via the {@link #finish} method, which returns a lazy
18+
* reference to the temporary file if data was spilled, or an in-memory binary item otherwise. The
19+
* temporary file is registered with the query context's resources for automatic deletion when the
20+
* query finishes.
21+
*
22+
* This class is used by {@link ArchiveCreateFrom} and {@link ArchiveCreate}.
23+
*
24+
* @author BaseX Team, BSD License
25+
* @author Vincent Lizzi
26+
*/
27+
final class SpillOutput extends OutputStream {
28+
/** Query context for registering the temporary file on spill. */
29+
private final QueryContext qc;
30+
/** Threshold in bytes before spilling to disk. */
31+
private final int threshold;
32+
33+
/** In-memory buffer. */
34+
private byte[] buffer = new byte[Array.INITIAL_CAPACITY];
35+
/** Number of bytes written to the in-memory buffer. */
36+
private int bufSize;
37+
/** Disk output stream ({@code null} before spilling). */
38+
private FileOutputStream tmpOutput;
39+
/** Temporary file ({@code null} before spilling). */
40+
private IOFile tmpFile;
41+
42+
/**
43+
* Constructor.
44+
* @param qc query context
45+
*/
46+
SpillOutput(final QueryContext qc) {
47+
this(qc, Array.MAX_SIZE);
48+
}
49+
50+
/**
51+
* Constructor with an explicit spill threshold (used for testing).
52+
* @param qc query context
53+
* @param threshold spill threshold in bytes
54+
*/
55+
SpillOutput(final QueryContext qc, final int threshold) {
56+
this.qc = qc;
57+
this.threshold = threshold;
58+
}
59+
60+
@Override
61+
public void write(final int b) throws IOException {
62+
if(tmpOutput == null && bufSize == threshold) spill();
63+
if(tmpOutput != null) {
64+
tmpOutput.write(b);
65+
} else {
66+
if(bufSize == buffer.length) {
67+
buffer = Arrays.copyOf(buffer, Array.newCapacity(bufSize));
68+
}
69+
buffer[bufSize++] = (byte) b;
70+
}
71+
}
72+
73+
@Override
74+
public void write(final byte[] b, final int off, final int len) throws IOException {
75+
if(tmpOutput == null && (long) bufSize + len > threshold) spill();
76+
if(tmpOutput != null) {
77+
tmpOutput.write(b, off, len);
78+
} else {
79+
final int newSize = bufSize + len;
80+
if(newSize > buffer.length) {
81+
buffer = Arrays.copyOf(buffer, Math.max(Array.newCapacity(buffer.length), newSize));
82+
}
83+
System.arraycopy(b, off, buffer, bufSize, len);
84+
bufSize = newSize;
85+
}
86+
}
87+
88+
/**
89+
* Returns the result as a binary item: a lazy reference to the temporary file
90+
* if data was spilled, or an in-memory binary item otherwise.
91+
* @param error error to raise if the temporary file cannot be read
92+
* @return binary item
93+
*/
94+
B64 finish(final QueryError error) {
95+
if(tmpFile != null) return new B64Lazy(tmpFile, error);
96+
return B64.get(bufSize == 0 ? Token.EMPTY : bufSize == buffer.length ? buffer :
97+
Arrays.copyOf(buffer, bufSize));
98+
}
99+
100+
/**
101+
* Closes the disk output stream if one was opened. {@code tmpFile} is intentionally
102+
* not nulled here because {@link #finish} may be called after {@code close} and still
103+
* needs it to determine whether data was spilled.
104+
*/
105+
@Override
106+
public void close() throws IOException {
107+
if(tmpOutput != null) {
108+
tmpOutput.close();
109+
tmpOutput = null;
110+
}
111+
}
112+
113+
/**
114+
* Spills the in-memory buffer to a temporary file, registers it for deletion
115+
* when the query context closes, and switches subsequent writes to disk.
116+
* @throws IOException I/O exception
117+
*/
118+
private void spill() throws IOException {
119+
tmpFile = new IOFile(File.createTempFile(Prop.NAME + '-', IO.TMPSUFFIX));
120+
qc.resources.index(TempFiles.class).add(tmpFile);
121+
tmpOutput = tmpFile.outputStream();
122+
tmpOutput.write(buffer, 0, bufSize);
123+
buffer = null;
124+
}
125+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.basex.query.func.archive;
2+
3+
import java.util.*;
4+
5+
import org.basex.io.*;
6+
import org.basex.query.*;
7+
8+
/**
9+
* Temporary files created during query evaluation.
10+
*
11+
* Temporary files that are created during query evaluation and registered in this class will be
12+
* deleted when the query context is closed, ensuring that no temporary files are left behind after
13+
* the query execution.
14+
*
15+
* @author BaseX Team, BSD License
16+
* @author Vincent Lizzi
17+
*/
18+
public final class TempFiles implements QueryResource {
19+
/** List of temporary files. */
20+
private final List<IOFile> files = new ArrayList<>();
21+
22+
/**
23+
* Adds a temporary file to be deleted on close.
24+
* @param file temporary file
25+
*/
26+
synchronized void add(final IOFile file) {
27+
files.add(file);
28+
}
29+
30+
@Override
31+
public synchronized void close() {
32+
for(final IOFile file : files) file.delete();
33+
files.clear();
34+
}
35+
}

basex-core/src/main/java/org/basex/query/func/file/FileWriteBinary.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ final void write(final boolean append, final QueryContext qc) throws QueryExcept
4646
} else {
4747
// write full file
4848
try(BufferOutput out = BufferOutput.get(new FileOutputStream(path.toFile(), append))) {
49-
if(arg(1).getClass() == ArchiveCreate.class) {
50-
// optimization: stream archive to disk (no support for ArchiveCreateFrom)
51-
((ArchiveCreate) arg(1)).create(out, qc);
49+
if(arg(1) instanceof final ArchiveCreate ac) {
50+
// optimization: stream archive to disk
51+
ac.create(out, qc);
5252
} else {
5353
IO.write(toBin(arg(1), qc).input(info), out);
5454
}

0 commit comments

Comments
 (0)