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