Skip to content

Commit fb31a01

Browse files
file:descendants: recursion, filtering. qtspecs#2647
1 parent 30f772a commit fb31a01

5 files changed

Lines changed: 166 additions & 40 deletions

File tree

basex-core/src/main/java/org/basex/query/func/Function.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,8 +1495,8 @@ EMPTY_SEQUENCE_Z, flag(UPD), DB_URI),
14951495
_FILE_DELETE(FileDelete::new, "delete(path[,recursive])",
14961496
params(STRING_O, BOOLEAN_ZO), EMPTY_SEQUENCE_Z, flag(NDT), FILE_URI, Perm.ADMIN),
14971497
/** XQuery function. */
1498-
_FILE_DESCENDANTS(FileDescendants::new, "descendants(dir)",
1499-
params(STRING_O), STRING_ZM, flag(NDT), FILE_URI, Perm.ADMIN),
1498+
_FILE_DESCENDANTS(FileDescendants::new, "descendants(dir[,options])",
1499+
params(STRING_O, MAP_ZO), STRING_ZM, flag(NDT, HOF), FILE_URI, Perm.ADMIN),
15001500
/** XQuery function. */
15011501
_FILE_DIR_SEPARATOR(FileDirSeparator::new, "dir-separator()",
15021502
params(), STRING_O, FILE_URI),

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import java.io.*;
44

55
import org.basex.query.*;
6+
import org.basex.query.func.*;
67
import org.basex.query.value.*;
8+
import org.basex.query.value.item.*;
9+
import org.basex.query.value.seq.*;
10+
import org.basex.util.list.*;
711

812
/**
913
* Function implementation.
@@ -14,6 +18,10 @@
1418
public final class FileChildren extends FileList {
1519
@Override
1620
public Value eval(final QueryContext qc) throws QueryException, IOException {
17-
return paths(false, qc);
21+
final TokenList tl = new TokenList();
22+
final FItem recurse = constantFn(false), filter = constantFn(true);
23+
list(toPath(arg(0), qc), recurse, new HofArgs(1), null, -1,
24+
filter, new HofArgs(1), tl, Integer.MAX_VALUE, true, qc);
25+
return StrSeq.get(tl);
1826
}
1927
}
Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package org.basex.query.func.file;
22

33
import java.io.*;
4+
import java.nio.file.*;
45

56
import org.basex.query.*;
7+
import org.basex.query.func.*;
68
import org.basex.query.value.*;
9+
import org.basex.query.value.item.*;
10+
import org.basex.query.value.seq.*;
11+
import org.basex.query.value.type.*;
12+
import org.basex.util.list.*;
13+
import org.basex.util.options.*;
714

815
/**
916
* Function implementation.
@@ -12,8 +19,37 @@
1219
* @author Christian Gruen
1320
*/
1421
public final class FileDescendants extends FileList {
22+
/** Descendants options. */
23+
public static class DescendantsOptions extends Options {
24+
/** Option: filter. */
25+
public static final ValueOption FILTER = new ValueOption("filter", Types.FUNCTION_O);
26+
/** Option: recurse. */
27+
public static final ValueOption RECURSE = new ValueOption("recurse", Types.FUNCTION_O);
28+
/** Option: maximum recursion depth. */
29+
public static final NumberOption DEPTH = new NumberOption("depth");
30+
}
31+
1532
@Override
1633
public Value eval(final QueryContext qc) throws QueryException, IOException {
17-
return paths(true, qc);
34+
final Path dir = toPath(arg(0), qc);
35+
final DescendantsOptions options = toOptions(arg(1), new DescendantsOptions(), qc);
36+
37+
final Value filterValue = options.get(DescendantsOptions.FILTER);
38+
final Value recurseValue = options.get(DescendantsOptions.RECURSE);
39+
final Integer depth = options.get(DescendantsOptions.DEPTH);
40+
final FItem filter = filterValue.isEmpty() ? constantFn(true) :
41+
toFunction(filterValue, 1, qc);
42+
final FItem recurse = recurseValue.isEmpty() ? constantFn(true) :
43+
toFunction(recurseValue, 1, qc);
44+
45+
final TokenList list = new TokenList();
46+
list(dir, recurse, new HofArgs(1), null, -1, filter, new HofArgs(1), list,
47+
depth != null ? depth : Integer.MAX_VALUE, true, qc);
48+
return StrSeq.get(list);
49+
}
50+
51+
@Override
52+
public int hofOffsets() {
53+
return functionOption(1) ? Integer.MAX_VALUE : 0;
1854
}
1955
}

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

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
import java.io.*;
44
import java.nio.file.*;
55
import java.util.*;
6-
import java.util.function.*;
76
import java.util.regex.*;
87

