diff --git a/main/src/com/google/refine/browsing/facets/TextSearchFacet.java b/main/src/com/google/refine/browsing/facets/TextSearchFacet.java index cde2e173b..03c6c1cf9 100644 --- a/main/src/com/google/refine/browsing/facets/TextSearchFacet.java +++ b/main/src/com/google/refine/browsing/facets/TextSearchFacet.java @@ -60,6 +60,7 @@ public class TextSearchFacet implements Facet { protected String _query; protected String _mode; protected boolean _caseSensitive; + protected boolean _invert; /* * Derived configuration @@ -80,6 +81,7 @@ public class TextSearchFacet implements Facet { writer.key("query"); writer.value(_query); writer.key("mode"); writer.value(_mode); writer.key("caseSensitive"); writer.value(_caseSensitive); + writer.key("invert"); writer.value(_invert); writer.endObject(); } @@ -110,6 +112,7 @@ public class TextSearchFacet implements Facet { _query = _query.toLowerCase(); } } + _invert = o.has("invert") && o.getBoolean("invert"); } @Override @@ -123,14 +126,14 @@ public class TextSearchFacet implements Facet { Evaluable eval = new VariableExpr("value"); if ("regex".equals(_mode)) { - return new ExpressionStringComparisonRowFilter(eval, _columnName, _cellIndex) { + return new ExpressionStringComparisonRowFilter(eval, _invert, _columnName, _cellIndex) { @Override protected boolean checkValue(String s) { return _pattern.matcher(s).find(); }; }; } else { - return new ExpressionStringComparisonRowFilter(eval, _columnName, _cellIndex) { + return new ExpressionStringComparisonRowFilter(eval, _invert, _columnName, _cellIndex) { @Override protected boolean checkValue(String s) { return (_caseSensitive ? s : s.toLowerCase()).contains(_query); diff --git a/main/src/com/google/refine/browsing/filters/ExpressionStringComparisonRowFilter.java b/main/src/com/google/refine/browsing/filters/ExpressionStringComparisonRowFilter.java index 569966e16..498534ec4 100644 --- a/main/src/com/google/refine/browsing/filters/ExpressionStringComparisonRowFilter.java +++ b/main/src/com/google/refine/browsing/filters/ExpressionStringComparisonRowFilter.java @@ -52,11 +52,13 @@ import com.google.refine.model.Row; */ abstract public class ExpressionStringComparisonRowFilter implements RowFilter { final protected Evaluable _evaluable; + final protected Boolean _invert; final protected String _columnName; final protected int _cellIndex; - public ExpressionStringComparisonRowFilter(Evaluable evaluable, String columnName, int cellIndex) { + public ExpressionStringComparisonRowFilter(Evaluable evaluable, Boolean invert, String columnName, int cellIndex) { _evaluable = evaluable; + _invert = invert; _columnName = columnName; _cellIndex = cellIndex; } @@ -67,23 +69,23 @@ abstract public class ExpressionStringComparisonRowFilter implements RowFilter { Properties bindings = ExpressionUtils.createBindings(project); ExpressionUtils.bind(bindings, row, rowIndex, _columnName, cell); - + Boolean invert = _invert; Object value = _evaluable.evaluate(bindings); if (value != null) { if (value.getClass().isArray()) { Object[] a = (Object[]) value; for (Object v : a) { if (checkValue(v instanceof String ? ((String) v) : v.toString())) { - return true; + return !invert; } } } else if (value instanceof Collection) { for (Object v : ExpressionUtils.toObjectCollection(value)) { if (checkValue(v.toString())) { - return true; + return !invert; } } - return false; + return invert; } else if (value instanceof JSONArray) { JSONArray a = (JSONArray) value; int l = a.length(); @@ -91,20 +93,20 @@ abstract public class ExpressionStringComparisonRowFilter implements RowFilter { for (int i = 0; i < l; i++) { try { if (checkValue(a.get(i).toString())) { - return true; + return !invert; } } catch (JSONException e) { // ignore } } - return false; + return invert; } else { if (checkValue(value instanceof String ? ((String) value) : value.toString())) { - return true; + return !invert; } } } - return false; + return invert; } abstract protected boolean checkValue(String s); diff --git a/main/tests/server/src/com/google/refine/tests/browsing/facets/TextSearchFacetTests.java b/main/tests/server/src/com/google/refine/tests/browsing/facets/TextSearchFacetTests.java new file mode 100644 index 000000000..ad6876fe5 --- /dev/null +++ b/main/tests/server/src/com/google/refine/tests/browsing/facets/TextSearchFacetTests.java @@ -0,0 +1,265 @@ +/* + +Copyright 2010, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +package com.google.refine.tests.model; + +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.ArrayList; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.google.refine.ProjectManager; +import com.google.refine.ProjectMetadata; +import com.google.refine.RefineServlet; +import com.google.refine.importers.SeparatorBasedImporter; +import com.google.refine.importing.ImportingJob; +import com.google.refine.importing.ImportingManager; +import com.google.refine.io.FileProjectManager; +import com.google.refine.model.ModelException; +import com.google.refine.model.Project; +import com.google.refine.browsing.RowFilter; +import com.google.refine.browsing.facets.TextSearchFacet; +import com.google.refine.tests.RefineServletStub; +import com.google.refine.tests.RefineTest; +import com.google.refine.tests.util.TestUtils; + + +public class TextSearchFacetTests extends RefineTest { + // dependencies + private RefineServlet servlet; + private Project project; + private ProjectMetadata pm; + private JSONObject options; + private ImportingJob job; + private SeparatorBasedImporter importer; + private TextSearchFacet textfilter; + private JSONObject textsearchfacet; + private RowFilter rowfilter; + + @Override + @BeforeTest + public void init() { + logger = LoggerFactory.getLogger(this.getClass()); + } + + @BeforeMethod + public void setUp() throws JSONException, IOException, ModelException { + servlet = new RefineServletStub(); + File dir = TestUtils.createTempDirectory("openrefine-test-workspace-dir"); + FileProjectManager.initialize(dir); + project = new Project(); + pm = new ProjectMetadata(); + pm.setName("TextSearchFacet test"); + ProjectManager.singleton.registerProject(project, pm); + options = mock(JSONObject.class); + + ImportingManager.initialize(servlet); + job = ImportingManager.createJob(); + importer = new SeparatorBasedImporter(); + + String csv = "Value\n" + + "a\n" + + "b\n" + + "ab\n" + + "Abc\n"; + prepareOptions(",", 10, 0, 0, 1, false, false); + List exceptions = new ArrayList(); + importer.parseOneFile(project, pm, job, "filesource", new StringReader(csv), -1, options, exceptions); + project.update(); + ProjectManager.singleton.registerProject(project, pm); + } + + @AfterMethod + public void tearDown() { + ImportingManager.disposeJob(job.id); + ProjectManager.singleton.deleteProject(project.id); + job = null; + project = null; + pm = null; + options = null; + } + + /** + * Test to demonstrate the intended behaviour of the function + */ + + @Test + public void testTextFilter() throws Exception { + //Apply text filter "a" + + //Column: "Value" + //Filter Query: "a" + //Mode: "text" + //Case sensitive: False + //Invert: False + String filter = "{\"type\":\"text\"," + + "\"name\":\"Value\"," + + "\"columnName\":\"Value\"," + + "\"mode\":\"text\"," + + "\"caseSensitive\":false," + + "\"invert\":false," + + "\"query\":\"a\"}"; + + //Add the facet to the project and create a row filter + textfilter = new TextSearchFacet(); + textsearchfacet = new JSONObject(filter); + textfilter.initializeFromJSON(project,textsearchfacet); + rowfilter = textfilter.getRowFilter(project); + + //Check each row in the project against the filter + Assert.assertEquals(rowfilter.filterRow(project, 0, project.rows.get(0)),true); + Assert.assertEquals(rowfilter.filterRow(project, 1, project.rows.get(1)),false); + Assert.assertEquals(rowfilter.filterRow(project, 2, project.rows.get(2)),true); + Assert.assertEquals(rowfilter.filterRow(project, 3, project.rows.get(3)),true); + } + + @Test + public void testInvertedTextFilter() throws Exception { + //Apply inverted text filter "a" + + //Column: "Value" + //Filter Query: "a" + //Mode: "text" + //Case sensitive: False + //Invert: True + String filter = "{\"type\":\"text\"," + + "\"name\":\"Value\"," + + "\"columnName\":\"Value\"," + + "\"mode\":\"text\"," + + "\"caseSensitive\":false," + + "\"invert\":true," + + "\"query\":\"a\"}"; + + //Add the facet to the project and create a row filter + textfilter = new TextSearchFacet(); + textsearchfacet = new JSONObject(filter); + textfilter.initializeFromJSON(project,textsearchfacet); + rowfilter = textfilter.getRowFilter(project); + + //Check each row in the project against the filter + Assert.assertEquals(rowfilter.filterRow(project, 0, project.rows.get(0)),false); + Assert.assertEquals(rowfilter.filterRow(project, 1, project.rows.get(1)),true); + Assert.assertEquals(rowfilter.filterRow(project, 2, project.rows.get(2)),false); + Assert.assertEquals(rowfilter.filterRow(project, 3, project.rows.get(3)),false); + } + + @Test + public void testRegExFilter() throws Exception { + //Apply regular expression filter "[bc]" + + //Column: "Value" + //Filter Query: "[bc]" + //Mode: "regex" + //Case sensitive: False + //Invert: False + String filter = "{\"type\":\"text\"," + + "\"name\":\"Value\"," + + "\"columnName\":\"Value\"," + + "\"mode\":\"regex\"," + + "\"caseSensitive\":false," + + "\"invert\":false," + + "\"query\":\"[bc]\"}"; + + //Add the facet to the project and create a row filter + textfilter = new TextSearchFacet(); + textsearchfacet = new JSONObject(filter); + textfilter.initializeFromJSON(project,textsearchfacet); + rowfilter = textfilter.getRowFilter(project); + + //Check each row in the project against the filter + Assert.assertEquals(rowfilter.filterRow(project, 0, project.rows.get(0)),false); + Assert.assertEquals(rowfilter.filterRow(project, 1, project.rows.get(1)),true); + Assert.assertEquals(rowfilter.filterRow(project, 2, project.rows.get(2)),true); + Assert.assertEquals(rowfilter.filterRow(project, 3, project.rows.get(3)),true); + } + + @Test + public void testCaseSensitiveFilter() throws Exception { + //Apply case-sensitive filter "A" + + //Column: "Value" + //Filter Query: "A" + //Mode: "text" + //Case sensitive: True + //Invert: False + String filter = "{\"type\":\"text\"," + + "\"name\":\"Value\"," + + "\"columnName\":\"Value\"," + + "\"mode\":\"text\"," + + "\"caseSensitive\":true," + + "\"invert\":false," + + "\"query\":\"A\"}"; + + //Add the facet to the project and create a row filter + textfilter = new TextSearchFacet(); + textsearchfacet = new JSONObject(filter); + textfilter.initializeFromJSON(project,textsearchfacet); + rowfilter = textfilter.getRowFilter(project); + + //Check each row in the project against the filter + //Expect to retrieve one row containing "Abc" + Assert.assertEquals(rowfilter.filterRow(project, 0, project.rows.get(0)),false); + Assert.assertEquals(rowfilter.filterRow(project, 1, project.rows.get(1)),false); + Assert.assertEquals(rowfilter.filterRow(project, 2, project.rows.get(2)),false); + Assert.assertEquals(rowfilter.filterRow(project, 3, project.rows.get(3)),true); + } + + private void prepareOptions( + String sep, int limit, int skip, int ignoreLines, + int headerLines, boolean guessValueType, boolean ignoreQuotes) { + + whenGetStringOption("separator", options, sep); + whenGetIntegerOption("limit", options, limit); + whenGetIntegerOption("skipDataLines", options, skip); + whenGetIntegerOption("ignoreLines", options, ignoreLines); + whenGetIntegerOption("headerLines", options, headerLines); + whenGetBooleanOption("guessCellValueTypes", options, guessValueType); + whenGetBooleanOption("processQuotes", options, !ignoreQuotes); + whenGetBooleanOption("storeBlankCellsAsNulls", options, true); + } + + +} + diff --git a/main/webapp/modules/core/scripts/facets/list-facet.js b/main/webapp/modules/core/scripts/facets/list-facet.js index 42500b05c..558362413 100644 --- a/main/webapp/modules/core/scripts/facets/list-facet.js +++ b/main/webapp/modules/core/scripts/facets/list-facet.js @@ -129,7 +129,7 @@ ListFacet.prototype.updateState = function(data) { }; ListFacet.prototype._reSortChoices = function() { - this._data.choices.sort(this._options.sort == "name" ? + this._data.choices.sort(this._options.sort === "name" ? function(a, b) { return a.v.l.toLowerCase().localeCompare(b.v.l.toLowerCase()); } : @@ -146,7 +146,7 @@ ListFacet.prototype._initializeUI = function() { var facet_id = this._div.attr("id"); this._div.empty().show().html( - '
' + + '
' + '
' + '' + '
 ' + @@ -259,9 +259,11 @@ ListFacet.prototype._update = function(resetScroll) { var invert = this._config.invert; if (invert) { + this._elmts.facetTitle.addClass("facet-title-inverted"); this._elmts.bodyInnerDiv.addClass("facet-mode-inverted"); this._elmts.invertButton.addClass("facet-mode-inverted"); } else { + this._elmts.facetTitle.removeClass("facet-title-inverted"); this._elmts.bodyInnerDiv.removeClass("facet-mode-inverted"); this._elmts.invertButton.removeClass("facet-mode-inverted"); } @@ -277,7 +279,7 @@ ListFacet.prototype._update = function(resetScroll) { //this._elmts.statusDiv.hide(); this._elmts.controlsDiv.hide(); - if (this._data.error == "Too many choices") { + if (this._data.error === "Too many choices") { this._elmts.bodyInnerDiv.empty(); var messageDiv = $('
') @@ -345,7 +347,7 @@ ListFacet.prototype._update = function(resetScroll) { this._elmts.invertButton.hide(); } - if (this._options.sort == "name") { + if (this._options.sort === "name") { this._elmts.sortByNameLink.removeClass("action").addClass("selected"); this._elmts.sortByCountLink.removeClass("selected").addClass("action"); } else { @@ -359,7 +361,7 @@ ListFacet.prototype._update = function(resetScroll) { return temp.text(s).html(); }; - var renderEdit = this._config.expression == "value"; + var renderEdit = this._config.expression === "value"; var renderChoice = function(index, choice, customLabel) { var label = customLabel || choice.v.l; var count = choice.c; @@ -400,9 +402,9 @@ ListFacet.prototype._update = function(resetScroll) { var getChoice = function(elmt) { var index = parseInt(elmt.attr("choiceIndex"),10); - if (index == -1) { + if (index === -1) { return self._blankChoice; - } else if (index == -2) { + } else if (index === -2) { return self._errorChoice; } else { return choices[index]; @@ -448,7 +450,7 @@ ListFacet.prototype._update = function(resetScroll) { bodyInnerDiv.on('mouseenter mouseleave', '.facet-choice', function(e) { e.preventDefault(); var visibility = 'visible'; - if (e.type == 'mouseleave') { + if (e.type === 'mouseleave') { visibility = 'hidden'; } $(this).find('.facet-choice-edit').css("visibility", visibility); @@ -577,7 +579,7 @@ ListFacet.prototype._editChoice = function(choice, choiceDiv) { var gotSelection = false; for (var i = 0; i < self._selection.length; i++) { var choice = self._selection[i]; - if (choice.v.v == originalContent) { + if (choice.v.v === originalContent) { if (gotSelection) { continue; } @@ -597,9 +599,9 @@ ListFacet.prototype._editChoice = function(choice, choiceDiv) { .text(originalContent) .keydown(function(evt) { if (!evt.shiftKey) { - if (evt.keyCode == 13) { + if (evt.keyCode === 13) { commit(); - } else if (evt.keyCode == 27) { + } else if (evt.keyCode === 27) { MenuSystem.dismissAll(); } } @@ -697,7 +699,7 @@ ListFacet.prototype._editExpression = function() { self._elmts.expressionDiv.text(self._config.expression); self._elmts.changeButton.attr("title", $.i18n._('core-facets')["current-exp"]+": " + self._config.expression); - if (self._config.expression == "value" || self._config.expression == "grel:value") { + if (self._config.expression === "value" || self._config.expression === "grel:value") { self._elmts.clusterLink.show(); } else { self._elmts.clusterLink.hide(); @@ -726,9 +728,9 @@ ListFacet.prototype._setChoiceCountLimit = function(choiceCount) { value : n }, function(o) { - if (o.code == "ok") { + if (o.code === "ok") { ui.browsingEngine.update(); - } else if (o.code == "error") { + } else if (o.code === "error") { alert(o.message); } }, diff --git a/main/webapp/modules/core/scripts/facets/text-search-facet.js b/main/webapp/modules/core/scripts/facets/text-search-facet.js index dcd35fbeb..7b23eccf3 100644 --- a/main/webapp/modules/core/scripts/facets/text-search-facet.js +++ b/main/webapp/modules/core/scripts/facets/text-search-facet.js @@ -34,12 +34,17 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. function TextSearchFacet(div, config, options) { this._div = div; this._config = config; + if (!("invert" in this._config)) { + this._config.invert = false; + } + this._options = options; this._query = config.query || null; this._timerID = null; this._initializeUI(); + this._update(); } TextSearchFacet.reconstruct = function(div, uiState) { @@ -70,6 +75,7 @@ TextSearchFacet.prototype.getJSON = function() { columnName: this._config.columnName, mode: this._config.mode, caseSensitive: this._config.caseSensitive, + invert: this._config.invert, query: this._query }; return o; @@ -82,11 +88,13 @@ TextSearchFacet.prototype.hasSelection = function() { TextSearchFacet.prototype._initializeUI = function() { var self = this; this._div.empty().show().html( - '
' + + '
' + '
' + '' + '' + '
 ' + - '' + this._config.name + '' + + ''+$.i18n._('core-facets')["reset"]+'' + + ''+$.i18n._('core-facets')["invert"]+'' + + '' + '
' + '
' + @@ -99,24 +107,27 @@ TextSearchFacet.prototype._initializeUI = function() { '
' ); - var elmts = DOM.bind(this._div); + this._elmts = DOM.bind(this._div); + this._elmts.titleSpan.text(this._config.name); if (this._config.caseSensitive) { - elmts.caseSensitiveCheckbox.attr("checked", "true"); + this._elmts.caseSensitiveCheckbox.attr("checked", "true"); } - if (this._config.mode == "regex") { - elmts.regexCheckbox.attr("checked", "true"); + if (this._config.mode === "regex") { + this._elmts.regexCheckbox.attr("checked", "true"); } - elmts.removeButton.click(function() { self._remove(); }); + this._elmts.removeButton.click(function() { self._remove(); }); + this._elmts.resetButton.click(function() { self._reset(); }); + this._elmts.invertButton.click(function() { self._invert(); }); - elmts.caseSensitiveCheckbox.bind("change", function() { + this._elmts.caseSensitiveCheckbox.bind("change", function() { self._config.caseSensitive = this.checked; if (self._query !== null && self._query.length > 0) { self._scheduleUpdate(); } }); - elmts.regexCheckbox.bind("change", function() { + this._elmts.regexCheckbox.bind("change", function() { self._config.mode = this.checked ? "regex" : "text"; if (self._query !== null && self._query.length > 0) { self._scheduleUpdate(); @@ -124,10 +135,10 @@ TextSearchFacet.prototype._initializeUI = function() { }); if (this._query) { - elmts.input[0].value = this._query; + this._elmts.input[0].value = this._query; } - elmts.input.bind("keyup change input",function(evt) { + this._elmts.input.bind("keyup change input",function(evt) { // Ignore non-character keyup changes if(evt.type === "keyup" && (this.value === self._query || this.value === '' && !self._query)) { return; @@ -139,6 +150,7 @@ TextSearchFacet.prototype._initializeUI = function() { }; TextSearchFacet.prototype.updateState = function(data) { + this._update(); }; TextSearchFacet.prototype.render = function() { @@ -147,6 +159,19 @@ TextSearchFacet.prototype.render = function() { TextSearchFacet.prototype._reset = function() { this._query = null; + this._config.mode = "text"; + this._config.caseSensitive = false; + this._elmts.input.val([]); + this._elmts.caseSensitiveCheckbox.prop("checked", false); + this._elmts.regexCheckbox.prop("checked", false); + this._config.invert = false; + + this._updateRest(); +}; + +TextSearchFacet.prototype._invert = function() { + this._config.invert = !this._config.invert; + this._updateRest(); }; @@ -158,6 +183,17 @@ TextSearchFacet.prototype._remove = function() { this._options = null; }; +TextSearchFacet.prototype._update = function () { + var invert = this._config.invert; + if (invert) { + this._elmts.facetTitle.addClass("facet-title-inverted"); + this._elmts.invertButton.addClass("facet-mode-inverted"); + } else { + this._elmts.facetTitle.removeClass("facet-title-inverted"); + this._elmts.invertButton.removeClass("facet-mode-inverted"); + } +}; + TextSearchFacet.prototype._scheduleUpdate = function() { if (!this._timerID) { var self = this; diff --git a/main/webapp/modules/core/styles/project/facets.less b/main/webapp/modules/core/styles/project/facets.less index 29596d9fc..ac72a8888 100644 --- a/main/webapp/modules/core/styles/project/facets.less +++ b/main/webapp/modules/core/styles/project/facets.less @@ -57,6 +57,11 @@ li.facet-container { } +.facet-title-inverted { + background: #ffa500; + border-bottom: 1px solid #ffc04d; + } + a.facet-title-remove { display: block; width: 12px;