diff --git a/manifest/build.gradle b/manifest/build.gradle new file mode 100644 index 00000000..a7145dfc --- /dev/null +++ b/manifest/build.gradle @@ -0,0 +1,28 @@ +description = 'Sonar Plugin API - Manifest' + +dependencies { + compileOnly libs.jsr305 + + implementation libs.commons.lang3 + + testImplementation libs.junit5 + testImplementation libs.assertj + testImplementation libs.mockito + testRuntimeOnly libs.jupiter.engine +} + +artifactoryPublish.skip = false + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'sonar-plugin-manifest' + artifact sourcesJar + artifact javadocJar + } + } +} + +test { + useJUnitPlatform() +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginKeyUtils.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginKeyUtils.java new file mode 100644 index 00000000..8fe7004c --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginKeyUtils.java @@ -0,0 +1,62 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang3.Strings; + +public final class PluginKeyUtils { + + private static final String SONAR_PLUGIN_SUFFIX = "-sonar-plugin"; + private static final String SONAR_PREFIX = "sonar-"; + private static final String PLUGIN_SUFFIX = "-plugin"; + + private PluginKeyUtils() { + // only static methods + } + + @CheckForNull + public static String sanitize(@Nullable String mavenArtifactId) { + if (mavenArtifactId == null) { + return null; + } + + String key = mavenArtifactId; + if (Strings.CS.startsWith(mavenArtifactId, SONAR_PREFIX) && Strings.CS.endsWith(mavenArtifactId, PLUGIN_SUFFIX)) { + key = Strings.CS.removeEnd(Strings.CS.removeStart(mavenArtifactId, SONAR_PREFIX), PLUGIN_SUFFIX); + } else if (Strings.CS.endsWith(mavenArtifactId, SONAR_PLUGIN_SUFFIX)) { + key = Strings.CS.removeEnd(mavenArtifactId, SONAR_PLUGIN_SUFFIX); + } + return keepLettersAndDigits(key); + } + + private static String keepLettersAndDigits(String key) { + StringBuilder sb = new StringBuilder(); + for (int index = 0; index < key.length(); index++) { + char character = key.charAt(index); + if (Character.isLetter(character) || Character.isDigit(character)) { + sb.append(character); + } + } + return sb.toString(); + } + +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java new file mode 100644 index 00000000..54ba5d92 --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifest.java @@ -0,0 +1,268 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import java.io.File; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + +/** + * This class loads Sonar plugin metadata from JAR manifest. + */ +public final class PluginManifest { + + public static final String DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ"; + + private final String key; + private final String name; + private final String mainClass; + private final String description; + private final String organization; + private final String organizationUrl; + private final String license; + private final String version; + private final String displayVersion; + @Nullable + private final Version sonarPluginApiMinVersion; + private final List dependencies; + private final String homepage; + private final String termsConditionsUrl; + private final ZonedDateTime buildDate; + private final String issueTrackerUrl; + private final boolean useChildFirstClassLoader; + private final String basePlugin; + private final String implementationBuild; + private final String sourcesUrl; + private final List developers; + private final List requiredPlugins; + private final boolean sonarlintSupported; + private final List requiredForLanguages; + @Nullable + private final Version jreMinVersion; + @Nullable + private final Version nodeJsMinVersion; + + /** + * Load the manifest from a JAR file. + */ + public PluginManifest(File jarFile) { + this(loadManifestFromFile(jarFile.toPath())); + } + + /** + * Load the manifest from a JAR file. + */ + public PluginManifest(Path jarFilePath) { + this(loadManifestFromFile(jarFilePath)); + } + + private static Manifest loadManifestFromFile(Path path) { + try (JarFile jar = new JarFile(path.toFile())) { + Manifest manifest = jar.getManifest(); + return manifest != null ? manifest : new Manifest(); + } catch (Exception e) { + throw new IllegalStateException("Unable to read plugin manifest from jar : " + path.toAbsolutePath(), e); + } + } + + /** + * @param manifest can not be null + */ + public PluginManifest(Manifest manifest) { + Attributes attributes = manifest.getMainAttributes(); + this.key = PluginKeyUtils.sanitize(attributes.getValue(PluginManifestProperty.KEY.getKey())); + this.mainClass = attributes.getValue(PluginManifestProperty.MAIN_CLASS.getKey()); + this.name = attributes.getValue(PluginManifestProperty.NAME.getKey()); + this.description = attributes.getValue(PluginManifestProperty.DESCRIPTION.getKey()); + this.license = attributes.getValue(PluginManifestProperty.LICENSE.getKey()); + this.organization = attributes.getValue(PluginManifestProperty.ORGANIZATION.getKey()); + this.organizationUrl = attributes.getValue(PluginManifestProperty.ORGANIZATION_URL.getKey()); + this.version = attributes.getValue(PluginManifestProperty.VERSION.getKey()); + this.displayVersion = attributes.getValue(PluginManifestProperty.DISPLAY_VERSION.getKey()); + this.homepage = attributes.getValue(PluginManifestProperty.HOMEPAGE.getKey()); + this.termsConditionsUrl = attributes.getValue(PluginManifestProperty.TERMS_CONDITIONS_URL.getKey()); + this.sonarPluginApiMinVersion = parseVersion(attributes, PluginManifestProperty.SONAR_VERSION); + this.issueTrackerUrl = attributes.getValue(PluginManifestProperty.ISSUE_TRACKER_URL.getKey()); + this.buildDate = parseInstant(attributes.getValue(PluginManifestProperty.BUILD_DATE.getKey())); + this.useChildFirstClassLoader = "true".equalsIgnoreCase(attributes.getValue(PluginManifestProperty.USE_CHILD_FIRST_CLASSLOADER.getKey())); + this.sonarlintSupported = "true".equalsIgnoreCase(attributes.getValue(PluginManifestProperty.SONARLINT_SUPPORTED.getKey())); + this.basePlugin = attributes.getValue(PluginManifestProperty.BASE_PLUGIN.getKey()); + this.implementationBuild = attributes.getValue(PluginManifestProperty.IMPLEMENTATION_BUILD.getKey()); + this.sourcesUrl = attributes.getValue(PluginManifestProperty.SOURCES_URL.getKey()); + + String deps = attributes.getValue(PluginManifestProperty.DEPENDENCIES.getKey()); + this.dependencies = List.of(StringUtils.split(StringUtils.defaultString(deps), ' ')); + + String devs = attributes.getValue(PluginManifestProperty.DEVELOPERS.getKey()); + this.developers = List.of(StringUtils.split(StringUtils.defaultString(devs), ',')); + + String requires = attributes.getValue(PluginManifestProperty.REQUIRE_PLUGINS.getKey()); + this.requiredPlugins = Stream.of(StringUtils.split(StringUtils.defaultString(requires), ',')) + .map(RequiredPlugin::parse).collect(Collectors.toUnmodifiableList()); + + String languages = attributes.getValue(PluginManifestProperty.LANGUAGES.getKey()); + this.requiredForLanguages = List.of(StringUtils.split(StringUtils.defaultString(languages), ',')); + + this.jreMinVersion = parseVersion(attributes, PluginManifestProperty.JRE_MIN_VERSION); + this.nodeJsMinVersion = parseVersion(attributes, PluginManifestProperty.NODEJS_MIN_VERSION); + } + + @Nullable + private static Version parseVersion(Attributes attributes, PluginManifestProperty manifestProperty) { + return Optional.ofNullable(attributes.getValue(manifestProperty.getKey())) + .map(Version::create) + .orElse(null); + } + + public String getKey() { + return key; + } + + public String getName() { + return name; + } + + public List getRequiredPlugins() { + return requiredPlugins; + } + + public String getDescription() { + return description; + } + + public String getOrganization() { + return organization; + } + + public String getOrganizationUrl() { + return organizationUrl; + } + + public String getLicense() { + return license; + } + + public String getVersion() { + return version; + } + + public String getDisplayVersion() { + return displayVersion; + } + + public Optional getSonarPluginApiMinVersion() { + return Optional.ofNullable(sonarPluginApiMinVersion); + } + + public String getMainClass() { + return mainClass; + } + + public List getDependencies() { + return dependencies; + } + + public ZonedDateTime getBuildDate() { + return buildDate; + } + + public String getHomepage() { + return homepage; + } + + public String getTermsConditionsUrl() { + return termsConditionsUrl; + } + + public String getIssueTrackerUrl() { + return issueTrackerUrl; + } + + public boolean isUseChildFirstClassLoader() { + return useChildFirstClassLoader; + } + + public boolean isSonarLintSupported() { + return sonarlintSupported; + } + + public String getBasePlugin() { + return basePlugin; + } + + public String getImplementationBuild() { + return implementationBuild; + } + + public String getSourcesUrl() { + return sourcesUrl; + } + + public List getDevelopers() { + return developers; + } + + public List getRequiredForLanguages() { + return requiredForLanguages; + } + + public Optional getJreMinVersion() { + return Optional.ofNullable(jreMinVersion); + } + + public Optional getNodeJsMinVersion() { + return Optional.ofNullable(nodeJsMinVersion); + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this).toString(); + } + + public boolean isValid() { + return StringUtils.isNotBlank(key) && StringUtils.isNotBlank(version); + } + + public static ZonedDateTime parseInstant(String s) { + try { + if (StringUtils.isNotBlank(s)) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.US); + return ZonedDateTime.parse(s, dateTimeFormatter); + } + return null; + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("The following value does not respect the date pattern " + DATETIME_PATTERN + ": " + s, e); + } + } + +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifestProperty.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifestProperty.java new file mode 100644 index 00000000..4bf19b4a --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/PluginManifestProperty.java @@ -0,0 +1,175 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +public enum PluginManifestProperty { + + /** + * (required) Contains only letters/digits and is unique among all plugins. + * Constructed from the Maven artifactId. Given an artifactId of sonar-widget-lab-plugin, the pluginKey will be widgetlab. + */ + KEY("Plugin-Key", "Key"), + + /** + * (required) Plugin name displayed in various parts of the SonarQube products. + */ + NAME("Plugin-Name", "Name"), + + /** + * Plugin description displayed in various parts of the SonarQube products. + */ + DESCRIPTION("Plugin-Description", "Description"), + + /** + * (required) Name of the entry-point class that extends org.sonar.api.SonarPlugin. + * Example: org.mycompany.sonar.plugins.widgetlab.WidgetLabPlugin + */ + MAIN_CLASS("Plugin-Class", "Entry-point Class"), + + /** + * The organization which develops the plugin, displayed in various parts of the SonarQube products. + */ + ORGANIZATION("Plugin-Organization", "Organization"), + + /** + * URL of the organization, displayed in various parts of the SonarQube products. + */ + ORGANIZATION_URL("Plugin-OrganizationUrl", "Organization URL"), + + /** + * Plugin license. + */ + LICENSE("Plugin-License", "Licensing"), + + /** + * (required) Plugin "raw" version. Should follow common version formats to be comparable. + */ + VERSION("Plugin-Version", "Version"), + + /** + * The version label, displayed in various parts of the SonarQube products. + * By default, it's the raw version (e.g., "1.2"), but can be overridden to "1.2 (build 12345)" for instance. + */ + DISPLAY_VERSION("Plugin-Display-Version", "Display Version"), + + /** + * Minimal version of supported Sonar Plugin API at runtime. + * For example, if the value is 9.8.0.203, then deploying the plugin on SonarQube versions + * with sonar-plugin-api 9.6.1.114 (i.e., SonarQube 9.5) and lower will fail. + */ + SONAR_VERSION("Sonar-Version", "Minimal Sonar Plugin API Version"), + + /** + * List of Maven dependencies packaged with the plugin. + */ + DEPENDENCIES("Plugin-Dependencies", "Dependencies"), + + /** + * Homepage of website, for example {@code https://github.com/SonarQubeCommunity/sonar-widget-lab} + */ + HOMEPAGE("Plugin-Homepage", "Homepage URL"), + + /** + * Users must read this document when installing the plugin from Marketplace. + */ + TERMS_CONDITIONS_URL("Plugin-TermsConditionsUrl", "Terms and Conditions"), + + /** + * Build date of the plugin. Should follow the pattern yyyy-MM-dd'T'HH:mm:ssZ + */ + BUILD_DATE("Plugin-BuildDate", "Build date"), + + /** + * URL of the issue tracker, for example {@code https://github.com/SonarQubeCommunity/sonar-widget-lab/issues} + */ + ISSUE_TRACKER_URL("Plugin-IssueTrackerUrl", "Issue Tracker URL"), + + /** + * Comma-separated list of plugin keys that this plugin depends on. + * Can also specify a minimal version for each required plugin, using the format "pluginKey:version", for example "widgetlab:1.2.0". + */ + REQUIRE_PLUGINS("Plugin-RequirePlugins", "Required Plugins"), + + /** + * Whether the language plugin supports SonarLint or not. + * Only Sonar analyzers and custom rules plugins for Sonar analyzers should set this to true. + */ + SONARLINT_SUPPORTED("SonarLint-Supported", "Does the plugin support SonarLint?"), + + /** + * Each plugin is executed in an isolated classloader, which inherits a shared classloader that contains API and some other classes. + * By default, the loading strategy of classes is parent-first (look up in shared classloader then in plugin classloader). + * If the property is true, then the strategy is child-first. + * This property is mainly used when building plugin against API < 5.2, as the shared classloader contained many 3rd party libraries (guava 10, commons-lang, …). + */ + USE_CHILD_FIRST_CLASSLOADER("Plugin-ChildFirstClassLoader", "Use Child-first ClassLoader"), + + /** + * If specified, then the plugin is executed in the same classloader as the base plugin. + */ + BASE_PLUGIN("Plugin-Base", "Base Plugin"), + + /** + * Identifier of build or commit, for example the Git SHA1 (94638028f0099de59f769cdca776e506684235d6). + * It is displayed for debugging purposes in logs when the SonarQube server starts. + */ + IMPLEMENTATION_BUILD("Implementation-Build", "Implementation Build"), + + /** + * URL of SCM repository for open-source plugins, displayed in various parts of the SonarQube products. + */ + SOURCES_URL("Plugin-SourcesUrl", "Sources URL"), + + /** + * A list of developers, displayed in various parts of the SonarQube products. + */ + DEVELOPERS("Plugin-Developers", "Developers"), + + /** + * Minimal JRE specification version required to run the plugin. + */ + JRE_MIN_VERSION("Jre-Min-Version", "Minimal JRE Specification Version"), + + /** + * Minimal Node.js version required to run the plugin. + */ + NODEJS_MIN_VERSION("NodeJs-Min-Version", "Minimal Node.js Version"), + + /** + * Comma-separated list of languages for which this plugin should be downloaded. + */ + LANGUAGES("Plugin-RequiredForLanguages", "Languages for which this plugin should be downloaded"); + + private final String key; + private final String label; + + PluginManifestProperty(String key, String label) { + this.key = key; + this.label = label; + } + + public String getKey() { + return key; + } + + public String getLabel() { + return label; + } +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java new file mode 100644 index 00000000..e56db1ab --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/RequiredPlugin.java @@ -0,0 +1,53 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +public class RequiredPlugin { + + private static final Pattern PARSER = Pattern.compile("\\w+:.+"); + + private final String key; + private final Version minimalVersion; + + public RequiredPlugin(String key, Version minimalVersion) { + this.key = key; + this.minimalVersion = minimalVersion; + } + + public String getKey() { + return key; + } + + public Version getMinimalVersion() { + return minimalVersion; + } + + static RequiredPlugin parse(String s) { + if (!PARSER.matcher(s).matches()) { + throw new IllegalArgumentException("Manifest field does not have correct format: " + s); + } + var fields = StringUtils.split(s, ':'); + return new RequiredPlugin(fields[0], Version.create(fields[1]).removeQualifier()); + } + +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java new file mode 100644 index 00000000..39d7a7f3 --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/Version.java @@ -0,0 +1,149 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import java.util.Arrays; + +public class Version implements Comparable { + + private final String name; + private final String nameWithoutQualifier; + private final int[] numbers; + private final String qualifier; + + private Version(String version) { + this.name = version.trim(); + var qualifierPosition = name.indexOf("-"); + if (qualifierPosition != -1) { + this.qualifier = name.substring(qualifierPosition + 1); + this.nameWithoutQualifier = name.substring(0, qualifierPosition); + } else { + this.qualifier = ""; + this.nameWithoutQualifier = this.name; + } + final var split = this.nameWithoutQualifier.split("\\."); + numbers = new int[split.length]; + for (var i = 0; i < split.length; i++) { + numbers[i] = Integer.parseInt(split[i]); + } + } + + private Version(String name, String nameWithoutQualifier, int[] numbers, String qualifier) { + this.name = name; + this.nameWithoutQualifier = nameWithoutQualifier; + this.numbers = Arrays.copyOf(numbers, numbers.length); + this.qualifier = qualifier; + } + + public int getMajor() { + return numbers.length > 0 ? numbers[0] : 0; + } + + public int getMinor() { + return numbers.length > 1 ? numbers[1] : 0; + } + + public int getPatch() { + return numbers.length > 2 ? numbers[2] : 0; + } + + public int getBuild() { + return numbers.length > 3 ? numbers[3] : 0; + } + + public String getName() { + return name; + } + + public String getQualifier() { + return qualifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Version)) { + return false; + } + Version other = (Version) o; + return getMajor() == other.getMajor() + && getMinor() == other.getMinor() + && getPatch() == other.getPatch() + && getBuild() == other.getBuild() + && qualifier.equals(other.qualifier); + } + + @Override + public int hashCode() { + var result = Integer.hashCode(getMajor()); + result = 31 * result + Integer.hashCode(getMinor()); + result = 31 * result + Integer.hashCode(getPatch()); + result = 31 * result + Integer.hashCode(getBuild()); + result = 31 * result + qualifier.hashCode(); + return result; + } + + @Override + public int compareTo(Version other) { + var c = compareToIgnoreQualifier(other); + if (c == 0) { + if ("".equals(qualifier)) { + c = "".equals(other.qualifier) ? 0 : 1; + } else if ("".equals(other.qualifier)) { + c = -1; + } else { + c = qualifier.compareTo(other.qualifier); + } + } + return c; + } + + public int compareToIgnoreQualifier(Version other) { + var maxNumbers = Math.max(numbers.length, other.numbers.length); + var myNumbers = Arrays.copyOf(numbers, maxNumbers); + var otherNumbers = Arrays.copyOf(other.numbers, maxNumbers); + for (var i = 0; i < maxNumbers; i++) { + var compare = Integer.compare(myNumbers[i], otherNumbers[i]); + if (compare != 0) { + return compare; + } + } + return 0; + } + + @Override + public String toString() { + return name; + } + + public static Version create(String version) { + return new Version(version); + } + + public Version removeQualifier() { + return new Version(nameWithoutQualifier, nameWithoutQualifier, numbers, ""); + } + + public boolean satisfiesMinRequirement(Version minRequirement) { + return this.compareToIgnoreQualifier(minRequirement) >= 0; + } +} diff --git a/manifest/src/main/java/org/sonar/plugin/api/manifest/package-info.java b/manifest/src/main/java/org/sonar/plugin/api/manifest/package-info.java new file mode 100644 index 00000000..b44ee5af --- /dev/null +++ b/manifest/src/main/java/org/sonar/plugin/api/manifest/package-info.java @@ -0,0 +1,23 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.plugin.api.manifest; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginKeyUtilsTest.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginKeyUtilsTest.java new file mode 100644 index 00000000..d65aa188 --- /dev/null +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginKeyUtilsTest.java @@ -0,0 +1,45 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PluginKeyUtilsTest { + + @Test + void shouldSanitizeMavenArtifactId() { + assertThat(PluginKeyUtils.sanitize("sonar-test-plugin")).isEqualTo("test"); + assertThat(PluginKeyUtils.sanitize("test-sonar-plugin")).isEqualTo("test"); + assertThat(PluginKeyUtils.sanitize("test")).isEqualTo("test"); + + assertThat(PluginKeyUtils.sanitize("sonar-test-foo-plugin")).isEqualTo("testfoo"); + assertThat(PluginKeyUtils.sanitize("test-foo-sonar-plugin")).isEqualTo("testfoo"); + assertThat(PluginKeyUtils.sanitize("test-foo")).isEqualTo("testfoo"); + assertThat(PluginKeyUtils.sanitize("keep.only-digits%12345&and*letters")).isEqualTo("keeponlydigits12345andletters"); + assertThat(PluginKeyUtils.sanitize(" remove whitespaces ")).isEqualTo("removewhitespaces"); + } + + @Test + void shouldReturnNullWhenSanitizingNull() { + assertThat(PluginKeyUtils.sanitize(null)).isNull(); + } +} diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java new file mode 100644 index 00000000..b2e6a421 --- /dev/null +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/PluginManifestTest.java @@ -0,0 +1,279 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PluginManifestTest { + + @Test + void test() { + File fakeJar = new File("fake.jar"); + assertThatThrownBy(() -> new PluginManifest(fakeJar)) + .isInstanceOf(RuntimeException.class); + } + + @Test + void should_create_manifest() throws URISyntaxException { + URL jar = getClass().getResource("/checkstyle-plugin.jar"); + + PluginManifest manifest = new PluginManifest(Path.of(jar.toURI())); + + assertThat(manifest.getKey()).isEqualTo("checkstyle"); + assertThat(manifest.getName()).isEqualTo("Checkstyle"); + assertThat(manifest.getRequiredPlugins()).isEmpty(); + assertThat(manifest.getMainClass()).isEqualTo("org.sonar.plugins.checkstyle.CheckstylePlugin"); + assertThat(manifest.getVersion().length()).isGreaterThan(1); + assertThat(manifest.isUseChildFirstClassLoader()).isFalse(); + assertThat(manifest.isSonarLintSupported()).isFalse(); + assertThat(manifest.getDependencies()).hasSize(2); + assertThat(manifest.getDependencies()).containsOnly("META-INF/lib/antlr-2.7.7.jar", "META-INF/lib/checkstyle-5.5.jar"); + assertThat(manifest.getImplementationBuild()).isEqualTo("b9283404030db9ce1529b1fadfb98331686b116d"); + assertThat(manifest.getRequiredForLanguages()).isEmpty(); + } + + @Test + void do_not_fail_when_no_old_plugin_manifest() throws URISyntaxException { + URL jar = getClass().getResource("/old-plugin.jar"); + + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + assertThat(manifest.getKey()).isNull(); + assertThat(manifest.getName()).isNull(); + assertThat(manifest.getRequiredPlugins()).isEmpty(); + assertThat(manifest.getMainClass()).isEqualTo("org.sonar.plugins.checkstyle.CheckstylePlugin"); + assertThat(manifest.isUseChildFirstClassLoader()).isFalse(); + assertThat(manifest.getDependencies()).isEmpty(); + assertThat(manifest.getImplementationBuild()).isNull(); + assertThat(manifest.getDevelopers()).isEmpty(); + assertThat(manifest.getSourcesUrl()).isNull(); + } + + @Test + void should_add_develpers() throws URISyntaxException { + URL jar = getClass().getResource("/plugin-with-devs.jar"); + + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + assertThat(manifest.getDevelopers()).contains("Firstname1 Name1", "Firstname2 Name2"); + } + + @Test + void should_add_sources_url() throws URISyntaxException { + URL jar = getClass().getResource("/plugin-with-sources.jar"); + + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + assertThat(manifest.getSourcesUrl()).isEqualTo("https://github.com/SonarSource/project"); + } + + @Test + void should_add_requires_plugins() throws URISyntaxException { + URL jar = getClass().getResource("/plugin-with-require-plugins.jar"); + + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + assertThat(manifest.getRequiredPlugins()).hasSize(2); + assertThat(manifest.getRequiredPlugins().get(0).getKey()).isEqualTo("scm"); + assertThat(manifest.getRequiredPlugins().get(0).getMinimalVersion()).hasToString("1.0"); + assertThat(manifest.getRequiredPlugins().get(1).getKey()).isEqualTo("fake"); + assertThat(manifest.getRequiredPlugins().get(1).getMinimalVersion()).hasToString("1.1"); + } + + @Test + void should_add_languages() throws URISyntaxException { + URL jar = getClass().getResource("/plugin-with-require-for-languages.jar"); + + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + assertThat(manifest.getRequiredForLanguages()).hasSize(2); + assertThat(manifest.getRequiredForLanguages().get(0)).isEqualTo("java"); + assertThat(manifest.getRequiredForLanguages().get(1)).isEqualTo("xml"); + } + + @Test + void should_create_manifest_from_manifest_object() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), "test-key"); + attributes.putValue(PluginManifestProperty.NAME.getKey(), "Test Plugin"); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), "1.0"); + attributes.putValue(PluginManifestProperty.MAIN_CLASS.getKey(), "com.example.TestPlugin"); + attributes.putValue(PluginManifestProperty.DESCRIPTION.getKey(), "Test Description"); + attributes.putValue(PluginManifestProperty.ORGANIZATION.getKey(), "Test Org"); + attributes.putValue(PluginManifestProperty.ORGANIZATION_URL.getKey(), "https://test.org"); + attributes.putValue(PluginManifestProperty.LICENSE.getKey(), "MIT"); + attributes.putValue(PluginManifestProperty.HOMEPAGE.getKey(), "https://test.com"); + attributes.putValue(PluginManifestProperty.TERMS_CONDITIONS_URL.getKey(), "https://test.com/terms"); + attributes.putValue(PluginManifestProperty.SONAR_VERSION.getKey(), "9.0"); + attributes.putValue(PluginManifestProperty.ISSUE_TRACKER_URL.getKey(), "https://test.com/issues"); + attributes.putValue(PluginManifestProperty.USE_CHILD_FIRST_CLASSLOADER.getKey(), "true"); + attributes.putValue(PluginManifestProperty.SONARLINT_SUPPORTED.getKey(), "true"); + attributes.putValue(PluginManifestProperty.BASE_PLUGIN.getKey(), "base-plugin"); + attributes.putValue(PluginManifestProperty.IMPLEMENTATION_BUILD.getKey(), "abc123"); + attributes.putValue(PluginManifestProperty.SOURCES_URL.getKey(), "https://github.com/test"); + attributes.putValue(PluginManifestProperty.DISPLAY_VERSION.getKey(), "1.0.0"); + attributes.putValue(PluginManifestProperty.BUILD_DATE.getKey(), "2024-01-15T10:30:00+0000"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.getKey()).isEqualTo("testkey"); + assertThat(pluginManifest.getName()).isEqualTo("Test Plugin"); + assertThat(pluginManifest.getVersion()).isEqualTo("1.0"); + assertThat(pluginManifest.getMainClass()).isEqualTo("com.example.TestPlugin"); + assertThat(pluginManifest.getDescription()).isEqualTo("Test Description"); + assertThat(pluginManifest.getOrganization()).isEqualTo("Test Org"); + assertThat(pluginManifest.getOrganizationUrl()).isEqualTo("https://test.org"); + assertThat(pluginManifest.getLicense()).isEqualTo("MIT"); + assertThat(pluginManifest.getHomepage()).isEqualTo("https://test.com"); + assertThat(pluginManifest.getTermsConditionsUrl()).isEqualTo("https://test.com/terms"); + assertThat(pluginManifest.getSonarPluginApiMinVersion()).hasValue(Version.create("9.0")); + assertThat(pluginManifest.getIssueTrackerUrl()).isEqualTo("https://test.com/issues"); + assertThat(pluginManifest.isUseChildFirstClassLoader()).isTrue(); + assertThat(pluginManifest.isSonarLintSupported()).isTrue(); + assertThat(pluginManifest.getBasePlugin()).isEqualTo("base-plugin"); + assertThat(pluginManifest.getImplementationBuild()).isEqualTo("abc123"); + assertThat(pluginManifest.getSourcesUrl()).isEqualTo("https://github.com/test"); + assertThat(pluginManifest.getDisplayVersion()).isEqualTo("1.0.0"); + assertThat(pluginManifest.getBuildDate()).isNotNull(); + } + + @Test + void should_return_immutable_lists() throws URISyntaxException { + URL jar = getClass().getResource("/checkstyle-plugin.jar"); + PluginManifest manifest = new PluginManifest(new File(jar.toURI())); + + List dependencies = manifest.getDependencies(); + assertThat(dependencies).hasSize(2); + assertThat(dependencies.get(0)).isEqualTo("META-INF/lib/antlr-2.7.7.jar"); + + assertThatThrownBy(() -> dependencies.add("modified")) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void parseInstant_should_parse_valid_date() { + ZonedDateTime result = PluginManifest.parseInstant("2024-01-15T10:30:00+0000"); + + assertThat(result).isNotNull(); + assertThat(result.getYear()).isEqualTo(2024); + assertThat(result.getMonthValue()).isEqualTo(1); + assertThat(result.getDayOfMonth()).isEqualTo(15); + } + + @Test + void parseInstant_should_return_null_for_blank_string() { + assertThat(PluginManifest.parseInstant("")).isNull(); + assertThat(PluginManifest.parseInstant(" ")).isNull(); + assertThat(PluginManifest.parseInstant(null)).isNull(); + } + + @Test + void parseInstant_should_throw_exception_for_invalid_format() { + assertThatThrownBy(() -> PluginManifest.parseInstant("invalid-date")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("does not respect the date pattern"); + } + + @Test + void isValid_should_return_true_when_key_and_version_present() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), "mykey"); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), "1.0"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.isValid()).isTrue(); + } + + @Test + void isValid_should_return_false_when_key_missing() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), "1.0"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.isValid()).isFalse(); + } + + @Test + void isValid_should_return_false_when_version_missing() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), "mykey"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.isValid()).isFalse(); + } + + @Test + void isValid_should_return_false_when_key_blank() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), " "); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), "1.0"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.isValid()).isFalse(); + } + + @Test + void isValid_should_return_false_when_version_blank() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), "mykey"); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), " "); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + assertThat(pluginManifest.isValid()).isFalse(); + } + + @Test + void toString_should_return_string_representation() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(PluginManifestProperty.KEY.getKey(), "testkey"); + attributes.putValue(PluginManifestProperty.VERSION.getKey(), "1.0"); + + PluginManifest pluginManifest = new PluginManifest(manifest); + + String result = pluginManifest.toString(); + + assertThat(result) + .contains("testkey") + .contains("1.0"); + } +} diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java new file mode 100644 index 00000000..f6a196dc --- /dev/null +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/RequiredPluginTest.java @@ -0,0 +1,38 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RequiredPluginTest { + + @Test + void test_RequiredPlugin() { + var plugin = RequiredPlugin.parse("java:1.1"); + assertThat(plugin.getKey()).isEqualTo("java"); + assertThat(plugin.getMinimalVersion().getName()).isEqualTo("1.1"); + + assertThrows(IllegalArgumentException.class, () -> RequiredPlugin.parse("java")); + } + +} diff --git a/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java b/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java new file mode 100644 index 00000000..3de0f653 --- /dev/null +++ b/manifest/src/test/java/org/sonar/plugin/api/manifest/VersionTests.java @@ -0,0 +1,262 @@ +/* + * Sonar Plugin API + * Copyright (C) 2009-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugin.api.manifest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class VersionTests { + + @Test + void test_fields_of_snapshot_versions() { + var version = Version.create("1.2.3-SNAPSHOT"); + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + assertThat(version.getPatch()).isEqualTo(3); + assertThat(version.getBuild()).isZero(); + assertThat(version.getQualifier()).isEqualTo("SNAPSHOT"); + } + + @Test + void test_fields_of_releases() { + var version = Version.create("1.2"); + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + assertThat(version.getPatch()).isZero(); + assertThat(version.getBuild()).isZero(); + assertThat(version.getQualifier()).isEmpty(); + } + + @Test + void compare_releases() { + var version12 = Version.create("1.2"); + var version121 = Version.create("1.2.1"); + + assertThat(version12) + .hasToString("1.2") + .isEqualByComparingTo(version12); + assertThat(version121) + .isEqualByComparingTo(version121) + .isGreaterThan(version12); + } + + @Test + void compare_snapshots() { + var version12 = Version.create("1.2"); + var version12Snapshot = Version.create("1.2-SNAPSHOT"); + var version121Snapshot = Version.create("1.2.1-SNAPSHOT"); + var version12RC = Version.create("1.2-RC1"); + + assertThat(version12).isGreaterThan(version12Snapshot); + assertThat(version12Snapshot).isEqualByComparingTo(version12Snapshot); + assertThat(version121Snapshot).isGreaterThan(version12Snapshot); + assertThat(version12Snapshot).isGreaterThan(version12RC); + } + + @Test + void compare_release_candidates() { + var version12 = Version.create("1.2"); + var version12Snapshot = Version.create("1.2-SNAPSHOT"); + var version12RC1 = Version.create("1.2-RC1"); + var version12RC2 = Version.create("1.2-RC2"); + + assertThat(version12RC1) + .isLessThan(version12Snapshot) + .isEqualByComparingTo(version12RC1) + .isLessThan(version12RC2) + .isLessThan(version12); + } + + @Test + void testTrim() { + var version12 = Version.create(" 1.2 "); + + assertThat(version12.getName()).isEqualTo("1.2"); + assertThat(version12).isEqualTo(Version.create("1.2")); + } + + @Test + void testDefaultNumberIsZero() { + var version12 = Version.create("1.2"); + var version120 = Version.create("1.2.0"); + + assertThat(version12).isEqualTo(version120); + assertThat(version120).isEqualTo(version12); + } + + @Test + void testCompareOnTwoDigits() { + var version1dot10 = Version.create("1.10"); + var version1dot1 = Version.create("1.1"); + var version1dot9 = Version.create("1.9"); + + assertThat(version1dot10) + .isGreaterThan(version1dot1) + .isGreaterThan(version1dot9); + } + + @Test + void testFields() { + var version = Version.create("1.10.2"); + + assertThat(version.getName()).isEqualTo("1.10.2"); + assertThat(version).hasToString("1.10.2"); + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(10); + assertThat(version.getPatch()).isEqualTo(2); + assertThat(version.getBuild()).isZero(); + } + + @Test + void testPatchFieldsEquals() { + var version = Version.create("1.2.3.4"); + + assertThat(version.getPatch()).isEqualTo(3); + assertThat(version.getBuild()).isEqualTo(4); + + assertThat(version) + .isEqualTo(version) + .isEqualTo(Version.create("1.2.3.4")) + .isNotEqualTo(Version.create("1.2.3.5")); + } + + @Test + void removeQualifier() { + var version = Version.create("1.2.3-SNAPSHOT").removeQualifier(); + + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + assertThat(version.getPatch()).isEqualTo(3); + assertThat(version.getQualifier()).isEmpty(); + } + + @Test + void removeQualifier_when_no_qualifier() { + var version = Version.create("1.2.3").removeQualifier(); + + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + assertThat(version.getPatch()).isEqualTo(3); + assertThat(version.getQualifier()).isEmpty(); + assertThat(version.getName()).isEqualTo("1.2.3"); + } + + @Test + void satisfiesMinRequirement_should_return_true_when_equal() { + var version = Version.create("1.2.3"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void satisfiesMinRequirement_should_return_true_when_greater() { + var version = Version.create("1.3.0"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void satisfiesMinRequirement_should_return_false_when_lower() { + var version = Version.create("1.2.2"); + var minRequirement = Version.create("1.2.3"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isFalse(); + } + + @Test + void satisfiesMinRequirement_should_ignore_qualifier() { + var version = Version.create("1.2.3-SNAPSHOT"); + var minRequirement = Version.create("1.2.3-RC1"); + + assertThat(version.satisfiesMinRequirement(minRequirement)).isTrue(); + } + + @Test + void compareToIgnoreQualifier_should_return_zero_when_equal() { + var version1 = Version.create("1.2.3-SNAPSHOT"); + var version2 = Version.create("1.2.3-RC1"); + + assertThat(version1.compareToIgnoreQualifier(version2)).isZero(); + } + + @Test + void compareToIgnoreQualifier_should_return_positive_when_greater() { + var version1 = Version.create("1.3.0-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1.compareToIgnoreQualifier(version2)).isPositive(); + } + + @Test + void compareToIgnoreQualifier_should_return_negative_when_lower() { + var version1 = Version.create("1.2.2-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1.compareToIgnoreQualifier(version2)).isNegative(); + } + + @Test + void equals_should_return_false_for_null() { + var version = Version.create("1.2.3"); + + assertThat(version.equals(null)).isFalse(); + } + + @Test + void equals_should_return_false_for_different_type() { + var version = Version.create("1.2.3"); + + assertThat(version.equals("1.2.3")).isFalse(); + } + + @Test + void equals_should_return_true_for_same_instance() { + var version = Version.create("1.2.3"); + + assertThat(version.equals(version)).isTrue(); + } + + @Test + void hashCode_should_be_consistent() { + var version1 = Version.create("1.2.3-SNAPSHOT"); + var version2 = Version.create("1.2.3-SNAPSHOT"); + + assertThat(version1).hasSameHashCodeAs(version2); + } + + @Test + void hashCode_should_differ_for_different_versions() { + var version1 = Version.create("1.2.3"); + var version2 = Version.create("1.2.4"); + + assertThat(version1.hashCode()).isNotEqualTo(version2.hashCode()); + } + + @Test + void hashCode_should_differ_for_different_qualifiers() { + var version1 = Version.create("1.2.3-SNAPSHOT"); + var version2 = Version.create("1.2.3-RC1"); + + assertThat(version1.hashCode()).isNotEqualTo(version2.hashCode()); + } +} diff --git a/manifest/src/test/resources/checkstyle-plugin.jar b/manifest/src/test/resources/checkstyle-plugin.jar new file mode 100644 index 00000000..b3f475f1 Binary files /dev/null and b/manifest/src/test/resources/checkstyle-plugin.jar differ diff --git a/manifest/src/test/resources/old-plugin.jar b/manifest/src/test/resources/old-plugin.jar new file mode 100644 index 00000000..571c3a95 Binary files /dev/null and b/manifest/src/test/resources/old-plugin.jar differ diff --git a/manifest/src/test/resources/plugin-with-devs.jar b/manifest/src/test/resources/plugin-with-devs.jar new file mode 100644 index 00000000..7ac02e7e Binary files /dev/null and b/manifest/src/test/resources/plugin-with-devs.jar differ diff --git a/manifest/src/test/resources/plugin-with-require-for-languages.jar b/manifest/src/test/resources/plugin-with-require-for-languages.jar new file mode 100644 index 00000000..b865359d Binary files /dev/null and b/manifest/src/test/resources/plugin-with-require-for-languages.jar differ diff --git a/manifest/src/test/resources/plugin-with-require-plugins.jar b/manifest/src/test/resources/plugin-with-require-plugins.jar new file mode 100644 index 00000000..2df7ab46 Binary files /dev/null and b/manifest/src/test/resources/plugin-with-require-plugins.jar differ diff --git a/manifest/src/test/resources/plugin-with-sources.jar b/manifest/src/test/resources/plugin-with-sources.jar new file mode 100644 index 00000000..7909a106 Binary files /dev/null and b/manifest/src/test/resources/plugin-with-sources.jar differ diff --git a/settings.gradle b/settings.gradle index 68a22165..ead0da70 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ rootProject.name='sonar-plugin-api' include 'plugin-api' include 'check-api' include 'test-fixtures' +include 'manifest' dependencyResolutionManagement { versionCatalogs {