001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.StringReader; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.List; 011import java.util.Locale; 012import java.util.Objects; 013import java.util.stream.Collectors; 014 015import javax.swing.JCheckBox; 016import javax.swing.JPanel; 017 018import org.openstreetmap.josm.command.ChangePropertyCommand; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.preferences.BooleanProperty; 021import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 022import org.openstreetmap.josm.data.validation.Severity; 023import org.openstreetmap.josm.data.validation.Test.TagTest; 024import org.openstreetmap.josm.data.validation.TestError; 025import org.openstreetmap.josm.tools.GBC; 026import org.openstreetmap.josm.tools.Utils; 027 028import ch.poole.openinghoursparser.OpeningHoursParseException; 029import ch.poole.openinghoursparser.OpeningHoursParser; 030import ch.poole.openinghoursparser.Rule; 031import ch.poole.openinghoursparser.Util; 032 033/** 034 * Tests the correct usage of the opening hour syntax of the tags 035 * {@code opening_hours}, {@code collection_times}, {@code service_times} according to 036 * <a href="https://github.com/simonpoole/OpeningHoursParser">OpeningHoursParser</a>. 037 * 038 * @since 6370 (using opening_hours.js), 15978 (using OpeningHoursParser) 039 */ 040public class OpeningHourTest extends TagTest { 041 042 private static final Collection<String> KEYS_TO_CHECK = Arrays.asList("opening_hours", "collection_times", "service_times"); 043 private static final BooleanProperty PREF_STRICT_MODE = 044 new BooleanProperty(ValidatorPrefHelper.PREFIX + "." + OpeningHourTest.class.getSimpleName() + "." + "strict", false); 045 private final JCheckBox checkboxStrictMode = new JCheckBox(tr("Enable strict mode.")); 046 047 /** 048 * Constructs a new {@code OpeningHourTest}. 049 */ 050 public OpeningHourTest() { 051 super(tr("Opening hours syntax"), 052 tr("This test checks the correct usage of the opening hours syntax.")); 053 } 054 055 /** 056 * Returns the real test error given to JOSM validator. 057 * @param severity The error severity 058 * @param message The error message 059 * @param key The incriminated key, used for display. 060 * @param value The incriminated value, used for comparison with prettified value. 061 * @param prettifiedValue The prettified value 062 * @param p The incriminated OSM primitive. 063 * @return The real test error given to JOSM validator. Can be fixable or not if a prettified values has been determined. 064 */ 065 private TestError createTestError(Severity severity, String message, String key, String value, String prettifiedValue, OsmPrimitive p) { 066 final TestError.Builder error = TestError.builder(this, severity, 2901) 067 .message(tr("Opening hours syntax"), message) // todo obtain English message for ignore functionality 068 .primitives(p != null ? new OsmPrimitive[] {p} : new OsmPrimitive[] {}); 069 if (p == null || prettifiedValue == null || prettifiedValue.equals(value)) { 070 return error.build(); 071 } else { 072 return error.fix(() -> new ChangePropertyCommand(p, key, prettifiedValue)).build(); 073 } 074 } 075 076 /** 077 * Checks for a correct usage of the opening hour syntax of the {@code value} given, 078 * and returns a list containing validation errors or an empty list. Null values result in an empty list. 079 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message 080 * @param value the opening hour value to be checked. 081 * @return a list of {@link TestError} or an empty list 082 */ 083 public List<TestError> checkOpeningHourSyntax(final String key, final String value) { 084 return checkOpeningHourSyntax(key, value, null, Locale.getDefault()); 085 } 086 087 /** 088 * Checks for a correct usage of the opening hour syntax of the {@code value} given, 089 * and returns a list containing validation errors or an empty list. Null values result in an empty list. 090 * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). 091 * @param value the opening hour value to be checked. 092 * @param p the primitive to check/fix. 093 * @param locale the locale code used for localizing messages 094 * @return a list of {@link TestError} or an empty list 095 */ 096 List<TestError> checkOpeningHourSyntax(final String key, final String value, OsmPrimitive p, Locale locale) { 097 if (Utils.isEmpty(value)) { 098 return Collections.emptyList(); 099 } 100 101 ch.poole.openinghoursparser.I18n.setLocale(locale); 102 String prettifiedValue = null; 103 try { 104 final boolean strict = PREF_STRICT_MODE.get(); 105 final List<Rule> rules = new OpeningHoursParser(new StringReader(value)).rules(strict); 106 prettifiedValue = Util.rulesToOpeningHoursString(rules); 107 if (!Objects.equals(value, prettifiedValue) && !strict) { 108 // parse again in strict mode for detailed message 109 new OpeningHoursParser(new StringReader(value)).rules(true); 110 } 111 } catch (OpeningHoursParseException e) { 112 String message = e.getExceptions().stream() 113 .map(OpeningHoursParseException::getMessage) 114 .distinct() 115 .collect(Collectors.joining("; ")); 116 return Collections.singletonList(createTestError(Severity.WARNING, message, key, value, prettifiedValue, p)); 117 } 118 119 if (!includeOtherSeverityChecks() || Objects.equals(value, prettifiedValue)) { 120 return Collections.emptyList(); 121 } else { 122 final String message = tr("{0} value can be prettified", key); 123 return Collections.singletonList(createTestError(Severity.OTHER, message, key, value, prettifiedValue, p)); 124 } 125 } 126 127 @Override 128 public void check(final OsmPrimitive p) { 129 addErrorsForPrimitive(p, this.errors); 130 } 131 132 /** 133 * Checks the tags of the given primitive and adds validation errors to the given list. 134 * @param p The primitive to test 135 * @param errors The list to add validation errors to 136 * @since 17643 137 */ 138 public void addErrorsForPrimitive(OsmPrimitive p, Collection<TestError> errors) { 139 if (p.isTagged()) { 140 for (String key : KEYS_TO_CHECK) { 141 errors.addAll(checkOpeningHourSyntax(key, p.get(key), p, Locale.getDefault())); 142 } 143 // COVID-19, a few additional values are permitted, see #19048, see https://wiki.openstreetmap.org/wiki/Key:opening_hours:covid19 144 final String keyCovid19 = "opening_hours:covid19"; 145 if (p.hasTag(keyCovid19) && !p.hasTag(keyCovid19, "same", "restricted", "open", "off")) { 146 errors.addAll(checkOpeningHourSyntax(keyCovid19, p.get(keyCovid19), p, Locale.getDefault())); 147 } 148 } 149 } 150 151 @Override 152 public void addGui(JPanel testPanel) { 153 super.addGui(testPanel); 154 checkboxStrictMode.setSelected(PREF_STRICT_MODE.get()); 155 testPanel.add(checkboxStrictMode, GBC.eol().insets(20, 0, 0, 0)); 156 } 157 158 @Override 159 public boolean ok() { 160 super.ok(); 161 PREF_STRICT_MODE.put(checkboxStrictMode.isSelected()); 162 return false; 163 } 164}