98
import org.basex.io.*;
109
import org.basex.query.*;
10+
import org.basex.query.func.*;
11+
import org.basex.query.util.list.*;
1112
import org.basex.query.value.*;
13+
import org.basex.query.value.item.*;
1214
import org.basex.query.value.seq.*;
15+
import org.basex.query.value.type.*;
16+
import org.basex.query.var.*;
1317
import org.basex.util.*;
1418
import org.basex.util.list.*;
1519

@@ -29,44 +33,32 @@ public Value eval(final QueryContext qc) throws QueryException, IOException {
2933
final Pattern pttrn = pattern == null ? null :
3034
Pattern.compile(IOFile.regex(pattern, false), Prop.CASE ? 0 : Pattern.CASE_INSENSITIVE);
3135
final TokenList tl = new TokenList();
32-
list(dir, recursive, pttrn, tl, dir.getNameCount(), qc);
33-
return StrSeq.get(tl);
34-
}
35-
36-
/**
37-
* Returns full file paths.
38-
* @param recursive recursive flag
39-
* @param qc query context
40-
* @return file paths
41-
* @throws QueryException query exception
42-
* @throws IOException I/O exception
43-
*/
44-
Value paths(final boolean recursive, final QueryContext qc) throws QueryException, IOException {
45-
final TokenList tl = new TokenList();
46-
list(toPath(arg(0), qc), recursive, null, tl, -1, qc);
36+
final FItem recurse = constantFn(recursive), filter = constantFn(true);
37+
list(dir, recurse, new HofArgs(1), pttrn, dir.getNameCount(),
38+
filter, new HofArgs(1), tl, Integer.MAX_VALUE, true, qc);
4739
return StrSeq.get(tl);
4840
}
4941

