1
2
3
4
5
6
7 package org.repoweb.model.file.util;
8 import java.io.File;
9 import java.io.FileFilter;
10 import java.io.IOException;
11 import java.util.ArrayList;
12 import java.util.List;
13
14 /***
15 * Class for scanning a directory for files/directories which match certain
16 * criteria.
17 * <p>
18 * These criteria consist of selectors and patterns which have been specified.
19 * With the selectors you can select which files you want to have included.
20 * Files which are not selected are excluded. With patterns you can include
21 * or exclude files based on their filename.
22 * <p>
23 * The idea is simple. A given directory is recursively scanned for all files
24 * and directories. Each file/directory is matched against a set of selectors,
25 * including special support for matching against filenames with include and
26 * and exclude patterns. Only files/directories which match at least one
27 * pattern of the include pattern list or other file selector, and don't match
28 * any pattern of the exclude pattern list or fail to match against a required
29 * selector will be placed in the list of files/directories found.
30 * <p>
31 * When no list of include patterns is supplied, "**" will be used, which
32 * means that everything will be matched. When no list of exclude patterns is
33 * supplied, an empty list is used, such that nothing will be excluded. When
34 * no selectors are supplied, none are applied.
35 * <p>
36 * The filename pattern matching is done as follows:
37 * The name to be matched is split up in path segments. A path segment is the
38 * name of a directory or file, which is bounded by
39 * <code>File.separator</code> ('/' under UNIX, '\' under Windows).
40 * For example, "abc/def/ghi/xyz.java" is split up in the segments "abc",
41 * "def","ghi" and "xyz.java".
42 * The same is done for the pattern against which should be matched.
43 * <p>
44 * The segments of the name and the pattern are then matched against each
45 * other. When '**' is used for a path segment in the pattern, it matches
46 * zero or more path segments of the name.
47 * <p>
48 * There is a special case regarding the use of <code>File.separator</code>s
49 * at the beginning of the pattern and the string to match:<br>
50 * When a pattern starts with a <code>File.separator</code>, the string
51 * to match must also start with a <code>File.separator</code>.
52 * When a pattern does not start with a <code>File.separator</code>, the
53 * string to match may not start with a <code>File.separator</code>.
54 * When one of these rules is not obeyed, the string will not
55 * match.
56 * <p>
57 * When a name path segment is matched against a pattern path segment, the
58 * following special characters can be used:<br>
59 * '*' matches zero or more characters<br>
60 * '?' matches one character.
61 * <p>
62 * Examples:
63 * <p>
64 * "**\*.class" matches all .class files/dirs in a directory tree.
65 * <p>
66 * "test\a??.java" matches all files/dirs which start with an 'a', then two
67 * more characters and then ".java", in a directory called test.
68 * <p>
69 * "**" matches everything in a directory tree.
70 * <p>
71 * "**\test\**\XYZ*" matches all files/dirs which start with "XYZ" and where
72 * there is a parent directory called test (e.g. "abc\test\def\ghi\XYZ123").
73 * <p>
74 * Case sensitivity may be turned off if necessary. By default, it is
75 * turned on.
76 * <p>
77 * Example of usage:
78 * <pre>
79 * String[] includes = {"**//*.class"};
80 * String[] excludes = {"modules//*//**"};
81 * ds.setIncludes(includes);
82 * ds.setExcludes(excludes);
83 * ds.setBasedir(new File("test"));
84 * ds.setCaseSensitive(true);
85 * ds.scan();
86 *
87 * LOG.info("FILES:");
88 * String[] files = ds.getIncludedFiles();
89 * for (int i = 0; i < files.length; i++) {
90 * LOG.info(files[i]);
91 * }
92 * </pre>
93 * This will scan a directory called test for .class files, but excludes all
94 * files in all proper subdirectories of a directory called "modules"
95 *
96 * @author Arnout J. Kuiper
97 * <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
98 * @author Magesh Umasankar
99 * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
100 * @author <a href="mailto:levylambert@tiscali-dsl.de">Antoine Levy-Lambert</a>
101 */
102 public class DirectoryScanner {
103 private static final String EMPTY = "";
104 /***
105 * Patterns which should be excluded by default.
106 *
107 * @see #addDefaultExcludes()
108 */
109 private static final String[] DEFAULTEXCLUDES =
110 {
111
112 "**/*~",
113 "**/#*#",
114 "**/.#*",
115 "**/%*%",
116 "**/._*",
117 "**/CVS",
118 "**/CVS/**",
119 "**/.cvsignore",
120 "**/SCCS",
121 "**/SCCS/**",
122 "**/vssver.scc",
123 "**/.svn",
124 "**/.svn/**",
125 "**/.DS_Store"
126 };
127
128 /*** The base directory to be scanned. */
129 private File _basedir;
130
131 /*** The patterns for the files to be included. */
132 private String[] _includes;
133
134 /*** The patterns for the files to be excluded. */
135 private String[] _excludes;
136
137 /*** Selectors that will filter which files are in our candidate list. */
138 private FileFilter[] _selectors = null;
139
140 /*** The files which matched at least one include and no excludes
141 * and were selected.
142 */
143 private List _filesIncluded;
144
145 /*** The files which did not match any includes or selectors. */
146 private List _filesNotIncluded;
147
148 /***
149 * The files which matched at least one include and at least
150 * one exclude.
151 */
152 private List _filesExcluded;
153
154 /*** The directories which matched at least one include and no excludes
155 * and were selected.
156 */
157 private List _dirsIncluded;
158
159 /*** The directories which were found and did not match any includes. */
160 private List _dirsNotIncluded;
161
162 /***
163 * The directories which matched at least one include and at least one
164 * exclude.
165 */
166 private List _dirsExcluded;
167
168 /*** The files which matched at least one include and no excludes and
169 * which a selector discarded.
170 */
171 private List _filesDeselected;
172
173 /*** The directories which matched at least one include and no excludes
174 * but which a selector discarded.
175 */
176 private List _dirsDeselected;
177
178 /*** Whether or not our results were built by a slow scan. */
179 private boolean _haveSlowResults = false;
180
181 /***
182 * Whether or not the file system should be treated as a case sensitive
183 * one.
184 */
185 private boolean _isCaseSensitive = true;
186
187 /***
188 * Whether or not symbolic links should be followed.
189 *
190 * @since Ant 1.5
191 */
192 private boolean _followSymlinks = true;
193
194 /*** Whether or not everything tested so far has been included. */
195 private boolean _everythingIncluded = true;
196
197 /***
198 * Sole constructor.
199 */
200 public DirectoryScanner() {}
201
202 /***
203 * Tests whether or not a given path matches the start of a given
204 * pattern up to the first "**".
205 * <p>
206 * This is not a general purpose test and should only be used if you
207 * can live with false positives. For example, <code>pattern=**\a</code>
208 * and <code>str=b</code> will yield <code>true</code>.
209 *
210 * @param pattern The pattern to match against. Must not be
211 * <code>null</code>.
212 * @param str The path to match, as a String. Must not be
213 * <code>null</code>.
214 * @param isCaseSensitive Whether or not matching should be performed
215 * case sensitively.
216 *
217 * @return whether or not a given path matches the start of a given
218 * pattern up to the first "**".
219 */
220 private static boolean matchPatternStart(String pattern, String str,
221 boolean isCaseSensitive) {
222 return SelectorUtils.matchPatternStart(pattern, str, isCaseSensitive);
223 }
224
225
226 /***
227 * Tests whether or not a given path matches a given pattern.
228 *
229 * @param pattern The pattern to match against. Must not be
230 * <code>null</code>.
231 * @param str The path to match, as a String. Must not be
232 * <code>null</code>.
233 * @param isCaseSensitive Whether or not matching should be performed
234 * case sensitively.
235 *
236 * @return <code>true</code> if the pattern matches against the string,
237 * or <code>false</code> otherwise.
238 */
239 private static boolean matchPath(String pattern, String str, boolean isCaseSensitive) {
240 return SelectorUtils.matchPath(pattern, str, isCaseSensitive);
241 }
242
243
244 /***
245 * Sets the base directory to be scanned. This is the directory which is
246 * scanned recursively. All '/' and '\' characters are replaced by
247 * <code>File.separatorChar</code>, so the separator used need not match
248 * <code>File.separatorChar</code>.
249 *
250 * @param basedir The base directory to scan.
251 * Must not be <code>null</code>.
252 */
253 public void setBasedir(String basedir) {
254 setBasedir(new File(basedir.replace('/', File.separatorChar).replace('//',
255 File.separatorChar)));
256 }
257
258
259 /***
260 * Sets the base directory to be scanned. This is the directory which is
261 * scanned recursively.
262 *
263 * @param basedir The base directory for scanning.
264 * Should not be <code>null</code>.
265 */
266 public void setBasedir(File basedir) {
267 this._basedir = basedir;
268 }
269
270
271 /***
272 * Returns the base directory to be scanned.
273 * This is the directory which is scanned recursively.
274 *
275 * @return the base directory to be scanned
276 */
277 public File getBasedir() {
278 return _basedir;
279 }
280
281
282 /***
283 * Sets whether or not the file system should be regarded as case sensitive.
284 *
285 * @param isCaseSensitive whether or not the file system should be
286 * regarded as a case sensitive one
287 */
288 public void setCaseSensitive(boolean isCaseSensitive) {
289 this._isCaseSensitive = isCaseSensitive;
290 }
291
292
293 /***
294 * Sets whether or not symbolic links should be followed.
295 *
296 * @param followSymlinks whether or not symbolic links should be followed
297 */
298 public void setFollowSymlinks(boolean followSymlinks) {
299 this._followSymlinks = followSymlinks;
300 }
301
302
303 /***
304 * Sets the list of include patterns to use. All '/' and '\' characters
305 * are replaced by <code>File.separatorChar</code>, so the separator used
306 * need not match <code>File.separatorChar</code>.
307 * <p>
308 * When a pattern ends with a '/' or '\', "**" is appended.
309 *
310 * @param includes A list of include patterns.
311 * May be <code>null</code>, indicating that all files
312 * should be included. If a non-<code>null</code>
313 * list is given, all elements must be
314 * non-<code>null</code>.
315 */
316 public void setIncludes(String[] includes) {
317 this._includes = copyPatternArray(includes);
318 }
319
320
321 /***
322 * Sets the list of exclude patterns to use. All '/' and '\' characters
323 * are replaced by <code>File.separatorChar</code>, so the separator used
324 * need not match <code>File.separatorChar</code>.
325 * <p>
326 * When a pattern ends with a '/' or '\', "**" is appended.
327 *
328 * @param excludes A list of exclude patterns.
329 * May be <code>null</code>, indicating that no files
330 * should be excluded. If a non-<code>null</code> list is
331 * given, all elements must be non-<code>null</code>.
332 */
333 public void setExcludes(String[] excludes) {
334 this._excludes = copyPatternArray(excludes);
335 }
336
337
338 /***
339 * Sets the selectors that will select the filelist.
340 *
341 * @param selectors specifies the selectors to be invoked on a scan
342 */
343 public void setSelectors(FileFilter[] selectors) {
344 this._selectors = selectors;
345 }
346
347
348 /***
349 * Returns whether or not the scanner has included all the files or
350 * directories it has come across so far.
351 *
352 * @return <code>true</code> if all files and directories which have
353 * been found so far have been included.
354 */
355 public boolean isEverythingIncluded() {
356 return _everythingIncluded;
357 }
358
359
360 /***
361 * Scans the base directory for files which match at least one include
362 * pattern and don't match any exclude patterns. If there are selectors
363 * then the files must pass muster there, as well.
364 *
365 * @exception ScanException if the base directory was set
366 * incorrectly (i.e. if it is <code>null</code>, doesn't exist,
367 * or isn't a directory).
368 */
369 public void scan() {
370 if (_basedir == null) {
371 throw new ScanException("No basedir set");
372 }
373 if (!_basedir.exists()) {
374 throw new ScanException("basedir " + _basedir + " does not exist");
375 }
376 if (!_basedir.isDirectory()) {
377 throw new ScanException("basedir " + _basedir + " is not a directory");
378 }
379
380 if (_includes == null) {
381
382 _includes = new String[1];
383 _includes[0] = "**";
384 }
385 if (_excludes == null) {
386 _excludes = new String[0];
387 }
388
389 _filesIncluded = new ArrayList();
390 _filesNotIncluded = new ArrayList();
391 _filesExcluded = new ArrayList();
392 _filesDeselected = new ArrayList();
393 _dirsIncluded = new ArrayList();
394 _dirsNotIncluded = new ArrayList();
395 _dirsExcluded = new ArrayList();
396 _dirsDeselected = new ArrayList();
397
398 if (isIncluded(EMPTY)) {
399 if (!isExcluded(EMPTY)) {
400 if (isSelected(_basedir)) {
401 _dirsIncluded.add(EMPTY);
402 }
403 else {
404 _dirsDeselected.add(EMPTY);
405 }
406 }
407 else {
408 _dirsExcluded.add(EMPTY);
409 }
410 }
411 else {
412 _dirsNotIncluded.add(EMPTY);
413 }
414 scandir(_basedir, EMPTY, true);
415 }
416
417
418 /***
419 * Top level invocation for a slow scan. A slow scan builds up a full
420 * list of excluded/included files/directories, whereas a fast scan
421 * will only have full results for included files, as it ignores
422 * directories which can't possibly hold any included files/directories.
423 * <p>
424 * Returns immediately if a slow scan has already been completed.
425 */
426 private void slowScan() {
427 if (_haveSlowResults) {
428 return;
429 }
430
431 String[] excl = (String[])_dirsExcluded.toArray(new String[_dirsExcluded.size()]);
432
433 String[] notIncl =
434 (String[])_dirsNotIncluded.toArray(new String[_dirsNotIncluded.size()]);
435
436 for (int i = 0; i < excl.length; i++) {
437 if (!couldHoldIncluded(excl[i])) {
438 scandir(new File(_basedir, excl[i]), excl[i] + File.separator, false);
439 }
440 }
441
442 for (int i = 0; i < notIncl.length; i++) {
443 if (!couldHoldIncluded(notIncl[i])) {
444 scandir(new File(_basedir, notIncl[i]), notIncl[i] + File.separator, false);
445 }
446 }
447
448 _haveSlowResults = true;
449 }
450
451
452 /***
453 * Scans the given directory for files and directories. Found files and
454 * directories are placed in their respective collections, based on the
455 * matching of includes, excludes, and the selectors. When a directory
456 * is found, it is scanned recursively.
457 *
458 * @param dir The directory to scan. Must not be <code>null</code>.
459 * @param vpath The path relative to the base directory (needed to
460 * prevent problems with an absolute path when using
461 * dir). Must not be <code>null</code>.
462 * @param fast Whether or not this call is part of a fast scan.
463 *
464 * @see #_filesIncluded
465 * @see #_filesNotIncluded
466 * @see #_filesExcluded
467 * @see #_dirsIncluded
468 * @see #_dirsNotIncluded
469 * @see #_dirsExcluded
470 */
471 private void scandir(File dir, String vpath, boolean fast) {
472 String[] newfiles = dir.list();
473
474 if (newfiles == null) {
475
476
477
478
479
480
481
482 throw new ScanException("IO error scanning directory "
483 + dir.getAbsolutePath());
484 }
485
486 if (!_followSymlinks) {
487 List noLinks = new ArrayList();
488 for (int i = 0; i < newfiles.length; i++) {
489 try {
490 if (isSymbolicLink(dir, newfiles[i])) {
491 String name = vpath + newfiles[i];
492 File file = new File(dir, newfiles[i]);
493 if (file.isDirectory()) {
494 _dirsExcluded.add(name);
495 }
496 else {
497 _filesExcluded.add(name);
498 }
499 }
500 else {
501 noLinks.add(newfiles[i]);
502 }
503 }
504 catch (IOException ioe) {
505 String msg =
506 "IOException caught while checking "
507 + "for links, couldn't get cannonical path!";
508
509
510 System.err.println(msg);
511 noLinks.add(newfiles[i]);
512 }
513 }
514 newfiles = (String[])noLinks.toArray(new String[noLinks.size()]);
515 }
516
517 for (int i = 0; i < newfiles.length; i++) {
518 String name = vpath + newfiles[i];
519 File file = new File(dir, newfiles[i]);
520 if (file.isDirectory()) {
521 if (isIncluded(name)) {
522 if (!isExcluded(name)) {
523 if (isSelected(file)) {
524 _dirsIncluded.add(file);
525 if (fast) {
526 scandir(file, name + File.separator, fast);
527 }
528 }
529 else {
530 _everythingIncluded = false;
531 _dirsDeselected.add(name);
532 if (fast && couldHoldIncluded(name)) {
533 scandir(file, name + File.separator, fast);
534 }
535 }
536 }
537 else {
538 _everythingIncluded = false;
539 _dirsExcluded.add(name);
540 if (fast && couldHoldIncluded(name)) {
541 scandir(file, name + File.separator, fast);
542 }
543 }
544 }
545 else {
546 _everythingIncluded = false;
547 _dirsNotIncluded.add(name);
548 if (fast && couldHoldIncluded(name)) {
549 scandir(file, name + File.separator, fast);
550 }
551 }
552 if (!fast) {
553 scandir(file, name + File.separator, fast);
554 }
555 }
556 else if (file.isFile()) {
557 if (isIncluded(name)) {
558 if (!isExcluded(name)) {
559 if (isSelected(file)) {
560 _filesIncluded.add(file);
561 }
562 else {
563 _everythingIncluded = false;
564 _filesDeselected.add(name);
565 }
566 }
567 else {
568 _everythingIncluded = false;
569 _filesExcluded.add(name);
570 }
571 }
572 else {
573 _everythingIncluded = false;
574 _filesNotIncluded.add(name);
575 }
576 }
577 }
578 }
579
580
581 /***
582 * Tests whether or not a name matches against at least one include
583 * pattern.
584 *
585 * @param name The name to match. Must not be <code>null</code>.
586 * @return <code>true</code> when the name matches against at least one
587 * include pattern, or <code>false</code> otherwise.
588 */
589 private boolean isIncluded(String name) {
590 for (int i = 0; i < _includes.length; i++) {
591 if (matchPath(_includes[i], name, _isCaseSensitive)) {
592 return true;
593 }
594 }
595 return false;
596 }
597
598
599 /***
600 * Tests whether or not a name matches the start of at least one include
601 * pattern.
602 *
603 * @param name The name to match. Must not be <code>null</code>.
604 * @return <code>true</code> when the name matches against the start of at
605 * least one include pattern, or <code>false</code> otherwise.
606 */
607 private boolean couldHoldIncluded(String name) {
608 for (int i = 0; i < _includes.length; i++) {
609 if (matchPatternStart(_includes[i], name, _isCaseSensitive)) {
610 return true;
611 }
612 }
613 return false;
614 }
615
616
617 /***
618 * Tests whether or not a name matches against at least one exclude
619 * pattern.
620 *
621 * @param name The name to match. Must not be <code>null</code>.
622 * @return <code>true</code> when the name matches against at least one
623 * exclude pattern, or <code>false</code> otherwise.
624 */
625 private boolean isExcluded(String name) {
626 for (int i = 0; i < _excludes.length; i++) {
627 if (matchPath(_excludes[i], name, _isCaseSensitive)) {
628 return true;
629 }
630 }
631 return false;
632 }
633
634
635 /***
636 * Tests whether a name should be selected.
637 *
638 * @param file the java.io.File object for this filename
639 * @return <code>false</code> when the selectors says that the file
640 * should not be selected, <code>true</code> otherwise.
641 */
642 private boolean isSelected(File file) {
643 if (_selectors != null) {
644 for (int i = 0; i < _selectors.length; i++) {
645 if (!(_selectors[i].accept(file))) {
646 return false;
647 }
648 }
649 }
650 return true;
651 }
652
653
654 /***
655 * Returns the names of the files which matched at least one of the
656 * include patterns and none of the exclude patterns.
657 * The names are relative to the base directory.
658 *
659 * @return the names of the files which matched at least one of the
660 * include patterns and none of the exclude patterns.
661 */
662 public File[] getIncludedFiles() {
663 return (File[])_filesIncluded.toArray(new File[_filesIncluded.size()]);
664 }
665
666
667 /***
668 * Returns the names of the files which matched none of the include
669 * patterns. The names are relative to the base directory. This involves
670 * performing a slow scan if one has not already been completed.
671 *
672 * @return the names of the files which matched none of the include
673 * patterns.
674 */
675 public String[] getNotIncludedFiles() {
676 slowScan();
677 return (String[])_filesNotIncluded.toArray(new String[_filesNotIncluded.size()]);
678 }
679
680
681 /***
682 * Returns the names of the files which matched at least one of the
683 * include patterns and at least one of the exclude patterns.
684 * The names are relative to the base directory. This involves
685 * performing a slow scan if one has not already been completed.
686 *
687 * @return the names of the files which matched at least one of the
688 * include patterns and at at least one of the exclude patterns.
689 */
690 public String[] getExcludedFiles() {
691 slowScan();
692 return (String[])_filesExcluded.toArray(new String[_filesExcluded.size()]);
693 }
694
695
696 /***
697 * <p>Returns the names of the files which were selected out and
698 * therefore not ultimately included.</p>
699 *
700 * <p>The names are relative to the base directory. This involves
701 * performing a slow scan if one has not already been completed.</p>
702 *
703 * @return the names of the files which were deselected.
704 */
705 public String[] getDeselectedFiles() {
706 slowScan();
707 return (String[])_filesDeselected.toArray(new String[_filesDeselected.size()]);
708 }
709
710
711 /***
712 * Returns the names of the directories which matched at least one of the
713 * include patterns and none of the exclude patterns.
714 * The names are relative to the base directory.
715 *
716 * @return the names of the directories which matched at least one of the
717 * include patterns and none of the exclude patterns.
718 */
719 public File[] getIncludedDirectories() {
720 return (File[])_dirsIncluded.toArray(new File[_dirsIncluded.size()]);
721 }
722
723
724 /***
725 * Returns the names of the directories which matched none of the include
726 * patterns. The names are relative to the base directory. This involves
727 * performing a slow scan if one has not already been completed.
728 *
729 * @return the names of the directories which matched none of the include
730 * patterns.
731 */
732 public String[] getNotIncludedDirectories() {
733 slowScan();
734 return (String[])_dirsNotIncluded.toArray(new String[_dirsNotIncluded.size()]);
735 }
736
737
738 /***
739 * Returns the names of the directories which matched at least one of the
740 * include patterns and at least one of the exclude patterns.
741 * The names are relative to the base directory. This involves
742 * performing a slow scan if one has not already been completed.
743 *
744 * @return the names of the directories which matched at least one of the
745 * include patterns and at least one of the exclude patterns.
746 */
747 public String[] getExcludedDirectories() {
748 slowScan();
749 return (String[])_dirsExcluded.toArray(new String[_dirsExcluded.size()]);
750 }
751
752
753 /***
754 * <p>Returns the names of the directories which were selected out and
755 * therefore not ultimately included.</p>
756 *
757 * <p>The names are relative to the base directory. This involves
758 * performing a slow scan if one has not already been completed.</p>
759 *
760 * @return the names of the directories which were deselected.
761 */
762 public String[] getDeselectedDirectories() {
763 slowScan();
764 return (String[])_dirsDeselected.toArray(new String[_dirsDeselected.size()]);
765 }
766
767
768 /***
769 * Adds default exclusions to the current exclusions set.
770 */
771 public void addDefaultExcludes() {
772 int excludesLength = _excludes == null ? 0 : _excludes.length;
773 String[] newExcludes;
774 newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
775 if (excludesLength > 0) {
776 System.arraycopy(_excludes, 0, newExcludes, 0, excludesLength);
777 }
778 for (int i = 0; i < DEFAULTEXCLUDES.length; i++) {
779 newExcludes[i + excludesLength] =
780 DEFAULTEXCLUDES[i].replace('/', File.separatorChar).replace('//',
781 File.separatorChar);
782 }
783 _excludes = newExcludes;
784 }
785
786
787 private static boolean isSymbolicLink(File parent, String name)
788 throws IOException {
789 File resolvedParent = new File(parent.getCanonicalPath());
790 File toTest = new File(resolvedParent, name);
791 return !toTest.getAbsolutePath().equals(toTest.getCanonicalPath());
792 }
793
794
795 private static final String[] copyPatternArray(String[] source) {
796 if (source == null) {
797 return null;
798 }
799
800 String[] results = new String[source.length];
801 for (int i = 0; i < source.length; i++) {
802 String pattern;
803 pattern =
804 source[i].replace('/', File.separatorChar).replace('//',
805 File.separatorChar);
806 if (pattern.endsWith(File.separator)) {
807 pattern += "**";
808 }
809 results[i] = pattern;
810 }
811 return results;
812 }
813 }