diff --git a/sop-java/src/main/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java deleted file mode 100644 index 4ea9e71..0000000 --- a/sop-java/src/main/java/sop/Profile.java +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.util.Optional; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Tuple class bundling a profile name and description. - * - * @see - * SOP Spec - Profile - */ -public class Profile { - - private final String name; - private final Optional description; - - /** - * Create a new {@link Profile} object. - * The {@link #toString()} representation MUST NOT exceed a length of 1000 bytes. - * - * @param name profile name - * @param description profile description - */ - public Profile(@Nonnull String name, @Nullable String description) { - if (name.trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be empty."); - } - if (name.contains(":")) { - throw new IllegalArgumentException("Name cannot contain ':'."); - } - if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) { - throw new IllegalArgumentException("Name cannot contain whitespace characters."); - } - - this.name = name; - - if (description == null) { - this.description = Optional.ofEmpty(); - } else { - String trimmedDescription = description.trim(); - if (trimmedDescription.isEmpty()) { - this.description = Optional.ofEmpty(); - } else { - this.description = Optional.of(trimmedDescription); - } - } - - if (exceeds1000CharLineLimit(this)) { - throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes."); - } - } - - public Profile(String name) { - this(name, null); - } - - /** - * Parse a {@link Profile} from its string representation. - * - * @param string string representation - * @return profile - */ - public static Profile parse(String string) { - if (string.contains(": ")) { - // description after colon, e.g. "default: Use implementers recommendations." - String name = string.substring(0, string.indexOf(": ")); - String description = string.substring(string.indexOf(": ") + 2); - return new Profile(name, description.trim()); - } - - if (string.endsWith(":")) { - // empty description, e.g. "default:" - return new Profile(string.substring(0, string.length() - 1)); - } - - // no description - return new Profile(string.trim()); - } - - /** - * Return the name (also known as identifier) of the profile. - * A profile name is a UTF-8 string that has no whitespace in it. - * Similar to OpenPGP Notation names, profile names are divided into two namespaces: - * The IETF namespace and the user namespace. - * A profile name in the user namespace ends with the
@
character (0x40) followed by a DNS domain name. - * A profile name in the IETF namespace does not have an
@
character. - * A profile name in the user space is owned and controlled by the owner of the domain in the suffix. - * A profile name in the IETF namespace that begins with the string
rfc
should have semantics that hew as - * closely as possible to the referenced RFC. - * Similarly, a profile name in the IETF namespace that begins with the string
draft-
should have - * semantics that hew as closely as possible to the referenced Internet Draft. - * - * @return name - */ - @Nonnull - public String getName() { - return name; - } - - /** - * Return a free-form description of the profile. - * - * @return description - */ - @Nonnull - public Optional getDescription() { - return description; - } - - public boolean hasDescription() { - return description.isPresent(); - } - - /** - * Convert the profile into a String for displaying. - * - * @return string - */ - @Override - public String toString() { - if (getDescription().isEmpty()) { - return getName(); - } - return getName() + ": " + getDescription().get(); - } - - /** - * Test if the string representation of the profile exceeds the limit of 1000 bytes length. - * @param profile profile - * @return
true
if the profile exceeds 1000 bytes,
false
otherwise. - */ - private static boolean exceeds1000CharLineLimit(Profile profile) { - String line = profile.toString(); - return line.getBytes(UTF8Util.UTF8).length > 1000; - } -} diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt new file mode 100644 index 0000000..2125c57 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional +import sop.util.UTF8Util + +/** + * Tuple class bundling a profile name and description. + * + * @param name profile name. A profile name is a UTF-8 string that has no whitespace in it. Similar + * to OpenPGP Notation names, profile names are divided into two namespaces: The IETF namespace + * and the user namespace. A profile name in the user namespace ends with the `@` character (0x40) + * followed by a DNS domain name. A profile name in the IETF namespace does not have an `@` + * character. A profile name in the user space is owned and controlled by the owner of the domain + * in the suffix. A profile name in the IETF namespace that begins with the string `rfc` should + * have semantics that hew as closely as possible to the referenced RFC. Similarly, a profile name + * in the IETF namespace that begins with the string `draft-` should have semantics that hew as + * closely as possible to the referenced Internet Draft. + * @param description a free-form description of the profile. + * @see + * SOP Spec - Profile + */ +data class Profile(val name: String, val description: Optional) { + + @JvmOverloads + constructor( + name: String, + description: String? = null + ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null })) + + init { + require(name.trim().isNotBlank()) { "Name cannot be empty." } + require(!name.contains(":")) { "Name cannot contain ':'." } + require(listOf(" ", "\n", "\t", "\r").none { name.contains(it) }) { + "Name cannot contain whitespace characters." + } + require(!exceeds1000CharLineLimit(this)) { + "The line representation of a profile MUST NOT exceed 1000 bytes." + } + } + + fun hasDescription() = description.isPresent + + /** + * Convert the profile into a String for displaying. + * + * @return string + */ + override fun toString(): String = + if (description.isEmpty) name else "$name: ${description.get()}" + + companion object { + + /** + * Parse a [Profile] from its string representation. + * + * @param string string representation + * @return profile + */ + @JvmStatic + fun parse(string: String): Profile { + return if (string.contains(": ")) { + Profile( + string.substring(0, string.indexOf(": ")), + string.substring(string.indexOf(": ") + 2).trim()) + } else if (string.endsWith(":")) { + Profile(string.substring(0, string.length - 1)) + } else { + Profile(string.trim()) + } + } + + /** + * Test if the string representation of the profile exceeds the limit of 1000 bytes length. + * + * @param profile profile + * @return `true` if the profile exceeds 1000 bytes, `false` otherwise. + */ + @JvmStatic + private fun exceeds1000CharLineLimit(profile: Profile): Boolean = + profile.toString().toByteArray(UTF8Util.UTF8).size > 1000 + } +}