5042
/**
5143
* Collects the subdirectories and files of the specified directory.
5244
* @param root root path
53-
* @param recursive recursive flag
45+
* @param recurse subtree predicate
46+
* @param recurseArgs arguments for the subtree predicate
5447
* @param pattern file name pattern; ignored if {@code null}
48+
* @param index index of root path for relative paths; {@code -1} for absolute paths
49+
* @param filter inclusion predicate
50+
* @param filterArgs arguments for the inclusion predicate
5551
* @param list file list
56-
* @param index index of root path
52+
* @param depth maximum number of subdirectory levels to descend ({@code 0}: none)
53+
* @param top {@code true} on the initial call (errors will be propagated)
5754
* @param qc query context
55+
* @throws QueryException query exception
5856
* @throws IOException I/O exception
5957
*/
60-
private static void list(final Path root, final boolean recursive, final Pattern pattern,
61-
final TokenList list, final int index, final QueryContext qc) throws IOException {
62-
63-
// filter function for adding results
64-
final BiConsumer<Path, Boolean> add = (child, dir) -> {
65-
if(pattern == null || pattern.matcher(child.getFileName().toString()).matches()) {
66-
final Path path = index < 0 ? child : child.subpath(index, child.getNameCount());
67-
list.add(get(path, dir).string());
68-
}
69-
};
58+
final void list(final Path root, final FItem recurse, final HofArgs recurseArgs,
59+
final Pattern pattern, final int index, final FItem filter, final HofArgs filterArgs,
60+
final TokenList list, final int depth, final boolean top, final QueryContext qc)
61+
throws QueryException, IOException {
7062

7163
// collect directories and files first (reduces number of open directory streams)
7264
final ArrayList<Path> dirs = new ArrayList<>(), files = new ArrayList<>();
@@ -77,24 +69,73 @@ private static void list(final Path root, final boolean recursive, final Pattern
7769
}
7870
} catch(final IOException ex) {
7971
// skip entries that cannot be accessed; throw exception only on root level
80-
if(index == -1 || index == root.getNameCount()) {
72+
if(top) {
8173
Util.debug(ex);
8274
throw ex;
8375
}
76+
return;
8477
}
8578

8679
// add directories
87-
for(final Path dir : dirs) {
88-
add.accept(dir, true);
89-
// recursive traversal: do not follow links
90-
if(recursive && !Files.isSymbolicLink(dir)) {
91-
list(dir, true, pattern, list, index == -1 ? -2 : index, qc);
80+
for(final Path child : dirs) {
81+
final Str path = add(child, true, pattern, filter, filterArgs, index, list, qc);
82+
// recursive traversal: descend if the depth allows it, do not follow links
83+
if(depth > 0 && !Files.isSymbolicLink(child)) {
84+
final Str p = path != null ? path : get(subPath(child, index), true);
85+
if(test(recurse, recurseArgs.set(0, p), qc)) {
86+
list(child, recurse, recurseArgs, pattern, index, filter, filterArgs,
87+
list, depth - 1, false, qc);
88+
}
9289
}
9390
}
9491

9592
// add files
96-
for(final Path file : files) {
97-
add.accept(file, false);
93+
for(final Path child : files) {
94+
add(child, false, pattern, filter, filterArgs, index, list, qc);
9895
}
9996
}
97+
98+
/**
99+
* Adds an entry to the result list, applying pattern and filter checks.
100+
* @param child raw path
101+
* @param isDir directory flag
102+
* @param pattern file name pattern (can be {@code null})
103+
* @param filter inclusion predicate
104+
* @param filterArgs arguments for the inclusion predicate
105+
* @param index index of root path (or {@code -1})
106+
* @param list file list
107+
* @param qc query context
108+
* @return display path, or {@code null} if the pattern excluded the entry
109+
* @throws QueryException query exception
110+
*/
111+
private Str add(final Path child, final boolean isDir, final Pattern pattern,
112+
final FItem filter, final HofArgs filterArgs, final int index, final TokenList list,
113+
final QueryContext qc) throws QueryException {
114+
// pattern check (operates on the file name)
115+
if(pattern != null && !pattern.matcher(child.getFileName().toString()).matches()) return null;
116+
117+
final Str path = get(subPath(child, index), isDir);
118+
if(test(filter, filterArgs.set(0, path), qc)) list.add(path.string());
119+
return path;
120+
}
121+
122+
/**
123+
* Returns the effective result path.
124+
* @param child raw path
125+
* @param index index of root path for relative paths; {@code -1} for absolute paths
126+
* @return display path
127+
*/
128+
private static Path subPath(final Path child, final int index) {
129+
return index < 0 ? child : child.subpath(index, child.getNameCount());
130+
}
131+
132+
/**
133+
* Creates a boolean predicate function.
134+
* @param value return value
135+
* @return function item
136+
*/
137+
final FItem constantFn(final boolean value) {
138+
return new FuncItem(info, Bln.get(value), new Var[0], AnnList.EMPTY,
139+
FuncType.get(Types.BOOLEAN_O), 0, null);
140+
}
100141
}

basex-core/src/test/java/org/basex/query/func/FileModuleTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,47 @@ public final class FileModuleTest extends SandboxTest {
228228
error(func.args(PATH3), FILE_NO_DIR_X);
229229
error(func.args(PATH3 + NAME), FILE_NOT_FOUND_X);
230230
contains(func.args(PATH1), "x");
231+
232+
// build a fresh tree for option-based tests: PATH1/x/y/file.txt
233+
query(_FILE_DELETE.args(PATH3));
234+
query(_FILE_CREATE_DIR.args(PATH4));
235+
query(_FILE_WRITE.args(PATH4 + "/file.txt", "abcd"));
236+
237+
// default: three entries (x/, x/y/, x/y/file.txt)
238+
query("count(" + func.args(PATH1) + ")", 3);
239+
240+
// filter: include only the .txt file
241+
final String filterTxt = " { 'filter': fn($p) { ends-with($p, '.txt') } }";
242+
query("count(" + func.args(PATH1, filterTxt) + ")", 1);
243+
contains(func.args(PATH1, filterTxt), "file.txt");
244+
245+
// recurse: prune the 'x' subtree -> only x/ itself is returned
246+
final String prune = " { 'recurse': fn($d) "
247+
+ "{ not(ends-with($d, 'x' || file:dir-separator())) } }";
248+
query("count(" + func.args(PATH1, prune) + ")", 1);
249+
250+
// combined: prune ahead of filter -> empty result
251+
final String both = " { 'filter': fn($p) { ends-with($p, '.txt') }, "
252+
+ "'recurse': fn($d) { not(ends-with($d, 'x' || file:dir-separator())) } }";
253+
query("count(" + func.args(PATH1, both) + ")", 0);
254+
255+
// empty options map: equivalent to no options
256+
query("count(" + func.args(PATH1, " {}") + ")", 3);
257+
258+
// empty sequence as predicate result: treated as false
259+
query("count(" + func.args(PATH1, " { 'filter': fn($p) { () } }") + ")", 0);
260+
query("count(" + func.args(PATH1, " { 'recurse': fn($d) { () } }") + ")", 1);
261+
262+
// depth: limit the number of subdirectory levels that are traversed
263+
query("count(" + func.args(PATH1, " { 'depth': 0 }") + ")", 1);
264+
query("count(" + func.args(PATH1, " { 'depth': 1 }") + ")", 2);
265+
query("count(" + func.args(PATH1, " { 'depth': 2 }") + ")", 3);
266+
// depth combined with filter: the .txt file lies below the depth limit
267+
query("count(" + func.args(PATH1,
268+
" { 'depth': 1, 'filter': fn($p) { ends-with($p, '.txt') } }") + ")", 0);
269+
270+
// unknown option
271+
error(func.args(PATH1, " { 'unknown': true() }"), INVALIDOPTION_X);
231272
}
232273

233274
/** Test method. */

0 commit comments

Comments
 (0)