001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.util.function.Supplier;
008
009import org.openstreetmap.josm.command.ChangePropertyCommand;
010import org.openstreetmap.josm.command.Command;
011import org.openstreetmap.josm.data.osm.Node;
012import org.openstreetmap.josm.data.osm.OsmPrimitive;
013import org.openstreetmap.josm.data.osm.Relation;
014import org.openstreetmap.josm.data.osm.Way;
015import org.openstreetmap.josm.data.validation.Severity;
016import org.openstreetmap.josm.data.validation.Test;
017import org.openstreetmap.josm.data.validation.TestError;
018import org.openstreetmap.josm.data.validation.routines.AbstractValidator;
019import org.openstreetmap.josm.data.validation.routines.EmailValidator;
020import org.openstreetmap.josm.data.validation.routines.UrlValidator;
021
022/**
023 * Performs validation tests on internet-related tags (websites, e-mail addresses, etc.).
024 * @since 7489
025 */
026public class InternetTags extends Test {
027
028    /** Error code for an invalid URL */
029    public static final int INVALID_URL = 3301;
030    /** Error code for an invalid e-mail */
031    public static final int INVALID_EMAIL = 3302;
032
033    /**
034     * List of keys subject to URL validation.
035     */
036    private static final String[] URL_KEYS = new String[] {
037        "url", "source:url",
038        "website", "contact:website", "heritage:website", "source:website"
039    };
040
041    /**
042     * List of keys subject to email validation.
043     */
044    private static final String[] EMAIL_KEYS = new String[] {
045        "email", "contact:email"
046    };
047
048    /**
049     * Constructs a new {@code InternetTags} test.
050     */
051    public InternetTags() {
052        super(tr("Internet tags"), tr("Checks for errors in internet-related tags."));
053    }
054
055    /**
056     * Potentially validates a given primitive key against a given validator.
057     * @param p The OSM primitive to test
058     * @param k The key to validate
059     * @param keys The list of keys to check. If {@code k} is not inside this collection, do nothing
060     * @param validator The validator to run if {@code k} is inside {@code keys}
061     * @param code The error code to set if the validation fails
062     * @return {@code true} if the validation fails. In this case, a new error has been created.
063     */
064    private boolean doTest(OsmPrimitive p, String k, String[] keys, AbstractValidator validator, int code) {
065        for (String i : keys) {
066            if (i.equals(k)) {
067                TestError error = validateTag(p, k, validator, code);
068                if (error != null) {
069                    errors.add(error);
070                }
071                break;
072            }
073        }
074        return false;
075    }
076
077    /**
078     * Validates a given primitive tag against a given validator.
079     * @param p The OSM primitive to test
080     * @param k The key to validate
081     * @param validator The validator to run
082     * @param code The error code to set if the validation fails
083     * @return The error if the validation fails, {@code null} otherwise
084     * @since 7824
085     */
086    public TestError validateTag(OsmPrimitive p, String k, AbstractValidator validator, int code) {
087        return doValidateTag(p, k, null, validator, code);
088    }
089
090    /**
091     * Validates a given primitive tag against a given validator.
092     * @param p The OSM primitive to test
093     * @param k The key to validate
094     * @param v The value to validate. May be {@code null} to use {@code p.get(k)}
095     * @param validator The validator to run
096     * @param code The error code to set if the validation fails
097     * @return The error if the validation fails, {@code null} otherwise
098     */
099    private TestError doValidateTag(OsmPrimitive p, String k, String v, AbstractValidator validator, int code) {
100        TestError error = null;
101        String value = v != null ? v : p.get(k);
102        if (!validator.isValid(value)) {
103            Supplier<Command> fix = null;
104            String errMsg = validator.getErrorMessage();
105            if (tr("URL contains an invalid protocol: {0}", (String) null).equals(errMsg)) {
106                // Special treatment to allow URLs without protocol. See UrlValidator#isValid
107                String proto = validator instanceof EmailValidator ? "mailto://" : "http://";
108                return doValidateTag(p, k, proto+value, validator, code);
109            } else if (tr("URL contains an invalid authority: {0}", (String) null).equals(errMsg)
110                    && value.contains("\\") && validator.isValid(value.replaceAll("\\\\", "/"))) {
111                // Special treatment to autofix URLs with backslashes. See UrlValidator#isValid
112                errMsg = tr("URL contains backslashes instead of slashes");
113                fix = () -> new ChangePropertyCommand(p, k, value.replaceAll("\\\\", "/"));
114            }
115            error = TestError.builder(this, Severity.WARNING, code)
116                    .message(validator.getValidatorName(), marktr("''{0}'': {1}"), k, errMsg)
117                    .primitives(p)
118                    .fix(fix)
119                    .build();
120        }
121        return error;
122    }
123
124    private void test(OsmPrimitive p) {
125        for (String k : p.keySet()) {
126            // Test key against URL validator
127            if (!doTest(p, k, URL_KEYS, UrlValidator.getInstance(), INVALID_URL)) {
128                // Test key against e-mail validator only if the URL validator did not fail
129                doTest(p, k, EMAIL_KEYS, EmailValidator.getInstance(), INVALID_EMAIL);
130            }
131        }
132    }
133
134    @Override
135    public void visit(Node n) {
136        test(n);
137    }
138
139    @Override
140    public void visit(Way w) {
141        test(w);
142    }
143
144    @Override
145    public void visit(Relation r) {
146        test(r);
147    }
148}