• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

coditory / quark-uri / #3

pending completion
#3

push

github-actions

ogesaku
init

1212 of 1212 new or added lines in 21 files covered. (100.0%)

926 of 1212 relevant lines covered (76.4%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.3
/src/main/java/com/coditory/quark/uri/UrlValidator.java
1
package com.coditory.quark.uri;
2

3
import org.jetbrains.annotations.NotNull;
4
import org.jetbrains.annotations.Nullable;
5

6
import java.util.Set;
7
import java.util.regex.Matcher;
8
import java.util.regex.Pattern;
9

10
import static com.coditory.quark.uri.InetAddressValidator.isValidInetV4Address;
11
import static com.coditory.quark.uri.Preconditions.expectNonNull;
12
import static com.coditory.quark.uri.Strings.isNullOrEmpty;
13

14
public final class UrlValidator {
15
    private static final UrlValidator INSTANCE = builder().build();
1✔
16

17
    public static UrlValidator instance() {
18
        return INSTANCE;
×
19
    }
20

21
    public static boolean isValidUrl(String url) {
22
        return INSTANCE.isValid(url);
1✔
23
    }
24

25
    public static boolean isValidUrl(UriComponents uriComponents) {
26
        return INSTANCE.isValid(uriComponents);
1✔
27
    }
28

29
    private static final String ALPHA_CHARS = "a-zA-Z";
30
    private static final String SPECIAL_CHARS = ";/@&=,.?:+$";
31
    private static final String VALID_CHARS = "[^\\s" + SPECIAL_CHARS + "]";
32
    private static final String ATOM = VALID_CHARS + '+';
33
    private static final Pattern SCHEME_PATTERN = Pattern.compile("^\\p{Alpha}[\\p{Alnum}\\+\\-\\.]*");
1✔
34
    private static final Pattern PATH_PATTERN = Pattern.compile("^(/[-\\w:@&?=+,.!/~*'%$_;]*)?$");
1✔
35
    private static final Pattern DOMAIN_PATTERN = Pattern.compile("^" + ATOM + "(\\." + ATOM + ")*$");
1✔
36
    private static final Pattern ATOM_PATTERN = Pattern.compile("^(" + ATOM + ").*?$");
1✔
37
    private static final Pattern ALPHA_PATTERN = Pattern.compile("^[" + ALPHA_CHARS + "]");
1✔
38
    private final boolean allowFragments;
39
    private final Set<String> allowedSchemes;
40

41
    UrlValidator(boolean allowFragments, Set<String> allowedSchemes) {
1✔
42
        this.allowFragments = allowFragments;
1✔
43
        this.allowedSchemes = Set.copyOf(allowedSchemes);
1✔
44
    }
1✔
45

46
    public boolean isValid(String url) {
47
        expectNonNull(url, "url");
1✔
48
        UriComponents uriComponents;
49
        try {
50
            uriComponents = UriComponents.fromHttpUrl(url);
1✔
51
        } catch (Exception e) {
1✔
52
            return false;
1✔
53
        }
1✔
54
        return isValid(uriComponents);
1✔
55
    }
56

57
    public boolean isValid(UriComponents uriComponents) {
58
        expectNonNull(uriComponents, "uriComponents");
1✔
59
        if (!isValidScheme(uriComponents.getScheme())) {
1✔
60
            return false;
1✔
61
        }
62
        if (!isValidHost(uriComponents.getHost())) {
1✔
63
            return false;
1✔
64
        }
65
        if (!isValidPath(uriComponents.getPath())) {
1✔
66
            return false;
1✔
67
        }
68
        if (!isValidFragment(uriComponents.getFragment())) {
1✔
69
            return false;
×
70
        }
71
        return true;
1✔
72
    }
73

74
    boolean isValidScheme(@Nullable String scheme) {
75
        if (scheme == null) {
1✔
76
            return false;
1✔
77
        }
78
        if (!SCHEME_PATTERN.matcher(scheme).matches()) {
1✔
79
            return false;
×
80
        }
81
        return allowedSchemes == null || allowedSchemes.contains(scheme);
1✔
82
    }
83

84
    boolean isValidHost(@Nullable String host) {
85
        if (isNullOrEmpty(host)) {
1✔
86
            return false;
1✔
87
        }
88
        boolean hostname = false;
1✔
89
        boolean ipV4Address = isValidInetV4Address(host);
1✔
90
        if (!ipV4Address) {
1✔
91
            hostname = DOMAIN_PATTERN.matcher(host).matches();
1✔
92
        }
93
        if (hostname) {
1✔
94
            char[] chars = host.toCharArray();
1✔
95
            int size = 1;
1✔
96
            for (char element : chars) {
1✔
97
                if (element == '.') {
1✔
98
                    size++;
1✔
99
                }
100
            }
101
            String[] domainSegment = new String[size];
1✔
102
            boolean match = true;
1✔
103
            int segmentCount = 0;
1✔
104
            int segmentLength = 0;
1✔
105
            while (match) {
1✔
106
                Matcher atomMatcher = ATOM_PATTERN.matcher(host);
1✔
107
                match = atomMatcher.matches();
1✔
108
                if (match) {
1✔
109
                    domainSegment[segmentCount] = atomMatcher.group(1);
1✔
110
                    segmentLength = domainSegment[segmentCount].length() + 1;
1✔
111
                    host =
112
                            (segmentLength >= host.length())
1✔
113
                                    ? ""
1✔
114
                                    : host.substring(segmentLength);
1✔
115

116
                    segmentCount++;
1✔
117
                }
118
            }
1✔
119
            String topLevel = domainSegment[segmentCount - 1];
1✔
120
            if (topLevel.length() < 2 || topLevel.length() > 4) {
1✔
121
                return false;
×
122
            }
123
            if (!ALPHA_PATTERN.matcher(topLevel.substring(0, 1)).matches()) {
1✔
124
                return false;
×
125
            }
126
            if (segmentCount < 2) {
1✔
127
                return false;
×
128
            }
129
        }
130
        return hostname || ipV4Address;
1✔
131
    }
132

133
    boolean isValidPath(@Nullable String path) {
134
        if (isNullOrEmpty(path)) {
1✔
135
            return true;
1✔
136
        }
137
        if (!PATH_PATTERN.matcher(path).matches()) {
1✔
138
            return false;
×
139
        }
140
        int slashCount = countToken("/", path);
1✔
141
        int dot2Count = countToken("..", path);
1✔
142
        return dot2Count <= 0 || (slashCount - 2) > dot2Count;
1✔
143
    }
144

145
    boolean isValidFragment(@Nullable String fragment) {
146
        return fragment == null || allowFragments;
1✔
147
    }
148

149
    private int countToken(String token, String target) {
150
        int tokenIndex = 0;
1✔
151
        int count = 0;
1✔
152
        while (tokenIndex != -1) {
1✔
153
            tokenIndex = target.indexOf(token, tokenIndex);
1✔
154
            if (tokenIndex > -1) {
1✔
155
                tokenIndex++;
1✔
156
                count++;
1✔
157
            }
158
        }
159
        return count;
1✔
160
    }
161

162
    @NotNull
163
    public static UrlValidatorBuilder builder() {
164
        return new UrlValidatorBuilder();
1✔
165
    }
166

167
    public static class UrlValidatorBuilder {
1✔
168
        private boolean allowFragments = true;
1✔
169
        private Set<String> allowedSchemes = Set.of("http", "https");
1✔
170

171
        public UrlValidatorBuilder allowFragments(boolean allowFragments) {
172
            this.allowFragments = allowFragments;
×
173
            return this;
×
174
        }
175

176
        public UrlValidatorBuilder allowedSchemes(Set<String> allowedSchemes) {
177
            this.allowedSchemes = expectNonNull(allowedSchemes, "allowedSchemes");
×
178
            return this;
×
179
        }
180

181
        public UrlValidator build() {
182
            return new UrlValidator(allowFragments, allowedSchemes);
1✔
183
        }
184
    }
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc