extensions/net.sf.basedb.varsearch/trunk/src/net/sf/basedb/varsearch/LuceneColumnFactory.java

Code
Comments
Other
Rev Date Author Line
6109 25 Jan 21 nicklas 1 package net.sf.basedb.varsearch;
6109 25 Jan 21 nicklas 2
6136 18 Feb 21 nicklas 3 import java.io.IOException;
6130 15 Feb 21 nicklas 4 import java.util.ArrayList;
6136 18 Feb 21 nicklas 5 import java.util.Arrays;
6111 29 Jan 21 nicklas 6 import java.util.Collection;
6109 25 Jan 21 nicklas 7 import java.util.List;
6109 25 Jan 21 nicklas 8
6109 25 Jan 21 nicklas 9 import org.apache.lucene.document.Document;
6109 25 Jan 21 nicklas 10
6848 17 Oct 22 nicklas 11 import net.sf.basedb.clients.web.Base;
6109 25 Jan 21 nicklas 12 import net.sf.basedb.clients.web.extensions.AbstractJspActionFactory;
6156 02 Mar 21 nicklas 13 import net.sf.basedb.clients.web.extensions.ExtensionsControl;
6109 25 Jan 21 nicklas 14 import net.sf.basedb.clients.web.extensions.JspContext;
6109 25 Jan 21 nicklas 15 import net.sf.basedb.clients.web.extensions.list.AbstractListColumnBean;
6109 25 Jan 21 nicklas 16 import net.sf.basedb.clients.web.extensions.list.ListColumnAction;
6551 26 Jan 22 nicklas 17 import net.sf.basedb.clients.web.util.HTML;
6109 25 Jan 21 nicklas 18 import net.sf.basedb.core.BasicItem;
6109 25 Jan 21 nicklas 19 import net.sf.basedb.core.DbControl;
6136 18 Feb 21 nicklas 20 import net.sf.basedb.core.RawBioAssay;
6156 02 Mar 21 nicklas 21 import net.sf.basedb.core.SessionControl;
6109 25 Jan 21 nicklas 22 import net.sf.basedb.core.Type;
6139 18 Feb 21 nicklas 23 import net.sf.basedb.util.Values;
6109 25 Jan 21 nicklas 24 import net.sf.basedb.util.extensions.InvokationContext;
6848 17 Oct 22 nicklas 25 import net.sf.basedb.util.formatter.CollectionFormatter;
6130 15 Feb 21 nicklas 26 import net.sf.basedb.util.formatter.Formatter;
6848 17 Oct 22 nicklas 27 import net.sf.basedb.util.formatter.PrefixSuffixFormatter;
6130 15 Feb 21 nicklas 28 import net.sf.basedb.util.formatter.ToStringFormatter;
6132 16 Feb 21 nicklas 29 import net.sf.basedb.varsearch.index.LuceneIndex;
6408 20 Sep 21 nicklas 30 import net.sf.basedb.varsearch.index.LuceneIndex.Status;
6132 16 Feb 21 nicklas 31 import net.sf.basedb.varsearch.query.AllDocsCollector;
6132 16 Feb 21 nicklas 32 import net.sf.basedb.varsearch.query.LuceneQueryFactory;
6236 18 May 21 nicklas 33 import net.sf.basedb.varsearch.query.LuceneQueryFactory.LuceneFilterAction;
6241 21 May 21 nicklas 34 import net.sf.basedb.varsearch.query.QueryResult;
6263 28 May 21 nicklas 35 import net.sf.basedb.varsearch.query.SortByChromAndPos;
6111 29 Jan 21 nicklas 36 import net.sf.basedb.varsearch.service.VarSearchService;
6129 12 Feb 21 nicklas 37 import net.sf.basedb.varsearch.util.NullSafeStringBuilder;
6109 25 Jan 21 nicklas 38
6109 25 Jan 21 nicklas 39
6109 25 Jan 21 nicklas 40 /**
6109 25 Jan 21 nicklas 41   Proof-of-concept implementation that is adding a "Lucene" column 
6109 25 Jan 21 nicklas 42   in the RawBioassays table. Filter property is set to "!x.lucene"
6109 25 Jan 21 nicklas 43   and filtering is handled by {@link LuceneQueryFactory.LuceneFilterAction}.
6109 25 Jan 21 nicklas 44   Information is only displayed if there is an active filter.
6109 25 Jan 21 nicklas 45 */
6109 25 Jan 21 nicklas 46 public class LuceneColumnFactory 
6109 25 Jan 21 nicklas 47   extends AbstractJspActionFactory<ListColumnAction<BasicItem, String>>
6109 25 Jan 21 nicklas 48 {
6109 25 Jan 21 nicklas 49
6236 18 May 21 nicklas 50   /**
6236 18 May 21 nicklas 51     Max number of variants to display per raw bioassay. This value
6236 18 May 21 nicklas 52     may be reduced if the rows-per-page setting is high in order to
6236 18 May 21 nicklas 53     keep the total number of variants in a list page lower than
6236 18 May 21 nicklas 54     MAX_HITS_PER_PAGE.
6236 18 May 21 nicklas 55   */
6236 18 May 21 nicklas 56   public static final int MAX_HITS_PER_RBA = 50;
6236 18 May 21 nicklas 57
6236 18 May 21 nicklas 58   /**
6236 18 May 21 nicklas 59     Max number of variants to display in a single table.
6236 18 May 21 nicklas 60   */
6236 18 May 21 nicklas 61   public static final int MAX_HITS_PER_TABLE = 1000;
6236 18 May 21 nicklas 62
6109 25 Jan 21 nicklas 63   public LuceneColumnFactory() 
6109 25 Jan 21 nicklas 64   {}
6109 25 Jan 21 nicklas 65   
6109 25 Jan 21 nicklas 66   @Override
6109 25 Jan 21 nicklas 67   public boolean prepareContext(InvokationContext<? super ListColumnAction<BasicItem, String>> context) 
6109 25 Jan 21 nicklas 68   {
6111 29 Jan 21 nicklas 69     // We need the service to provide us with information about existing 
6111 29 Jan 21 nicklas 70     // indexes and IndexReader/IndexSearcher instances
6129 12 Feb 21 nicklas 71     return VarSearchService.getInstance().isRunning() && super.prepareContext(context);
6109 25 Jan 21 nicklas 72   }
6109 25 Jan 21 nicklas 73   
6109 25 Jan 21 nicklas 74   @SuppressWarnings({"unchecked", "rawtypes"})
6109 25 Jan 21 nicklas 75   @Override
6109 25 Jan 21 nicklas 76   public ListColumnAction<BasicItem, String>[] getActions(InvokationContext<? super ListColumnAction<BasicItem, String>> context) 
6109 25 Jan 21 nicklas 77   {
6109 25 Jan 21 nicklas 78     JspContext jspContext = (JspContext)context.getClientContext();
6156 02 Mar 21 nicklas 79     SessionControl sc = jspContext.getSessionControl();
6848 17 Oct 22 nicklas 80     // If this is TRUE we are generating columns in a PARENT list
6848 17 Oct 22 nicklas 81     boolean relatedItemContext = "RelatedItemColumns".equals(jspContext.getGuiContext().getSubContext());
6109 25 Jan 21 nicklas 82     
6111 29 Jan 21 nicklas 83     Collection<LuceneIndex> indexes = VarSearchService.getInstance().getIndexes();
6408 20 Sep 21 nicklas 84     List<ListColumnAction> actions = new ArrayList<ListColumnAction>();
6156 02 Mar 21 nicklas 85     
6156 02 Mar 21 nicklas 86     String home = ExtensionsControl.getHomeUrl(context.getExtension().getId());
6111 29 Jan 21 nicklas 87     for (LuceneIndex idx : indexes)
6111 29 Jan 21 nicklas 88     {
6544 19 Jan 22 nicklas 89       if (idx.getQueryStatus() != Status.ENABLED) continue;
6408 20 Sep 21 nicklas 90       
6111 29 Jan 21 nicklas 91       // The Query:s are created by the LuceneFilterAction 
6111 29 Jan 21 nicklas 92       // if there is a filter in this column. We need the results to display
6111 29 Jan 21 nicklas 93       // information in the column. If there is no filter, the attribute is null
6111 29 Jan 21 nicklas 94       // and we don't display anything in the column
6552 26 Jan 22 nicklas 95       LuceneFilterAction<?> filter = jspContext.getAttribute("lucene-filter-"+idx.getId());
6156 02 Mar 21 nicklas 96
6552 26 Jan 22 nicklas 97       LuceneColumnAction<?, ?> action = idx.createColumnAction(filter);
6552 26 Jan 22 nicklas 98       String visibleColumns = Values.getStringOrNull(sc.getUserClientSetting("net.sf.basedb.varsearch.visible-columns."+idx.getId()));
6552 26 Jan 22 nicklas 99       if (visibleColumns != null) action.setColumns(SubColumn.getByName(visibleColumns.split(",")));
6552 26 Jan 22 nicklas 100       action.setHome(home);
6552 26 Jan 22 nicklas 101       action.setId("lucene."+idx.getId());
6552 26 Jan 22 nicklas 102       action.setProperty("!x.lucene."+idx.getId());
6552 26 Jan 22 nicklas 103       action.setFilterable(true);
6552 26 Jan 22 nicklas 104       action.setExportable(true);
6552 26 Jan 22 nicklas 105       action.setValueType(Type.STRING);
6552 26 Jan 22 nicklas 106       action.setTitle(idx.getName());
6848 17 Oct 22 nicklas 107       if (relatedItemContext)  action.setRelatedItemContext();
6552 26 Jan 22 nicklas 108       
6148 23 Feb 21 nicklas 109       StringBuilder sb = new StringBuilder();
6241 21 May 21 nicklas 110       if (filter != null && filter.getQueries().size() > 0)
6136 18 Feb 21 nicklas 111       {
6848 17 Oct 22 nicklas 112         SubColumn[] columns = action.getColumns();
6848 17 Oct 22 nicklas 113         StringBuilder gridTemplate = new StringBuilder();
6848 17 Oct 22 nicklas 114         gridTemplate.append("1.6rem ");
6848 17 Oct 22 nicklas 115         for (int colNo = 0; colNo < columns.length; colNo++)
6848 17 Oct 22 nicklas 116         {
6848 17 Oct 22 nicklas 117           gridTemplate.append(columns[colNo].getWidth()).append(" ");
6848 17 Oct 22 nicklas 118         }
6848 17 Oct 22 nicklas 119         
6848 17 Oct 22 nicklas 120         gridTemplate.append("18px 18px");
6236 18 May 21 nicklas 121         action.setMaxHitsPerRba(Math.min(MAX_HITS_PER_RBA, 1+MAX_HITS_PER_TABLE/filter.getItemContext().getRowsPerPage()));
6190 29 Mar 21 nicklas 122         sb.append("<div id=\"varsearch-header."+idx.getId()+"\" class=\"varsearch-grid\" style=\"grid-template-columns: "+gridTemplate+";\">");
6552 26 Jan 22 nicklas 123         QueryResult result = filter.getQueryResult();
6552 26 Jan 22 nicklas 124         String resultSummary = action.getResultSummary();
6552 26 Jan 22 nicklas 125         if (resultSummary != null)
6241 21 May 21 nicklas 126         {
6552 26 Jan 22 nicklas 127           String summaryClass = result != null && result.hasTimedOut() ? "varsearch-timeout" : "varsearch-summary";
6552 26 Jan 22 nicklas 128           sb.append("<div class=\""+summaryClass+"\" style=\"grid-column: span "+(columns.length+3)+";\">");
6552 26 Jan 22 nicklas 129           sb.append(resultSummary);
6552 26 Jan 22 nicklas 130           sb.append("</div>");
6241 21 May 21 nicklas 131         }
6552 26 Jan 22 nicklas 132         int colNo = 0;
6552 26 Jan 22 nicklas 133         boolean enableHideColumn = columns.length > 1;
6236 18 May 21 nicklas 134         sb.append("<div class=\"varsearch-hitno\">#</div>");
6156 02 Mar 21 nicklas 135         for (SubColumn col : columns)
6156 02 Mar 21 nicklas 136         {
6190 29 Mar 21 nicklas 137           sb.append("<div title=\"").append(col.getTooltip()).append("\"");
6236 18 May 21 nicklas 138           sb.append(" class=\"gridcol-"+colNo+"\"");
6190 29 Mar 21 nicklas 139           sb.append(" data-index-id=\""+idx.getId()+"\" data-col=\""+col.name()+"\" data-col-no=\""+colNo+"\"");
6190 29 Mar 21 nicklas 140           if (enableHideColumn) sb.append(" draggable=\"true\"");
6190 29 Mar 21 nicklas 141           sb.append(">");
6189 29 Mar 21 nicklas 142           if (enableHideColumn)
6189 29 Mar 21 nicklas 143           {
6190 29 Mar 21 nicklas 144             sb.append("<span class=\"hide-col auto-init\" data-auto-init=\"varsearch-hide-col\" title=\"Hide this column\"></span>");
6189 29 Mar 21 nicklas 145           }
6189 29 Mar 21 nicklas 146           sb.append(col.getTitle()).append("</div>");
6190 29 Mar 21 nicklas 147           colNo++;
6156 02 Mar 21 nicklas 148         }
6136 18 Feb 21 nicklas 149       }
6148 23 Feb 21 nicklas 150       else
6148 23 Feb 21 nicklas 151       {
6848 17 Oct 22 nicklas 152         sb.append("<div class=\"varsearch-grid\" style=\"grid-template-columns: "+(relatedItemContext?"3fr ":"")+"1fr 1fr 18px 18px;\">");
6848 17 Oct 22 nicklas 153         // Also include the Name of the matching rawbioassay
6848 17 Oct 22 nicklas 154         if (relatedItemContext) sb.append("<div>Name</div>");
6148 23 Feb 21 nicklas 155         sb.append("<div>Indexed</div><div># Variants</div>");
6148 23 Feb 21 nicklas 156       }
6149 25 Feb 21 nicklas 157       sb.append("<div style=\"border-left-width: 0; padding: 1px;\">");
6241 21 May 21 nicklas 158       sb.append("<span class=\"varsearch-help link no-print auto-init\" data-auto-init=\"varsearch-options\" data-index-id=\""+idx.getId()+"\" title=\"Configuration options\"><img src=\""+home+"/images/gear.svg\"></span>");
6170 17 Mar 21 nicklas 159       sb.append("</div>");
6170 17 Mar 21 nicklas 160       sb.append("<div style=\"border-left-width: 0; padding: 1px;\">");
6312 11 Jun 21 nicklas 161       sb.append("<a class=\"varsearch-help link no-print\" title=\"Get help about searching for variants\" href=\"https://baseplugins.thep.lu.se/wiki/net.sf.basedb.varsearch/using\" target=\"_blank\"><img src=\""+home+"/images/help.svg\"></a>");
6148 23 Feb 21 nicklas 162       sb.append("</div>");
6148 23 Feb 21 nicklas 163       sb.append("</div>");
6241 21 May 21 nicklas 164
6148 23 Feb 21 nicklas 165       action.setSubtitle(sb.toString());
6148 23 Feb 21 nicklas 166       action.setCellClass("varsearch");
6148 23 Feb 21 nicklas 167       action.setDisableOverflowCheck(true);
6129 12 Feb 21 nicklas 168       
6408 20 Sep 21 nicklas 169       actions.add(action);
6111 29 Jan 21 nicklas 170     }
6109 25 Jan 21 nicklas 171     
6408 20 Sep 21 nicklas 172     return actions.toArray(new ListColumnAction[actions.size()]);
6109 25 Jan 21 nicklas 173   }
6109 25 Jan 21 nicklas 174
6109 25 Jan 21 nicklas 175   
6552 26 Jan 22 nicklas 176   public abstract static class LuceneColumnAction<I extends LuceneIndex, F extends LuceneFilterAction<I>>
6136 18 Feb 21 nicklas 177     extends AbstractListColumnBean<RawBioAssay, String>
6109 25 Jan 21 nicklas 178   {
6109 25 Jan 21 nicklas 179
6552 26 Jan 22 nicklas 180     protected final I idx;
6552 26 Jan 22 nicklas 181     protected final F filter;
6552 26 Jan 22 nicklas 182     private SubColumn[] columns;
6236 18 May 21 nicklas 183     private int maxHitsPerRba;
6848 17 Oct 22 nicklas 184     private String home = ".";    
6848 17 Oct 22 nicklas 185     private boolean relatedItemContext = false;
6109 25 Jan 21 nicklas 186     
6552 26 Jan 22 nicklas 187     public LuceneColumnAction(I idx, F filter) 
6109 25 Jan 21 nicklas 188     {
6136 18 Feb 21 nicklas 189       this.idx = idx;
6236 18 May 21 nicklas 190       this.filter = filter;
6236 18 May 21 nicklas 191       this.maxHitsPerRba = MAX_HITS_PER_RBA;
6552 26 Jan 22 nicklas 192       this.columns = SubColumn.DEFAULT;
6552 26 Jan 22 nicklas 193     }
6552 26 Jan 22 nicklas 194     
6552 26 Jan 22 nicklas 195     /**
6552 26 Jan 22 nicklas 196       Get the default subcolumns. 
6552 26 Jan 22 nicklas 197     */
6552 26 Jan 22 nicklas 198     public SubColumn[] getDefaultColumns()
6552 26 Jan 22 nicklas 199     {
6552 26 Jan 22 nicklas 200       return SubColumn.DEFAULT;
6552 26 Jan 22 nicklas 201     }
6552 26 Jan 22 nicklas 202     
6552 26 Jan 22 nicklas 203     /**
6552 26 Jan 22 nicklas 204       Set the subcolumns to display. It is assumed that the
6552 26 Jan 22 nicklas 205       specified columns only contain allowed columns.
6552 26 Jan 22 nicklas 206     */
6552 26 Jan 22 nicklas 207     public void setColumns(SubColumn... columns)
6552 26 Jan 22 nicklas 208     {
6156 02 Mar 21 nicklas 209       this.columns = columns;
6109 25 Jan 21 nicklas 210     }
6109 25 Jan 21 nicklas 211     
6552 26 Jan 22 nicklas 212     /**
6552 26 Jan 22 nicklas 213       Get the subcolumns to display.
6552 26 Jan 22 nicklas 214     */
6552 26 Jan 22 nicklas 215     public SubColumn[] getColumns()
6552 26 Jan 22 nicklas 216     {
6552 26 Jan 22 nicklas 217       return columns;
6552 26 Jan 22 nicklas 218     }
6552 26 Jan 22 nicklas 219     
6848 17 Oct 22 nicklas 220     /**
6848 17 Oct 22 nicklas 221       Set this flag if the action is displaying result in a list for a parent/child item
6848 17 Oct 22 nicklas 222       via the extension mechanism in BASE.
6848 17 Oct 22 nicklas 223       @since 1.7
6848 17 Oct 22 nicklas 224     */
6848 17 Oct 22 nicklas 225     public void setRelatedItemContext()
6848 17 Oct 22 nicklas 226     {
6848 17 Oct 22 nicklas 227       this.relatedItemContext = true;
6848 17 Oct 22 nicklas 228     }
6848 17 Oct 22 nicklas 229     
6552 26 Jan 22 nicklas 230     public String getResultSummary()
6552 26 Jan 22 nicklas 231     {
6552 26 Jan 22 nicklas 232       StringBuilder sb = new StringBuilder();
6552 26 Jan 22 nicklas 233       QueryResult result = filter.getQueryResult();
6552 26 Jan 22 nicklas 234       if (result != null)
6552 26 Jan 22 nicklas 235       {
6552 26 Jan 22 nicklas 236         if (result.hasTimedOut())
6552 26 Jan 22 nicklas 237         {
6552 26 Jan 22 nicklas 238           int loadedHits = result.getTotalDocuments()-result.getDocumentsAfterTimeout();
6552 26 Jan 22 nicklas 239           sb.append("The query did not finish within " + result.getTimeoutInSeconds() + " seconds. ");
6552 26 Jan 22 nicklas 240           sb.append("Showing results based on <span title=\""+VarSearch.niceCount(loadedHits,"","")+"\">"+(VarSearch.formatCount(loadedHits, " hit", " hits"))+"</span>");
6552 26 Jan 22 nicklas 241           sb.append(" out of <span title=\""+VarSearch.niceCount(result.getTotalDocuments(),"","")+"\">"+VarSearch.formatCount(result.getTotalDocuments(), " total hit", " total hits")+"</span>.");
6552 26 Jan 22 nicklas 242         }
6552 26 Jan 22 nicklas 243         else if (result.getHits() >= 0)
6552 26 Jan 22 nicklas 244         {
6552 26 Jan 22 nicklas 245           sb.append("Found <span title=\""+VarSearch.niceCount(result.getTotalDocuments(),"","")+"\">");
6552 26 Jan 22 nicklas 246           sb.append(VarSearch.formatCount(result.getTotalDocuments()," variant", " variants")+"</span>");
6552 26 Jan 22 nicklas 247           sb.append(" in "+VarSearch.niceCount(result.getHits(), " raw bioassay", " raw bioassays")+".");
6552 26 Jan 22 nicklas 248         }
6552 26 Jan 22 nicklas 249       }
6552 26 Jan 22 nicklas 250       return sb.length() == 0 ? null : sb.toString();
6552 26 Jan 22 nicklas 251     }
6552 26 Jan 22 nicklas 252     
6109 25 Jan 21 nicklas 253     @Override
6136 18 Feb 21 nicklas 254     public String getValue(DbControl dc, RawBioAssay item) 
6109 25 Jan 21 nicklas 255     {
6241 21 May 21 nicklas 256       if (filter == null || filter.getQueries().isEmpty()) 
6136 18 Feb 21 nicklas 257       {
6136 18 Feb 21 nicklas 258         try
6136 18 Feb 21 nicklas 259         {
6141 19 Feb 21 nicklas 260           Integer numVariants = idx.countVariants(item);
6848 17 Oct 22 nicklas 261           if (relatedItemContext && numVariants == null) return null;
6136 18 Feb 21 nicklas 262           
6136 18 Feb 21 nicklas 263           StringBuilder sb = new StringBuilder();
6848 17 Oct 22 nicklas 264           sb.append("<div class=\"varsearch-grid\" style=\"grid-template-columns: "+(relatedItemContext?"3fr ":"")+"1fr 1fr 18px 18px;\">");
6848 17 Oct 22 nicklas 265           int colNo = 0;
6848 17 Oct 22 nicklas 266           if (relatedItemContext)
6848 17 Oct 22 nicklas 267           {
6848 17 Oct 22 nicklas 268             sb.append("<div class=\"varsearch-item gridrow-0 gridcol-"+(colNo++)+"\">").append(Base.getLinkedName(dc.getSessionControl().getId(), item, false, true)).append("</div>");
6848 17 Oct 22 nicklas 269           }
6848 17 Oct 22 nicklas 270           sb.append("<div class=\"gridcol-"+(colNo++)+" gridrow-0\">").append(numVariants == null ? "No" : "Yes").append("</div>");
6848 17 Oct 22 nicklas 271           sb.append("<div class=\"gridcol-"+(colNo++)+" gridrow-0\" style=\"grid-column: span 2;\">").append(numVariants == null ? "" : numVariants).append("</div>");
6522 20 Dec 21 nicklas 272           sb.append("<div class=\"varsearch-zoom gridrow-0\">");
6525 20 Dec 21 nicklas 273           if (numVariants != null && numVariants > 0 && idx.viewAllVariantsEnabled())
6522 20 Dec 21 nicklas 274           {
6522 20 Dec 21 nicklas 275             sb.append("<span class=\"link no-print auto-init\" data-auto-init=\"varsearch-rba-hits\" data-idx=\""+idx.getId()+"\" data-rba=\""+item.getId()+"\" title=\"List variants...\"><img src=\""+home+"/images/zoom.svg\"></span>");
6522 20 Dec 21 nicklas 276           }
6136 18 Feb 21 nicklas 277           sb.append("</div>");
6522 20 Dec 21 nicklas 278           sb.append("</div>");
6136 18 Feb 21 nicklas 279           return sb.toString();
6136 18 Feb 21 nicklas 280         }
6136 18 Feb 21 nicklas 281         catch (IOException ex)
6136 18 Feb 21 nicklas 282         {}
6136 18 Feb 21 nicklas 283         return null;
6136 18 Feb 21 nicklas 284       }
6109 25 Jan 21 nicklas 285       
6550 25 Jan 22 nicklas 286       AllDocsCollector hits = filter.getRawBioAssayHits(item.getId(),
6550 25 Jan 22 nicklas 287           new AllDocsCollector(maxHitsPerRba, new SortByChromAndPos()));
6848 17 Oct 22 nicklas 288       if (hits.getTotalHits() == 0) return null;
6130 15 Feb 21 nicklas 289       
6130 15 Feb 21 nicklas 290       NullSafeStringBuilder sb = new NullSafeStringBuilder();
6236 18 May 21 nicklas 291       sb.append("<div class=\"varsearch-grid\" style=\"grid-template-columns: 1.6rem ");
6156 02 Mar 21 nicklas 292       for (SubColumn col : columns)
6156 02 Mar 21 nicklas 293       {
6156 02 Mar 21 nicklas 294         sb.append(col.getWidth()).append(" ");
6156 02 Mar 21 nicklas 295       }
6170 17 Mar 21 nicklas 296       sb.append("18px 18px;\">");
6848 17 Oct 22 nicklas 297       int rowNo = 0;
6848 17 Oct 22 nicklas 298       if (relatedItemContext)
6848 17 Oct 22 nicklas 299       {
6848 17 Oct 22 nicklas 300         // Display the name of the raw bioassay in a separate row
6848 17 Oct 22 nicklas 301         sb.append("<div class=\"varsearch-item gridrow-0 gridcol-0\" style=\"grid-column-start: span "+(columns.length+3)+"\">").append(Base.getLinkedName(dc.getSessionControl().getId(), item, false, true)).append("</div>");
6848 17 Oct 22 nicklas 302         rowNo++;
6848 17 Oct 22 nicklas 303       }
6132 16 Feb 21 nicklas 304       int hitNo = 0;
6156 02 Mar 21 nicklas 305       int lastCol = columns.length - 1;
6530 20 Dec 21 nicklas 306       boolean indexAllGenotypes = idx.getIndexAllGenotypes();
6248 25 May 21 nicklas 307       for (Document hit : hits.getDocuments())
6130 15 Feb 21 nicklas 308       {
6530 20 Dec 21 nicklas 309         String gt = hit.get("gt");
6848 17 Oct 22 nicklas 310         String cls = " gridrow-"+rowNo;
6530 20 Dec 21 nicklas 311         if (indexAllGenotypes)
6530 20 Dec 21 nicklas 312         {
6541 17 Jan 22 nicklas 313           if (gt == null || "./.".equals(gt))
6530 20 Dec 21 nicklas 314           {
6530 20 Dec 21 nicklas 315             cls += " no-data";
6530 20 Dec 21 nicklas 316           }
6530 20 Dec 21 nicklas 317           else if (!gt.equals("0/0"))
6530 20 Dec 21 nicklas 318           {
6530 20 Dec 21 nicklas 319             cls += " is-variant";
6530 20 Dec 21 nicklas 320           }
6530 20 Dec 21 nicklas 321         }
6530 20 Dec 21 nicklas 322         sb.append("<div class=\"varsearch-hitno"+cls+"\">"+(hitNo+1)+"</div>");
6156 02 Mar 21 nicklas 323         int colNo = 0;
6156 02 Mar 21 nicklas 324         for (SubColumn col : columns)
6156 02 Mar 21 nicklas 325         {
6530 20 Dec 21 nicklas 326           sb.append("<div class=\"gridcol-"+colNo+cls+"\"");
6156 02 Mar 21 nicklas 327           if (colNo == lastCol) sb.append(" style=\"grid-column: span 2;\"");
6156 02 Mar 21 nicklas 328           sb.append(">");
6156 02 Mar 21 nicklas 329           col.display(hit, sb);
6156 02 Mar 21 nicklas 330           sb.append("</div>");
6156 02 Mar 21 nicklas 331           colNo++;
6156 02 Mar 21 nicklas 332         }
6530 20 Dec 21 nicklas 333         sb.append("<div class=\"varsearch-zoom "+cls+"\">");
6553 27 Jan 22 nicklas 334         sb.append("<span class=\"link no-print auto-init\" data-auto-init=\"varsearch-hit\" data-idx=\""+idx.getId()+"\" data-rba=\""+item.getId()+"\" data-file=\""+hit.get("file")+"\" data-line=\""+hit.get("line")+"\" data-snp=\""+HTML.encodeTags(hit.get("snpId"))+"\" title=\"View more details...\"><img src=\""+home+"/images/zoom.svg\"></span>");
6170 17 Mar 21 nicklas 335         sb.append("</div>");
6132 16 Feb 21 nicklas 336         hitNo++;
6848 17 Oct 22 nicklas 337         rowNo++;
6248 25 May 21 nicklas 338       }
6248 25 May 21 nicklas 339       if (hits.getTotalHits() >= maxHitsPerRba)
6248 25 May 21 nicklas 340       {
6248 25 May 21 nicklas 341         int moreHits = hits.getTotalHits()-hitNo;
6248 25 May 21 nicklas 342         if (moreHits > 1)
6236 18 May 21 nicklas 343         {
6248 25 May 21 nicklas 344           sb.append("<div class=\"varsearch-morehits\" style=\"grid-column: span "+(columns.length+3)+";\">");
6248 25 May 21 nicklas 345           sb.append("+"+moreHits+" more hits...");
6248 25 May 21 nicklas 346           sb.append("</div>");
6236 18 May 21 nicklas 347         }
6130 15 Feb 21 nicklas 348       }
6130 15 Feb 21 nicklas 349       sb.append("</div>\n");
6130 15 Feb 21 nicklas 350       return sb.toString();
6130 15 Feb 21 nicklas 351     }
6130 15 Feb 21 nicklas 352     
6130 15 Feb 21 nicklas 353     @Override
6130 15 Feb 21 nicklas 354     public Formatter<?> getExportFormatter() 
6130 15 Feb 21 nicklas 355     {
6130 15 Feb 21 nicklas 356       return new ToStringFormatter<>();
6130 15 Feb 21 nicklas 357     }
6848 17 Oct 22 nicklas 358     
6848 17 Oct 22 nicklas 359     /**
6848 17 Oct 22 nicklas 360       This will fix the display problem with multiple hits and overflow due to 100% height of each hit.
6848 17 Oct 22 nicklas 361       Requires BASE 3.19.4
6848 17 Oct 22 nicklas 362       @since 1.7
6848 17 Oct 22 nicklas 363     */
7059 13 Mar 23 nicklas 364     @SuppressWarnings({ "rawtypes", "unchecked" })
7059 13 Mar 23 nicklas 365     @Override
7059 13 Mar 23 nicklas 366     public Formatter<Collection<? super String>> getCollectionFormatter()
6848 17 Oct 22 nicklas 367     {
7059 13 Mar 23 nicklas 368       return new PrefixSuffixFormatter(
6848 17 Oct 22 nicklas 369           "<div class=\"varsearch-collection\">", false, 
6848 17 Oct 22 nicklas 370           new CollectionFormatter<>("", true, getFormatter()), 
6848 17 Oct 22 nicklas 371           "</div>", false);
6848 17 Oct 22 nicklas 372     }
6130 15 Feb 21 nicklas 373
6130 15 Feb 21 nicklas 374     @Override
6136 18 Feb 21 nicklas 375     public List<String> getExportValue(DbControl dc, RawBioAssay item) 
6130 15 Feb 21 nicklas 376     {
6241 21 May 21 nicklas 377       if (filter == null || filter.getQueries().isEmpty()) 
6136 18 Feb 21 nicklas 378       {
6136 18 Feb 21 nicklas 379         try
6136 18 Feb 21 nicklas 380         {
6141 19 Feb 21 nicklas 381           Integer numVariants = idx.countVariants(item);
6141 19 Feb 21 nicklas 382           return numVariants == null ? null : Arrays.asList(Integer.toString(numVariants));
6136 18 Feb 21 nicklas 383         }
6136 18 Feb 21 nicklas 384         catch (IOException ex)
6136 18 Feb 21 nicklas 385         {}
6136 18 Feb 21 nicklas 386         return null;
6136 18 Feb 21 nicklas 387       }
6130 15 Feb 21 nicklas 388       
6550 25 Jan 22 nicklas 389       AllDocsCollector hits = filter.getRawBioAssayHits(item.getId(),
6550 25 Jan 22 nicklas 390           new AllDocsCollector(Integer.MAX_VALUE, new SortByChromAndPos()));
6248 25 May 21 nicklas 391       List<String> result = new ArrayList<>(hits.getTotalHits());
6130 15 Feb 21 nicklas 392       
6248 25 May 21 nicklas 393       for (Document hit : hits.getDocuments())
6130 15 Feb 21 nicklas 394       {
6130 15 Feb 21 nicklas 395         NullSafeStringBuilder sb = new NullSafeStringBuilder();
6848 17 Oct 22 nicklas 396         if (relatedItemContext)
6848 17 Oct 22 nicklas 397         {
6848 17 Oct 22 nicklas 398           sb.append("bioassay="+item.getName()+";");
6848 17 Oct 22 nicklas 399         }
6156 02 Mar 21 nicklas 400         for (SubColumn col : columns)
6156 02 Mar 21 nicklas 401         {
6156 02 Mar 21 nicklas 402           col.export(hit, sb);
6156 02 Mar 21 nicklas 403         }
6130 15 Feb 21 nicklas 404         // Comma is typically used by the exporter as separator for multi-value fields
6130 15 Feb 21 nicklas 405         // so we change (hgvsc and hgvsp) to use pipe instead
6130 15 Feb 21 nicklas 406         result.add(sb.toString().replace(", ", "|"));
6130 15 Feb 21 nicklas 407       }
6130 15 Feb 21 nicklas 408       return result;
6130 15 Feb 21 nicklas 409     }
6130 15 Feb 21 nicklas 410
6522 20 Dec 21 nicklas 411     /**
6522 20 Dec 21 nicklas 412       Set the path to the HOME directory (so that URLs to images are correct).
6522 20 Dec 21 nicklas 413       The default path is '.'.
6522 20 Dec 21 nicklas 414       @since 1.4
6522 20 Dec 21 nicklas 415     */
6522 20 Dec 21 nicklas 416     public void setHome(String home)
6170 17 Mar 21 nicklas 417     {
6170 17 Mar 21 nicklas 418       this.home = home;
6170 17 Mar 21 nicklas 419     }
6170 17 Mar 21 nicklas 420     
6522 20 Dec 21 nicklas 421     /**
6522 20 Dec 21 nicklas 422       Set the max number of hits to display per raw bioassay.
6522 20 Dec 21 nicklas 423       @since 1.4
6522 20 Dec 21 nicklas 424     */
6522 20 Dec 21 nicklas 425     public void setMaxHitsPerRba(int maxHits)
6236 18 May 21 nicklas 426     {
6236 18 May 21 nicklas 427       this.maxHitsPerRba = maxHits;
6236 18 May 21 nicklas 428     }
6236 18 May 21 nicklas 429     
6156 02 Mar 21 nicklas 430   }
6156 02 Mar 21 nicklas 431   
6156 02 Mar 21 nicklas 432   /**
6156 02 Mar 21 nicklas 433     Represents a sub-column in the variant search column.
6156 02 Mar 21 nicklas 434   */
6156 02 Mar 21 nicklas 435   public static enum SubColumn
6156 02 Mar 21 nicklas 436   {
6189 29 Mar 21 nicklas 437     
6156 02 Mar 21 nicklas 438     /**
6156 02 Mar 21 nicklas 439       Gene name.
6156 02 Mar 21 nicklas 440     */
6171 18 Mar 21 nicklas 441     GENE("Gene", null, "2fr", "gene"),
6156 02 Mar 21 nicklas 442     /**
6156 02 Mar 21 nicklas 443       Genomic position is a combination of the chromosome:position.
6156 02 Mar 21 nicklas 444       The value is linked to Genome Browser.
6156 02 Mar 21 nicklas 445     */
6171 18 Mar 21 nicklas 446     POS("Position", null, "2fr") 
6130 15 Feb 21 nicklas 447     {
6156 02 Mar 21 nicklas 448       @Override
6156 02 Mar 21 nicklas 449       public void display(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 450       {
6156 02 Mar 21 nicklas 451         String chr = hit.get("chrom");
6156 02 Mar 21 nicklas 452         String pos = hit.get("pos");
6156 02 Mar 21 nicklas 453         if (chr == null || pos == null) return;
6156 02 Mar 21 nicklas 454         
6156 02 Mar 21 nicklas 455         long lpos = Values.getLong(pos);
6263 28 May 21 nicklas 456
6156 02 Mar 21 nicklas 457         sb.append("<a target=\"_blank\" title=\"View in Genome Browser (new window)\"");
6156 02 Mar 21 nicklas 458         sb.append(" href=\"http://genome.ucsc.edu/cgi-bin/hgTracks?db=hg38");
6156 02 Mar 21 nicklas 459         sb.append("&position="+chr+":"+(lpos-30)+"-"+(lpos+30));
6156 02 Mar 21 nicklas 460         sb.append("&highlight="+chr+":"+pos+"-"+pos);
6156 02 Mar 21 nicklas 461         sb.append("\">");
6156 02 Mar 21 nicklas 462         sb.append(chr+":"+pos+"</a>");
6156 02 Mar 21 nicklas 463       }
6156 02 Mar 21 nicklas 464       
6156 02 Mar 21 nicklas 465       @Override
6156 02 Mar 21 nicklas 466       public void export(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 467       {
6156 02 Mar 21 nicklas 468         sb.append(keyVal("chrom", hit.get("chrom")));
6156 02 Mar 21 nicklas 469         sb.append(keyVal("pos", hit.get("pos")));
6156 02 Mar 21 nicklas 470       }
6156 02 Mar 21 nicklas 471     },
6156 02 Mar 21 nicklas 472     /**
6156 02 Mar 21 nicklas 473       Reference and alternate alleles.
6156 02 Mar 21 nicklas 474     */
6171 18 Mar 21 nicklas 475     REF_ALT("Ref â€º Alt", null, "1fr") 
6156 02 Mar 21 nicklas 476     {
6156 02 Mar 21 nicklas 477       @Override
6156 02 Mar 21 nicklas 478       public void display(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 479       {
6156 02 Mar 21 nicklas 480         String ref = hit.get("ref");
6156 02 Mar 21 nicklas 481         String alt = hit.get("alt");
6156 02 Mar 21 nicklas 482         if(ref == null || alt == null) return;
6156 02 Mar 21 nicklas 483         
6156 02 Mar 21 nicklas 484         if (ref.length() > 6) 
6156 02 Mar 21 nicklas 485         {
6156 02 Mar 21 nicklas 486           ref = "<span title=\""+ref+"\">"+ref.substring(0, 5)+"...</span>";
6156 02 Mar 21 nicklas 487         }
6156 02 Mar 21 nicklas 488         if (alt.length() > 6) 
6156 02 Mar 21 nicklas 489         {
6156 02 Mar 21 nicklas 490           alt = "<span title=\""+alt+"\">"+alt.substring(0, 5)+"...</span>";
6156 02 Mar 21 nicklas 491         }
6156 02 Mar 21 nicklas 492         sb.append(ref + "&#8239;›&#8239;" + alt); // &#8239; is a narrow non-breaking space
6156 02 Mar 21 nicklas 493       }
6156 02 Mar 21 nicklas 494       @Override
6156 02 Mar 21 nicklas 495       public void export(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 496       {
6156 02 Mar 21 nicklas 497         sb.append(keyVal("ref", hit.get("ref")));
6156 02 Mar 21 nicklas 498         sb.append(keyVal("alt", hit.get("alt")));
6156 02 Mar 21 nicklas 499       }
6156 02 Mar 21 nicklas 500     },
6156 02 Mar 21 nicklas 501     /**
6156 02 Mar 21 nicklas 502       Type of variant. SNV, Insertion, Deletion or Complex.
6156 02 Mar 21 nicklas 503      */
6171 18 Mar 21 nicklas 504     TYPE("Type", null, "1fr", "type"),
6156 02 Mar 21 nicklas 505     /**
6174 19 Mar 21 nicklas 506       Expected effect of the variant.
6174 19 Mar 21 nicklas 507      */
6174 19 Mar 21 nicklas 508     EFFECT("Effect", null, "2fr", "effect"),
6174 19 Mar 21 nicklas 509     /**
6156 02 Mar 21 nicklas 510       Transcript change.
6156 02 Mar 21 nicklas 511     */
6171 18 Mar 21 nicklas 512     HGVSC("HGVS.c", null, "2fr", "c", "hgvsc"),
6156 02 Mar 21 nicklas 513     /**
6156 02 Mar 21 nicklas 514       Protein change.
6156 02 Mar 21 nicklas 515     */
6171 18 Mar 21 nicklas 516     HGVSP("HGVS.p", null, "2fr", "p", "hgvsp"),
6156 02 Mar 21 nicklas 517     /**
6156 02 Mar 21 nicklas 518       Links to COSMIC and dbSNP (rsId) if they exists.
6156 02 Mar 21 nicklas 519     */
6171 18 Mar 21 nicklas 520     LINKS("Links", null, "3fr")
6156 02 Mar 21 nicklas 521     {
6156 02 Mar 21 nicklas 522
6156 02 Mar 21 nicklas 523       @Override
6156 02 Mar 21 nicklas 524       public void display(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 525       {
6156 02 Mar 21 nicklas 526         String cosmicId = hit.get("cosmic");
6156 02 Mar 21 nicklas 527         String rsId = hit.get("rsid");
6156 02 Mar 21 nicklas 528         
6156 02 Mar 21 nicklas 529         if (cosmicId == null && rsId == null) return;
6156 02 Mar 21 nicklas 530         String sep = "";
6156 02 Mar 21 nicklas 531         sb.append("<div>");
6156 02 Mar 21 nicklas 532         if (cosmicId != null)
6156 02 Mar 21 nicklas 533         {
6156 02 Mar 21 nicklas 534           sb.append("<a target=\"_blank\" title=\"View in COSMIC (new window)\"");
6156 02 Mar 21 nicklas 535           sb.append(" href=\"https://cancer.sanger.ac.uk/cosmic/search?q="+cosmicId+"\">");
6156 02 Mar 21 nicklas 536           sb.append(cosmicId+"</a>");
6156 02 Mar 21 nicklas 537           sep = ", ";
6156 02 Mar 21 nicklas 538         }
6156 02 Mar 21 nicklas 539         if (rsId != null)
6156 02 Mar 21 nicklas 540         {
6156 02 Mar 21 nicklas 541           sb.append(sep);
6156 02 Mar 21 nicklas 542           sb.append("<a target=\"_blank\" title=\"View in dbSNP (new window)\"");
6156 02 Mar 21 nicklas 543           sb.append(" href=\"https://www.ncbi.nlm.nih.gov/snp/"+rsId+"\">");
6156 02 Mar 21 nicklas 544           sb.append(rsId+"</a>");
6156 02 Mar 21 nicklas 545         }
6156 02 Mar 21 nicklas 546         sb.append("</div>");
6156 02 Mar 21 nicklas 547       }
6156 02 Mar 21 nicklas 548       @Override
6156 02 Mar 21 nicklas 549       public void export(Document hit, NullSafeStringBuilder sb) 
6156 02 Mar 21 nicklas 550       {
6156 02 Mar 21 nicklas 551         sb.append(keyVal("cosmic", hit.get("cosmic")));
6156 02 Mar 21 nicklas 552         sb.append(keyVal("rsid", hit.get("rsid")));
6156 02 Mar 21 nicklas 553       }
6171 18 Mar 21 nicklas 554     },
6171 18 Mar 21 nicklas 555     GT("GT", "Genotype", "1fr", "gt"),
6171 18 Mar 21 nicklas 556     DP("DP", "Depth", "1fr", "dp"),
6171 18 Mar 21 nicklas 557     VD("VD", "Variant depth", "1fr", "vd"),
6171 18 Mar 21 nicklas 558     AF("AF", "Allele frequency", "1fr", "af")
6171 18 Mar 21 nicklas 559     ;
6156 02 Mar 21 nicklas 560     
6189 29 Mar 21 nicklas 561     public static SubColumn[] DEFAULT = { GENE, POS, REF_ALT, TYPE, HGVSC, HGVSP, LINKS };
6189 29 Mar 21 nicklas 562     
6189 29 Mar 21 nicklas 563     public static SubColumn[] getByName(String[] cols)
6189 29 Mar 21 nicklas 564     {
6189 29 Mar 21 nicklas 565       List<SubColumn> result = new ArrayList<>();
6189 29 Mar 21 nicklas 566       for (String c : cols)
6189 29 Mar 21 nicklas 567       {
6189 29 Mar 21 nicklas 568         try
6189 29 Mar 21 nicklas 569         {
6189 29 Mar 21 nicklas 570           result.add(SubColumn.valueOf(c));
6189 29 Mar 21 nicklas 571         }
6189 29 Mar 21 nicklas 572         catch (RuntimeException ex)
6189 29 Mar 21 nicklas 573         {}
6189 29 Mar 21 nicklas 574       }
6189 29 Mar 21 nicklas 575       return result.size() == 0 ? DEFAULT : result.toArray(new SubColumn[result.size()]);
6189 29 Mar 21 nicklas 576     }
6189 29 Mar 21 nicklas 577     
6156 02 Mar 21 nicklas 578     private final String title;
6171 18 Mar 21 nicklas 579     private final String tooltip;
6156 02 Mar 21 nicklas 580     private final String width;
6156 02 Mar 21 nicklas 581     private final String docField;
6156 02 Mar 21 nicklas 582     private final String exportField;
6156 02 Mar 21 nicklas 583     
6171 18 Mar 21 nicklas 584     SubColumn(String title, String tooltip, String width)
6156 02 Mar 21 nicklas 585     {
6171 18 Mar 21 nicklas 586       this(title, tooltip, width, null, null);
6130 15 Feb 21 nicklas 587     }
6156 02 Mar 21 nicklas 588
6171 18 Mar 21 nicklas 589     SubColumn(String title, String tooltip, String width, String field)
6156 02 Mar 21 nicklas 590     {
6171 18 Mar 21 nicklas 591       this(title, tooltip, width, field, field);
6156 02 Mar 21 nicklas 592     }
6130 15 Feb 21 nicklas 593     
6171 18 Mar 21 nicklas 594     SubColumn(String title, String tooltip, String width, String docField, String exportField)
6156 02 Mar 21 nicklas 595     {
6156 02 Mar 21 nicklas 596       this.title = title;
6171 18 Mar 21 nicklas 597       this.tooltip = tooltip == null ? title : tooltip;
6156 02 Mar 21 nicklas 598       this.width = width;
6156 02 Mar 21 nicklas 599       this.docField = docField;
6156 02 Mar 21 nicklas 600       this.exportField = exportField;
6156 02 Mar 21 nicklas 601     }
6142 22 Feb 21 nicklas 602     
6156 02 Mar 21 nicklas 603     /**
6156 02 Mar 21 nicklas 604       The column title.
6156 02 Mar 21 nicklas 605     */
6156 02 Mar 21 nicklas 606     public String getTitle()
6139 18 Feb 21 nicklas 607     {
6156 02 Mar 21 nicklas 608       return title;
6139 18 Feb 21 nicklas 609     }
6139 18 Feb 21 nicklas 610     
6156 02 Mar 21 nicklas 611     /**
6171 18 Mar 21 nicklas 612       The column tooltip.
6171 18 Mar 21 nicklas 613     */
6171 18 Mar 21 nicklas 614     public String getTooltip()
6171 18 Mar 21 nicklas 615     {
6171 18 Mar 21 nicklas 616       return tooltip;
6171 18 Mar 21 nicklas 617     }
6171 18 Mar 21 nicklas 618     
6171 18 Mar 21 nicklas 619     /**
6156 02 Mar 21 nicklas 620       The width of the column to use in the grid layout.
6156 02 Mar 21 nicklas 621     */
6156 02 Mar 21 nicklas 622     public String getWidth()
6142 22 Feb 21 nicklas 623     {
6156 02 Mar 21 nicklas 624       return width;
6142 22 Feb 21 nicklas 625     }
6142 22 Feb 21 nicklas 626     
6156 02 Mar 21 nicklas 627     /**
6156 02 Mar 21 nicklas 628       Display the column. The default implementation get the value
6156 02 Mar 21 nicklas 629       from the defined field. 
6156 02 Mar 21 nicklas 630     */
6156 02 Mar 21 nicklas 631     public void display(Document hit, NullSafeStringBuilder sb)
6129 12 Feb 21 nicklas 632     {
6156 02 Mar 21 nicklas 633       sb.append(hit.get(docField));
6129 12 Feb 21 nicklas 634     }
6129 12 Feb 21 nicklas 635     
6156 02 Mar 21 nicklas 636     /**
6156 02 Mar 21 nicklas 637       Export the column. The default implementation get the key and 
6156 02 Mar 21 nicklas 638       value from the defined field. 
6156 02 Mar 21 nicklas 639     */
6156 02 Mar 21 nicklas 640     public void export(Document hit, NullSafeStringBuilder sb)
6129 12 Feb 21 nicklas 641     {
6156 02 Mar 21 nicklas 642       sb.append(keyVal(exportField, hit.get(docField)));
6129 12 Feb 21 nicklas 643     }
6156 02 Mar 21 nicklas 644     
6156 02 Mar 21 nicklas 645     private static String keyVal(String key, String val)
6156 02 Mar 21 nicklas 646     {
6156 02 Mar 21 nicklas 647       if (val == null || val.isBlank()) return null;
6156 02 Mar 21 nicklas 648       return key+"="+val+";";
6156 02 Mar 21 nicklas 649     }
6109 25 Jan 21 nicklas 650
6109 25 Jan 21 nicklas 651   }
6156 02 Mar 21 nicklas 652   
6109 25 Jan 21 nicklas 653 }