Skip to content

Commit 3b5db42

Browse files
authored
Merge pull request #174 from evolvedbinary/7.x.x/hotfix/arrow-operator-memory-leak
[7.x.x] Fix a memory leak in the XPath 3.1 Arrow Operator implementation
2 parents 63a07be + f8ef9ee commit 3b5db42

4 files changed

Lines changed: 244 additions & 64 deletions

File tree

exist-core/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@
761761
<include>src/main/java/org/exist/xmlrpc/ArrayWrapperParser.java</include>
762762
<include>src/main/java/org/exist/xmlrpc/ArrayWrapperSerializer.java</include>
763763
<include>src/main/java/org/exist/xmlrpc/XmlRpcExtensionConstants.java</include>
764+
<include>src/test/java/org/exist/xquery/ArrowOperatorTest.java</include>
764765
<include>src/test/resources-filtered/org/exist/xquery/import-from-pkg-test.conf.xml</include>
765766
<include>src/test/java/org/exist/xquery/ImportFromPkgTest.java</include>
766767
<include>src/main/java/org/exist/xquery/JavaBinding.java</include>
@@ -1207,6 +1208,7 @@
12071208
<include>src/main/java/org/exist/xqj/Marshaller.java</include>
12081209
<include>src/test/java/org/exist/xqj/MarshallerTest.java</include>
12091210
<include>src/main/java/org/exist/xquery/AbstractInternalModule.java</include>
1211+
<include>src/main/java/org/exist/xquery/ArrowOperator.java</include>
12101212
<include>src/test/java/org/exist/xquery/CardinalityTest.java</include>
12111213
<include>src/test/java/org/exist/xquery/CleanupTest.java</include>
12121214
<include>src/test/java/org/exist/xquery/ConstructedNodesRecoveryTest.java</include>
@@ -2011,6 +2013,8 @@
20112013
<exclude>src/main/java/org/exist/xqj/Marshaller.java</exclude>
20122014
<exclude>src/test/java/org/exist/xqj/MarshallerTest.java</exclude>
20132015
<exclude>src/main/java/org/exist/xquery/AbstractInternalModule.java</exclude>
2016+
<exclude>src/main/java/org/exist/xquery/ArrowOperator.java</exclude>
2017+
<exclude>src/test/java/org/exist/xquery/ArrowOperatorTest.java</exclude>
20142018
<exclude>src/main/java/org/exist/xquery/Cardinality.java</exclude>
20152019
<exclude>src/test/java/org/exist/xquery/CardinalityTest.java</exclude>
20162020
<exclude>src/test/java/org/exist/xquery/CastExpressionTest.java</exclude>
Lines changed: 125 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,28 @@
11
/*
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+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; version 2.1.
11+
*
12+
* This library is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+
* Lesser General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public
18+
* License along with this library; if not, write to the Free Software
19+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20+
*
21+
* NOTE: Parts of this file contain code from 'The eXist-db Authors'.
22+
* The original license header is included below.
23+
*
24+
* =====================================================================
25+
*
226
* eXist-db Open Source Native XML Database
327
* Copyright (C) 2001 The eXist-db Authors
428
*
@@ -29,21 +53,23 @@
2953
import org.exist.xquery.value.Sequence;
3054
import org.exist.xquery.value.Type;
3155

56+
import javax.annotation.Nullable;
3257
import java.util.ArrayList;
3358
import java.util.List;
3459

3560
/**
3661
* Implements the XQuery 3.1 arrow operator.
3762
*
3863
* @author wolf
64+
* @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
3965
*/
4066
public class ArrowOperator extends AbstractExpression {
4167

42-
private QName qname = null;
68+
private @Nullable QName functionName = null;
4369
private Expression leftExpr;
44-
private FunctionCall fcall = null;
45-
private Expression funcSpec = null;
46-
private List<Expression> parameters;
70+
@Nullable FunctionCall functionCall = null;
71+
private @Nullable Expression functionSpecExpr = null;
72+
private List<Expression> functionParameters;
4773
private AnalyzeContextInfo cachedContextInfo;
4874

4975
public ArrowOperator(final XQueryContext context, final Expression leftExpr) throws
@@ -52,38 +78,53 @@ public ArrowOperator(final XQueryContext context, final Expression leftExpr) thr
5278
this.leftExpr = leftExpr;
5379
}
5480

