1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.validation.tests;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 |
|
---|
6 | import java.io.StringReader;
|
---|
7 | import java.util.Arrays;
|
---|
8 | import java.util.Collection;
|
---|
9 | import java.util.Collections;
|
---|
10 | import java.util.List;
|
---|
11 | import java.util.Locale;
|
---|
12 | import java.util.Objects;
|
---|
13 | import java.util.stream.Collectors;
|
---|
14 |
|
---|
15 | import javax.swing.JCheckBox;
|
---|
16 | import javax.swing.JPanel;
|
---|
17 |
|
---|
18 | import org.openstreetmap.josm.command.ChangePropertyCommand;
|
---|
19 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
20 | import org.openstreetmap.josm.data.preferences.BooleanProperty;
|
---|
21 | import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
|
---|
22 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
23 | import org.openstreetmap.josm.data.validation.Test.TagTest;
|
---|
24 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
25 | import org.openstreetmap.josm.tools.GBC;
|
---|
26 | import org.openstreetmap.josm.tools.Utils;
|
---|
27 |
|
---|
28 | import ch.poole.openinghoursparser.OpeningHoursParseException;
|
---|
29 | import ch.poole.openinghoursparser.OpeningHoursParser;
|
---|
30 | import ch.poole.openinghoursparser.Rule;
|
---|
31 | import ch.poole.openinghoursparser.Util;
|
---|
32 |
|
---|
33 | /**
|
---|
34 | * Tests the correct usage of the opening hour syntax of the tags
|
---|
35 | * {@code opening_hours}, {@code collection_times}, {@code service_times} according to
|
---|
36 | * <a href="https://github.com/simonpoole/OpeningHoursParser">OpeningHoursParser</a>.
|
---|
37 | *
|
---|
38 | * @since 6370 (using opening_hours.js), 15978 (using OpeningHoursParser)
|
---|
39 | */
|
---|
40 | public class OpeningHourTest extends TagTest {
|
---|
41 |
|
---|
42 | private static final Collection<String> KEYS_TO_CHECK = Arrays.asList("opening_hours", "collection_times", "service_times");
|
---|
43 | private static final BooleanProperty PREF_STRICT_MODE =
|
---|
44 | new BooleanProperty(ValidatorPrefHelper.PREFIX + "." + OpeningHourTest.class.getSimpleName() + "." + "strict", false);
|
---|
45 | private final JCheckBox checkboxStrictMode = new JCheckBox(tr("Enable strict mode."));
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * Constructs a new {@code OpeningHourTest}.
|
---|
49 | */
|
---|
50 | public OpeningHourTest() {
|
---|
51 | super(tr("Opening hours syntax"),
|
---|
52 | tr("This test checks the correct usage of the opening hours syntax."));
|
---|
53 | }
|
---|
54 |
|
---|
55 | /**
|
---|
56 | * Returns the real test error given to JOSM validator.
|
---|
57 | * @param severity The error severity
|
---|
58 | * @param message The error message
|
---|
59 | * @param key The incriminated key, used for display.
|
---|
60 | * @param value The incriminated value, used for comparison with prettified value.
|
---|
61 | * @param prettifiedValue The prettified value
|
---|
62 | * @param p The incriminated OSM primitive.
|
---|
63 | * @return The real test error given to JOSM validator. Can be fixable or not if a prettified values has been determined.
|
---|
64 | */
|
---|
65 | private TestError createTestError(Severity severity, String message, String key, String value, String prettifiedValue, OsmPrimitive p) {
|
---|
66 | final TestError.Builder error = TestError.builder(this, severity, 2901)
|
---|
67 | .message(tr("Opening hours syntax"), message) // todo obtain English message for ignore functionality
|
---|
68 | .primitives(p != null ? new OsmPrimitive[] {p} : new OsmPrimitive[] {});
|
---|
69 | if (p == null || prettifiedValue == null || prettifiedValue.equals(value)) {
|
---|
70 | return error.build();
|
---|
71 | } else {
|
---|
72 | return error.fix(() -> new ChangePropertyCommand(p, key, prettifiedValue)).build();
|
---|
73 | }
|
---|
74 | }
|
---|
75 |
|
---|
76 | /**
|
---|
77 | * Checks for a correct usage of the opening hour syntax of the {@code value} given,
|
---|
78 | * and returns a list containing validation errors or an empty list. Null values result in an empty list.
|
---|
79 | * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times"). Used in error message
|
---|
80 | * @param value the opening hour value to be checked.
|
---|
81 | * @return a list of {@link TestError} or an empty list
|
---|
82 | */
|
---|
83 | public List<TestError> checkOpeningHourSyntax(final String key, final String value) {
|
---|
84 | return checkOpeningHourSyntax(key, value, null, Locale.getDefault());
|
---|
85 | }
|
---|
86 |
|
---|
87 | /**
|
---|
88 | * Checks for a correct usage of the opening hour syntax of the {@code value} given,
|
---|
89 | * and returns a list containing validation errors or an empty list. Null values result in an empty list.
|
---|
90 | * @param key the OSM key (should be "opening_hours", "collection_times" or "service_times").
|
---|
91 | * @param value the opening hour value to be checked.
|
---|
92 | * @param p the primitive to check/fix.
|
---|
93 | * @param locale the locale code used for localizing messages
|
---|
94 | * @return a list of {@link TestError} or an empty list
|
---|
95 | */
|
---|
96 | List<TestError> checkOpeningHourSyntax(final String key, final String value, OsmPrimitive p, Locale locale) {
|
---|
97 | if (Utils.isEmpty(value)) {
|
---|
98 | return Collections.emptyList();
|
---|
99 | }
|
---|
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, false);
|
---|
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, false);
|
---|
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 | }
|
---|