diff --git a/src/main/java/com/github/packageurl/MalformedPackageURLException.java b/src/main/java/com/github/packageurl/MalformedPackageURLException.java index 775a131..a6d21d6 100644 --- a/src/main/java/com/github/packageurl/MalformedPackageURLException.java +++ b/src/main/java/com/github/packageurl/MalformedPackageURLException.java @@ -34,8 +34,6 @@ public class MalformedPackageURLException extends Exception { /** * Constructs a {@code MalformedPackageURLException} with no detail message. - * - * @since 1.0.0 */ public MalformedPackageURLException() {} @@ -44,7 +42,6 @@ public MalformedPackageURLException() {} * specified detail message. * * @param msg the detail message - * @since 1.0.0 */ public MalformedPackageURLException(@Nullable String msg) { super(msg); diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index a7adc5b..18db0f9 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -46,9 +46,9 @@ * *

* Components are separated by a specific character for unambiguous parsing. - * A purl must NOT contain a URL Authority i.e. there is no support for username, + * A purl must NOT contain a URL Authority, i.e., there is no support for username, * password, host and port components. A namespace segment may sometimes look - * like a host but its interpretation is specific to a type. + * like a host, but its interpretation is specific to a type. *

*

SPEC: https://github.com/package-url/purl-spec

* @@ -112,7 +112,6 @@ public final class PackageURL implements Serializable { * @param purl a valid package URL string to parse * @throws MalformedPackageURLException if parsing fails * @throws NullPointerException if {@code purl} is {@code null} - * @since 1.0.0 */ public PackageURL(final String purl) throws MalformedPackageURLException { parse(requireNonNull(purl, "purl")); @@ -125,24 +124,22 @@ public PackageURL(final String purl) throws MalformedPackageURLException { * @param type the type of package (i.e. maven, npm, gem, etc) * @param name the name of the package * @throws MalformedPackageURLException if parsing fails - * @since 1.0.0 */ public PackageURL(final String type, final String name) throws MalformedPackageURLException { - this(type, null, name, null, null, null); + this(type, null, name, null, (Map) null, null); } /** * Constructs a new PackageURL object. * - * @param type the type of package (i.e. maven, npm, gem, etc), not {@code null} - * @param namespace the name prefix (i.e. group, owner, organization) + * @param type the type of package (i.e., maven, npm, gem, etc.), not {@code null} + * @param namespace the name prefix (i.e., group, owner, organization) * @param name the name of the package, not {@code null} * @param version the version of the package * @param qualifiers an array of key/value pair qualifiers * @param subpath the subpath string * @throws MalformedPackageURLException if parsing fails * @throws NullPointerException if {@code type} or {@code name} are {@code null} - * @since 1.0.0 * @deprecated use {@link #PackageURL(String, String, String, String, Map, String)} instead */ @Deprecated @@ -154,20 +151,14 @@ public PackageURL( final @Nullable TreeMap qualifiers, final @Nullable String subpath) throws MalformedPackageURLException { - this.type = toLowerCase(validateType(requireNonNull(type, "type"))); - this.namespace = validateNamespace(this.type, namespace); - this.name = validateName(this.type, requireNonNull(name, "name")); - this.version = validateVersion(type, version); - this.qualifiers = parseQualifiers(qualifiers); - this.subpath = validateSubpath(subpath); - verifyTypeConstraints(this.type, this.namespace, this.name); + this(type, namespace, name, version, qualifiers != null ? (Map) qualifiers : null, subpath); } /** * Constructs a new PackageURL object. * * @param type the type of package (i.e. maven, npm, gem, etc) - * @param namespace the name prefix (i.e. group, owner, organization) + * @param namespace the name prefix (i.e., group, owner, organization) * @param name the name of the package * @param version the version of the package * @param qualifiers an array of key/value pair qualifiers @@ -181,33 +172,34 @@ public PackageURL( final @Nullable String namespace, final String name, final @Nullable String version, - final @Nullable Map qualifiers, + final @Nullable Map qualifiers, final @Nullable String subpath) throws MalformedPackageURLException { - this(type, namespace, name, version, (qualifiers != null) ? new TreeMap<>(qualifiers) : null, subpath); + this.type = toLowerCase(validateType(requireNonNull(type, "type"))); + this.namespace = validateNamespace(this.type, namespace); + this.name = validateName(this.type, requireNonNull(name, "name")); + this.version = validateVersion(this.type, version); + this.qualifiers = parseQualifiers(qualifiers); + this.subpath = validateSubpath(subpath); + verifyTypeConstraints(this.type, this.namespace, this.name); } /** * Converts this {@link PackageURL} to a {@link PackageURLBuilder}. * * @return the builder - * @deprecated Use {@link PackageURLBuilder#aPackageURL(PackageURL)} or {@link PackageURLBuilder#aPackageURL(String)} + * @since 1.5.0 + * @deprecated use {@link PackageURLBuilder#aPackageURL(PackageURL)} or {@link PackageURLBuilder#aPackageURL(String)} */ + @Deprecated public PackageURLBuilder toBuilder() { - return PackageURLBuilder.aPackageURL() - .withType(getType()) - .withNamespace(getNamespace()) - .withName(getName()) - .withVersion(getVersion()) - .withQualifiers(getQualifiers()) - .withSubpath(getSubpath()); + return PackageURLBuilder.aPackageURL(this); } /** * Returns the package url scheme. * * @return the scheme - * @since 1.0.0 */ public String getScheme() { return SCHEME; @@ -217,17 +209,15 @@ public String getScheme() { * Returns the package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc. * * @return the type - * @since 1.0.0 */ public String getType() { return type; } /** - * Returns the name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization. + * Returns the name prefix such as a Maven groupId, a Docker image owner, a GitHub user or organization. * * @return the namespace - * @since 1.0.0 */ public @Nullable String getNamespace() { return namespace; @@ -237,7 +227,6 @@ public String getType() { * Returns the name of the package. * * @return the name of the package - * @since 1.0.0 */ public String getName() { return name; @@ -247,7 +236,6 @@ public String getName() { * Returns the version of the package. * * @return the version of the package - * @since 1.0.0 */ public @Nullable String getVersion() { return version; @@ -258,7 +246,6 @@ public String getName() { * This method returns an UnmodifiableMap. * * @return all the qualifiers, or an empty map if none are set - * @since 1.0.0 */ public Map getQualifiers() { return qualifiers != null ? Collections.unmodifiableMap(qualifiers) : Collections.emptyMap(); @@ -268,7 +255,6 @@ public Map getQualifiers() { * Returns extra subpath within a package, relative to the package root. * * @return the subpath - * @since 1.0.0 */ public @Nullable String getSubpath() { return subpath; @@ -484,7 +470,6 @@ public String toString() { * Returns the canonicalized representation of the purl. * * @return the canonicalized representation of the purl - * @since 1.0.0 */ public String canonicalize() { return canonicalize(false); @@ -625,7 +610,7 @@ private static byte percentDecode(final byte[] bytes, final int start) { return ((byte) ((c1 << 4) + c2)); } - private static String percentDecode(final String source) { + static String percentDecode(final String source) { if (source.isEmpty()) { return source; } @@ -657,8 +642,16 @@ private static String percentDecode(final String source) { return new String(buffer.array(), 0, buffer.position(), StandardCharsets.UTF_8); } + /** + * URI decodes the given string. + * + * @param source the encoded string + * @return the decoded string + * @since 1.4.2 + * @deprecated this method was made public in error in version 1.4.2 and will be removed without a replacement + */ @Deprecated - public String uriDecode(final String source) { + public @Nullable String uriDecode(final @Nullable String source) { return source != null ? percentDecode(source) : null; } @@ -666,7 +659,7 @@ private static boolean isPercent(int c) { return (c == PERCENT_CHAR); } - private static String percentEncode(final String source) { + static String percentEncode(final String source) { if (source.isEmpty()) { return source; } @@ -851,10 +844,11 @@ private static String encodePath(final String path) { /** * Evaluates if the specified Package URL has the same values up to, but excluding - * the qualifier (querystring). This includes equivalence of: scheme, type, namespace, - * name, and version, but excludes qualifier and subpath from evaluation. - * @deprecated - * This method is no longer recommended and will be removed from a future release. + * the qualifier (querystring). + * This includes equivalence of the scheme, type, namespace, name, and version, but excludes qualifier and subpath + * from evaluation. + * + * @deprecated This method is no longer recommended and will be removed from a future release. *

Use {@link PackageURL#isCoordinatesEquals} instead.

* * @param purl the Package URL to evaluate @@ -869,8 +863,9 @@ public boolean isBaseEquals(final PackageURL purl) { /** * Evaluates if the specified Package URL has the same values up to, but excluding - * the qualifier (querystring). This includes equivalence of: scheme, type, namespace, - * name, and version, but excludes qualifier and subpath from evaluation. + * the qualifier (querystring). + * This includes equivalence of the scheme, type, namespace, name, and version, but excludes qualifier and subpath + * from evaluation. * * @param purl the Package URL to evaluate, not {@code null} * @return true if equivalence passes, false if not @@ -931,47 +926,195 @@ public int hashCode() { /** * Convenience constants that defines common Package-URL 'type's. - * - * @since 1.0.0 */ public static final class StandardTypes { + /** + * Arch Linux and other users of the libalpm/pacman package manager. + * + * @since 1.6.0 + */ public static final String ALPM = "alpm"; + /** + * APK-based packages. + * + * @since 1.6.0 + */ public static final String APK = "apk"; + /** + * Bitbucket-based packages. + */ public static final String BITBUCKET = "bitbucket"; + /** + * Bitnami-based packages. + * + * @since 1.6.0 + */ public static final String BITNAMI = "bitnami"; + /** + * Rust. + * + * @since 1.2.0 + */ public static final String CARGO = "cargo"; + /** + * CocoaPods. + * + * @since 1.6.0 + */ public static final String COCOAPODS = "cocoapods"; + /** + * Composer PHP packages. + */ public static final String COMPOSER = "composer"; + /** + * Conan C/C++ packages. + * + * @since 1.6.0 + */ public static final String CONAN = "conan"; + /** + * Conda packages. + * + * @since 1.6.0 + */ public static final String CONDA = "conda"; + /** + * CPAN Perl packages. + * + * @since 1.6.0 + */ public static final String CPAN = "cpan"; + /** + * CRAN R packages. + * + * @since 1.6.0 + */ public static final String CRAN = "cran"; + /** + * Debian, Debian derivatives, and Ubuntu packages. + * + * @since 1.6.0 + */ public static final String DEB = "deb"; + /** + * Docker images. + */ public static final String DOCKER = "docker"; + /** + * RubyGems. + */ public static final String GEM = "gem"; + /** + * Plain, generic packages that do not fit anywhere else, such as for "upstream-from-distro" packages. + */ public static final String GENERIC = "generic"; + /** + * GitHub-based packages. + */ public static final String GITHUB = "github"; + /** + * Go packages. + */ public static final String GOLANG = "golang"; + /** + * Haskell packages. + */ public static final String HACKAGE = "hackage"; + /** + * Hex packages. + * + * @since 1.6.0 + */ public static final String HEX = "hex"; + /** + * Hugging Face ML models. + * + * @since 1.6.0 + */ public static final String HUGGINGFACE = "huggingface"; + /** + * Lua packages installed with LuaRocks. + * + * @since 1.6.0 + */ public static final String LUAROCKS = "luarocks"; + /** + * Maven JARs and related artifacts. + */ public static final String MAVEN = "maven"; + /** + * MLflow ML models (Azure ML, Databricks, etc.). + * + * @since 1.6.0 + */ public static final String MLFLOW = "mlflow"; + /** + * Nixos packages + * + * @since 1.6.0 + */ public static final String NIX = "nix"; + /** + * Node NPM packages. + */ public static final String NPM = "npm"; + /** + * NuGet .NET packages. + */ public static final String NUGET = "nuget"; + /** + * All artifacts stored in registries that conform to the + * OCI Distribution Specification, including + * container images built by Docker and others. + * + * @since 1.6.0 + */ public static final String OCI = "oci"; + /** + * Dart and Flutter packages. + * + * @since 1.6.0 + */ public static final String PUB = "pub"; + /** + * Python packages. + */ public static final String PYPI = "pypi"; + /** + * QNX packages. + * + * @since 1.6.0 + */ public static final String QPKG = "qpkg"; + /** + * RPMs. + */ public static final String RPM = "rpm"; + /** + * ISO-IEC 19770-2 Software Identification (SWID) tags. + * + * @since 1.6.0 + */ public static final String SWID = "swid"; + /** + * Swift packages. + * + * @since 1.6.0 + */ public static final String SWIFT = "swift"; - + /** + * Debian, Debian derivatives, and Ubuntu packages. + * + * @deprecated use {@link #DEB} instead + */ @Deprecated public static final String DEBIAN = "deb"; - + /** + * Nixos packages. + * + * @since 1.1.0 + * @deprecated use {@link #NIX} instead + */ @Deprecated public static final String NIXPKGS = "nix"; diff --git a/src/main/java/com/github/packageurl/PackageURLBuilder.java b/src/main/java/com/github/packageurl/PackageURLBuilder.java index a82c7f8..ccdb8de 100644 --- a/src/main/java/com/github/packageurl/PackageURLBuilder.java +++ b/src/main/java/com/github/packageurl/PackageURLBuilder.java @@ -31,6 +31,8 @@ /** * A builder construct for Package-URL objects. + * + * @since 1.1.0 */ public final class PackageURLBuilder { private @Nullable String type = null; @@ -53,14 +55,25 @@ public static PackageURLBuilder aPackageURL() { return new PackageURLBuilder(); } + private static PackageURLBuilder toBuilder(PackageURL packageURL) { + return PackageURLBuilder.aPackageURL() + .withType(packageURL.getType()) + .withNamespace(packageURL.getNamespace()) + .withName(packageURL.getName()) + .withVersion(packageURL.getVersion()) + .withQualifiers(packageURL.getQualifiers()) + .withSubpath(packageURL.getSubpath()); + } + /** * Obtains a reference to a new builder object initialized with the existing {@link PackageURL} object. * * @param packageURL the existing Package URL object * @return a new builder object + * @since 1.6.0 */ public static PackageURLBuilder aPackageURL(final PackageURL packageURL) { - return packageURL.toBuilder(); + return toBuilder(packageURL); } /** @@ -69,9 +82,10 @@ public static PackageURLBuilder aPackageURL(final PackageURL packageURL) { * @param purl the existing Package URL string * @return a new builder object * @throws MalformedPackageURLException if an error occurs while parsing the input + * @since 1.6.0 */ public static PackageURLBuilder aPackageURL(final String purl) throws MalformedPackageURLException { - return new PackageURL(purl).toBuilder(); + return toBuilder(new PackageURL(purl)); } /** @@ -143,7 +157,7 @@ public PackageURLBuilder withSubpath(final @Nullable String subpath) { * If {@code value} is empty or {@code null}, the given qualifier is removed instead. *

* - * @param key the package qualifier key, not {@code null} + * @param key the package qualifier key, not {@code null} * @param value the package qualifier value or {@code null} * @return a reference to the builder * @throws NullPointerException if {@code key} is {@code null} @@ -170,6 +184,7 @@ public PackageURLBuilder withQualifier(final String key, final @Nullable String * @param qualifiers the package qualifiers, or {@code null} * @return a reference to the builder * @see PackageURL#getQualifiers() + * @since 1.6.0 */ public PackageURLBuilder withQualifiers(final @Nullable Map qualifiers) { if (qualifiers == null) { @@ -190,6 +205,7 @@ public PackageURLBuilder withQualifiers(final @Nullable Map qual * @param key the package qualifier key to remove * @return a reference to the builder * @throws NullPointerException if {@code key} is {@code null} + * @since 1.5.0 */ public PackageURLBuilder withoutQualifier(final String key) { if (qualifiers != null) { @@ -205,6 +221,7 @@ public PackageURLBuilder withoutQualifier(final String key) { * Removes a package qualifier. This is a no-op if the qualifier is not present. * @param keys the package qualifier keys to remove * @return a reference to the builder + * @since 1.6.0 */ public PackageURLBuilder withoutQualifiers(final Set keys) { if (this.qualifiers != null) { @@ -219,6 +236,7 @@ public PackageURLBuilder withoutQualifiers(final Set keys) { /** * Removes all qualifiers, if any. * @return a reference to this builder. + * @since 1.6.0 */ public PackageURLBuilder withoutQualifiers() { qualifiers = null; @@ -229,16 +247,19 @@ public PackageURLBuilder withoutQualifiers() { * Removes all qualifiers, if any. * * @return a reference to this builder. + * @deprecated use {@link #withoutQualifiers()} instead + * @since 1.5.0 */ + @Deprecated public PackageURLBuilder withNoQualifiers() { - qualifiers = null; - return this; + return withoutQualifiers(); } /** * Returns current type value set in the builder. * * @return type set in this builder + * @since 1.5.0 */ public @Nullable String getType() { return type; @@ -248,6 +269,7 @@ public PackageURLBuilder withNoQualifiers() { * Returns current namespace value set in the builder. * * @return namespace set in this builder + * @since 1.5.0 */ public @Nullable String getNamespace() { return namespace; @@ -257,6 +279,7 @@ public PackageURLBuilder withNoQualifiers() { * Returns current name value set in the builder. * * @return name set in this builder + * @since 1.5.0 */ public @Nullable String getName() { return name; @@ -266,6 +289,7 @@ public PackageURLBuilder withNoQualifiers() { * Returns current version value set in the builder. * * @return version set in this builder + * @since 1.5.0 */ public @Nullable String getVersion() { return version; @@ -275,6 +299,7 @@ public PackageURLBuilder withNoQualifiers() { * Returns current subpath value set in the builder. * * @return subpath set in this builder + * @since 1.5.0 */ public @Nullable String getSubpath() { return subpath; @@ -285,6 +310,7 @@ public PackageURLBuilder withNoQualifiers() { * An empty map is returned if no qualifiers are set * * @return all qualifiers set in this builder, or an empty map if none are set + * @since 1.5.0 */ public Map getQualifiers() { return qualifiers != null ? Collections.unmodifiableMap(qualifiers) : Collections.emptyMap(); @@ -295,6 +321,7 @@ public Map getQualifiers() { *s * @param key qualifier key * @return qualifier value or {@code null} if one is not set + * @since 1.5.0 */ public @Nullable String getQualifier(String key) { return qualifiers == null ? null : qualifiers.get(requireNonNull(key)); diff --git a/src/main/java/com/github/packageurl/package-info.java b/src/main/java/com/github/packageurl/package-info.java index 5312caa..cba345b 100644 --- a/src/main/java/com/github/packageurl/package-info.java +++ b/src/main/java/com/github/packageurl/package-info.java @@ -19,6 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ + /** *

Java implementation of the Package-URL Specification.

*

https://github.com/package-url/purl-spec

diff --git a/src/main/java/com/github/packageurl/validator/PackageURL.java b/src/main/java/com/github/packageurl/validator/PackageURL.java index cfbb695..3da2095 100644 --- a/src/main/java/com/github/packageurl/validator/PackageURL.java +++ b/src/main/java/com/github/packageurl/validator/PackageURL.java @@ -37,11 +37,25 @@ @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PackageURLConstraintValidator.class) public @interface PackageURL { - + /** + * Gets the error message. + * + * @return the error message + */ String message() default "The Package URL (purl) must be a valid URI and conform to https://github.com/package-url/purl-spec"; + /** + * Gets the validation groups. + * + * @return the validation groups + */ Class[] groups() default {}; + /** + * Gets the payload for the constraint. + * + * @return the payload for the constraint + */ Class[] payload() default {}; } diff --git a/src/main/java/com/github/packageurl/validator/package-info.java b/src/main/java/com/github/packageurl/validator/package-info.java index 5cd633e..55302df 100644 --- a/src/main/java/com/github/packageurl/validator/package-info.java +++ b/src/main/java/com/github/packageurl/validator/package-info.java @@ -19,6 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ + /** * This package contains a validator for Jakarta Validation. */ diff --git a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java index 046184a..4dda8a9 100644 --- a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java +++ b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java @@ -176,12 +176,11 @@ void packageURLBuilderException6() { @Test void editBuilder1() throws MalformedPackageURLException { - PackageURL p = new PackageURL("pkg:generic/namespace/name@1.0.0?k=v#s"); - PackageURLBuilder b = p.toBuilder(); + PackageURLBuilder b = PackageURLBuilder.aPackageURL(p); assertBuilderMatch(p, b); - assertBuilderMatch(new PackageURL("pkg:generic/namespace/name@1.0.0#s"), b.withNoQualifiers()); + assertBuilderMatch(new PackageURL("pkg:generic/namespace/name@1.0.0#s"), b.withoutQualifiers()); b.withType("maven") .withNamespace("org.junit") .withName("junit5") diff --git a/src/test/java/com/github/packageurl/PackageURLTest.java b/src/test/java/com/github/packageurl/PackageURLTest.java index 9dd58ee..18fb345 100644 --- a/src/test/java/com/github/packageurl/PackageURLTest.java +++ b/src/test/java/com/github/packageurl/PackageURLTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.util.Locale; +import java.util.Map; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; @@ -62,7 +63,8 @@ static void resetLocale() { @Test void validPercentEncoding() throws MalformedPackageURLException { - PackageURL purl = new PackageURL("maven", "com.google.summit", "summit-ast", "2.2.0\n", null, null); + PackageURL purl = + new PackageURL("maven", "com.google.summit", "summit-ast", "2.2.0\n", (Map) null, null); assertEquals("pkg:maven/com.google.summit/summit-ast@2.2.0%0A", purl.toString()); PackageURL purl2 = new PackageURL("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5"); @@ -83,11 +85,11 @@ void invalidPercentEncoding() throws MalformedPackageURLException { PackageURL purl = new PackageURL("pkg:maven/com.google.summit/summit-ast@2.2.0"); Throwable t1 = assertThrowsExactly(ValidationException.class, () -> purl.uriDecode("%")); assertEquals("Incomplete percent encoding at offset 0 with value '%'", t1.getMessage()); - Throwable t2 = assertThrowsExactly(ValidationException.class, () -> purl.uriDecode("a%0")); + Throwable t2 = assertThrowsExactly(ValidationException.class, () -> PackageURL.percentDecode("a%0")); assertEquals("Incomplete percent encoding at offset 1 with value '%0'", t2.getMessage()); - Throwable t3 = assertThrowsExactly(ValidationException.class, () -> purl.uriDecode("aaaa%%0A")); + Throwable t3 = assertThrowsExactly(ValidationException.class, () -> PackageURL.percentDecode("aaaa%%0A")); assertEquals("Invalid percent encoding char 1 at offset 5 with value '%'", t3.getMessage()); - Throwable t4 = assertThrowsExactly(ValidationException.class, () -> purl.uriDecode("%0G")); + Throwable t4 = assertThrowsExactly(ValidationException.class, () -> PackageURL.percentDecode("%0G")); assertEquals("Invalid percent encoding char 2 at offset 2 with value 'G'", t4.getMessage()); }