001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.InputStreamReader; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collections; 010import java.util.List; 011 012import javax.script.Invocable; 013import javax.script.ScriptEngine; 014import javax.script.ScriptEngineManager; 015import javax.script.ScriptException; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.command.ChangePropertyCommand; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.Relation; 022import org.openstreetmap.josm.data.osm.Way; 023import org.openstreetmap.josm.data.validation.FixableTestError; 024import org.openstreetmap.josm.data.validation.Severity; 025import org.openstreetmap.josm.data.validation.Test; 026import org.openstreetmap.josm.data.validation.TestError; 027import org.openstreetmap.josm.io.MirroredInputStream; 028 029/** 030 * Tests the correct usage of the opening hour syntax of the tags 031 * {@code opening_hours}, {@code collection_times}, {@code service_times} according to 032 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a>. 033 * 034 * @since 6370 035 */ 036public class OpeningHourTest extends Test { 037 038 /** 039 * Javascript engine 040 */ 041 public static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("JavaScript"); 042 043 /** 044 * Constructs a new {@code OpeningHourTest}. 045 */ 046 public OpeningHourTest() { 047 super(tr("Opening hours syntax"), 048 tr("This test checks the correct usage of the opening hours syntax.")); 049 } 050 051 @Override 052 public void initialize() throws Exception { 053 super.initialize(); 054 if (ENGINE != null) { 055 ENGINE.eval(new InputStreamReader(new MirroredInputStream("resource://data/validator/opening_hours.js"), "UTF-8")); 056 // fake country/state to not get errors on holidays 057 ENGINE.eval("var nominatimJSON = {address: {state: 'Bayern', country_code: 'de'}};"); 058 ENGINE.eval("" + 059 "var oh = function (value, mode) {" + 060 " try {" + 061 " var r= new opening_hours(value, nominatimJSON, mode);" + 062 " r.getErrors = function() {return [];};" + 063 " return r;" + 064 " } catch(err) {" + 065 " return {" + 066 " getWarnings: function() {return [];}," + 067 " getErrors: function() {return [err.toString()]}" + 068 " };" + 069 " }" + 070 "};"); 071 } else { 072 Main.warn("Unable to initialize OpeningHourTest because no JavaScript engine has been found"); 073 } 074 } 075 076 static enum CheckMode { 077 TIME_RANGE(0), POINTS_IN_TIME(1), BOTH(2); 078 final int code; 079 080 CheckMode(int code) { 081 this.code = code; 082 } 083 } 084 085 protected Object parse(String value, CheckMode mode) throws ScriptException, NoSuchMethodException { 086 return ((Invocable) ENGINE).invokeFunction("oh", value, mode.code); 087 } 088 089 @SuppressWarnings("unchecked") 090 protected List<Object> getList(Object obj) throws ScriptException, NoSuchMethodException { 091 if (obj == null || "".equals(obj)) { 092 return Arrays.asList(); 093 } else if (obj instanceof String) { 094 final Object[] strings = ((String) obj).split("\\\\n"); 095 return Arrays.asList(strings); 096 } else if (obj instanceof List) { 097 return (List<Object>) obj; 098 } else { 099 // recursively call getList() with argument converted to newline-separated string 100 return getList(((Invocable) ENGINE).invokeMethod(obj, "join", "\\n")); 101 } 102 } 103 104 /** 105 * An error concerning invalid syntax for an "opening_hours"-like tag. 106 */ 107 public class OpeningHoursTestError { 108 final Severity severity; 109 final String message, prettifiedValue; 110 111 /** 112 * Constructs a new {@code OpeningHoursTestError} with a known pretiffied value. 113 * @param message The error message 114 * @param severity The error severity 115 * @param prettifiedValue The prettified value 116 */ 117 public OpeningHoursTestError(String message, Severity severity, String prettifiedValue) { 118 this.message = message; 119 this.severity = severity; 120 this.prettifiedValue = prettifiedValue; 121 } 122 123 /** 124 * Constructs a new {@code OpeningHoursTestError}. 125 * @param message The error message 126 * @param severity The error severity 127 */ 128 public OpeningHoursTestError(String message, Severity severity) { 129 this(message, severity, null); 130 } 131 132 /** 133 * Returns the real test error given to JOSM validator. 134 * @param p The incriminated OSM primitive. 135 * @param key The incriminated key, used for display. 136 * @return The real test error given to JOSM validator. Can be fixable or not if a prettified values has been determined. 137 */ 138 public TestError getTestError(final OsmPrimitive p, final String key) { 139 if (prettifiedValue == null) { 140 return new TestError(OpeningHourTest.this, severity, message, 2901, p); 141 } else { 142 return new FixableTestError(OpeningHourTest.this, severity, message, 2901, p, 143 new ChangePropertyCommand(p, key, prettifiedValue)); 144 } 145 } 146 147 /** 148 * Returns the error message. 149 * @return The error message. 150 */ 151 public String getMessage() { 152 return message; 153 } 154 155 /** 156 * Returns the prettified value. 157 * @return The prettified value. 158 */ 159 public String getPrettifiedValue() { 160 return prettifiedValue; 161 } 162 163 /** 164 * Returns the error severity. 165 * @return The error severity. 166 */ 167 public Severity getSeverity() { 168 return severity; 169 } 170 } 171 172 /** 173 * Checks for a correct usage of the opening hour syntax of the {@code value} given according to 174 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing 175 * validation errors or an empty list. Null values result in an empty list. 176 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message 177 * @param value the opening hour value to be checked. 178 * @param mode whether to validate {@code value} as a time range, or points in time, or both. 179 * @return a list of {@link TestError} or an empty list 180 */ 181 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value, CheckMode mode) { 182 if (ENGINE == null || value == null || value.trim().isEmpty()) { 183 return Collections.emptyList(); 184 } 185 try { 186 final Object r = parse(value, mode); 187 final List<OpeningHoursTestError> errors = new ArrayList<OpeningHoursTestError>(); 188 String prettifiedValue = null; 189 try { 190 prettifiedValue = (String) ((Invocable) ENGINE).invokeMethod(r, "prettifyValue"); 191 } catch (Exception e) { 192 Main.debug(e.getMessage()); 193 } 194 for (final Object i : getList(((Invocable) ENGINE).invokeMethod(r, "getErrors"))) { 195 errors.add(new OpeningHoursTestError(key + " - " + i.toString().trim(), Severity.ERROR, prettifiedValue)); 196 } 197 for (final Object i : getList(((Invocable) ENGINE).invokeMethod(r, "getWarnings"))) { 198 errors.add(new OpeningHoursTestError(i.toString().trim(), Severity.WARNING, prettifiedValue)); 199 } 200 return errors; 201 } catch (final Exception ex) { 202 throw new RuntimeException(ex); 203 } 204 } 205 206 /** 207 * Checks for a correct usage of the opening hour syntax of the {@code value} given, in time range mode, according to 208 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing 209 * validation errors or an empty list. Null values result in an empty list. 210 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message 211 * @param value the opening hour value to be checked. 212 * @return a list of {@link TestError} or an empty list 213 */ 214 public List<OpeningHoursTestError> checkOpeningHourSyntax(final String key, final String value) { 215 return checkOpeningHourSyntax(key, value, CheckMode.TIME_RANGE); 216 } 217 218 protected void check(final OsmPrimitive p, final String key, CheckMode mode) { 219 for (OpeningHoursTestError e : checkOpeningHourSyntax(key, p.get(key), mode)) { 220 errors.add(e.getTestError(p, key)); 221 } 222 } 223 224 protected void check(final OsmPrimitive p) { 225 check(p, "opening_hours", CheckMode.TIME_RANGE); 226 check(p, "collection_times", CheckMode.BOTH); 227 check(p, "service_times", CheckMode.BOTH); 228 } 229 230 @Override 231 public void visit(final Node n) { 232 check(n); 233 } 234 235 @Override 236 public void visit(final Relation r) { 237 check(r); 238 } 239 240 @Override 241 public void visit(final Way w) { 242 check(w); 243 } 244}