Skip to content

Commit 5f553ad

Browse files
committed
Move the plugin manifest tooling to the sonar-plugin-api repo
This make sense because the Manifest is part of the plugin contract, even if is not directly part of the Java API. Many components are duplicating this code (Scanners, SonarLint, SQS/SQC, Update Center, Maven Packaging plugin...)
1 parent 0fb2683 commit 5f553ad

18 files changed

+1383
-0
lines changed

manifest/build.gradle

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
description = 'Sonar Plugin API - Manifest'
2+
3+
dependencies {
4+
compileOnly libs.jsr305
5+
6+
implementation libs.commons.lang3
7+
8+
testImplementation libs.junit5
9+
testImplementation libs.assertj
10+
testImplementation libs.mockito
11+
testRuntimeOnly libs.jupiter.engine
12+
}
13+
14+
artifactoryPublish.skip = false
15+
16+
publishing {
17+
publications {
18+
mavenJava(MavenPublication) {
19+
artifactId = 'sonar-plugin-manifest'
20+
artifact sourcesJar
21+
artifact javadocJar
22+
}
23+
}
24+
}
25+
26+
test {
27+
useJUnitPlatform()
28+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Sonar Plugin API
3+
* Copyright (C) 2009-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.plugin.api.manifest;
21+
22+
import javax.annotation.CheckForNull;
23+
import javax.annotation.Nullable;
24+
import org.apache.commons.lang3.StringUtils;
25+
26+
public final class PluginKeyUtils {
27+
28+
private static final String SONAR_PLUGIN_SUFFIX = "-sonar-plugin";
29+
private static final String SONAR_PREFIX = "sonar-";
30+
private static final String PLUGIN_SUFFIX = "-plugin";
31+
32+
private PluginKeyUtils() {
33+
// only static methods
34+
}
35+
36+
@CheckForNull
37+
public static String sanitize(@Nullable String mavenArtifactId) {
38+
if (mavenArtifactId == null) {
39+
return null;
40+
}
41+
42+
String key = mavenArtifactId;
43+
if (StringUtils.startsWith(mavenArtifactId, SONAR_PREFIX) && StringUtils.endsWith(mavenArtifactId, PLUGIN_SUFFIX)) {
44+
key = StringUtils.removeEnd(StringUtils.removeStart(mavenArtifactId, SONAR_PREFIX), PLUGIN_SUFFIX);
45+
} else if (StringUtils.endsWith(mavenArtifactId, SONAR_PLUGIN_SUFFIX)) {
46+
key = StringUtils.removeEnd(mavenArtifactId, SONAR_PLUGIN_SUFFIX);
47+
}
48+
return keepLettersAndDigits(key);
49+
}
50+
51+
private static String keepLettersAndDigits(String key) {
52+
StringBuilder sb = new StringBuilder();
53+
for (int index = 0; index < key.length(); index++) {
54+
char character = key.charAt(index);
55+
if (Character.isLetter(character) || Character.isDigit(character)) {
56+
sb.append(character);
57+
}
58+
}
59+
return sb.toString();
60+
}
61+
62+
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Sonar Plugin API
3+
* Copyright (C) 2009-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.plugin.api.manifest;
21+
22+
import java.io.File;
23+
import java.nio.file.Path;
24+
import java.time.ZonedDateTime;
25+
import java.time.format.DateTimeFormatter;
26+
import java.time.format.DateTimeParseException;
27+
import java.util.List;
28+
import java.util.Locale;
29+
import java.util.Optional;
30+
import java.util.jar.Attributes;
31+
import java.util.jar.JarFile;
32+
import java.util.jar.Manifest;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
import javax.annotation.Nullable;
36+
import org.apache.commons.lang3.StringUtils;
37+
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
38+
39+
/**
40+
* This class loads Sonar plugin metadata from JAR manifest.
41+
*/
42+
public final class PluginManifest {
43+
44+
public static final String DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ";
45+
46+
private final String key;
47+
private final String name;
48+
private final String mainClass;
49+
private final String description;
50+
private final String organization;
51+
private final String organizationUrl;
52+
private final String license;
53+
private final String version;
54+
private final String displayVersion;
55+
@Nullable
56+
private final Version sonarPluginApiMinVersion;
57+
private final List<String> dependencies;
58+
private final String homepage;
59+
private final String termsConditionsUrl;
60+
private final ZonedDateTime buildDate;
61+
private final String issueTrackerUrl;
62+
private final boolean useChildFirstClassLoader;
63+
private final String basePlugin;
64+
private final String implementationBuild;
65+
private final String sourcesUrl;
66+
private final List<String> developers;
67+
private final List<RequiredPlugin> requiredPlugins;
68+
private final boolean sonarlintSupported;
69+
private final List<String> requiredForLanguages;
70+
@Nullable
71+
private final Version jreMinVersion;
72+
@Nullable
73+
private final Version nodeJsMinVersion;
74+
75+
/**
76+
* Load the manifest from a JAR file.
77+
*/
78+
public PluginManifest(File jarFile) {
79+
this(loadManifestFromFile(jarFile.toPath()));
80+
}
81+
82+
/**
83+
* Load the manifest from a JAR file.
84+
*/
85+
public PluginManifest(Path jarFilePath) {
86+
this(loadManifestFromFile(jarFilePath));
87+
}
88+
89+
private static Manifest loadManifestFromFile(Path path) {
90+
try (JarFile jar = new JarFile(path.toFile())) {
91+
Manifest manifest = jar.getManifest();
92+
return manifest != null ? manifest : new Manifest();
93+
} catch (Exception e) {
94+
throw new IllegalStateException("Unable to read plugin manifest from jar : " + path.toAbsolutePath(), e);
95+
}
96+
}
97+
98+
/**
99+
* @param manifest can not be null
100+
*/
101+
public PluginManifest(Manifest manifest) {
102+
Attributes attributes = manifest.getMainAttributes();
103+
this.key = PluginKeyUtils.sanitize(attributes.getValue(PluginManifestProperty.KEY.getKey()));
104+
this.mainClass = attributes.getValue(PluginManifestProperty.MAIN_CLASS.getKey());
105+
this.name = attributes.getValue(PluginManifestProperty.NAME.getKey());
106+
this.description = attributes.getValue(PluginManifestProperty.DESCRIPTION.getKey());
107+
this.license = attributes.getValue(PluginManifestProperty.LICENSE.getKey());
108+
this.organization = attributes.getValue(PluginManifestProperty.ORGANIZATION.getKey());
109+
this.organizationUrl = attributes.getValue(PluginManifestProperty.ORGANIZATION_URL.getKey());
110+
this.version = attributes.getValue(PluginManifestProperty.VERSION.getKey());
111+
this.displayVersion = attributes.getValue(PluginManifestProperty.DISPLAY_VERSION.getKey());
112+
this.homepage = attributes.getValue(PluginManifestProperty.HOMEPAGE.getKey());
113+
this.termsConditionsUrl = attributes.getValue(PluginManifestProperty.TERMS_CONDITIONS_URL.getKey());
114+
this.sonarPluginApiMinVersion = parseVersion(attributes, PluginManifestProperty.SONAR_VERSION);
115+
this.issueTrackerUrl = attributes.getValue(PluginManifestProperty.ISSUE_TRACKER_URL.getKey());
116+
this.buildDate = parseInstant(attributes.getValue(PluginManifestProperty.BUILD_DATE.getKey()));
117+
this.useChildFirstClassLoader = "true".equalsIgnoreCase(attributes.getValue(PluginManifestProperty.USE_CHILD_FIRST_CLASSLOADER.getKey()));
118+
this.sonarlintSupported = "true".equalsIgnoreCase(attributes.getValue(PluginManifestProperty.SONARLINT_SUPPORTED.getKey()));
119+
this.basePlugin = attributes.getValue(PluginManifestProperty.BASE_PLUGIN.getKey());
120+
this.implementationBuild = attributes.getValue(PluginManifestProperty.IMPLEMENTATION_BUILD.getKey());
121+
this.sourcesUrl = attributes.getValue(PluginManifestProperty.SOURCES_URL.getKey());
122+
123+
String deps = attributes.getValue(PluginManifestProperty.DEPENDENCIES.getKey());
124+
this.dependencies = List.of(StringUtils.split(StringUtils.defaultString(deps), ' '));
125+
126+
String devs = attributes.getValue(PluginManifestProperty.DEVELOPERS.getKey());
127+
this.developers = List.of(StringUtils.split(StringUtils.defaultString(devs), ','));
128+
129+
String requires = attributes.getValue(PluginManifestProperty.REQUIRE_PLUGINS.getKey());
130+
this.requiredPlugins = Stream.of(StringUtils.split(StringUtils.defaultString(requires), ','))
131+
.map(RequiredPlugin::parse).collect(Collectors.toUnmodifiableList());
132+
133+
String languages = attributes.getValue(PluginManifestProperty.LANGUAGES.getKey());
134+
this.requiredForLanguages = List.of(StringUtils.split(StringUtils.defaultString(languages), ','));
135+
136+
this.jreMinVersion = parseVersion(attributes, PluginManifestProperty.JRE_MIN_VERSION);
137+
this.nodeJsMinVersion = parseVersion(attributes, PluginManifestProperty.NODEJS_MIN_VERSION);
138+
}
139+
140+
@Nullable
141+
private static Version parseVersion(Attributes attributes, PluginManifestProperty manifestProperty) {
142+
return Optional.ofNullable(attributes.getValue(manifestProperty.getKey()))
143+
.map(Version::create)
144+
.orElse(null);
145+
}
146+
147+
public String getKey() {
148+
return key;
149+
}
150+
151+
public String getName() {
152+
return name;
153+
}
154+
155+
public List<RequiredPlugin> getRequiredPlugins() {
156+
return requiredPlugins;
157+
}
158+
159+
public String getDescription() {
160+
return description;
161+
}
162+
163+
public String getOrganization() {
164+
return organization;
165+
}
166+
167+
public String getOrganizationUrl() {
168+
return organizationUrl;
169+
}
170+
171+
public String getLicense() {
172+
return license;
173+
}
174+
175+
public String getVersion() {
176+
return version;
177+
}
178+
179+
public String getDisplayVersion() {
180+
return displayVersion;
181+
}
182+
183+
public Optional<Version> getSonarPluginApiMinVersion() {
184+
return Optional.ofNullable(sonarPluginApiMinVersion);
185+
}
186+
187+
public String getMainClass() {
188+
return mainClass;
189+
}
190+
191+
public List<String> getDependencies() {
192+
return dependencies;
193+
}
194+
195+
public ZonedDateTime getBuildDate() {
196+
return buildDate;
197+
}
198+
199+
public String getHomepage() {
200+
return homepage;
201+
}
202+
203+
public String getTermsConditionsUrl() {
204+
return termsConditionsUrl;
205+
}
206+
207+
public String getIssueTrackerUrl() {
208+
return issueTrackerUrl;
209+
}
210+
211+
public boolean isUseChildFirstClassLoader() {
212+
return useChildFirstClassLoader;
213+
}
214+
215+
public boolean isSonarLintSupported() {
216+
return sonarlintSupported;
217+
}
218+
219+
public String getBasePlugin() {
220+
return basePlugin;
221+
}
222+
223+
public String getImplementationBuild() {
224+
return implementationBuild;
225+
}
226+
227+
public String getSourcesUrl() {
228+
return sourcesUrl;
229+
}
230+
231+
public List<String> getDevelopers() {
232+
return developers;
233+
}
234+
235+
public List<String> getRequiredForLanguages() {
236+
return requiredForLanguages;
237+
}
238+
239+
public Optional<Version> getJreMinVersion() {
240+
return Optional.ofNullable(jreMinVersion);
241+
}
242+
243+
public Optional<Version> getNodeJsMinVersion() {
244+
return Optional.ofNullable(nodeJsMinVersion);
245+
}
246+
247+
@Override
248+
public String toString() {
249+
return new ReflectionToStringBuilder(this).toString();
250+
}
251+
252+
public boolean isValid() {
253+
return StringUtils.isNotBlank(key) && StringUtils.isNotBlank(version);
254+
}
255+
256+
public static ZonedDateTime parseInstant(String s) {
257+
try {
258+
if (StringUtils.isNotBlank(s)) {
259+
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.US);
260+
return ZonedDateTime.parse(s, dateTimeFormatter);
261+
}
262+
return null;
263+
} catch (DateTimeParseException e) {
264+
throw new IllegalArgumentException("The following value does not respect the date pattern " + DATETIME_PATTERN + ": " + s, e);
265+
}
266+
}
267+
268+
}

0 commit comments

Comments
 (0)