<commit_before>package com.github.fengtan.sophie.tables;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.NoOpResponseParser;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.LukeResponse.FieldInfo;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.luke.FieldFlag;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;

import com.github.fengtan.sophie.Sophie;
import com.github.fengtan.sophie.beans.Config;
import com.github.fengtan.sophie.beans.SolrUtils;
import com.github.fengtan.sophie.beans.SophieException;
import com.github.fengtan.sophie.dialogs.DocumentEditDateValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditListValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditTextValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditValueDialog;
import com.github.fengtan.sophie.dialogs.ExceptionDialog;
import com.github.fengtan.sophie.toolbars.ChangeListener;

public class DocumentsTable { // TODO extend Composite ?

	private static final Color YELLOW = Display.getCurrent().getSystemColor(SWT.COLOR_YELLOW);
	private static final Color RED = Display.getCurrent().getSystemColor(SWT.COLOR_RED);
	private static final Color GREEN = Display.getCurrent().getSystemColor(SWT.COLOR_GREEN);
	
	private static final char SIGNIFIER_SORTED_ASC = '\u25B4';
	private static final char SIGNIFIER_SORTED_DESC = '\u25BE';
	private static final char SIGNIFIER_UNSORTABLE = '\u2205';
	
	private static final String LABEL_NOT_STORED = "(not stored)";
	private static final String LABEL_EMPTY = "(empty)";
	
	// How many documents do we fetch from Solr at a time.
	private static final int PAGE_SIZE = Config.getDocumentsPageSize();
	
	// How many facets values do we display at most.
	private static final int FACET_LIMIT = Config.getDocumentsFacetsLimit();
	
	private Map<Integer, SolrDocumentList> pages;
	private List<FieldInfo> fields;
	private Map<String, FacetField> facets;
	private Map<String, String> filters = new HashMap<String, String>();
	private String uniqueField; // TODO update when refresh ?()
	private String sortField; // TODO support sort on multiple fields ?
	private ORDER sortOrder = ORDER.asc;

	// List of documents locally updated/deleted/added.
	private List<SolrDocument> documentsUpdated;
	private List<SolrDocument> documentsDeleted;
	private List<SolrDocument> documentsAdded;
	
	// Change listener, allows the 'Upload' button to detect whether there are changes to send to Solr.
	private ChangeListener changeListener;
	
	private Table table;

	public DocumentsTable(Composite parent, SelectionListener selectionListener, ChangeListener changeListener) throws SophieException {
		this.fields = SolrUtils.getRemoteFields();
		this.facets = getRemoteFacets();
		this.uniqueField = SolrUtils.getRemoteUniqueField();
		this.sortField = uniqueField; // By default we sort documents by uniqueKey TODO what if uniqueKey is not sortable ?
		this.table = createTable(parent, selectionListener);
		this.changeListener = changeListener;
		// Initialize cache + row count.
		refresh();
	}
	
