source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java@ 15640

Last change on this file since 15640 was 15640, checked in by Don-vip, 4 years ago

fix #18455 - detect objects not matching their presets object type (info level)

  • Property svn:eol-style set to native
File size: 14.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.Reader;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collections;
10import java.util.List;
11
12import javax.script.Invocable;
13import javax.script.ScriptEngine;
14import javax.script.ScriptException;
15import javax.swing.JOptionPane;
16
17import org.openstreetmap.josm.command.ChangePropertyCommand;
18import org.openstreetmap.josm.data.osm.OsmPrimitive;
19import org.openstreetmap.josm.data.validation.Severity;
20import org.openstreetmap.josm.data.validation.Test.TagTest;
21import org.openstreetmap.josm.data.validation.TestError;
22import org.openstreetmap.josm.gui.Notification;
23import org.openstreetmap.josm.gui.util.GuiHelper;
24import org.openstreetmap.josm.io.CachedFile;
25import org.openstreetmap.josm.tools.LanguageInfo;
26import org.openstreetmap.josm.tools.Logging;
27import org.openstreetmap.josm.tools.Utils;
28
29/**
30 * Tests the correct usage of the opening hour syntax of the tags
31 * {@code opening_hours}, {@code collection_times}, {@code service_times} according to
32 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a>.
33 *
34 * @since 6370
35 */
36public class OpeningHourTest extends TagTest {
37
38 /**
39 * Javascript engine
40 */
41 public static final ScriptEngine ENGINE = Utils.getJavaScriptEngine();
42
43 /**
44 * Constructs a new {@code OpeningHourTest}.
45 */
46 public OpeningHourTest() {
47 super(tr("Opening hours syntax"),
48 tr("This test checks the correct usage of the opening hours syntax."));
49 }
50
51 @Override
52 public void initialize() throws Exception {
53 super.initialize();
54 if (ENGINE != null) {
55 try (CachedFile cf = new CachedFile("resource://data/validator/opening_hours.js");
56 Reader reader = cf.getContentReader()) {
57 ENGINE.eval("var console={};console.debug=print;console.log=print;console.warn=print;console.error=print;");
58 ENGINE.eval(reader);
59 ENGINE.eval("var opening_hours = require('opening_hours');");
60 // fake country/state to not get errors on holidays
61 ENGINE.eval("var nominatimJSON = {address: {state: 'Bayern', country_code: 'de'}};");
62 ENGINE.eval(
63 "var oh = function (value, tag_key, mode, locale) {" +
64 " try {" +
65 " var conf = {tag_key: tag_key, locale: locale};" +
66 " if (mode > -1) {" +
67 " conf.mode = mode;" +
68 " }" +
69 " var r = new opening_hours(value, nominatimJSON, conf);" +
70 " r.getErrors = function() {return [];};" +
71 " return r;" +
72 " } catch (err) {" +
73 " return {" +
74 " prettifyValue: function() {return null;}," +
75 " getWarnings: function() {return [];}," +
76 " getErrors: function() {return [err.toString()]}" +
77 " };" +
78 " }" +
79 "};");
80 }
81 } else {
82 Logging.warn("Unable to initialize OpeningHourTest because no JavaScript engine has been found");
83 }
84 }
85
86 /**
87 * In OSM, the syntax originally designed to describe opening hours, is now used to describe a few other things as well.
88 * Some of those other tags work with points in time instead of time ranges.
89 * To support this the mode can be specified.
90 * @since 13147
91 */
92 public enum CheckMode {
93 /** time ranges (opening_hours, lit, …) default */
94 TIME_RANGE(0),
95 /** points in time */
96 POINTS_IN_TIME(1),
97 /** both (time ranges and points in time, used by collection_times, service_times, …) */
98 BOTH(2);
99 private final int code;
100
101 CheckMode(int code) {
102 this.code = code;
103 }
104 }
105
106 /**
107 * Parses the opening hour syntax of the {@code value} given according to
108 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns an object on which
109 * methods can be called to extract information.
110 * @param value the opening hour value to be checked
111 * @param tagKey the OSM key (should be "opening_hours", "collection_times" or "service_times")
112 * @param mode whether to validate {@code value} as a time range, or points in time, or both. Can be null
113 * @param locale the locale code used for localizing messages
114 * @return The value returned by the underlying method. Usually a {@code jdk.nashorn.api.scripting.ScriptObjectMirror}
115 * @throws ScriptException if an error occurs during invocation of the underlying method
116 * @throws NoSuchMethodException if underlying method with given name or matching argument types cannot be found
117 * @since 13147
118 */
119 public Object parse(String value, String tagKey, CheckMode mode, String locale) throws ScriptException, NoSuchMethodException {
120 return ((Invocable) ENGINE).invokeFunction("oh", value, tagKey, mode != null ? mode.code : -1, locale);
121 }
122
123 @SuppressWarnings("unchecked")
124 protected List<Object> getList(Object obj) throws ScriptException, NoSuchMethodException {
125 if (obj == null || "".equals(obj)) {
126 return Arrays.asList();
127 } else if (obj instanceof String) {
128 final Object[] strings = ((String) obj).split("\\\\n");
129 return Arrays.asList(strings);
130 } else if (obj instanceof List) {
131 return (List<Object>) obj;
132 } else {
133 // recursively call getList() with argument converted to newline-separated string
134 return getList(((Invocable) ENGINE).invokeMethod(obj, "join", "\\n"));
135 }
136 }
137
138 /**
139 * An error concerning invalid syntax for an "opening_hours"-like tag.
140 */
141 public class OpeningHoursTestError {
142 private final Severity severity;
143 private final String message;
144 private final String prettifiedValue;
145
146 /**
147 * Constructs a new {@code OpeningHoursTestError} with a known pretiffied value.
148 * @param message The error message
149 * @param severity The error severity
150 * @param prettifiedValue The prettified value
151 */
152 public OpeningHoursTestError(String message, Severity severity, String prettifiedValue) {
153 this.message = message;
154 this.severity = severity;
155 this.prettifiedValue = prettifiedValue;
156 }
157
158 /**
159 * Returns the real test error given to JOSM validator.
160 * @param p The incriminated OSM primitive.
161 * @param key The incriminated key, used for display.
162 * @return The real test error given to JOSM validator. Can be fixable or not if a prettified values has been determined.
163 */
164 public TestError getTestError(final OsmPrimitive p, final String key) {
165 final TestError.Builder error = TestError.builder(OpeningHourTest.this, severity, 2901)
166 .message(tr("Opening hours syntax"), message) // todo obtain English message for ignore functionality
167 .primitives(p);
168 if (prettifiedValue == null || prettifiedValue.equals(p.get(key))) {
169 return error.build();
170 } else {
171 return error.fix(() -> new ChangePropertyCommand(p, key, prettifiedValue)).build();
172 }
173 }
174
175 /**
176 * Returns the error message.
177 * @return The error message.
178 */
179 public String getMessage() {
180 return message;
181 }
182
183 /**
184 * Returns the prettified value.
185 * @return The prettified value.
186 */
187 public String getPrettifiedValue() {
188 return prettifiedValue;
189 }
190
191 /**
192 * Returns the error severity.
193 * @return The error severity.
194 */
195 public Severity getSeverity() {
196 return severity;
197 }
198
199 @Override
200 public String toString() {
201 return getMessage() + " => " + getPrettifiedValue();
202 }
203 }
204
205 /**
206 * Checks for a correct usage of the opening hour syntax of the {@code value} given according to
207 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing
208 * validation errors or an empty list. Null values result in an empty list.
209 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message
210 * @param value the opening hour value to be checked.
211 * @return a list of {@link TestError} or an empty list
212 */
213 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value) {
214 return checkOpeningHourSyntax(key, value, null, false, LanguageInfo.getJOSMLocaleCode());
215 }
216
217 /**
218 * Checks for a correct usage of the opening hour syntax of the {@code value} given according to
219 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing
220 * validation errors or an empty list. Null values result in an empty list.
221 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times").
222 * @param value the opening hour value to be checked.
223 * @param mode whether to validate {@code value} as a time range, or points in time, or both. Can be null
224 * @param ignoreOtherSeverity whether to ignore errors with {@link Severity#OTHER}.
225 * @param locale the locale code used for localizing messages
226 * @return a list of {@link TestError} or an empty list
227 */
228 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value, CheckMode mode,
229 boolean ignoreOtherSeverity, String locale) {
230 if (ENGINE == null || value == null || value.isEmpty()) {
231 return Collections.emptyList();
232 }
233 final List<OpeningHoursTestError> errors = new ArrayList<>();
234 try {
235 final Object r = parse(value, key, mode, locale);
236 String prettifiedValue = null;
237 try {
238 prettifiedValue = getOpeningHoursPrettifiedValues(r);
239 } catch (ScriptException | NoSuchMethodException e) {
240 Logging.warn(e);
241 }
242 for (final Object i : getOpeningHoursErrors(r)) {
243 errors.add(new OpeningHoursTestError(getErrorMessage(key, i), Severity.ERROR, prettifiedValue));
244 }
245 for (final Object i : getOpeningHoursWarnings(r)) {
246 errors.add(new OpeningHoursTestError(getErrorMessage(key, i), Severity.WARNING, prettifiedValue));
247 }
248 if (!ignoreOtherSeverity && errors.isEmpty() && prettifiedValue != null && !value.equals(prettifiedValue)) {
249 errors.add(new OpeningHoursTestError(tr("opening_hours value can be prettified"), Severity.OTHER, prettifiedValue));
250 }
251 } catch (ScriptException | NoSuchMethodException ex) {
252 Logging.error(ex);
253 GuiHelper.runInEDT(() -> new Notification(Utils.getRootCause(ex).getMessage()).setIcon(JOptionPane.ERROR_MESSAGE).show());
254 }
255 return errors;
256 }
257
258 /**
259 * Returns the prettified value returned by the opening hours parser.
260 * @param r result of {@link #parse}
261 * @return the prettified value returned by the opening hours parser
262 * @throws NoSuchMethodException if method "prettifyValue" or matching argument types cannot be found
263 * @throws ScriptException if an error occurs during invocation of the JavaScript method
264 * @since 13296
265 */
266 public final String getOpeningHoursPrettifiedValues(Object r) throws NoSuchMethodException, ScriptException {
267 return (String) ((Invocable) ENGINE).invokeMethod(r, "prettifyValue");
268 }
269
270 /**
271 * Returns the list of errors returned by the opening hours parser.
272 * @param r result of {@link #parse}
273 * @return the list of errors returned by the opening hours parser
274 * @throws NoSuchMethodException if method "getErrors" or matching argument types cannot be found
275 * @throws ScriptException if an error occurs during invocation of the JavaScript method
276 * @since 13296
277 */
278 public final List<Object> getOpeningHoursErrors(Object r) throws NoSuchMethodException, ScriptException {
279 return getList(((Invocable) ENGINE).invokeMethod(r, "getErrors"));
280 }
281
282 /**
283 * Returns the list of warnings returned by the opening hours parser.
284 * @param r result of {@link #parse}
285 * @return the list of warnings returned by the opening hours parser
286 * @throws NoSuchMethodException if method "getWarnings" or matching argument types cannot be found
287 * @throws ScriptException if an error occurs during invocation of the JavaScript method
288 * @since 13296
289 */
290 public final List<Object> getOpeningHoursWarnings(Object r) throws NoSuchMethodException, ScriptException {
291 return getList(((Invocable) ENGINE).invokeMethod(r, "getWarnings"));
292 }
293
294 /**
295 * Translates and shortens the error/warning message.
296 * @param o error/warning message returned by {@link #getOpeningHoursErrors} or {@link #getOpeningHoursWarnings}
297 * @return translated/shortened error/warning message
298 * @since 13298
299 */
300 public static String getErrorMessage(Object o) {
301 return o.toString().trim()
302 .replace("Unexpected token:", tr("Unexpected token:"))
303 .replace("Unexpected token (school holiday parser):", tr("Unexpected token (school holiday parser):"))
304 .replace("Unexpected token in number range:", tr("Unexpected token in number range:"))
305 .replace("Unexpected token in week range:", tr("Unexpected token in week range:"))
306 .replace("Unexpected token in weekday range:", tr("Unexpected token in weekday range:"))
307 .replace("Unexpected token in month range:", tr("Unexpected token in month range:"))
308 .replace("Unexpected token in year range:", tr("Unexpected token in year range:"))
309 .replace("This means that the syntax is not valid at that point or it is currently not supported.", tr("Invalid/unsupported syntax."));
310 }
311
312 /**
313 * Translates and shortens the error/warning message.
314 * @param key OSM key
315 * @param o error/warning message returned by {@link #getOpeningHoursErrors} or {@link #getOpeningHoursWarnings}
316 * @return translated/shortened error/warning message
317 */
318 static String getErrorMessage(String key, Object o) {
319 return key + " - " + getErrorMessage(o);
320 }
321
322 protected void check(final OsmPrimitive p, final String key) {
323 for (OpeningHoursTestError e : checkOpeningHourSyntax(key, p.get(key))) {
324 errors.add(e.getTestError(p, key));
325 }
326 }
327
328 @Override
329 public void check(final OsmPrimitive p) {
330 check(p, "opening_hours");
331 check(p, "collection_times");
332 check(p, "service_times");
333 }
334}
Note: See TracBrowser for help on using the repository browser.