Skip to content

Commit 6659ce7

Browse files
authored
Merge branch 'master' into package-url-builder-with-qualifiers
2 parents a888fa7 + a0f623e commit 6659ce7

File tree

4 files changed

+166
-67
lines changed

4 files changed

+166
-67
lines changed

src/main/java/com/github/packageurl/PackageURL.java

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.Map;
3232
import java.util.Objects;
3333
import java.util.TreeMap;
34+
import java.util.function.IntPredicate;
3435
import java.util.regex.Pattern;
3536
import java.util.stream.Collectors;
3637

@@ -98,13 +99,31 @@ public PackageURL(final String type, final String namespace, final String name,
9899
this.namespace = validateNamespace(namespace);
99100
this.name = validateName(name);
100101
this.version = validateVersion(version);
101-
this.qualifiers = validateQualifiers(qualifiers);
102+
this.qualifiers = parseQualifiers(qualifiers);
102103
this.subpath = validatePath(subpath, true);
103104
verifyTypeConstraints(this.type, this.namespace, this.name);
104105
}
105106

106107
/**
107-
* The PackageURL scheme constant.
108+
* Constructs a new PackageURL object.
109+
*
110+
* @param type the type of package (i.e. maven, npm, gem, etc)
111+
* @param namespace the name prefix (i.e. group, owner, organization)
112+
* @param name the name of the package
113+
* @param version the version of the package
114+
* @param qualifiers an array of key/value pair qualifiers
115+
* @param subpath the subpath string
116+
* @throws MalformedPackageURLException if parsing fails
117+
* @since 1.6.0
118+
*/
119+
public PackageURL(final String type, final String namespace, final String name, final String version,
120+
final Map<String, String> qualifiers, final String subpath)
121+
throws MalformedPackageURLException {
122+
this(type, namespace, name, version, (qualifiers != null) ? new TreeMap<>(qualifiers) : null, subpath);
123+
}
124+
125+
/**
126+
* The PackageURL scheme constant
108127
*/
109128
public static final String SCHEME = "pkg";
110129

