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

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

see #15582 - simplify our usage of opening_hours.js library (drop mode: it is deduced from tag key) + make the parse method public so it can be called from opening_hours plugin

  • Property svn:eol-style set to native
File size: 11.3 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.ScriptEngineManager;
15import javax.script.ScriptException;
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.io.CachedFile;
23import org.openstreetmap.josm.tools.LanguageInfo;
24import org.openstreetmap.josm.tools.Logging;
25
26/**
27 * Tests the correct usage of the opening hour syntax of the tags
28 * {@code opening_hours}, {@code collection_times}, {@code service_times} according to
29 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a>.
30 *
31 * @since 6370
32 */
33public class OpeningHourTest extends Test.TagTest {
34
35 /**
36 * Javascript engine
37 */
38 public static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("JavaScript");
39
40 /**
41 * Constructs a new {@code OpeningHourTest}.
42 */
43 public OpeningHourTest() {
44 super(tr("Opening hours syntax"),
45 tr("This test checks the correct usage of the opening hours syntax."));
46 }
47
48 @Override
49 public void initialize() throws Exception {
50 super.initialize();
51 if (ENGINE != null) {
52 try (CachedFile cf = new CachedFile("resource://data/validator/opening_hours.js");
53 Reader reader = cf.getContentReader()) {
54 ENGINE.eval(reader);
55 ENGINE.eval("var opening_hours = require('opening_hours');");
56 // fake country/state to not get errors on holidays
57 ENGINE.eval("var nominatimJSON = {address: {state: 'Bayern', country_code: 'de'}};");
58 ENGINE.eval(
59 "var oh = function (value, tag_key, locale) {" +
60 " try {" +
61 " var r = new opening_hours(value, nominatimJSON, {tag_key: tag_key, locale: locale});" +
62 " r.getErrors = function() {return [];};" +
63 " return r;" +
64 " } catch (err) {" +
65 " return {" +
66 " prettifyValue: function() {return null;}," +
67 " getWarnings: function() {return [];}," +
68 " getErrors: function() {return [err.toString()]}" +
69 " };" +
70 " }" +
71 "};");
72 }
73 } else {
74 Logging.warn("Unable to initialize OpeningHourTest because no JavaScript engine has been found");
75 }
76 }
77
78 /**
79 * Parses the opening hour syntax of the {@code value} given according to
80 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns an object on which
81 * methods can be called to extract information.
82 * @param value the opening hour value to be checked
83 * @param tagKey the OSM key (should be "opening_hours", "collection_times" or "service_times")
84 * @param locale the locale code used for localizing messages
85 * @return The value returned by the underlying method. Usually a {@code jdk.nashorn.api.scripting.ScriptObjectMirror}
86 * @throws ScriptException if an error occurs during invocation of the underlying method
87 * @throws NoSuchMethodException if underlying method with given name or matching argument types cannot be found
88 * @since 13145
89 */
90 public Object parse(String value, String tagKey, String locale) throws ScriptException, NoSuchMethodException {
91 return ((Invocable) ENGINE).invokeFunction("oh", value, tagKey, locale);
92 }
93
94 @SuppressWarnings("unchecked")
95 protected List<Object> getList(Object obj) throws ScriptException, NoSuchMethodException {
96 if (obj == null || "".equals(obj)) {
97 return Arrays.asList();
98 } else if (obj instanceof String) {
99 final Object[] strings = ((String) obj).split("\\\\n");
100 return Arrays.asList(strings);
101 } else if (obj instanceof List) {
102 return (List<Object>) obj;
103 } else {
104 // recursively call getList() with argument converted to newline-separated string
105 return getList(((Invocable) ENGINE).invokeMethod(obj, "join", "\\n"));
106 }
107 }
108
109 /**
110 * An error concerning invalid syntax for an "opening_hours"-like tag.
111 */
112 public class OpeningHoursTestError {
113 private final Severity severity;
114 private final String message;
115 private final String prettifiedValue;
116
117 /**
118 * Constructs a new {@code OpeningHoursTestError} with a known pretiffied value.
119 * @param message The error message
120 * @param severity The error severity
121 * @param prettifiedValue The prettified value
122 */
123 public OpeningHoursTestError(String message, Severity severity, String prettifiedValue) {
124 this.message = message;
125 this.severity = severity;
126 this.prettifiedValue = prettifiedValue;
127 }
128
129 /**
130 * Returns the real test error given to JOSM validator.
131 * @param p The incriminated OSM primitive.
132 * @param key The incriminated key, used for display.
133 * @return The real test error given to JOSM validator. Can be fixable or not if a prettified values has been determined.
134 */
135 public TestError getTestError(final OsmPrimitive p, final String key) {
136 final TestError.Builder error = TestError.builder(OpeningHourTest.this, severity, 2901)
137 .message(tr("Opening hours syntax"), message) // todo obtain English message for ignore functionality
138 .primitives(p);
139 if (prettifiedValue == null || prettifiedValue.equals(p.get(key))) {
140 return error.build();
141 } else {
142 return error.fix(() -> new ChangePropertyCommand(p, key, prettifiedValue)).build();
143 }
144 }
145
146 /**
147 * Returns the error message.
148 * @return The error message.
149 */
150 public String getMessage() {
151 return message;
152 }
153
154 /**
155 * Returns the prettified value.
156 * @return The prettified value.
157 */
158 public String getPrettifiedValue() {
159 return prettifiedValue;
160 }
161
162 /**
163 * Returns the error severity.
164 * @return The error severity.
165 */
166 public Severity getSeverity() {
167 return severity;
168 }
169
170 @Override
171 public String toString() {
172 return getMessage() + " => " + getPrettifiedValue();
173 }
174 }
175
176 /**
177 * Checks for a correct usage of the opening hour syntax of the {@code value} given according to
178 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing
179 * validation errors or an empty list. Null values result in an empty list.
180 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message
181 * @param value the opening hour value to be checked.
182 * @return a list of {@link TestError} or an empty list
183 */
184 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value) {
185 return checkOpeningHourSyntax(key, value, false, LanguageInfo.getJOSMLocaleCode());
186 }
187
188 /**
189 * Checks for a correct usage of the opening hour syntax of the {@code value} given according to
190 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing
191 * validation errors or an empty list. Null values result in an empty list.
192 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times").
193 * @param value the opening hour value to be checked.
194 * @param ignoreOtherSeverity whether to ignore errors with {@link Severity#OTHER}.
195 * @param locale the locale code used for localizing messages
196 * @return a list of {@link TestError} or an empty list
197 */
198 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value,
199 boolean ignoreOtherSeverity, String locale) {
200 if (ENGINE == null || value == null || value.isEmpty()) {
201 return Collections.emptyList();
202 }
203 final List<OpeningHoursTestError> errors = new ArrayList<>();
204 try {
205 final Object r = parse(value, key, locale);
206 String prettifiedValue = null;
207 try {
208 prettifiedValue = (String) ((Invocable) ENGINE).invokeMethod(r, "prettifyValue");
209 } catch (ScriptException | NoSuchMethodException e) {
210 Logging.warn(e);
211 }
212 for (final Object i : getList(((Invocable) ENGINE).invokeMethod(r, "getErrors"))) {
213 errors.add(new OpeningHoursTestError(getErrorMessage(key, i), Severity.ERROR, prettifiedValue));
214 }
215 for (final Object i : getList(((Invocable) ENGINE).invokeMethod(r, "getWarnings"))) {
216 errors.add(new OpeningHoursTestError(getErrorMessage(key, i), Severity.WARNING, prettifiedValue));
217 }
218 if (!ignoreOtherSeverity && errors.isEmpty() && prettifiedValue != null && !value.equals(prettifiedValue)) {
219 errors.add(new OpeningHoursTestError(tr("opening_hours value can be prettified"), Severity.OTHER, prettifiedValue));
220 }
221 } catch (ScriptException | NoSuchMethodException ex) {
222 Logging.error(ex);
223 }
224 return errors;
225 }
226
227 /**
228 * Translates and shortens the error/warning message.
229 * @param key OSM key
230 * @param o error/warnign message
231 * @return translated/shortened error/warnign message
232 */
233 private static String getErrorMessage(String key, Object o) {
234 String msg = o.toString().trim()
235 .replace("Unexpected token:", tr("Unexpected token:"))
236 .replace("Unexpected token (school holiday parser):", tr("Unexpected token (school holiday parser):"))
237 .replace("Unexpected token in number range:", tr("Unexpected token in number range:"))
238 .replace("Unexpected token in week range:", tr("Unexpected token in week range:"))
239 .replace("Unexpected token in weekday range:", tr("Unexpected token in weekday range:"))
240 .replace("Unexpected token in month range:", tr("Unexpected token in month range:"))
241 .replace("Unexpected token in year range:", tr("Unexpected token in year range:"))
242 .replace("This means that the syntax is not valid at that point or it is currently not supported.", tr("Invalid/unsupported syntax."));
243 return key + " - " + msg;
244 }
245
246 protected void check(final OsmPrimitive p, final String key) {
247 for (OpeningHoursTestError e : checkOpeningHourSyntax(key, p.get(key))) {
248 errors.add(e.getTestError(p, key));
249 }
250 }
251
252 @Override
253 public void check(final OsmPrimitive p) {
254 check(p, "opening_hours");
255 check(p, "collection_times");
256 check(p, "service_times");
257 }
258}
Note: See TracBrowser for help on using the repository browser.