You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PluginInfo.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  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.core.platform;
  21. import com.google.common.annotations.VisibleForTesting;
  22. import com.google.common.base.Joiner;
  23. import com.google.common.collect.ComparisonChain;
  24. import com.google.common.collect.Ordering;
  25. import java.io.File;
  26. import java.io.IOException;
  27. import java.util.Arrays;
  28. import java.util.HashSet;
  29. import java.util.Objects;
  30. import java.util.Optional;
  31. import java.util.Set;
  32. import java.util.jar.JarFile;
  33. import java.util.regex.Pattern;
  34. import java.util.zip.ZipEntry;
  35. import javax.annotation.CheckForNull;
  36. import javax.annotation.Nullable;
  37. import org.apache.commons.lang.StringUtils;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;
  40. import org.sonar.api.utils.MessageException;
  41. import org.sonar.updatecenter.common.PluginManifest;
  42. import org.sonar.updatecenter.common.Version;
  43. import static java.util.Objects.requireNonNull;
  44. public class PluginInfo implements Comparable<PluginInfo> {
  45. private static final Logger LOGGER = LoggerFactory.getLogger(PluginInfo.class);
  46. private static final Joiner SLASH_JOINER = Joiner.on(" / ").skipNulls();
  47. private final String key;
  48. private String name;
  49. @CheckForNull
  50. private File jarFile;
  51. @CheckForNull
  52. private String mainClass;
  53. @CheckForNull
  54. private Version version;
  55. private String displayVersion;
  56. @CheckForNull
  57. private Version minimalSonarPluginApiVersion;
  58. @CheckForNull
  59. private String description;
  60. @CheckForNull
  61. private String organizationName;
  62. @CheckForNull
  63. private String organizationUrl;
  64. @CheckForNull
  65. private String license;
  66. @CheckForNull
  67. private String homepageUrl;
  68. @CheckForNull
  69. private String issueTrackerUrl;
  70. private boolean useChildFirstClassLoader;
  71. @CheckForNull
  72. private String basePlugin;
  73. @CheckForNull
  74. private String implementationBuild;
  75. @CheckForNull
  76. private boolean sonarLintSupported;
  77. @CheckForNull
  78. private String documentationPath;
  79. private final Set<RequiredPlugin> requiredPlugins = new HashSet<>();
  80. private final Set<String> requiredForLanguages = new HashSet<>();
  81. public PluginInfo(String key) {
  82. requireNonNull(key, "Plugin key is missing from manifest");
  83. this.key = key;
  84. this.name = key;
  85. }
  86. public PluginInfo setJarFile(@Nullable File f) {
  87. this.jarFile = f;
  88. return this;
  89. }
  90. @CheckForNull
  91. public File getJarFile() {
  92. return jarFile;
  93. }
  94. public File getNonNullJarFile() {
  95. requireNonNull(jarFile);
  96. return jarFile;
  97. }
  98. public String getKey() {
  99. return key;
  100. }
  101. public String getName() {
  102. return name;
  103. }
  104. @CheckForNull
  105. public Version getVersion() {
  106. return version;
  107. }
  108. @CheckForNull
  109. public String getDisplayVersion() {
  110. return displayVersion;
  111. }
  112. public PluginInfo setDisplayVersion(@Nullable String displayVersion) {
  113. this.displayVersion = displayVersion;
  114. return this;
  115. }
  116. @CheckForNull
  117. public Version getMinimalSonarPluginApiVersion() {
  118. return minimalSonarPluginApiVersion;
  119. }
  120. @CheckForNull
  121. public String getMainClass() {
  122. return mainClass;
  123. }
  124. @CheckForNull
  125. public String getDescription() {
  126. return description;
  127. }
  128. @CheckForNull
  129. public String getOrganizationName() {
  130. return organizationName;
  131. }
  132. @CheckForNull
  133. public String getOrganizationUrl() {
  134. return organizationUrl;
  135. }
  136. @CheckForNull
  137. public String getLicense() {
  138. return license;
  139. }
  140. @CheckForNull
  141. public String getHomepageUrl() {
  142. return homepageUrl;
  143. }
  144. @CheckForNull
  145. public String getIssueTrackerUrl() {
  146. return issueTrackerUrl;
  147. }
  148. public boolean isUseChildFirstClassLoader() {
  149. return useChildFirstClassLoader;
  150. }
  151. public boolean isSonarLintSupported() {
  152. return sonarLintSupported;
  153. }
  154. public String getDocumentationPath() {
  155. return documentationPath;
  156. }
  157. @CheckForNull
  158. public String getBasePlugin() {
  159. return basePlugin;
  160. }
  161. @CheckForNull
  162. public String getImplementationBuild() {
  163. return implementationBuild;
  164. }
  165. public Set<RequiredPlugin> getRequiredPlugins() {
  166. return requiredPlugins;
  167. }
  168. public Set<String> getRequiredForLanguages() {
  169. return requiredForLanguages;
  170. }
  171. public PluginInfo setName(@Nullable String name) {
  172. this.name = (name != null ? name : this.key);
  173. return this;
  174. }
  175. public PluginInfo setVersion(Version version) {
  176. this.version = version;
  177. return this;
  178. }
  179. public PluginInfo setMinimalSonarPluginApiVersion(@Nullable Version v) {
  180. this.minimalSonarPluginApiVersion = v;
  181. return this;
  182. }
  183. public PluginInfo setDocumentationPath(@Nullable String documentationPath) {
  184. this.documentationPath = documentationPath;
  185. return this;
  186. }
  187. /**
  188. * Required
  189. */
  190. public PluginInfo setMainClass(String mainClass) {
  191. this.mainClass = mainClass;
  192. return this;
  193. }
  194. public PluginInfo setDescription(@Nullable String description) {
  195. this.description = description;
  196. return this;
  197. }
  198. public PluginInfo setOrganizationName(@Nullable String s) {
  199. this.organizationName = s;
  200. return this;
  201. }
  202. public PluginInfo setOrganizationUrl(@Nullable String s) {
  203. this.organizationUrl = s;
  204. return this;
  205. }
  206. public PluginInfo setLicense(@Nullable String license) {
  207. this.license = license;
  208. return this;
  209. }
  210. public PluginInfo setHomepageUrl(@Nullable String s) {
  211. this.homepageUrl = s;
  212. return this;
  213. }
  214. public PluginInfo setIssueTrackerUrl(@Nullable String s) {
  215. this.issueTrackerUrl = s;
  216. return this;
  217. }
  218. public PluginInfo setUseChildFirstClassLoader(boolean b) {
  219. this.useChildFirstClassLoader = b;
  220. return this;
  221. }
  222. public PluginInfo setSonarLintSupported(boolean sonarLintPlugin) {
  223. this.sonarLintSupported = sonarLintPlugin;
  224. return this;
  225. }
  226. public PluginInfo setBasePlugin(@Nullable String s) {
  227. if ("l10nen".equals(s)) {
  228. LOGGER.info("Plugin [{}] defines 'l10nen' as base plugin. " +
  229. "This metadata can be removed from manifest of l10n plugins since version 5.2.", key);
  230. basePlugin = null;
  231. } else {
  232. basePlugin = s;
  233. }
  234. return this;
  235. }
  236. public PluginInfo setImplementationBuild(@Nullable String implementationBuild) {
  237. this.implementationBuild = implementationBuild;
  238. return this;
  239. }
  240. public PluginInfo addRequiredPlugin(RequiredPlugin p) {
  241. this.requiredPlugins.add(p);
  242. return this;
  243. }
  244. public PluginInfo addRequiredForLanguage(String lang) {
  245. this.requiredForLanguages.add(lang);
  246. return this;
  247. }
  248. /**
  249. * Find out if this plugin is compatible with a given version of Sonar Plugin API.
  250. * The version of plugin api embedded in SQ must be greater than or equal to the minimal version
  251. * needed by the plugin.
  252. */
  253. public boolean isCompatibleWith(String runtimePluginApiVersion) {
  254. if (null == this.minimalSonarPluginApiVersion) {
  255. // no constraint defined on the plugin
  256. return true;
  257. }
  258. Version effectiveMin = Version.create(minimalSonarPluginApiVersion.getName()).removeQualifier();
  259. Version effectiveVersion = Version.create(runtimePluginApiVersion).removeQualifier();
  260. if (runtimePluginApiVersion.endsWith("-SNAPSHOT")) {
  261. // check only the major and minor versions (two first fields)
  262. effectiveMin = Version.create(effectiveMin.getMajor() + "." + effectiveMin.getMinor());
  263. }
  264. return effectiveVersion.compareTo(effectiveMin) >= 0;
  265. }
  266. @Override
  267. public String toString() {
  268. return String.format("[%s]", SLASH_JOINER.join(key, version, implementationBuild));
  269. }
  270. @Override
  271. public boolean equals(@Nullable Object o) {
  272. if (this == o) {
  273. return true;
  274. }
  275. if (o == null || getClass() != o.getClass()) {
  276. return false;
  277. }
  278. PluginInfo info = (PluginInfo) o;
  279. return Objects.equals(key, info.key) && Objects.equals(version, info.version);
  280. }
  281. @Override
  282. public int hashCode() {
  283. return Objects.hash(key, version);
  284. }
  285. @Override
  286. public int compareTo(PluginInfo that) {
  287. return ComparisonChain.start()
  288. .compare(this.name, that.name)
  289. .compare(this.version, that.version, Ordering.natural().nullsFirst())
  290. .result();
  291. }
  292. public static PluginInfo create(File jarFile) {
  293. try {
  294. PluginManifest manifest = new PluginManifest(jarFile);
  295. return create(jarFile, manifest);
  296. } catch (IOException e) {
  297. throw new IllegalStateException("Fail to extract plugin metadata from file: " + jarFile, e);
  298. }
  299. }
  300. @VisibleForTesting
  301. static PluginInfo create(File jarFile, PluginManifest manifest) {
  302. validateManifest(jarFile, manifest);
  303. PluginInfo info = new PluginInfo(manifest.getKey());
  304. info.fillFields(jarFile, manifest);
  305. return info;
  306. }
  307. private static void validateManifest(File jarFile, PluginManifest manifest) {
  308. if (StringUtils.isBlank(manifest.getKey())) {
  309. throw MessageException.of(String.format("File is not a plugin. Please delete it and restart: %s", jarFile.getAbsolutePath()));
  310. }
  311. }
  312. protected void fillFields(File jarFile, PluginManifest manifest) {
  313. setJarFile(jarFile);
  314. setName(manifest.getName());
  315. setMainClass(manifest.getMainClass());
  316. setVersion(Version.create(manifest.getVersion()));
  317. setDocumentationPath(getDocumentationPath(jarFile));
  318. // optional fields
  319. setDescription(manifest.getDescription());
  320. setLicense(manifest.getLicense());
  321. setOrganizationName(manifest.getOrganization());
  322. setOrganizationUrl(manifest.getOrganizationUrl());
  323. setDisplayVersion(manifest.getDisplayVersion());
  324. String minSonarPluginApiVersion = manifest.getSonarVersion();
  325. if (minSonarPluginApiVersion != null) {
  326. setMinimalSonarPluginApiVersion(Version.create(minSonarPluginApiVersion));
  327. }
  328. setHomepageUrl(manifest.getHomepage());
  329. setIssueTrackerUrl(manifest.getIssueTrackerUrl());
  330. setUseChildFirstClassLoader(manifest.isUseChildFirstClassLoader());
  331. setSonarLintSupported(manifest.isSonarLintSupported());
  332. setBasePlugin(manifest.getBasePlugin());
  333. setImplementationBuild(manifest.getImplementationBuild());
  334. String[] requiredPluginsFromManifest = manifest.getRequirePlugins();
  335. if (requiredPluginsFromManifest != null) {
  336. Arrays.stream(requiredPluginsFromManifest)
  337. .map(RequiredPlugin::parse)
  338. .filter(t -> !"license".equals(t.key))
  339. .forEach(this::addRequiredPlugin);
  340. }
  341. String[] requiredForLanguagesFromManifest = manifest.getRequiredForLanguages();
  342. if (requiredForLanguagesFromManifest != null) {
  343. Arrays.stream(requiredForLanguagesFromManifest)
  344. .forEach(this::addRequiredForLanguage);
  345. }
  346. }
  347. private static String getDocumentationPath(File file) {
  348. try (JarFile jarFile = new JarFile(file)) {
  349. return Optional.ofNullable(jarFile.getEntry("static/documentation.md"))
  350. .map(ZipEntry::getName)
  351. .orElse(null);
  352. } catch (IOException e) {
  353. LOGGER.warn("Could not retrieve documentation path from " + file, e);
  354. }
  355. return null;
  356. }
  357. public static class RequiredPlugin {
  358. private static final Pattern PARSER = Pattern.compile("\\w+:.+");
  359. private final String key;
  360. private final Version minimalVersion;
  361. public RequiredPlugin(String key, Version minimalVersion) {
  362. this.key = key;
  363. this.minimalVersion = minimalVersion;
  364. }
  365. public String getKey() {
  366. return key;
  367. }
  368. public Version getMinimalVersion() {
  369. return minimalVersion;
  370. }
  371. public static RequiredPlugin parse(String s) {
  372. if (!PARSER.matcher(s).matches()) {
  373. throw new IllegalArgumentException("Manifest field does not have correct format: " + s);
  374. }
  375. String[] fields = StringUtils.split(s, ':');
  376. return new RequiredPlugin(fields[0], Version.create(fields[1]).removeQualifier());
  377. }
  378. @Override
  379. public boolean equals(Object o) {
  380. if (this == o) {
  381. return true;
  382. }
  383. if (o == null || getClass() != o.getClass()) {
  384. return false;
  385. }
  386. RequiredPlugin that = (RequiredPlugin) o;
  387. return key.equals(that.key);
  388. }
  389. @Override
  390. public int hashCode() {
  391. return key.hashCode();
  392. }
  393. @Override
  394. public String toString() {
  395. return key + ':' + minimalVersion.getName();
  396. }
  397. }
  398. }