Skip to content

Commit 16bcd7a

Browse files
committed
Implement qvar highlighting by creating $q var
Just like $x references hit, XQG now generates a $q variable which is a map of qvar names to their xml:id attributes. * Created additional unit test to test nested qvars and multiple qvar constraints and mappings * Refactor generateQvarConstraints() to use iterators to improve readability
1 parent 502acaa commit 16bcd7a

7 files changed

Lines changed: 122 additions & 53 deletions

File tree

src/main/java/com/formulasearchengine/mathmlquerygenerator/XQueryGenerator.java

Lines changed: 80 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.io.IOException;
1414
import java.util.ArrayList;
1515
import java.util.HashMap;
16+
import java.util.Iterator;
1617
import java.util.Map;
1718
import java.util.regex.Pattern;
1819

@@ -22,6 +23,8 @@
2223
* Converts MathML queries into XQueries.
2324
* The result is then wrapped around with a header and a footer (defaults to a DB2 header/footer if not given).
2425
* The variable $x always represents a hit, so you can refer to $x in the footer as the result node.
26+
* The variable $q always represents a map of qvars to their respective formula ID, so you can refer to $q in the footer
27+
* to return qvar results.
2528
* Created by Moritz Schubotz on 9/3/14.
2629
* Translated from http://git.wikimedia.org/blob/mediawiki%2Fextensions%2FMathSearch.git/31a80ae48d1aaa50da9103cea2e45a8dc2204b39/XQueryGenerator.php
2730
*/
@@ -31,7 +34,10 @@ public class XQueryGenerator {
3134
//Qvar map of qvar name to XPaths referenced by each qvar
3235
private Map<String, ArrayList<String>> qvar = new HashMap<>();
3336
private String relativeXPath = "";
37+
private String exactMatchXQuery = "";
3438
private String lengthConstraint = "";
39+
private String qvarConstraint = "";
40+
private String qvarMapVariable = "";
3541
private String header = "declare default element namespace \"http://www.w3.org/1998/Math/MathML\";\n" +
3642
"for $m in db2-fn:xmlcolumn(\"math.math_mathml\") return\n";
3743
private String footer = "data($m/*[1]/@alttext)";
@@ -134,80 +140,101 @@ public void setMainElement( Node mainElement ) {
134140

135141
/**
136142
* Generates the constraints of the XQuery and then builds the XQuery and returns it as a string
137-
* @return XQuery as string
143+
* @return XQuery as string. Returns null if no main element set.
138144
*/
139145
public String toString() {
140146
if ( mainElement == null ) {
141147
return null;
142148
}
143-
final String fixedConstraints = generateConstraints( mainElement, true );
144-
final StringBuilder qvarConstraintString = new StringBuilder();
145-
//Generate qvar constraint string
146-
//This specifies that the same qvars must refer to the same nodes, using the XQuery "=" equality
147-
//This is equality based on: same text, same node names, and same children by the "=" equality
148-
for ( Map.Entry<String, ArrayList<String>> entry : qvar.entrySet() ) {
149-
final StringBuilder addString = new StringBuilder();
150-
if ( entry.getValue().size() > 1 ) {
151-
final String firstEntry = entry.getValue().get( 0 );
152-
if ( qvarConstraintString.length() != 0 ) {
153-
addString.append("\n and ");
154-
}
155-
String lastEntry = "";
156-
boolean newContent = false;
157-
//begins at second entry
158-
for ( final String currentEntry : entry.getValue() ) {
159-
if ( !currentEntry.equals( firstEntry ) ) {
160-
if ( !lastEntry.isEmpty() ) {
161-
addString.append(" and ");
162-
}
163-
addString.append("$x").append(firstEntry).append(" = $x").append(currentEntry);
164-
lastEntry = currentEntry;
165-
newContent = true;
166-
}
167-
}
168-
if ( newContent ) {
169-
qvarConstraintString.append(addString);
170-
}
171-
}
172-
}
173-
return getString( mainElement, fixedConstraints, qvarConstraintString.toString() );
149+
exactMatchXQuery = generateSimpleConstraints( mainElement, true );
150+
generateQvarConstraints();
151+
152+
return getString( mainElement, exactMatchXQuery, lengthConstraint, qvarConstraint, qvarMapVariable, header, footer );
174153
}
175154

176155
/**
177-
* Builds the XQuery as a string with the set header and footer, given constraint strings and the main element.
156+
* Builds the XQuery as a string given constraint strings, header, footer, qvar map, and the main element.
178157
* @param mainElement Node from which to build XQuery
179158
* @param fixedConstraints Constraint string for basic exact formula matching
159+
* @param lengthConstraint Constraint string for length of variables
180160
* @param qvarConstraintString Constraint string for qvar matching
161+
* @param qvarMapVariable Qvar map variable
162+
* @param header Header
163+
* @param footer Footer
181164
* @return XQuery as string
182165
*/
183-
public String getString( Node mainElement, String fixedConstraints, String qvarConstraintString ) {
166+
public static String getString( Node mainElement, String fixedConstraints, String lengthConstraint,
167+
String qvarConstraintString, String qvarMapVariable, String header, String footer ) {
184168
String out = header;
185169
out += "for $x in $m//*:" + getFirstChild( mainElement ).getLocalName() + "\n" +
186170
fixedConstraints + "\n";
187-
out += getConstraints( qvarConstraintString );
188-
out +=
189-
"return" + "\n" + getFooter();
171+
if ( !lengthConstraint.isEmpty() || !qvarConstraintString.isEmpty() ) {
172+
out += "where" + "\n";
173+
if ( lengthConstraint.isEmpty() ) {
174+
out += qvarConstraintString;
175+
} else {
176+
out += lengthConstraint + (qvarConstraintString.isEmpty() ? "" : " and " + qvarConstraintString);
177+
}
178+
}
179+
out += qvarMapVariable;
180+
out += "\n";
181+
out += "return" + "\n" + footer;
190182
return out;
191183
}
192184

193185
/**
194-
* Appends constraint strings together
195-
* @param qvarConstraintString Qvar constraint portion of query
196-
* @return All constraint strings appended together
186+
* Uses the qvar map to generate a XQuery string containing qvar constraints,
187+
* and the qvar map variable which maps qvar names to their respective formula ID's in the result.
197188
*/
198-
private String getConstraints( String qvarConstraintString ) {
199-
String out = lengthConstraint +
200-
(((qvarConstraintString.length() > 0) && (lengthConstraint.length() > 0)) ? " and " : "") +
201-
qvarConstraintString;
202-
if ( out.trim().length() > 0 ) {
203-
return "where" + "\n" + out + "\n";
204-
} else {
205-
return "";
189+
private void generateQvarConstraints() {
190+
final StringBuilder qvarConstrBuilder = new StringBuilder();
191+
final StringBuilder qvarMapStrBuilder = new StringBuilder();
192+
final Iterator<Map.Entry<String, ArrayList<String>>> entryIterator = qvar.entrySet().iterator();
193+
if ( entryIterator.hasNext() ) {
194+
qvarMapStrBuilder.append( "\nlet $q := map {" );
195+
196+
while ( entryIterator.hasNext() ) {
197+
final Map.Entry<String, ArrayList<String>> currentEntry = entryIterator.next();
198+
199+
final Iterator<String> valueIterator = currentEntry.getValue().iterator();
200+
final String firstValue = valueIterator.next();
201+
202+
qvarMapStrBuilder.append( '"' ).append( currentEntry.getKey() ).append( '"' )
203+
.append( " : (data($x" ).append( firstValue ).append( "/@xml:id)" );
204+
205+
//check if there are additional values that we need to constrain
206+
if ( valueIterator.hasNext() ) {
207+
if ( qvarConstrBuilder.length() > 0 ) {
208+
//only add beginning and if it's an additional constraint in the aggregate qvar string
209+
qvarConstrBuilder.append( "\n and " );
210+
}
211+
while ( valueIterator.hasNext() ) {
212+
//process second value onwards
213+
final String currentValue = valueIterator.next();
214+
qvarMapStrBuilder.append( ",data($x" ).append( currentValue ).append( "/@xml-id)" );
215+
//These constraints specify that the same qvars must refer to the same nodes,
216+
//using the XQuery "=" equality
217+
//This is equality based on: same text, same node names, and same children nodes
218+
qvarConstrBuilder.append( "$x" ).append( firstValue ).append( " = $x" ).append( currentValue );
219+
if ( valueIterator.hasNext() ) {
220+
qvarConstrBuilder.append( " and " );
221+
}
222+
}
223+
}
224+
qvarMapStrBuilder.append( ')' );
225+
if ( entryIterator.hasNext() ) {
226+
qvarMapStrBuilder.append( ',' );
227+
}
228+
}
229+
qvarMapStrBuilder.append( '}' );
206230
}
231+
qvarMapVariable = qvarMapStrBuilder.toString();
232+
qvarConstraint = qvarConstrBuilder.toString();
207233
}
208234

209-
private String generateConstraint( Node node ) {
210-
return generateConstraints( node, false );
235+
236+
private String generateSimpleConstraints( Node node ) {
237+
return generateSimpleConstraints( node, false );
211238
}
212239

213240
/**
@@ -218,7 +245,7 @@ private String generateConstraint( Node node ) {
218245
* is not added as a constraint here, but in getString())
219246
* @return Exact match XQuery string
220247
*/
221-
private String generateConstraints( Node node, boolean isRoot ) {
248+
private String generateSimpleConstraints( Node node, boolean isRoot ) {
222249
//Index of child node
223250
int childElementIndex = 0;
224251
final StringBuilder out = new StringBuilder();
@@ -264,10 +291,10 @@ private String generateConstraints( Node node, boolean isRoot ) {
264291
//Add relative constraint so this can be recursively called
265292
out.append( " and *[" ).append( childElementIndex ).append( "]" );
266293
}
267-
final String constraint = generateConstraint( child );
294+
final String constraint = generateSimpleConstraints( child );
268295
if ( !constraint.isEmpty() ) {
269296
//This constraint appears as a child of the relative constraint above (e.g. [*1][constraint])
270-
out.append( "[" ).append( constraint ).append( "]");
297+
out.append( "[" ).append( constraint ).append( "]" );
271298
}
272299
}
273300
}

src/test/java/com/formulasearchengine/mathmlquerygenerator/XQueryGeneratorTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ public void testNoRestriction() throws Exception {
109109
"[*[1]/name() = 'plus' and *[2]/name() = 'apply' and *[2][*[1]/name() = 'csymbol' and *[1][./text() = 'superscript'] and *[3]/name() = 'cn' and *[3][./text() = '2']]]\n" +
110110
"where\n" +
111111
"$x/*[2]/*[2] = $x/*[3]\n" +
112+
"let $q := map {\"x\" : (data($x/*[2]/*[2]/@xml:id),data($x/*[3]/@xml-id))}\n" +
112113
"return\n";
113114
Document query = XMLHelper.String2Doc( testInput );
114115
XQueryGenerator xQueryGenerator = new XQueryGenerator( query );

src/test/resources/com/formulasearchengine/mathmlquerygenerator/mws/qqx2.xq

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ fn:count($x/*[1]/*) = 0
77
and fn:count($x/*[3]/*) = 0
88
and fn:count($x/*) = 3
99

10+
let $q := map {"x" : (data($x/*[2]/@xml:id))}
1011
return
1112
data($m/*[1]/@alttext)

src/test/resources/com/formulasearchengine/mathmlquerygenerator/mws/qqx2v2.xq

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ fn:count($x/*[1]/*) = 0
77
and fn:count($x/*[3]/*) = 0
88
and fn:count($x/*) = 3
99

10+
let $q := map {"x" : (data($x/*[2]/@xml:id))}
1011
return
1112
data($m/*[1]/@alttext)

src/test/resources/com/formulasearchengine/mathmlquerygenerator/mws/qqx2x.xq

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ fn:count($x/*[2]/*[1]/*) = 0
88
and fn:count($x/*[2]/*) = 3
99
and fn:count($x/*) = 3
1010
and $x/*[2]/*[2] = $x/*[3]
11+
let $q := map {"x" : (data($x/*[2]/*[2]/@xml:id),data($x/*[3]/@xml-id))}
1112
return
1213
data($m/*[1]/@alttext)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<mws:query xmlns:mws="http://search.mathweb.org/ns" xmlns:m="http://www.w3.org/1998/Math/MathML" limitmin="0"
3+
answsize="30">
4+
<mws:expr>
5+
<m:apply xml:id="p1.1.m1.1.5.cmml" xref="p1.1.m1.1.5">
6+
<m:plus xml:id="p1.3.7.1.4.cmml" xref="p1.765.21.1"/>
7+
<m:apply xml:id="p5.1.3.1.cmml" xref="p4.2.6.21">
8+
<m:plus xml:id="p1.1.m1.1.3.cmml" xref="p1.1.m1.1.3"/>
9+
<m:apply xml:id="p1.1.m1.1.5.1.cmml" xref="p1.1.m1.1.5.1">
10+
<m:csymbol cd="ambiguous" xml:id="p1.1.m1.1.5.1.1.cmml">superscript</m:csymbol>
11+
<mws:qvar>x</mws:qvar>
12+
<m:apply xml:id="p1.1.m1.1.5.1.2.cmml" xref="p1.2.4.2.cmml">
13+
<m:times xml:id="p1.1.m1.1.6.1.3.cmml" xref="p2.31.41.cmml"/>
14+
<mws:qvar>y</mws:qvar>
15+
<mws:qvar>y</mws:qvar>
16+
</m:apply>
17+
</m:apply>
18+
<mws:qvar>x</mws:qvar>
19+
</m:apply>
20+
<mws:qvar>x</mws:qvar>
21+
</m:apply>
22+
</mws:expr>
23+
</mws:query>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
declare default element namespace "http://www.w3.org/1998/Math/MathML";
2+
for $m in db2-fn:xmlcolumn("math.math_mathml") return
3+
for $x in $m//*:apply
4+
[*[1]/name() = 'plus' and *[2]/name() = 'apply' and *[2][*[1]/name() = 'plus' and *[2]/name() = 'apply' and *[2][*[1]/name() = 'csymbol' and *[1][./text() = 'superscript'] and *[3]/name() = 'apply' and *[3][*[1]/name() = 'times']]]]
5+
where
6+
fn:count($x/*[2]/*[2]/*[1]/*) = 0
7+
and fn:count($x/*[2]/*[2]/*[3]/*) = 3
8+
and fn:count($x/*[2]/*[2]/*) = 3
9+
and fn:count($x/*[2]/*) = 3
10+
and fn:count($x/*) = 3
11+
and $x/*[2]/*[2]/*[2] = $x/*[2]/*[3] and $x/*[2]/*[2]/*[2] = $x/*[3]
12+
and $x/*[2]/*[2]/*[3]/*[2] = $x/*[2]/*[2]/*[3]/*[3]
13+
let $q := map {"x" : (data($x/*[2]/*[2]/*[2]/@xml:id),data($x/*[2]/*[3]/@xml-id),data($x/*[3]/@xml-id)),"y" : (data($x/*[2]/*[2]/*[3]/*[2]/@xml:id),data($x/*[2]/*[2]/*[3]/*[3]/@xml-id))}
14+
return
15+
data($m/*[1]/@alttext)

0 commit comments

Comments
 (0)