source: josm/trunk/src/org/openstreetmap/josm/tools/SearchCompilerQueryWizard.java@ 17534

Last change on this file since 17534 was 17336, checked in by GerdP, 3 years ago

fix #20037: Overpass query wizard generates wrong query with "*=value"

  • remove obsolete getInstance()
  • fix bug and add unit test
File size: 10.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.util.ArrayList;
5import java.util.Collections;
6import java.util.EnumSet;
7import java.util.List;
8import java.util.Locale;
9import java.util.Optional;
10import java.util.Set;
11import java.util.regex.Matcher;
12import java.util.regex.Pattern;
13import java.util.stream.Collectors;
14import java.util.stream.Stream;
15
16import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
17import org.openstreetmap.josm.data.osm.search.SearchCompiler;
18import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
19import org.openstreetmap.josm.data.osm.search.SearchParseError;
20
21/**
22 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} query.
23 *
24 * @since 8744 (using tyrasd/overpass-wizard), 16262 (standalone)
25 */
26public final class SearchCompilerQueryWizard {
27
28 private SearchCompilerQueryWizard() {
29 // private constructor for utility class
30 }
31
32 /**
33 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
34 * @param search the {@link org.openstreetmap.josm.actions.search.SearchAction} like query
35 * @return an Overpass QL query
36 * @throws UncheckedParseException when the parsing fails
37 */
38 public static String constructQuery(final String search) {
39 try {
40 Matcher matcher = Pattern.compile("\\s+GLOBAL\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
41 if (matcher.find()) {
42 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
43 return constructQuery(match, ";", "");
44 }
45
46 matcher = Pattern.compile("\\s+IN BBOX\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
47 if (matcher.find()) {
48 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
49 return constructQuery(match, "[bbox:{{bbox}}];", "");
50 }
51
52 matcher = Pattern.compile("\\s+(?<mode>IN|AROUND)\\s+(?<area>[^\" ]+|\"[^\"]+\")\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
53 if (matcher.find()) {
54 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
55 final String mode = matcher.group("mode").toUpperCase(Locale.ENGLISH);
56 final String area = Utils.strip(matcher.group("area"), "\"");
57 if ("IN".equals(mode)) {
58 return constructQuery(match, ";\n{{geocodeArea:" + area + "}}->.searchArea;", "(area.searchArea)");
59 } else if ("AROUND".equals(mode)) {
60 return constructQuery(match, ";\n{{radius=1000}}", "(around:{{radius}},{{geocodeCoords:" + area + "}})");
61 } else {
62 throw new IllegalStateException(mode);
63 }
64 }
65
66 final Match match = SearchCompiler.compile(search);
67 return constructQuery(match, "[bbox:{{bbox}}];", "");
68 } catch (SearchParseError | UnsupportedOperationException e) {
69 throw new UncheckedParseException(e);
70 }
71 }
72
73 private static String constructQuery(final Match match, final String bounds, final String queryLineSuffix) {
74 final List<Match> normalized = normalizeToDNF(match);
75 final List<String> queryLines = new ArrayList<>();
76 queryLines.add("[out:xml][timeout:90]" + bounds);
77 queryLines.add("(");
78 for (Match conjunction : normalized) {
79 final EnumSet<OsmPrimitiveType> types = EnumSet.noneOf(OsmPrimitiveType.class);
80 final String query = constructQuery(conjunction, types);
81 (types.isEmpty() || types.size() == 3
82 ? Stream.of("nwr")
83 : types.stream().map(OsmPrimitiveType::getAPIName))
84 .forEach(type -> queryLines.add(" " + type + query + queryLineSuffix + ";"));
85 }
86 queryLines.add(");");
87 queryLines.add("(._;>;);");
88 queryLines.add("out meta;");
89 return String.join("\n", queryLines);
90 }
91
92 private static String constructQuery(Match match, final Set<OsmPrimitiveType> types) {
93 final boolean negated;
94 if (match instanceof SearchCompiler.Not) {
95 negated = true;
96 match = ((SearchCompiler.Not) match).getMatch();
97 } else {
98 negated = false;
99 }
100 if (match instanceof SearchCompiler.And) {
101 return ((SearchCompiler.And) match).map(m -> constructQuery(m, types), (s1, s2) -> s1 + s2);
102 } else if (match instanceof SearchCompiler.KeyValue) {
103 final String key = ((SearchCompiler.KeyValue) match).getKey();
104 final String value = ((SearchCompiler.KeyValue) match).getValue();
105 if ("newer".equals(key)) {
106 return "(newer:" + quote("{{date:" + value + "}}") + ")";
107 }
108 return "[~" + quote(key) + "~" + quote(value) + "]";
109 } else if (match instanceof SearchCompiler.ExactKeyValue) {
110 // https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide
111 // ["key"] -- filter objects tagged with this key and any value
112 // [!"key"] -- filter objects not tagged with this key and any value
113 // ["key"="value"] -- filter objects tagged with this key and this value
114 // ["key"!="value"] -- filter objects tagged with this key but not this value, or not tagged with this key
115 // ["key"~"value"] -- filter objects tagged with this key and a value matching a regular expression
116 // ["key"!~"value"] -- filter objects tagged with this key but a value not matching a regular expression
117 // [~"key"~"value"] -- filter objects tagged with a key and a value matching regular expressions
118 // [~"key"~"value", i] -- filter objects tagged with a key and a case-insensitive value matching regular expressions
119 final String key = ((SearchCompiler.ExactKeyValue) match).getKey();
120 final String value = ((SearchCompiler.ExactKeyValue) match).getValue();
121 final SearchCompiler.ExactKeyValue.Mode mode = ((SearchCompiler.ExactKeyValue) match).getMode();
122 switch (mode) {
123 case ANY_VALUE:
124 return "[" + (negated ? "!" : "") + quote(key) + "]";
125 case EXACT:
126 return "[" + quote(key) + (negated ? "!=" : "=") + quote(value) + "]";
127 case ANY_KEY: // *=value
128 // fall through
129 case EXACT_REGEXP:
130 final Matcher matcher = Pattern.compile("/(?<regex>.*)/(?<flags>i)?").matcher(value);
131 final String valueQuery = matcher.matches()
132 ? quote(matcher.group("regex")) + Optional.ofNullable(matcher.group("flags")).map(f -> "," + f).orElse("")
133 : quote(value);
134 if (mode == SearchCompiler.ExactKeyValue.Mode.ANY_KEY)
135 return "[~\"^.*$\"" + (negated ? "!~" : "~") + valueQuery + "]";
136 return "[" + quote(key) + (negated ? "!~" : "~") + valueQuery + "]";
137 case MISSING_KEY:
138 // special case for empty values, see https://github.com/drolbr/Overpass-API/issues/53
139 return "[" + quote(key) + (negated ? "!~" : "~") + quote("^$") + "]";
140 default:
141 return "";
142 }
143 } else if (match instanceof SearchCompiler.BooleanMatch) {
144 final String key = ((SearchCompiler.BooleanMatch) match).getKey();
145 return negated
146 ? "[" + quote(key) + "~\"false|no|0|off\"]"
147 : "[" + quote(key) + "~\"true|yes|1|on\"]";
148 } else if (match instanceof SearchCompiler.UserMatch) {
149 final String user = ((SearchCompiler.UserMatch) match).getUser();
150 return user.matches("\\d+")
151 ? "(uid:" + user + ")"
152 : "(user:" + quote(user) + ")";
153 } else if (match instanceof SearchCompiler.ExactType) {
154 types.add(((SearchCompiler.ExactType) match).getType());
155 return "";
156 }
157 Logging.warn("Unsupported match type {0}: {1}", match.getClass(), match);
158 return "/*" + match + "*/";
159 }
160
161 /**
162 * Quotes the given string for its use in Overpass QL
163 * @param s the string to quote
164 * @return the quoted string
165 */
166 private static String quote(final String s) {
167 return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
168 }
169
170 /**
171 * Normalizes the match to disjunctive normal form: A∧(B∨C) ⇔ (A∧B)∨(A∧C)
172 * @param match the match to normalize
173 * @return the match in disjunctive normal form
174 */
175 private static List<Match> normalizeToDNF(final Match match) {
176 if (match instanceof SearchCompiler.And) {
177 return ((SearchCompiler.And) match).map(SearchCompilerQueryWizard::normalizeToDNF, (lhs, rhs) -> lhs.stream()
178 .flatMap(l -> rhs.stream().map(r -> new SearchCompiler.And(l, r)))
179 .collect(Collectors.toList()));
180 } else if (match instanceof SearchCompiler.Or) {
181 return ((SearchCompiler.Or) match).map(SearchCompilerQueryWizard::normalizeToDNF, CompositeList::new);
182 } else if (match instanceof SearchCompiler.Xor) {
183 throw new UnsupportedOperationException(match.toString());
184 } else if (match instanceof SearchCompiler.Not) {
185 // only support negated KeyValue or ExactKeyValue matches
186 final Match innerMatch = ((SearchCompiler.Not) match).getMatch();
187 if (innerMatch instanceof SearchCompiler.BooleanMatch
188 || innerMatch instanceof SearchCompiler.KeyValue
189 || innerMatch instanceof SearchCompiler.ExactKeyValue) {
190 return Collections.singletonList(match);
191 }
192 throw new UnsupportedOperationException(match.toString());
193 } else {
194 return Collections.singletonList(match);
195 }
196 }
197
198}
Note: See TracBrowser for help on using the repository browser.