	/**
	 * Create the Table
	 */
	private Table createTable(final Composite parent, SelectionListener selectionListener) throws SophieException {
		int style = SWT.SINGLE | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.HIDE_SELECTION | SWT.VIRTUAL;

		final Table table = new Table(parent, style);

		GridData gridData = new GridData(GridData.FILL_BOTH);
		gridData.grabExcessVerticalSpace = true;
		table.setLayoutData(gridData);

		table.setLinesVisible(true);
		table.setHeaderVisible(true);
		table.addSelectionListener(selectionListener);
		
		// Add KeyListener to delete documents.
		// TODO hitting "suppr" or clicking the button (in the toolbar) a second time should remove the deletion.
		table.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent event) {
				if (event.keyCode == SWT.DEL) {
					deleteSelectedDocument();
				}
			}
		});

		// Initialize item count to 1 so we can populate the first row with filters.
		table.setItemCount(1);
		table.addListener(SWT.SetData, new Listener() {
			@Override
			public void handleEvent(Event event) {
	            TableItem item = (TableItem) event.item;
	            int rowIndex = table.indexOf(item);
	            // The first line is populated by filters.
	            if (rowIndex == 0) {
	            	// TODO might need to populate if existing filters
	            	return;
	            }
	            SolrDocument document;
	            // The last lines are populated by local additions.
	            if (rowIndex >= table.getItemCount() - documentsAdded.size()) {
	            	document = documentsAdded.get(documentsAdded.size() - table.getItemCount() + rowIndex);
	            	item.setBackground(GREEN);
	            } else {
		            // Use rowIndex - 1 since the first line is used for filters.
		            // TODO make sure the last document gets displayed.
	            	try {
			            document = getRemoteDocument(rowIndex - 1);	            		
	            	} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to populate table", e));
						return;
	            	}
	            }
	            // First column is used to show the row ID.
	            item.setText(0, Integer.toString(rowIndex));
	            // Subsequent columns are used to show field values.
		        for (int i=0; i<fields.size(); i++) {
		        	String fieldName = fields.get(i).getName();
		        	// If field is not stored, display message.
		        	// TODO disable doubleclick on unstored fields ?
		        	// TODO verify "(not stored)" is not sent to Solr when updating/creating a new document
		        	// TODO prepopulate if create a new document ?
					// field.getFlags() is not populated when lukeRequest.setSchema(false) so we parse flags ourselves based on field.getSchema() TODO open ticket
		        	if (!FieldInfo.parseFlags(fields.get(i).getSchema()).contains(FieldFlag.STORED)) {
		        		item.setText(i+1, LABEL_NOT_STORED);
		        	} else {
		        		Object value = document.getFieldValue(fieldName);
		            	item.setText(i+1, value == null ? StringUtils.EMPTY : value.toString());
		            }
		        }
		        // Store document in item.
		        item.setData("document", document);
		        // TODO use item.setText(String[] values) ?
		        // TODO store status insert/update/delete using item.setData() ?
			}
		});
		
		// Add columns. First one is used to show row IDs.
		TableColumn columnNumber = new TableColumn(table, SWT.LEFT);
		columnNumber.setText("#");
		columnNumber.pack(); // TODO needed ? might be worth to setLayout() to get rid of this
		for (final FieldInfo field:fields) {
			final TableColumn column = new TableColumn(table, SWT.LEFT);
			// Add space padding so we can see the sort signifier.
			// TODO set sort signifier on uniqueKey by default ?
			// TODO refactor with selection listener
			column.setText(field.getName()+(isFieldSortable(field) ? "     " : " "+SIGNIFIER_UNSORTABLE));
			// TODO do we getData("field") somewhere ? if not, then no need to setData("field")
			column.setData("field", field);
			if (!isFieldSortable(field)) {
				column.setToolTipText("Cannot sort on a field that is not indexed, is multivalued or has doc values");
			}
			// Sort column when click on the header
			column.addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					if (!isFieldSortable(field)) {
						return;
					}
					// Clicking on the current sort field toggles the direction.
					// Clicking on a new field changes the sort field.
					if (StringUtils.equals(sortField, field.getName())) {
						sortOrder = ORDER.asc.equals(sortOrder) ? ORDER.desc : ORDER.asc;
					} else {
						sortField = field.getName();
					}
					// Clear signifier on all columns, add signifier on sorted column.
					char signifier = ORDER.asc.equals(sortOrder) ? SIGNIFIER_SORTED_ASC : SIGNIFIER_SORTED_DESC;
					for (TableColumn c:table.getColumns()) {
						FieldInfo f = (FieldInfo) c.getData("field");
						if (f != null) {
							if (!isFieldSortable(f)) {
								c.setText(f.getName()+" "+SIGNIFIER_UNSORTABLE);
							} else {
								c.setText(f.getName()+((column == c) ? " "+signifier : StringUtils.EMPTY));	
							}
						}

					}
					// Re-populate table.
					try {
						refresh();
					} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to refresh documents from Solr server", e));
					}
				}
			});
			column.pack(); // TODO needed ? might be worth to setLayout() to get rid of this
		}
		
		// Add filters.
		TableItem[] items = table.getItems(); // TODO do we need to load all items ?
		TableEditor editor = new TableEditor(table);
		for(int i=0; i<fields.size(); i++) {
			// TODO check if map contains field ?
			final FacetField facet = facets.get(fields.get(i).getName());
			// If facet is null then we cannot filter on this field (e.g. the field is not indexed).
			if (facet == null) {
				// TODO grey out items[0] columns i
				continue;
			}
			/*
			 * TODO could use this instead of storing field name in setData() 
			 * Point point = new Point(event.x, event.y);
		     * TableItem item = table.getItem(point);
		     * if (item == null) {
		     *   return;
		     * }
		     * for (int i=0; i<fields.size(); i++) {
		     *   Rectangle rect = item.getBounds(i);
		     *   if (rect.contains(point)) {
		     *     SolrDocument document = (SolrDocument) item.getData("document");
		     *     dialog.open(item.getText(i), fields.get(i).getName(), document);
		     *   }
		     * }
			 */
			final CCombo combo = new CCombo(table, SWT.BORDER);			
			combo.add(StringUtils.EMPTY);
			// If the number of facet values is the max, then the list of facet values might not be complete. Hence we use a free text field instead of populating the combo.
			if (facet.getValueCount() < FACET_LIMIT) {
				for(Count count:facet.getValues()) {
					combo.add(Objects.toString(count.getName(), LABEL_EMPTY)+" ("+count.getCount()+")");
				}
				combo.addSelectionListener(new SelectionAdapter() {
					@Override
					public void widgetSelected(SelectionEvent event) {
						// Extract original value (e.g. "Foobar") from widget value (e.g. "Foobar (3)")
						Pattern pattern = Pattern.compile("^(.*) \\([0-9]+\\)$");
						Matcher matcher = pattern.matcher(combo.getText());
						// TODO if cannot find pattern, then should log WARNING
						String filterValue = matcher.find() ? matcher.group(1) : StringUtils.EMPTY;
						// Populate combo with original value.
						combo.setText(filterValue);
					};
				});
			} else {
				combo.add(LABEL_EMPTY);
			}
			
			// Fire filters + refresh when user selects a value.
			combo.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					updateFilters(facet.getName(), combo.getText());
					try {
						refresh();
					} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to refresh documents from Solr server", e));
					}
				}
			});
			
			// Filter (refresh) results when user hits "Enter" while editing one of the combos.
			combo.addListener(SWT.KeyUp, new Listener() {<commit_msg>Prevent unnecessary request when user selects the same filter.<commit_after>package com.github.fengtan.sophie.tables;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.NoOpResponseParser;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.LukeResponse.FieldInfo;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.luke.FieldFlag;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;

import com.github.fengtan.sophie.Sophie;
import com.github.fengtan.sophie.beans.Config;
import com.github.fengtan.sophie.beans.SolrUtils;
import com.github.fengtan.sophie.beans.SophieException;
import com.github.fengtan.sophie.dialogs.DocumentEditDateValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditListValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditTextValueDialog;
import com.github.fengtan.sophie.dialogs.DocumentEditValueDialog;
import com.github.fengtan.sophie.dialogs.ExceptionDialog;
import com.github.fengtan.sophie.toolbars.ChangeListener;

public class DocumentsTable { // TODO extend Composite ?

	private static final Color YELLOW = Display.getCurrent().getSystemColor(SWT.COLOR_YELLOW);
	private static final Color RED = Display.getCurrent().getSystemColor(SWT.COLOR_RED);
	private static final Color GREEN = Display.getCurrent().getSystemColor(SWT.COLOR_GREEN);
	
	private static final char SIGNIFIER_SORTED_ASC = '\u25B4';
	private static final char SIGNIFIER_SORTED_DESC = '\u25BE';
	private static final char SIGNIFIER_UNSORTABLE = '\u2205';
	
	private static final String LABEL_NOT_STORED = "(not stored)";
	private static final String LABEL_EMPTY = "(empty)";
	
	// How many documents do we fetch from Solr at a time.
	private static final int PAGE_SIZE = Config.getDocumentsPageSize();
	
	// How many facets values do we display at most.
	private static final int FACET_LIMIT = Config.getDocumentsFacetsLimit();
	
	private Map<Integer, SolrDocumentList> pages;
	private List<FieldInfo> fields;
	private Map<String, FacetField> facets;
	private Map<String, String> filters = new HashMap<String, String>();
	private String uniqueField; // TODO update when refresh ?()
	private String sortField; // TODO support sort on multiple fields ?
	private ORDER sortOrder = ORDER.asc;

	// List of documents locally updated/deleted/added.
	private List<SolrDocument> documentsUpdated;
	private List<SolrDocument> documentsDeleted;
	private List<SolrDocument> documentsAdded;
	
	// Change listener, allows the 'Upload' button to detect whether there are changes to send to Solr.
	private ChangeListener changeListener;
	
	private Table table;

	public DocumentsTable(Composite parent, SelectionListener selectionListener, ChangeListener changeListener) throws SophieException {
		this.fields = SolrUtils.getRemoteFields();
		this.facets = getRemoteFacets();
		this.uniqueField = SolrUtils.getRemoteUniqueField();
		this.sortField = uniqueField; // By default we sort documents by uniqueKey TODO what if uniqueKey is not sortable ?
		this.table = createTable(parent, selectionListener);
		this.changeListener = changeListener;
		// Initialize cache + row count.
		refresh();
	}
	
	/**
	 * Create the Table
	 */
	private Table createTable(final Composite parent, SelectionListener selectionListener) throws SophieException {
		int style = SWT.SINGLE | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.HIDE_SELECTION | SWT.VIRTUAL;

		final Table table = new Table(parent, style);

		GridData gridData = new GridData(GridData.FILL_BOTH);
		gridData.grabExcessVerticalSpace = true;
		table.setLayoutData(gridData);

		table.setLinesVisible(true);
		table.setHeaderVisible(true);
		table.addSelectionListener(selectionListener);
		
		// Add KeyListener to delete documents.
		// TODO hitting "suppr" or clicking the button (in the toolbar) a second time should remove the deletion.
		table.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent event) {
				if (event.keyCode == SWT.DEL) {
					deleteSelectedDocument();
				}
			}
		});

		// Initialize item count to 1 so we can populate the first row with filters.
		table.setItemCount(1);
		table.addListener(SWT.SetData, new Listener() {
			@Override
			public void handleEvent(Event event) {
	            TableItem item = (TableItem) event.item;
	            int rowIndex = table.indexOf(item);
	            // The first line is populated by filters.
	            if (rowIndex == 0) {
	            	// TODO might need to populate if existing filters
	            	return;
	            }
	            SolrDocument document;
	            // The last lines are populated by local additions.
	            if (rowIndex >= table.getItemCount() - documentsAdded.size()) {
	            	document = documentsAdded.get(documentsAdded.size() - table.getItemCount() + rowIndex);
	            	item.setBackground(GREEN);
	            } else {
		            // Use rowIndex - 1 since the first line is used for filters.
		            // TODO make sure the last document gets displayed.
	            	try {
			            document = getRemoteDocument(rowIndex - 1);	            		
	            	} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to populate table", e));
						return;
	            	}
	            }
	            // First column is used to show the row ID.
	            item.setText(0, Integer.toString(rowIndex));
	            // Subsequent columns are used to show field values.
		        for (int i=0; i<fields.size(); i++) {
		        	String fieldName = fields.get(i).getName();
		        	// If field is not stored, display message.
		        	// TODO disable doubleclick on unstored fields ?
		        	// TODO verify "(not stored)" is not sent to Solr when updating/creating a new document
		        	// TODO prepopulate if create a new document ?
					// field.getFlags() is not populated when lukeRequest.setSchema(false) so we parse flags ourselves based on field.getSchema() TODO open ticket
		        	if (!FieldInfo.parseFlags(fields.get(i).getSchema()).contains(FieldFlag.STORED)) {
		        		item.setText(i+1, LABEL_NOT_STORED);
		        	} else {
		        		Object value = document.getFieldValue(fieldName);
		            	item.setText(i+1, value == null ? StringUtils.EMPTY : value.toString());
		            }
		        }
		        // Store document in item.
		        item.setData("document", document);
		        // TODO use item.setText(String[] values) ?
		        // TODO store status insert/update/delete using item.setData() ?
			}
		});
		
		// Add columns. First one is used to show row IDs.
		TableColumn columnNumber = new TableColumn(table, SWT.LEFT);
		columnNumber.setText("#");
		columnNumber.pack(); // TODO needed ? might be worth to setLayout() to get rid of this
		for (final FieldInfo field:fields) {
			final TableColumn column = new TableColumn(table, SWT.LEFT);
			// Add space padding so we can see the sort signifier.
			// TODO set sort signifier on uniqueKey by default ?
			// TODO refactor with selection listener
			column.setText(field.getName()+(isFieldSortable(field) ? "     " : " "+SIGNIFIER_UNSORTABLE));
			// TODO do we getData("field") somewhere ? if not, then no need to setData("field")
			column.setData("field", field);
			if (!isFieldSortable(field)) {
				column.setToolTipText("Cannot sort on a field that is not indexed, is multivalued or has doc values");
			}
			// Sort column when click on the header
			column.addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					if (!isFieldSortable(field)) {
						return;
					}
					// Clicking on the current sort field toggles the direction.
					// Clicking on a new field changes the sort field.
					if (StringUtils.equals(sortField, field.getName())) {
						sortOrder = ORDER.asc.equals(sortOrder) ? ORDER.desc : ORDER.asc;
					} else {
						sortField = field.getName();
					}
					// Clear signifier on all columns, add signifier on sorted column.
					char signifier = ORDER.asc.equals(sortOrder) ? SIGNIFIER_SORTED_ASC : SIGNIFIER_SORTED_DESC;
					for (TableColumn c:table.getColumns()) {
						FieldInfo f = (FieldInfo) c.getData("field");
						if (f != null) {
							if (!isFieldSortable(f)) {
								c.setText(f.getName()+" "+SIGNIFIER_UNSORTABLE);
							} else {
								c.setText(f.getName()+((column == c) ? " "+signifier : StringUtils.EMPTY));	
							}
						}

					}
					// Re-populate table.
					try {
						refresh();
					} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to refresh documents from Solr server", e));
					}
				}
			});
			column.pack(); // TODO needed ? might be worth to setLayout() to get rid of this
		}
		
		// Add filters.
		TableItem[] items = table.getItems(); // TODO do we need to load all items ?
		TableEditor editor = new TableEditor(table);
		for(int i=0; i<fields.size(); i++) {
			// TODO check if map contains field ?
			final FacetField facet = facets.get(fields.get(i).getName());
			// If facet is null then we cannot filter on this field (e.g. the field is not indexed).
			if (facet == null) {
				// TODO grey out items[0] columns i
				continue;
			}
			/*
			 * TODO could use this instead of storing field name in setData() 
			 * Point point = new Point(event.x, event.y);
		     * TableItem item = table.getItem(point);
		     * if (item == null) {
		     *   return;
		     * }
		     * for (int i=0; i<fields.size(); i++) {
		     *   Rectangle rect = item.getBounds(i);
		     *   if (rect.contains(point)) {
		     *     SolrDocument document = (SolrDocument) item.getData("document");
		     *     dialog.open(item.getText(i), fields.get(i).getName(), document);
		     *   }
		     * }
			 */
			final CCombo combo = new CCombo(table, SWT.BORDER);			
			combo.add(StringUtils.EMPTY);
			// If the number of facet values is the max, then the list of facet values might not be complete. Hence we use a free text field instead of populating the combo.
			if (facet.getValueCount() < FACET_LIMIT) {
				for(Count count:facet.getValues()) {
					combo.add(Objects.toString(count.getName(), LABEL_EMPTY)+" ("+count.getCount()+")");
				}
				combo.addSelectionListener(new SelectionAdapter() {
					@Override
					public void widgetSelected(SelectionEvent event) {
						// Extract original value (e.g. "Foobar") from widget value (e.g. "Foobar (3)")
						Pattern pattern = Pattern.compile("^(.*) \\([0-9]+\\)$");
						Matcher matcher = pattern.matcher(combo.getText());
						// TODO if cannot find pattern, then should log WARNING
						String filterValue = matcher.find() ? matcher.group(1) : StringUtils.EMPTY;
						// Populate combo with original value.
						combo.setText(filterValue);
					};
				});
			} else {
				combo.add(LABEL_EMPTY);
			}
			
			// Fire filters + refresh when user selects a value.
			combo.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					String filterName = facet.getName();
					String filterNewValue = combo.getText();
					String filterOldValue = filters.get(filterName);
					// No need to re-send a request to Solr and the user selected the same filter as the current filter. 
					if (StringUtils.equals(filterOldValue, filterNewValue) || (StringUtils.isBlank(filterOldValue) && StringUtils.isBlank(filterNewValue))) {
						return;
					}
					updateFilters(filterName, filterNewValue);
					try {
						refresh();
					} catch (SophieException e) {
						ExceptionDialog.open(parent.getShell(), new SophieException("Unable to refresh documents from Solr server", e));
					}
				}
			});
			
			// Filter (refresh) results when user hits "Enter" while editing one of the combos.
			combo.addListener(SWT.KeyUp, new Listener() {
{'fraction_non_alphanumeric': 0.10858098068350669, 'fraction_numerical': 0.0019316493313521546, 'mean_word_length': 3.3191079736884324, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6986080', 'n_tokens_mistral': 9067, 'n_tokens_neox': 7909, 'n_words': 2146}