@@ -223,7 +242,7 @@ public String getVersion() {
223242
* @since 1.0.0
224243
*/
225244
public Map<String, String> getQualifiers() {
226-
return (qualifiers != null)? Collections.unmodifiableMap(qualifiers) : null;
245+
return (qualifiers != null) ? Collections.unmodifiableMap(qualifiers) : null;
227246
}
228247

229248
/**
@@ -247,18 +266,31 @@ private String validateType(final String value) throws MalformedPackageURLExcept
247266
throw new MalformedPackageURLException("The PackageURL type cannot be null or empty");
248267
}
249268

250-
if (isDigit(value.charAt(0))) {
251-
throw new MalformedPackageURLException("The PackageURL type cannot start with a number");
252-
}
269+
validateChars(value, PackageURL::isValidCharForType, "type");
270+
271+
return value;
272+
}
273+
274+
private static boolean isValidCharForType(int c) {
275+
return (isAlphaNumeric(c) || c == '.' || c == '+' || c == '-');
276+
}
253277

254-
if (!value.chars().allMatch(c -> (c == '.' || c == '+' || c == '-'
255-
|| isUpperCase(c)
256-
|| isLowerCase(c)
257-
|| isDigit(c)))) {
258-
throw new MalformedPackageURLException("The PackageURL type contains invalid characters");
278+
private static boolean isValidCharForKey(int c) {
279+
return (isAlphaNumeric(c) || c == '.' || c == '_' || c == '-');
280+
}
281+
282+
private static void validateChars(String value, IntPredicate predicate, String component) throws MalformedPackageURLException {
283+
char firstChar = value.charAt(0);
284+
285+
if (isDigit(firstChar)) {
286+
throw new MalformedPackageURLException("The PackageURL " + component + " cannot start with a number: " + firstChar);
259287
}
260288

261-
return value;
289+
String invalidChars = value.chars().filter(predicate.negate()).mapToObj(c -> String.valueOf((char) c)).collect(Collectors.joining(", "));
290+
291+
if (!invalidChars.isEmpty()) {
292+
throw new MalformedPackageURLException("The PackageURL " + component + " '" + value + "' contains invalid characters: " + invalidChars);
293+
}
262294
}
263295

264296
private String validateNamespace(final String value) throws MalformedPackageURLException {
@@ -319,7 +351,7 @@ private String validateVersion(final String value) {
319351
}
320352

321353
private Map<String, String> validateQualifiers(final Map<String, String> values) throws MalformedPackageURLException {
322-
if (values == null) {
354+
if (values == null || values.isEmpty()) {
323355
return null;
324356
}
325357
for (Map.Entry<String, String> entry : values.entrySet()) {
@@ -337,10 +369,7 @@ private void validateKey(final String value) throws MalformedPackageURLException
337369
throw new MalformedPackageURLException("Qualifier key is invalid: " + value);
338370
}
339371

340-
if (isDigit(value.charAt(0))
341-
|| !value.chars().allMatch(c -> isLowerCase(c) || (isDigit(c)) || c == '.' || c == '-' || c == '_')) {
342-
throw new MalformedPackageURLException("Qualifier key is invalid: " + value);
343-
}
372+
validateChars(value, PackageURL::isValidCharForKey, "qualifier key");
344373
}
345374

346375
private String validatePath(final String value, final boolean isSubpath) throws MalformedPackageURLException {
@@ -463,7 +492,7 @@ private static String uriEncode(String source, Charset charset) {
463492
}
464493

465494
private static boolean isUnreserved(int c) {
466-
return (isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c);
495+
return (isValidCharForKey(c) || c == '~');
467496
}
468497

469498
private static boolean isAlpha(int c) {
@@ -474,6 +503,10 @@ private static boolean isDigit(int c) {
474503
return (c >= '0' && c <= '9');
475504
}
476505

506+
private static boolean isAlphaNumeric(int c) {
507+
return (isDigit(c) || isAlpha(c));
508+
}
509+
477510
private static boolean isUpperCase(int c) {
478511
return (c >= 'A' && c <= 'Z');
479512
}
@@ -656,6 +689,23 @@ private void verifyTypeConstraints(String type, String namespace, String name) t
656689
}
657690
}
658691

692+
private Map<String, String> parseQualifiers(final Map<String, String> qualifiers) throws MalformedPackageURLException {
693+
if (qualifiers == null || qualifiers.isEmpty()) {
694+
return null;
695+
}
696+
697+
try {
698+
final TreeMap<String, String> results = qualifiers.entrySet().stream()
699+
.filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
700+
.collect(TreeMap::new,
701+
(map, value) -> map.put(toLowerCase(value.getKey()), value.getValue()),
702+
TreeMap::putAll);
703+
return validateQualifiers(results);
704+
} catch (ValidationException ex) {
705+
throw new MalformedPackageURLException(ex.getMessage());
706+
}
707+
}
708+
659709
@SuppressWarnings("StringSplitter")//reason: surprising behavior is okay in this case
660710
private Map<String, String> parseQualifiers(final String encodedString) throws MalformedPackageURLException {
661711
try {

src/main/java/com/github/packageurl/PackageURLBuilder.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import java.util.Map;
2525
import java.util.Set;
26+
import java.util.Collections;
27+
import java.util.Map;
2628
import java.util.TreeMap;
2729

2830
/**
@@ -240,11 +242,11 @@ public String getSubpath() {
240242
* An empty map is returned if no qualifiers is set.
241243
* @return all qualifiers set in this builder, or an empty map if none are set.
242244
*/
243-
public TreeMap<String, String> getQualifiers() {
245+
public Map<String, String> getQualifiers() {
244246
if (qualifiers == null) {
245-
return new TreeMap<>();
247+
return null;
246248
}
247-
return new TreeMap<>(qualifiers);
249+
return Collections.unmodifiableMap(qualifiers);
248250
}
249251

250252
/**

src/test/java/com/github/packageurl/PackageURLBuilderTest.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,26 +97,22 @@ public void testPackageURLBuilder() throws MalformedPackageURLException {
9797

9898
@Test
9999
public void testPackageURLBuilderException1() throws MalformedPackageURLException {
100-
exception.expect(MalformedPackageURLException.class);
101-
exception.expectMessage("contains a qualifier key with an empty or null");
102100
PackageURL purl = PackageURLBuilder.aPackageURL()
103101
.withType("type")
104102
.withName("name")
105103
.withQualifier("key","")
106104
.build();
107-
Assert.fail("Build should fail due to invalid qualifier (empty value)");
105+
assertNull(purl.getQualifiers());
108106
}
109107

110108
@Test
111109
public void testPackageURLBuilderException1Null() throws MalformedPackageURLException {
112-
exception.expect(MalformedPackageURLException.class);
113-
exception.expectMessage("contains a qualifier key with an empty or null");
114-
PackageURLBuilder.aPackageURL()
110+
PackageURL purl = PackageURLBuilder.aPackageURL()
115111
.withType("type")
116112
.withName("name")
117113
.withQualifier("key",null)
118114
.build();
119-
Assert.fail("Build should fail due to invalid qualifier (null value)");
115+
assertNull(purl.getQualifiers());
120116
}
121117

122118
@Test
@@ -230,15 +226,13 @@ private void assertBuilderMatch(PackageURL expected, PackageURLBuilder actual) t
230226
Map<String, String> eQualifiers = expected.getQualifiers();
231227
Map<String, String> aQualifiers = actual.getQualifiers();
232228

233-
if (eQualifiers != null) {
234-
eQualifiers.forEach((k,v)-> {
235-
Assert.assertEquals(v, aQualifiers.remove(k));
229+
assertEquals(eQualifiers, aQualifiers);
230+
231+
if (eQualifiers != null && aQualifiers != null) {
232+
eQualifiers.forEach((k,v) -> {
236233
Assert.assertEquals(v, actual.getQualifier(k));
237234
});
238235
}
239-
240-
Assert.assertTrue(aQualifiers.isEmpty());
241-
242236
}
243237

244238
}

0 commit comments

Comments
 (0)