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.net.IDN;
007import java.util.regex.Pattern;
008
009import org.openstreetmap.josm.data.osm.Node;
010import org.openstreetmap.josm.data.osm.OsmPrimitive;
011import org.openstreetmap.josm.data.osm.Relation;
012import org.openstreetmap.josm.data.osm.Way;
013import org.openstreetmap.josm.data.validation.Severity;
014import org.openstreetmap.josm.data.validation.Test;
015import org.openstreetmap.josm.data.validation.TestError;
016import org.openstreetmap.josm.data.validation.routines.AbstractValidator;
017import org.openstreetmap.josm.data.validation.routines.EmailValidator;
018import org.openstreetmap.josm.data.validation.routines.UrlValidator;
019
020/**
021 * Performs validation tests on internet-related tags (websites, e-mail addresses, etc.).
022 * @since 7489
023 */
024public class InternetTags extends Test {
025
026    /** Error code for an invalid URL */
027    public static final int INVALID_URL = 3301;
028    /** Error code for an invalid e-mail */
029    public static final int INVALID_EMAIL = 3302;
030
031    private static final Pattern ASCII_PATTERN = Pattern.compile("^\\p{ASCII}+$");
032
033    /**
034     * List of keys subject to URL validation.
035     */
036    private static 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 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        TestError error = doValidateTag(p, k, null, validator, code);
088        if (error != null) {
089            // Workaround to https://issues.apache.org/jira/browse/VALIDATOR-290
090            // Apache Commons Validator 1.4.1-SNAPSHOT does not support yet IDN URLs
091            // To remove if it gets fixed on Apache side
092            String v = p.get(k);
093            if (!ASCII_PATTERN.matcher(v).matches()) {
094                try {
095                    String protocol = "";
096                    if (v.contains("://")) {
097                        protocol = v.substring(0, v.indexOf("://")+3);
098                    }
099                    String domain = !protocol.isEmpty() ? v.substring(protocol.length(), v.length()) : v;
100                    String ending = "";
101                    if (domain.contains("/")) {
102                        int idx = domain.indexOf('/');
103                        ending = domain.substring(idx, domain.length());
104                        domain = domain.substring(0, idx);
105                    }
106                    // Try to apply ToASCII algorithm
107                    error = doValidateTag(p, k, protocol+IDN.toASCII(domain)+ending, validator, code);
108                } catch (IllegalArgumentException e) {
109                    error.setMessage(error.getMessage() +
110                            tr(" URL cannot be converted to ASCII: {0}", e.getMessage()));
111                }
112            }
113        }
114        return error;
115    }
116
117    /**
118     * Validates a given primitive tag against a given validator.
119     * @param p The OSM primitive to test
120     * @param k The key to validate
121     * @param v The value to validate. May be {@code null} to use {@code p.get(k)}
122     * @param validator The validator to run
123     * @param code The error code to set if the validation fails
124     * @return The error if the validation fails, {@code null} otherwise
125     */
126    private TestError doValidateTag(OsmPrimitive p, String k, String v, AbstractValidator validator, int code) {
127        TestError error = null;
128        String value = v != null ? v : p.get(k);
129        if (!validator.isValid(value)) {
130            String errMsg = validator.getErrorMessage();
131            // Special treatment to allow URLs without protocol. See UrlValidator#isValid
132            if (tr("URL contains an invalid protocol: {0}", (String) null).equals(errMsg)) {
133                String proto = validator instanceof EmailValidator ? "mailto://" : "http://";
134                return doValidateTag(p, k, proto+value, validator, code);
135            }
136            String msg = tr("''{0}'': {1}", k, errMsg);
137            // todo obtain English message for ignore functionality
138            error = new TestError(this, Severity.WARNING, validator.getValidatorName(), msg, msg, code, p);
139        }
140        return error;
141    }
142
143    private void test(OsmPrimitive p) {
144        for (String k : p.keySet()) {
145            // Test key against URL validator
146            if (!doTest(p, k, URL_KEYS, UrlValidator.getInstance(), INVALID_URL)) {
147                // Test key against e-mail validator only if the URL validator did not fail
148                doTest(p, k, EMAIL_KEYS, EmailValidator.getInstance(), INVALID_EMAIL);
149            }
150        }
151    }
152
153    @Override
154    public void visit(Node n) {
155        test(n);
156    }
157
158    @Override
159    public void visit(Way w) {
160        test(w);
161    }
162
163    @Override
164    public void visit(Relation r) {
165        test(r);
166    }
167}