// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.actions.search;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.junit.Rule;
import org.junit.Test;
import org.openstreetmap.josm.TestUtils;
import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationData;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.data.osm.User;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WayData;
import org.openstreetmap.josm.testutils.JOSMTestRules;
import org.openstreetmap.josm.tools.date.DateUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Unit tests for class {@link SearchCompiler}.
*/
public class SearchCompilerTest {
/**
* We need prefs for this. We access preferences when creating OSM primitives.
*/
@Rule
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public JOSMTestRules test = new JOSMTestRules().preferences();
private static final class SearchContext {
final DataSet ds = new DataSet();
final Node n1 = new Node(LatLon.ZERO);
final Node n2 = new Node(new LatLon(5, 5));
final Way w1 = new Way();
final Way w2 = new Way();
final Relation r1 = new Relation();
final Relation r2 = new Relation();
private final Match m;
private final Match n;
private SearchContext(String state) throws ParseError {
m = SearchCompiler.compile(state);
n = SearchCompiler.compile('-' + state);
ds.addPrimitive(n1);
ds.addPrimitive(n2);
w1.addNode(n1);
w1.addNode(n2);
w2.addNode(n1);
w2.addNode(n2);
ds.addPrimitive(w1);
ds.addPrimitive(w2);
r1.addMember(new RelationMember("", w1));
r1.addMember(new RelationMember("", w2));
r2.addMember(new RelationMember("", w1));
r2.addMember(new RelationMember("", w2));
ds.addPrimitive(r1);
ds.addPrimitive(r2);
}
private void match(OsmPrimitive p, boolean cond) {
if (cond) {
assertTrue(p.toString(), m.match(p));
assertFalse(p.toString(), n.match(p));
} else {
assertFalse(p.toString(), m.match(p));
assertTrue(p.toString(), n.match(p));
}
}
}
private static OsmPrimitive newPrimitive(String key, String value) {
final Node p = new Node();
p.put(key, value);
return p;
}
/**
* Search anything.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testAny() throws ParseError {
final SearchCompiler.Match c = SearchCompiler.compile("foo");
assertTrue(c.match(newPrimitive("foobar", "true")));
assertTrue(c.match(newPrimitive("name", "hello-foo-xy")));
assertFalse(c.match(newPrimitive("name", "X")));
assertEquals("foo", c.toString());
}
/**
* Search by equality key=value.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testEquals() throws ParseError {
final SearchCompiler.Match c = SearchCompiler.compile("foo=bar");
assertFalse(c.match(newPrimitive("foobar", "true")));
assertTrue(c.match(newPrimitive("foo", "bar")));
assertFalse(c.match(newPrimitive("fooX", "bar")));
assertFalse(c.match(newPrimitive("foo", "barX")));
assertEquals("foo=bar", c.toString());
}
/**
* Search by comparison.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testCompare() throws ParseError {
final SearchCompiler.Match c1 = SearchCompiler.compile("start_date>1950");
assertTrue(c1.match(newPrimitive("start_date", "1950-01-01")));
assertTrue(c1.match(newPrimitive("start_date", "1960")));
assertFalse(c1.match(newPrimitive("start_date", "1950")));
assertFalse(c1.match(newPrimitive("start_date", "1000")));
assertTrue(c1.match(newPrimitive("start_date", "101010")));
final SearchCompiler.Match c2 = SearchCompiler.compile("start_date<1960");
assertTrue(c2.match(newPrimitive("start_date", "1950-01-01")));
assertFalse(c2.match(newPrimitive("start_date", "1960")));
assertTrue(c2.match(newPrimitive("start_date", "1950")));
assertTrue(c2.match(newPrimitive("start_date", "1000")));
assertTrue(c2.match(newPrimitive("start_date", "200")));
final SearchCompiler.Match c3 = SearchCompiler.compile("name<I");
assertTrue(c3.match(newPrimitive("name", "Alpha")));
assertFalse(c3.match(newPrimitive("name", "Sigma")));
final SearchCompiler.Match c4 = SearchCompiler.compile("\"start_date\"<1960");
assertTrue(c4.match(newPrimitive("start_date", "1950-01-01")));
assertFalse(c4.match(newPrimitive("start_date", "2000")));
final SearchCompiler.Match c5 = SearchCompiler.compile("height>180");
assertTrue(c5.match(newPrimitive("height", "200")));
assertTrue(c5.match(newPrimitive("height", "99999")));
assertFalse(c5.match(newPrimitive("height", "50")));
assertFalse(c5.match(newPrimitive("height", "-9999")));
assertFalse(c5.match(newPrimitive("height", "fixme")));
final SearchCompiler.Match c6 = SearchCompiler.compile("name>C");
assertTrue(c6.match(newPrimitive("name", "Delta")));
assertFalse(c6.match(newPrimitive("name", "Alpha")));
}
/**
* Search by nth.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testNth() throws ParseError {
final DataSet dataSet = new DataSet();
final Way way = new Way();
final Node node0 = new Node(new LatLon(1, 1));
final Node node1 = new Node(new LatLon(2, 2));
final Node node2 = new Node(new LatLon(3, 3));
dataSet.addPrimitive(way);
dataSet.addPrimitive(node0);
dataSet.addPrimitive(node1);
dataSet.addPrimitive(node2);
way.addNode(node0);
way.addNode(node1);
way.addNode(node2);
assertFalse(SearchCompiler.compile("nth:2").match(node1));
assertTrue(SearchCompiler.compile("nth:1").match(node1));
assertFalse(SearchCompiler.compile("nth:0").match(node1));
assertTrue(SearchCompiler.compile("nth:0").match(node0));
assertTrue(SearchCompiler.compile("nth:2").match(node2));
assertTrue(SearchCompiler.compile("nth:-1").match(node2));
assertTrue(SearchCompiler.compile("nth:-2").match(node1));
assertTrue(SearchCompiler.compile("nth:-3").match(node0));
}
/**
* Search by negative nth.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testNthParseNegative() throws ParseError {
assertEquals("Nth{nth=-1, modulo=false}", SearchCompiler.compile("nth:-1").toString());
}
/**
* Search by modified status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testModified() throws ParseError {
SearchContext sc = new SearchContext("modified");
// Not modified but new
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
assertFalse(p.toString(), p.isModified());
assertTrue(p.toString(), p.isNewOrUndeleted());
sc.match(p, true);
}
// Modified and new
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
p.setModified(true);
assertTrue(p.toString(), p.isModified());
assertTrue(p.toString(), p.isNewOrUndeleted());
sc.match(p, true);
}
// Modified but not new
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
p.setOsmId(1, 1);
assertTrue(p.toString(), p.isModified());
assertFalse(p.toString(), p.isNewOrUndeleted());
sc.match(p, true);
}
// Not modified nor new
for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
p.setOsmId(2, 2);
assertFalse(p.toString(), p.isModified());
assertFalse(p.toString(), p.isNewOrUndeleted());
sc.match(p, false);
}
}
/**
* Search by selected status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testSelected() throws ParseError {
SearchContext sc = new SearchContext("selected");
// Not selected
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
assertFalse(p.toString(), p.isSelected());
sc.match(p, false);
}
// Selected
for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
sc.ds.addSelected(p);
assertTrue(p.toString(), p.isSelected());
sc.match(p, true);
}
}
/**
* Search by incomplete status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testIncomplete() throws ParseError {
SearchContext sc = new SearchContext("incomplete");
// Not incomplete
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
assertFalse(p.toString(), p.isIncomplete());
sc.match(p, false);
}
// Incomplete
sc.n2.setCoor(null);
WayData wd = new WayData();
wd.setIncomplete(true);
sc.w2.load(wd);
RelationData rd = new RelationData();
rd.setIncomplete(true);
sc.r2.load(rd);
for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
assertTrue(p.toString(), p.isIncomplete());
sc.match(p, true);
}
}
/**
* Search by untagged status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testUntagged() throws ParseError {
SearchContext sc = new SearchContext("untagged");
// Untagged
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
assertFalse(p.toString(), p.isTagged());
sc.match(p, true);
}
// Tagged
for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
p.put("foo", "bar");
assertTrue(p.toString(), p.isTagged());
sc.match(p, false);
}
}
/**
* Search by closed status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testClosed() throws ParseError {
SearchContext sc = new SearchContext("closed");
// Closed
sc.w1.addNode(sc.n1);
for (Way w : new Way[]{sc.w1}) {
assertTrue(w.toString(), w.isClosed());
sc.match(w, true);
}
// Unclosed
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w2, sc.r1, sc.r2}) {
sc.match(p, false);
}
}
/**
* Search by new status.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testNew() throws ParseError {
SearchContext sc = new SearchContext("new");
// New
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
assertTrue(p.toString(), p.isNew());
sc.match(p, true);
}
// Not new
for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
p.setOsmId(2, 2);
assertFalse(p.toString(), p.isNew());
sc.match(p, false);
}
}
/**
* Search for node objects.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testTypeNode() throws ParseError {
final SearchContext sc = new SearchContext("type:node");
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
sc.match(p, OsmPrimitiveType.NODE.equals(p.getType()));
}
}
/**
* Search for way objects.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testTypeWay() throws ParseError {
final SearchContext sc = new SearchContext("type:way");
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
sc.match(p, OsmPrimitiveType.WAY.equals(p.getType()));
}
}
/**
* Search for relation objects.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testTypeRelation() throws ParseError {
final SearchContext sc = new SearchContext("type:relation");
for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
sc.match(p, OsmPrimitiveType.RELATION.equals(p.getType()));
}
}
/**
* Search for users.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testUser() throws ParseError {
final SearchContext foobar = new SearchContext("user:foobar");
foobar.n1.setUser(User.createLocalUser("foobar"));
foobar.match(foobar.n1, true);
foobar.match(foobar.n2, false);
final SearchContext anonymous = new SearchContext("user:anonymous");
anonymous.n1.setUser(User.createLocalUser("foobar"));
anonymous.match(anonymous.n1, false);
anonymous.match(anonymous.n2, true);
}
/**
* Compiles "foo type bar" and tests the parse error message
*/
@Test
public void testFooTypeBar() {
try {
SearchCompiler.compile("foo type bar");
fail();
} catch (ParseError parseError) {
assertEquals("<html>Expecting <code>:</code> after <i>type</i>", parseError.getMessage());
}
}
/**
* Search for primitive timestamps.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testTimestamp() throws ParseError {
final Match search = SearchCompiler.compile("timestamp:2010/2011");
final Node n1 = new Node();
n1.setTimestamp(DateUtils.fromString("2010-01-22"));
assertTrue(search.match(n1));
n1.setTimestamp(DateUtils.fromString("2016-01-22"));
assertFalse(search.match(n1));
}
/**
* Tests the implementation of the Boolean logic.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testBooleanLogic() throws ParseError {
final SearchCompiler.Match c1 = SearchCompiler.compile("foo AND bar AND baz");
assertTrue(c1.match(newPrimitive("foobar", "baz")));
assertEquals("foo && bar && baz", c1.toString());
final SearchCompiler.Match c2 = SearchCompiler.compile("foo AND (bar OR baz)");
assertTrue(c2.match(newPrimitive("foobar", "yes")));
assertTrue(c2.match(newPrimitive("foobaz", "yes")));
assertEquals("foo && (bar || baz)", c2.toString());
final SearchCompiler.Match c3 = SearchCompiler.compile("foo OR (bar baz)");
assertEquals("foo || (bar && baz)", c3.toString());
final SearchCompiler.Match c4 = SearchCompiler.compile("foo1 OR (bar1 bar2 baz1 XOR baz2) OR foo2");
assertEquals("foo1 || (bar1 && bar2 && (baz1 ^ baz2)) || foo2", c4.toString());
final SearchCompiler.Match c5 = SearchCompiler.compile("foo1 XOR (baz1 XOR (bar baz))");
assertEquals("foo1 ^ baz1 ^ (bar && baz)", c5.toString());
final SearchCompiler.Match c6 = SearchCompiler.compile("foo1 XOR ((baz1 baz2) XOR (bar OR baz))");
assertEquals("foo1 ^ (baz1 && baz2) ^ (bar || baz)", c6.toString());
}
/**
* Tests {@code buildSearchStringForTag}.
* @throws ParseError if an error has been encountered while compiling
*/
@Test
public void testBuildSearchStringForTag() throws ParseError {
final Tag tag1 = new Tag("foo=", "bar\"");
final Tag tag2 = new Tag("foo=", "=bar");
final String search1 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), tag1.getValue());
assertEquals("\"foo=\"=\"bar\\\"\"", search1);
assertTrue(SearchCompiler.compile(search1).match(tag1));
assertFalse(SearchCompiler.compile(search1).match(tag2));
final String search2 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), "");
assertEquals("\"foo=\"=*", search2);
assertTrue(SearchCompiler.compile(search2).match(tag1));
assertTrue(SearchCompiler.compile(search2).match(tag2));
}
/**
* Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13870">Bug #13870</a>.
* @throws ParseError always
*/
@Test(expected = ParseError.class)
public void testPattern13870() throws ParseError {
// https://bugs.openjdk.java.net/browse/JI-9044959
SearchSetting setting = new SearchSetting();
setting.regexSearch = true;
setting.text = "[";
SearchCompiler.compile(setting);
}
/**
* Non-regression test for <a href="https://josm.openstreetmap.de/ticket/14217">Bug #14217</a>.
* @throws Exception never
*/
@Test
public void testTicket14217() throws Exception {
assertNotNull(SearchCompiler.compile(new String(Files.readAllBytes(
Paths.get(TestUtils.getRegressionDataFile(14217, "filter.txt"))), StandardCharsets.UTF_8)));
}
}