55-
public void setArrowFunction(final String fname, final List<Expression> params) throws XPathException {
81+
/**
82+
* Set the XPath/XQuery function to be called by name.
83+
*
84+
* @param functionName the fully qualified name of the function to call.
85+
* @param functionParameters the parameters for the function.
86+
* @throws XPathException if the fully qualified name is invalid.
87+
*/
88+
public void setArrowFunction(final String functionName, final List<Expression> functionParameters) throws XPathException {
5689
try {
57-
this.qname = QName.parse(context, fname, context.getDefaultFunctionNamespace());
58-
this.parameters = params;
90+
this.functionName = QName.parse(context, functionName, context.getDefaultFunctionNamespace());
91+
this.functionParameters = functionParameters;
5992
// defer resolving the function to analyze to make sure all functions are known
6093
} catch (final IllegalQNameException e) {
61-
throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + fname);
94+
throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + functionName);
6295
}
6396
}
6497

65-
public void setArrowFunction(final PathExpr funcSpec, final List<Expression> params) {
66-
this.funcSpec = funcSpec.simplify();
67-
this.parameters = params;
98+
/**
99+
* Set the XPath/XQuery function to be called by XPath expression.
100+
*
101+
* @param functionSpecExpr the expr that specifies the function to call.
102+
* @param functionParameters the parameters for the function.
103+
*/
104+
public void setArrowFunction(final PathExpr functionSpecExpr, final List<Expression> functionParameters) {
105+
this.functionSpecExpr = functionSpecExpr.simplify();
106+
this.functionParameters = functionParameters;
68107
}
69108

70109
@Override
71110
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
72-
if(getContext().getXQueryVersion() < 31) {
73-
throw new XPathException(this,
74-
ErrorCodes.EXXQDY0003,
75-
"arrow operator is not available before XQuery 3.1");
111+
if (getContext().getXQueryVersion() < 31) {
112+
throw new XPathException(this, ErrorCodes.EXXQDY0003, "The Arrow Operator is not available before XQuery 3.1");
76113
}
77-
if (qname != null) {
78-
fcall = NamedFunctionReference.lookupFunction(this, context, qname, parameters.size() + 1);
114+
115+
if (functionName != null) {
116+
functionCall = NamedFunctionReference.lookupFunction(this, context, functionName, functionParameters.size() + 1);
79117
}
118+
80119
this.cachedContextInfo = contextInfo;
81120
leftExpr.analyze(contextInfo);
82-
if (fcall != null) {
83-
fcall.analyze(contextInfo);
121+
122+
if (functionCall != null) {
123+
functionCall.analyze(contextInfo);
84124
}
85-
if (funcSpec != null) {
86-
funcSpec.analyze(contextInfo);
125+
126+
if (functionSpecExpr != null) {
127+
functionSpecExpr.analyze(contextInfo);
87128
}
88129
}
89130

@@ -94,107 +135,128 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) throws XP
94135
}
95136
contextSequence = leftExpr.eval(contextSequence, null);
96137

97-
final FunctionReference fref;
98-
if (fcall != null) {
99-
fref = new FunctionReference(this, fcall);
100-
} else {
101-
final Sequence funcSeq = funcSpec.eval(contextSequence, contextItem);
102-
if (funcSeq.getCardinality() != Cardinality.EXACTLY_ONE)
103-
{throw new XPathException(this, ErrorCodes.XPTY0004,
104-
"Expected exactly one item for the function to be called, got " + funcSeq.getItemCount() +
105-
". Expression: " + ExpressionDumper.dump(funcSpec));}
106-
final Item item0 = funcSeq.itemAt(0);
107-
if (!Type.subTypeOf(item0.getType(), Type.FUNCTION)) {
108-
throw new XPathException(this, ErrorCodes.XPTY0004,
109-
"Type error: expected function, got " + Type.getTypeName(item0.getType()));
110-
}
111-
fref = (FunctionReference)item0;
112-
}
138+
@Nullable FunctionReference functionReference = null;
113139
try {
114-
final List<Expression> fparams = new ArrayList<>(parameters.size() + 1);
140+
if (functionCall != null) {
141+
// Prepare call by name
142+
functionReference = new FunctionReference(this, functionCall);
143+
144+
} else {
145+
// Prepare call by expression
146+
final Sequence funcSeq = functionSpecExpr.eval(contextSequence, contextItem);
147+
if (funcSeq.getCardinality() != Cardinality.EXACTLY_ONE) {
148+
throw new XPathException(this, ErrorCodes.XPTY0004, "Expected exactly one function item, got " + funcSeq.getItemCount() + ". Expression: " + ExpressionDumper.dump(functionSpecExpr));
149+
}
150+
151+
final Item item0 = funcSeq.itemAt(0);
152+
if (!Type.subTypeOf(item0.getType(), Type.FUNCTION)) {
153+
throw new XPathException(this, ErrorCodes.XPTY0004, "Type error: expected function, got " + Type.getTypeName(item0.getType()));
154+
}
155+
156+
functionReference = (FunctionReference) item0;
157+
}
158+
159+
// Make the call
160+
final List<Expression> fparams = new ArrayList<>(functionParameters.size() + 1);
115161
fparams.add(new ContextParam(context, contextSequence));
116-
fparams.addAll(parameters);
162+
fparams.addAll(functionParameters);
117163

118-
fref.setArguments(fparams);
164+
functionReference.setArguments(fparams);
119165
// need to create a new AnalyzeContextInfo to avoid memory leak
120166
// cachedContextInfo will stay in memory
121-
fref.analyze(new AnalyzeContextInfo(cachedContextInfo));
167+
functionReference.analyze(new AnalyzeContextInfo(cachedContextInfo));
122168
// Evaluate the function
123-
return fref.eval(null);
169+
return functionReference.eval(null);
170+
124171
} finally {
125-
fref.close();
172+
if (functionReference != null) {
173+
functionReference.close();
174+
}
126175
}
127176
}
128177

129178
@Override
130179
public int returnsType() {
131-
return fcall == null ? Type.ITEM : fcall.returnsType();
180+
return functionCall == null ? Type.ITEM : functionCall.returnsType();
132181
}
133182

134183
@Override
135184
public Cardinality getCardinality() {
136-
return fcall == null ? super.getCardinality() : fcall.getCardinality();
185+
return functionCall == null ? super.getCardinality() : functionCall.getCardinality();
137186
}
138187

139188
@Override
140189
public void dump(final ExpressionDumper dumper) {
141190
leftExpr.dump(dumper);
142191
dumper.display(" => ");
143-
if (fcall != null) {
144-
dumper.display(fcall.getFunction().getName()).display('(');
192+
193+
if (functionCall != null) {
194+
dumper.display(functionCall.getFunction().getName()).display('(');
145195
} else {
146-
funcSpec.dump(dumper);
196+
functionSpecExpr.dump(dumper);
147197
}
148-
for (int i = 0; i < parameters.size(); i++) {
198+
199+
for (int i = 0; i < functionParameters.size(); i++) {
149200
if (i > 0) {
150201
dumper.display(", ");
151-
parameters.get(i).dump(dumper);
202+
functionParameters.get(i).dump(dumper);
152203
}
153204
}
154205
dumper.display(')');
155206
}
156207

157208
@Override
158-
public void resetState(boolean postOptimization) {
209+
public void resetState(final boolean postOptimization) {
159210
super.resetState(postOptimization);
160211
leftExpr.resetState(postOptimization);
161-
if (fcall != null) {
162-
fcall.resetState(postOptimization);
212+
213+
if (functionCall != null) {
214+
functionCall.resetState(postOptimization);
163215
}
164-
if (funcSpec != null) {
165-
funcSpec.resetState(postOptimization);
216+
217+
if (functionSpecExpr != null) {
218+
functionSpecExpr.resetState(postOptimization);
166219
}
167-
for (Expression param: parameters) {
220+
221+
for (final Expression param : functionParameters) {
168222
param.resetState(postOptimization);
169223
}
170224
}
171225

172-
private class ContextParam extends Function.Placeholder {
226+
static class ContextParam extends Function.Placeholder {
227+
@Nullable
228+
Sequence sequence;
173229

174-
private Sequence sequence;
175-
176-
ContextParam(XQueryContext context, Sequence sequence) {
230+
ContextParam(final XQueryContext context, @Nullable final Sequence sequence) {
177231
super(context);
178232
this.sequence = sequence;
179233
}
180234

181235
@Override
182-
public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
236+
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
183237
}
184238

185239
@Override
186-
public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
240+
public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException {
187241
return sequence;
188242
}
189243

190244
@Override
191245
public int returnsType() {
246+
if (sequence == null) {
247+
return Type.ITEM;
248+
}
192249
return sequence.getItemType();
193250
}
194251

195252
@Override
196-
public void dump(ExpressionDumper dumper) {
253+
public void dump(final ExpressionDumper dumper) {
254+
}
197255

256+
@Override
257+
public void resetState(final boolean postOptimization) {
258+
super.resetState(postOptimization);
259+
this.sequence = null;
198260
}
199261
}
200262
}

exist-core/src/main/java/org/exist/xquery/NamedFunctionReference.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,14 @@
5757
import org.exist.xquery.value.Sequence;
5858
import org.exist.xquery.value.Type;
5959

60+
import javax.annotation.Nullable;
61+
6062
public class NamedFunctionReference extends AbstractExpression {
6163

6264
private QName qname;
6365
private int arity;
6466

65-
private FunctionCall resolvedFunction = null;
67+
@Nullable FunctionCall resolvedFunction = null;
6668

6769
public NamedFunctionReference(XQueryContext context, QName qname, int arity) {
6870
super(context);

0 commit comments

Comments
 (0)