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.ArrayList;
008import java.util.List;
009import java.util.function.Supplier;
010
011import org.openstreetmap.josm.command.ChangePropertyCommand;
012import org.openstreetmap.josm.command.Command;
013import org.openstreetmap.josm.data.osm.Node;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.Relation;
016import org.openstreetmap.josm.data.osm.Way;
017import org.openstreetmap.josm.data.validation.Severity;
018import org.openstreetmap.josm.data.validation.Test;
019import org.openstreetmap.josm.data.validation.TestError;
020import org.openstreetmap.josm.data.validation.routines.AbstractValidator;
021import org.openstreetmap.josm.data.validation.routines.EmailValidator;
022import org.openstreetmap.josm.data.validation.routines.UrlValidator;
023
024/**
025 * Performs validation tests on internet-related tags (websites, e-mail addresses, etc.).
026 * @since 7489
027 */
028public class InternetTags extends Test {
029
030    /** Error code for an invalid URL */
031    public static final int INVALID_URL = 3301;
032    /** Error code for an invalid e-mail */
033    public static final int INVALID_EMAIL = 3302;
034
035    /**
036     * List of keys subject to URL validation.
037     */
038    private static final String[] URL_KEYS = {
039        "url", "source:url",
040        "website", "contact:website", "heritage:website", "source:website"
041    };
042
043    /**
044     * List of keys subject to email validation.
045     */
046    private static final String[] EMAIL_KEYS = {
047        "email", "contact:email"
048    };
049
050    /**
051     * Constructs a new {@code InternetTags} test.
052     */
053    public InternetTags() {
054        super(tr("Internet tags"), tr("Checks for errors in internet-related tags."));
055    }
056
057    /**
058     * Potentially validates a given primitive key against a given validator.
059     * @param p The OSM primitive to test
060     * @param k The key to validate
061     * @param keys The list of keys to check. If {@code k} is not inside this collection, do nothing
062     * @param validator The validator to run if {@code k} is inside {@code keys}
063     * @param code The error code to set if the validation fails
064     * @return {@code true} if the validation fails. In this case, a new error has been created.
065     */
066    private boolean doTest(OsmPrimitive p, String k, String[] keys, AbstractValidator validator, int code) {
067        for (String i : keys) {
068            if (i.equals(k)) {
069                return errors.addAll(validateTag(p, k, validator, code));
070            }
071        }
072        return false;
073    }
074
075    /**
076     * Validates a given primitive tag against a given validator.
077     * @param p The OSM primitive to test
078     * @param k The key to validate
079     * @param validator The validator to run
080     * @param code The error code to set if the validation fails
081     * @return The error if the validation fails, {@code null} otherwise
082     * @since 7824
083     * @since 14803 (return type)
084     */
085    public List<TestError> validateTag(OsmPrimitive p, String k, AbstractValidator validator, int code) {
086        return doValidateTag(p, k, null, validator, code);
087    }
088
089    /**
090     * Validates a given primitive tag against a given validator.
091     * @param p The OSM primitive to test
092     * @param k The key to validate
093     * @param v The value to validate. May be {@code null} to use {@code p.get(k)}
094     * @param validator The validator to run
095     * @param code The error code to set if the validation fails
096     * @return The error if the validation fails, {@code null} otherwise
097     */
098    private List<TestError> doValidateTag(OsmPrimitive p, String k, String v, AbstractValidator validator, int code) {
099        List<TestError> errors = new ArrayList<>();
100        String values = v != null ? v : p.get(k);
101        for (String value : values.split(";")) {
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                errors.add(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        }
122        return errors;
123    }
124
125    private void test(OsmPrimitive p) {
126        for (String k : p.keySet()) {
127            // Test key against URL validator
128            if (!doTest(p, k, URL_KEYS, UrlValidator.getInstance(), INVALID_URL)) {
129                // Test key against e-mail validator only if the URL validator did not fail
130                doTest(p, k, EMAIL_KEYS, EmailValidator.getInstance(), INVALID_EMAIL);
131            }
132        }
133    }
134
135    @Override
136    public void visit(Node n) {
137        test(n);
138    }
139
140    @Override
141    public void visit(Way w) {
142        test(w);
143    }
144
145    @Override
146    public void visit(Relation r) {
147        test(r);
148    }
149}