11package org .labkey .jbrowse ;
22
3+ import jakarta .servlet .http .HttpServletResponse ;
34import org .apache .commons .lang3 .StringUtils ;
45import org .apache .logging .log4j .Logger ;
56import org .apache .lucene .analysis .Analyzer ;
2021import org .apache .lucene .search .MatchAllDocsQuery ;
2122import org .apache .lucene .search .Query ;
2223import org .apache .lucene .search .QueryCachingPolicy ;
24+ import org .apache .lucene .search .ScoreDoc ;
2325import org .apache .lucene .search .Sort ;
2426import org .apache .lucene .search .SortField ;
2527import org .apache .lucene .search .TopFieldDocs ;
5052
5153import java .io .File ;
5254import java .io .IOException ;
55+ import java .io .PrintWriter ;
5356import java .net .URLDecoder ;
5457import java .nio .charset .StandardCharsets ;
5558import java .text .DecimalFormat ;
@@ -189,7 +192,17 @@ public String extractFieldName(String queryString)
189192 return parts .length > 0 ? parts [0 ].trim () : null ;
190193 }
191194
192- public JSONObject doSearch (User u , String searchString , final int pageSize , final int offset , String sortField , boolean sortReverse ) throws IOException , ParseException
195+ public JSONObject doSearchJSON (User u , String searchString , final int pageSize , final int offset , String sortField , boolean sortReverse ) throws IOException , ParseException {
196+ SearchConfig searchConfig = createSearchConfig (u , searchString , pageSize , offset , sortField , sortReverse );
197+ return paginateJSON (searchConfig );
198+ }
199+
200+ public void doSearchCSV (User u , String searchString , String sortField , boolean sortReverse , HttpServletResponse response ) throws IOException , ParseException {
201+ SearchConfig searchConfig = createSearchConfig (u , searchString , 0 , 0 , sortField , sortReverse );
202+ exportCSV (searchConfig , response );
203+ }
204+
205+ private SearchConfig createSearchConfig (User u , String searchString , final int pageSize , final int offset , String sortField , boolean sortReverse ) throws IOException , ParseException
193206 {
194207 searchString = tryUrlDecode (searchString );
195208 File indexPath = _jsonFile .getExpectedLocationOfLuceneIndex (true );
@@ -199,6 +212,7 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
199212 Analyzer analyzer = new StandardAnalyzer ();
200213
201214 List <String > stringQueryParserFields = new ArrayList <>();
215+ List <String > fieldsList = new ArrayList <>();
202216 Map <String , SortField .Type > numericQueryParserFields = new HashMap <>();
203217 PointsConfig intPointsConfig = new PointsConfig (new DecimalFormat (), Integer .class );
204218 PointsConfig doublePointsConfig = new PointsConfig (new DecimalFormat (), Double .class );
@@ -208,6 +222,7 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
208222 for (Map .Entry <String , JBrowseFieldDescriptor > entry : fields .entrySet ())
209223 {
210224 String field = entry .getKey ();
225+ fieldsList .add (field );
211226 JBrowseFieldDescriptor descriptor = entry .getValue ();
212227
213228 switch (descriptor .getType ())
@@ -267,14 +282,14 @@ else if (numericQueryParserFields.containsKey(fieldName))
267282 }
268283 catch (QueryNodeException e )
269284 {
270- _log .error ("Unable to parse query for field " + fieldName + ": " + queryString , e );
285+ _log .error ("Unable to parse query for field {}: {}" , fieldName , queryString , e );
271286
272287 throw new IllegalArgumentException ("Unable to parse query: " + queryString + " for field: " + fieldName );
273288 }
274289 }
275290 else
276291 {
277- _log .error ("No such field(s), or malformed query: " + queryString + " , field: " + fieldName );
292+ _log .error ("No such field(s), or malformed query: {} , field: {}" , queryString , fieldName );
278293
279294 throw new IllegalArgumentException ("No such field(s), or malformed query: " + queryString + ", field: " + fieldName );
280295 }
@@ -302,43 +317,79 @@ else if (numericQueryParserFields.containsKey(fieldName))
302317 sort = new Sort (new SortField (sortField + "_sort" , fieldType , sortReverse ));
303318 }
304319
320+ return new SearchConfig (cacheEntry , query , pageSize , offset , sort , fieldsList );
321+ }
322+
323+ private JSONObject paginateJSON (SearchConfig c ) throws IOException , ParseException {
305324 // Get chunks of size {pageSize}. Default to 1 chunk -- add to the offset to get more.
306325 // We then iterate over the range of documents we want based on the offset. This does grow in memory
307326 // linearly with the number of documents, but my understanding is that these are just score,id pairs
308327 // rather than full documents, so mem usage *should* still be pretty low.
309328 // Perform the search with sorting
310- TopFieldDocs topDocs = cacheEntry .indexSearcher .search (query , pageSize * (offset + 1 ), sort );
311-
329+ TopFieldDocs topDocs = c .cacheEntry .indexSearcher .search (c .query , c .pageSize * (c .offset + 1 ), c .sort );
312330 JSONObject results = new JSONObject ();
313331
314332 // Iterate over the doc list, (either to the total end or until the page ends) grab the requested docs,
315333 // and add to returned results
316334 List <JSONObject > data = new ArrayList <>();
317- for (int i = pageSize * offset ; i < Math .min (pageSize * (offset + 1 ), topDocs .scoreDocs .length ); i ++)
335+ for (int i = c . pageSize * c . offset ; i < Math .min (c . pageSize * (c . offset + 1 ), topDocs .scoreDocs .length ); i ++)
318336 {
319337 JSONObject elem = new JSONObject ();
320- Document doc = cacheEntry .indexSearcher .storedFields ().document (topDocs .scoreDocs [i ].doc );
338+ Document doc = c . cacheEntry .indexSearcher .storedFields ().document (topDocs .scoreDocs [i ].doc );
321339
322- for (IndexableField field : doc .getFields ()) {
340+ for (IndexableField field : doc .getFields ())
341+ {
323342 String fieldName = field .name ();
324343 String [] fieldValues = doc .getValues (fieldName );
325- if (fieldValues .length > 1 ) {
344+ if (fieldValues .length > 1 )
345+ {
326346 elem .put (fieldName , fieldValues );
327- } else {
347+ }
348+ else
349+ {
328350 elem .put (fieldName , fieldValues [0 ]);
329351 }
330352 }
331-
332353 data .add (elem );
333354 }
334355
335356 results .put ("data" , data );
336357 results .put ("totalHits" , topDocs .totalHits .value );
337358
338- //TODO: we should probably stream this
339359 return results ;
340360 }
341361
362+ private void exportCSV (SearchConfig c , HttpServletResponse response ) throws IOException
363+ {
364+ PrintWriter writer = response .getWriter ();
365+ IndexSearcher searcher = c .cacheEntry .indexSearcher ;
366+ TopFieldDocs topDocs = searcher .search (c .query , Integer .MAX_VALUE , c .sort );
367+
368+ writer .println (String .join ("," , c .fields ));
369+
370+ for (ScoreDoc scoreDoc : topDocs .scoreDocs )
371+ {
372+ Document doc = searcher .storedFields ().document (scoreDoc .doc );
373+ List <String > rowValues = new ArrayList <>();
374+
375+ for (String fieldName : c .fields )
376+ {
377+ String [] values = doc .getValues (fieldName );
378+ String value = values .length > 0
379+ ? String .join ("," , values )
380+ : "" ;
381+
382+ // Escape strings
383+ value = "\" " + value .replace ("\" " , "\" \" " ) + "\" " ;
384+ rowValues .add (value );
385+ }
386+
387+ writer .println (String .join ("," , rowValues ));
388+ }
389+
390+ writer .flush ();
391+ }
392+
342393 public static class DefaultJBrowseFieldCustomizer extends AbstractJBrowseFieldCustomizer
343394 {
344395 public DefaultJBrowseFieldCustomizer ()
@@ -583,7 +634,7 @@ public void cacheDefaultQuery()
583634 try
584635 {
585636 JBrowseLuceneSearch .clearCache (_jsonFile .getObjectId ());
586- doSearch (_user , ALL_DOCS , 100 , 0 , GENOMIC_POSITION , false );
637+ doSearchJSON (_user , ALL_DOCS , 100 , 0 , GENOMIC_POSITION , false );
587638 }
588639 catch (ParseException | IOException e )
589640 {
@@ -641,4 +692,22 @@ public void shutdownStarted()
641692 JBrowseLuceneSearch .emptyCache ();
642693 }
643694 }
695+
696+ private class SearchConfig {
697+ CacheEntry cacheEntry ;
698+ Query query ;
699+ int pageSize ;
700+ int offset ;
701+ Sort sort ;
702+ List <String > fields ;
703+
704+ public SearchConfig (CacheEntry cacheEntry , Query query , int pageSize , int offset , Sort sort , List <String > fields ) {
705+ this .cacheEntry = cacheEntry ;
706+ this .query = query ;
707+ this .pageSize = pageSize ;
708+ this .offset = offset ;
709+ this .sort = sort ;
710+ this .fields = fields ;
711+ }
712+ }
644713}
0 commit comments