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

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

fix #17932 - fix error message localization

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