From 3c25a824164a3f091a27215fd8086cbefd60da39 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Wed, 16 Jul 2014 10:52:00 +0200 Subject: [PATCH] SONAR-5001 Update API, ES and WS to support Markdown in rule descriptions --- .../server/platform/ServerComponents.java | 152 ++---------------- .../java/org/sonar/server/rule/NewRule.java | 12 +- .../org/sonar/server/rule/RegisterRules.java | 35 ++-- .../main/java/org/sonar/server/rule/Rule.java | 3 + .../org/sonar/server/rule/RuleCreator.java | 9 +- .../org/sonar/server/rule/index/RuleDoc.java | 6 + .../server/rule/index/RuleNormalizer.java | 25 ++- .../sonar/server/rule/ws/CreateAction.java | 4 +- .../org/sonar/server/rule/ws/RuleMapping.java | 16 +- .../server/rule/RuleCreatorMediumTest.java | 20 +-- .../server/rule/RuleServiceMediumTest.java | 2 +- .../org/sonar/server/rule/RuleTesting.java | 2 + .../org/sonar/server/rule/db/RuleDaoTest.java | 15 +- .../server/rule/ws/ShowActionMediumTest.java | 7 +- .../java/org/sonar/core/rule/RuleDto.java | 10 +- .../java/org/sonar/core/rule/RuleDaoTest.java | 2 +- .../api/server/rule/RulesDefinition.java | 45 ++++-- 17 files changed, 163 insertions(+), 202 deletions(-) diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index d711892df8d..d03e457c449 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -52,13 +52,7 @@ import org.sonar.core.measure.db.MeasureFilterDao; import org.sonar.core.metric.DefaultMetricFinder; import org.sonar.core.notification.DefaultNotificationManager; import org.sonar.core.permission.PermissionFacade; -import org.sonar.core.persistence.DaoUtils; -import org.sonar.core.persistence.DatabaseVersion; -import org.sonar.core.persistence.DefaultDatabase; -import org.sonar.core.persistence.MyBatis; -import org.sonar.core.persistence.PreviewDatabaseFactory; -import org.sonar.core.persistence.SemaphoreUpdater; -import org.sonar.core.persistence.SemaphoresImpl; +import org.sonar.core.persistence.*; import org.sonar.core.preview.PreviewCache; import org.sonar.core.profiling.Profiling; import org.sonar.core.purge.PurgeProfiler; @@ -96,33 +90,11 @@ import org.sonar.server.db.DbClient; import org.sonar.server.db.EmbeddedDatabaseFactory; import org.sonar.server.db.migrations.DatabaseMigrations; import org.sonar.server.db.migrations.DatabaseMigrator; -import org.sonar.server.debt.DebtCharacteristicsXMLImporter; -import org.sonar.server.debt.DebtModelBackup; -import org.sonar.server.debt.DebtModelLookup; -import org.sonar.server.debt.DebtModelOperations; -import org.sonar.server.debt.DebtModelPluginRepository; -import org.sonar.server.debt.DebtModelService; -import org.sonar.server.debt.DebtModelXMLExporter; -import org.sonar.server.debt.DebtRulesXMLImporter; +import org.sonar.server.debt.*; import org.sonar.server.duplication.ws.DuplicationsParser; import org.sonar.server.duplication.ws.DuplicationsWriter; import org.sonar.server.duplication.ws.DuplicationsWs; -import org.sonar.server.issue.ActionService; -import org.sonar.server.issue.AssignAction; -import org.sonar.server.issue.CommentAction; -import org.sonar.server.issue.DefaultIssueFinder; -import org.sonar.server.issue.InternalRubyIssueService; -import org.sonar.server.issue.IssueBulkChangeService; -import org.sonar.server.issue.IssueChangelogFormatter; -import org.sonar.server.issue.IssueChangelogService; -import org.sonar.server.issue.IssueCommentService; -import org.sonar.server.issue.IssueService; -import org.sonar.server.issue.IssueStatsFinder; -import org.sonar.server.issue.PlanAction; -import org.sonar.server.issue.PublicRubyIssueService; -import org.sonar.server.issue.ServerIssueStorage; -import org.sonar.server.issue.SetSeverityAction; -import org.sonar.server.issue.TransitionAction; +import org.sonar.server.issue.*; import org.sonar.server.issue.actionplan.ActionPlanService; import org.sonar.server.issue.actionplan.ActionPlanWs; import org.sonar.server.issue.filter.IssueFilterService; @@ -147,85 +119,22 @@ import org.sonar.server.platform.ws.L10nWs; import org.sonar.server.platform.ws.RestartHandler; import org.sonar.server.platform.ws.ServerWs; import org.sonar.server.platform.ws.SystemWs; -import org.sonar.server.plugins.BatchWs; -import org.sonar.server.plugins.InstalledPluginReferentialFactory; -import org.sonar.server.plugins.PluginDownloader; -import org.sonar.server.plugins.ServerExtensionInstaller; -import org.sonar.server.plugins.ServerPluginJarInstaller; -import org.sonar.server.plugins.ServerPluginJarsInstaller; -import org.sonar.server.plugins.ServerPluginRepository; -import org.sonar.server.plugins.UpdateCenterClient; -import org.sonar.server.plugins.UpdateCenterMatrixFactory; +import org.sonar.server.plugins.*; import org.sonar.server.qualitygate.QgateProjectFinder; import org.sonar.server.qualitygate.QualityGates; import org.sonar.server.qualitygate.RegisterQualityGates; -import org.sonar.server.qualitygate.ws.QGatesAppAction; -import org.sonar.server.qualitygate.ws.QGatesCopyAction; -import org.sonar.server.qualitygate.ws.QGatesCreateAction; -import org.sonar.server.qualitygate.ws.QGatesCreateConditionAction; -import org.sonar.server.qualitygate.ws.QGatesDeleteConditionAction; -import org.sonar.server.qualitygate.ws.QGatesDeselectAction; -import org.sonar.server.qualitygate.ws.QGatesDestroyAction; -import org.sonar.server.qualitygate.ws.QGatesListAction; -import org.sonar.server.qualitygate.ws.QGatesRenameAction; -import org.sonar.server.qualitygate.ws.QGatesSearchAction; -import org.sonar.server.qualitygate.ws.QGatesSelectAction; -import org.sonar.server.qualitygate.ws.QGatesSetAsDefaultAction; -import org.sonar.server.qualitygate.ws.QGatesShowAction; -import org.sonar.server.qualitygate.ws.QGatesUnsetDefaultAction; -import org.sonar.server.qualitygate.ws.QGatesUpdateConditionAction; -import org.sonar.server.qualitygate.ws.QGatesWs; -import org.sonar.server.qualityprofile.BuiltInProfiles; -import org.sonar.server.qualityprofile.QProfileBackuper; -import org.sonar.server.qualityprofile.QProfileCopier; -import org.sonar.server.qualityprofile.QProfileExporters; -import org.sonar.server.qualityprofile.QProfileFactory; -import org.sonar.server.qualityprofile.QProfileLoader; -import org.sonar.server.qualityprofile.QProfileLookup; -import org.sonar.server.qualityprofile.QProfileProjectLookup; -import org.sonar.server.qualityprofile.QProfileProjectOperations; -import org.sonar.server.qualityprofile.QProfileRepositoryExporter; -import org.sonar.server.qualityprofile.QProfileReset; -import org.sonar.server.qualityprofile.QProfileService; -import org.sonar.server.qualityprofile.QProfiles; -import org.sonar.server.qualityprofile.RegisterQualityProfiles; -import org.sonar.server.qualityprofile.RuleActivator; -import org.sonar.server.qualityprofile.RuleActivatorContextFactory; +import org.sonar.server.qualitygate.ws.*; +import org.sonar.server.qualityprofile.*; import org.sonar.server.qualityprofile.db.ActiveRuleDao; import org.sonar.server.qualityprofile.index.ActiveRuleIndex; import org.sonar.server.qualityprofile.index.ActiveRuleNormalizer; -import org.sonar.server.qualityprofile.ws.BulkRuleActivationActions; -import org.sonar.server.qualityprofile.ws.ProfilesWs; -import org.sonar.server.qualityprofile.ws.QProfileRestoreBuiltInAction; -import org.sonar.server.qualityprofile.ws.QProfilesWs; -import org.sonar.server.qualityprofile.ws.RuleActivationActions; -import org.sonar.server.rule.DefaultRuleFinder; -import org.sonar.server.rule.DeprecatedRulesDefinition; -import org.sonar.server.rule.RegisterRules; -import org.sonar.server.rule.RubyRuleService; -import org.sonar.server.rule.RuleCreator; -import org.sonar.server.rule.RuleDefinitionsLoader; -import org.sonar.server.rule.RuleDeleter; -import org.sonar.server.rule.RuleOperations; -import org.sonar.server.rule.RuleRepositories; -import org.sonar.server.rule.RuleService; -import org.sonar.server.rule.RuleUpdater; +import org.sonar.server.qualityprofile.ws.*; +import org.sonar.server.rule.*; import org.sonar.server.rule.db.RuleDao; import org.sonar.server.rule.index.RuleIndex; import org.sonar.server.rule.index.RuleNormalizer; -import org.sonar.server.rule.ws.ActiveRuleCompleter; -import org.sonar.server.rule.ws.AppAction; -import org.sonar.server.rule.ws.DeleteAction; -import org.sonar.server.rule.ws.RuleMapping; -import org.sonar.server.rule.ws.RulesWebService; -import org.sonar.server.rule.ws.SearchAction; -import org.sonar.server.rule.ws.TagsAction; -import org.sonar.server.rule.ws.UpdateAction; -import org.sonar.server.search.ESNode; -import org.sonar.server.search.IndexClient; -import org.sonar.server.search.IndexQueue; -import org.sonar.server.search.IndexQueueWorker; -import org.sonar.server.search.IndexSynchronizer; +import org.sonar.server.rule.ws.*; +import org.sonar.server.search.*; import org.sonar.server.source.CodeColorizers; import org.sonar.server.source.DeprecatedSourceDecorator; import org.sonar.server.source.HtmlSourceDecorator; @@ -234,27 +143,9 @@ import org.sonar.server.source.ws.ScmAction; import org.sonar.server.source.ws.ScmWriter; import org.sonar.server.source.ws.ShowAction; import org.sonar.server.source.ws.SourcesWs; -import org.sonar.server.startup.CleanPreviewAnalysisCache; -import org.sonar.server.startup.CopyRequirementsFromCharacteristicsToRules; -import org.sonar.server.startup.GeneratePluginIndex; -import org.sonar.server.startup.GwtPublisher; -import org.sonar.server.startup.JdbcDriverDeployer; -import org.sonar.server.startup.LogServerId; -import org.sonar.server.startup.RegisterDashboards; -import org.sonar.server.startup.RegisterDebtModel; -import org.sonar.server.startup.RegisterMetrics; -import org.sonar.server.startup.RegisterNewMeasureFilters; -import org.sonar.server.startup.RegisterPermissionTemplates; -import org.sonar.server.startup.RegisterServletFilters; -import org.sonar.server.startup.RenameDeprecatedPropertyKeys; -import org.sonar.server.startup.ServerMetadataPersister; +import org.sonar.server.startup.*; import org.sonar.server.test.CoverageService; -import org.sonar.server.test.ws.CoverageShowAction; -import org.sonar.server.test.ws.CoverageWs; -import org.sonar.server.test.ws.TestsCoveredFilesAction; -import org.sonar.server.test.ws.TestsShowAction; -import org.sonar.server.test.ws.TestsTestCasesAction; -import org.sonar.server.test.ws.TestsWs; +import org.sonar.server.test.ws.*; import org.sonar.server.text.MacroInterpreter; import org.sonar.server.text.RubyTextService; import org.sonar.server.ui.JRubyI18n; @@ -262,20 +153,9 @@ import org.sonar.server.ui.JRubyProfiling; import org.sonar.server.ui.PageDecorations; import org.sonar.server.ui.Views; import org.sonar.server.updatecenter.ws.UpdateCenterWs; -import org.sonar.server.user.DefaultUserService; -import org.sonar.server.user.DoPrivileged; -import org.sonar.server.user.GroupMembershipFinder; -import org.sonar.server.user.GroupMembershipService; -import org.sonar.server.user.NewUserNotifier; -import org.sonar.server.user.SecurityRealmFactory; +import org.sonar.server.user.*; import org.sonar.server.user.ws.UsersWs; -import org.sonar.server.util.BooleanTypeValidation; -import org.sonar.server.util.FloatTypeValidation; -import org.sonar.server.util.IntegerTypeValidation; -import org.sonar.server.util.StringListTypeValidation; -import org.sonar.server.util.StringTypeValidation; -import org.sonar.server.util.TextTypeValidation; -import org.sonar.server.util.TypeValidations; +import org.sonar.server.util.*; import org.sonar.server.ws.ListingWs; import org.sonar.server.ws.WebServiceEngine; @@ -326,6 +206,9 @@ class ServerComponents { MeasureFilterDao.class, ActivityDao.class, + // Text + MacroInterpreter.class, + // Elasticsearch ESNode.class, RuleNormalizer.class, @@ -610,7 +493,6 @@ class ServerComponents { pico.addSingleton(org.sonar.server.duplication.ws.ShowAction.class); // text - pico.addSingleton(MacroInterpreter.class); pico.addSingleton(RubyTextService.class); // Notifications diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/NewRule.java b/server/sonar-server/src/main/java/org/sonar/server/rule/NewRule.java index f6c4cabaee5..6af6fa8f369 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/NewRule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/NewRule.java @@ -34,7 +34,7 @@ public class NewRule { private String ruleKey; private RuleKey templateKey; - private String name, htmlDescription, severity; + private String name, htmlDescription, markdownDescription, severity; private RuleStatus status; private final Map parameters = Maps.newHashMap(); @@ -73,6 +73,16 @@ public class NewRule { return this; } + @CheckForNull + public String markdownDescription() { + return markdownDescription; + } + + public NewRule setMarkdownDescription(@Nullable String markdownDescription) { + this.markdownDescription = markdownDescription; + return this; + } + @CheckForNull public String severity() { return severity; diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/RegisterRules.java b/server/sonar-server/src/main/java/org/sonar/server/rule/RegisterRules.java index 2fac09447af..5e09f77dd06 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/RegisterRules.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/RegisterRules.java @@ -41,6 +41,7 @@ import org.sonar.core.persistence.DbSession; import org.sonar.core.qualityprofile.db.ActiveRuleDto; import org.sonar.core.qualityprofile.db.ActiveRuleParamDto; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import org.sonar.core.rule.RuleParamDto; import org.sonar.core.technicaldebt.db.CharacteristicDao; import org.sonar.core.technicaldebt.db.CharacteristicDto; @@ -51,12 +52,7 @@ import org.sonar.server.startup.RegisterDebtModel; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.google.common.collect.Lists.newArrayList; @@ -198,13 +194,19 @@ public class RegisterRules implements Startable { RuleDto ruleDto = RuleDto.createFor(RuleKey.of(ruleDef.repository().key(), ruleDef.key())) .setIsTemplate(ruleDef.template()) .setConfigKey(ruleDef.internalKey()) - .setDescription(ruleDef.htmlDescription()) .setLanguage(ruleDef.repository().language()) .setName(ruleDef.name()) .setSeverity(ruleDef.severity()) .setStatus(ruleDef.status()) .setEffortToFixDescription(ruleDef.effortToFixDescription()) .setSystemTags(ruleDef.tags()); + if (ruleDef.htmlDescription() != null) { + ruleDto.setDescription(ruleDef.htmlDescription()); + ruleDto.setDescriptionFormat(Format.HTML); + } else { + ruleDto.setDescription(ruleDef.markdownDescription()); + ruleDto.setDescriptionFormat(Format.MARKDOWN); + } dbClient.ruleDao().insert(session, ruleDto); return ruleDto; @@ -216,10 +218,7 @@ public class RegisterRules implements Startable { dto.setName(def.name()); changed = true; } - if (!StringUtils.equals(dto.getDescription(), def.htmlDescription())) { - dto.setDescription(def.htmlDescription()); - changed = true; - } + changed = mergeDescription(def, dto); if (!dto.getSystemTags().containsAll(def.tags())) { dto.setSystemTags(def.tags()); changed = true; @@ -249,6 +248,20 @@ public class RegisterRules implements Startable { return changed; } + private boolean mergeDescription(RulesDefinition.Rule def, RuleDto dto) { + boolean changed = false; + if (def.htmlDescription() != null && !StringUtils.equals(dto.getDescription(), def.htmlDescription())) { + dto.setDescription(def.htmlDescription()); + dto.setDescriptionFormat(Format.HTML); + changed = true; + } else if (def.markdownDescription() != null && !StringUtils.equals(dto.getDescription(), def.markdownDescription())) { + dto.setDescription(def.markdownDescription()); + dto.setDescriptionFormat(Format.MARKDOWN); + changed = true; + } + return changed; + } + private boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto, @Nullable CharacteristicDto subCharacteristic) { // Debt definitions are set to null if the sub-characteristic and the remediation function are null DebtRemediationFunction debtRemediationFunction = subCharacteristic != null ? def.debtRemediationFunction() : null; diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/Rule.java b/server/sonar-server/src/main/java/org/sonar/server/rule/Rule.java index c27d9c96736..076e6c6dd57 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/Rule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/Rule.java @@ -24,6 +24,7 @@ import org.sonar.api.rule.RuleStatus; import org.sonar.api.server.debt.DebtRemediationFunction; import javax.annotation.CheckForNull; + import java.util.Date; import java.util.List; @@ -40,6 +41,8 @@ public interface Rule { String htmlDescription(); + String markdownDescription(); + String effortToFixDescription(); /** diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/RuleCreator.java b/server/sonar-server/src/main/java/org/sonar/server/rule/RuleCreator.java index 30dd0f2c058..0a60c46eca9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/RuleCreator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/RuleCreator.java @@ -27,6 +27,7 @@ import org.sonar.api.rule.RuleStatus; import org.sonar.api.rule.Severity; import org.sonar.core.persistence.DbSession; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import org.sonar.core.rule.RuleParamDto; import org.sonar.server.db.DbClient; import org.sonar.server.rule.index.RuleDoc; @@ -131,7 +132,7 @@ public class RuleCreator implements ServerComponent { } private static void validateDescription(NewRule newRule){ - if (Strings.isNullOrEmpty(newRule.htmlDescription())) { + if (Strings.isNullOrEmpty(newRule.htmlDescription()) && Strings.isNullOrEmpty(newRule.markdownDescription())) { throw new IllegalArgumentException("The description is missing"); } } @@ -152,7 +153,8 @@ public class RuleCreator implements ServerComponent { .setTemplateId(templateRuleDto.getId()) .setConfigKey(templateRuleDto.getConfigKey()) .setName(newRule.name()) - .setDescription(newRule.htmlDescription()) + .setDescription(newRule.markdownDescription()) + .setDescriptionFormat(Format.MARKDOWN) .setSeverity(newRule.severity()) .setStatus(newRule.status()) .setLanguage(templateRuleDto.getLanguage()) @@ -186,7 +188,8 @@ public class RuleCreator implements ServerComponent { private RuleKey createManualRule(RuleKey ruleKey, NewRule newRule, DbSession dbSession){ RuleDto ruleDto = RuleDto.createFor(ruleKey) .setName(newRule.name()) - .setDescription(newRule.htmlDescription()) + .setDescription(newRule.markdownDescription()) + .setDescriptionFormat(Format.MARKDOWN) .setSeverity(newRule.severity()) .setStatus(RuleStatus.READY); dbClient.ruleDao().insert(dbSession, ruleDto); diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleDoc.java b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleDoc.java index 963bba036fb..05211a50811 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleDoc.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleDoc.java @@ -34,6 +34,7 @@ import org.sonar.server.search.IndexUtils; import javax.annotation.CheckForNull; import javax.annotation.Nullable; + import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -92,6 +93,11 @@ public class RuleDoc extends BaseDoc implements Rule { return getNullableField(RuleNormalizer.RuleField.HTML_DESCRIPTION.field()); } + @Override + public String markdownDescription() { + return getNullableField(RuleNormalizer.RuleField.MARKDOWN_DESCRIPTION.field()); + } + @Override @CheckForNull public String effortToFixDescription() { diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleNormalizer.java b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleNormalizer.java index e31c4c85b0b..fa01035ae08 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleNormalizer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleNormalizer.java @@ -32,23 +32,22 @@ import org.sonar.core.persistence.DbSession; import org.sonar.core.rule.RuleDto; import org.sonar.core.rule.RuleParamDto; import org.sonar.core.technicaldebt.db.CharacteristicDto; +import org.sonar.markdown.Markdown; import org.sonar.server.db.DbClient; import org.sonar.server.search.BaseNormalizer; import org.sonar.server.search.IndexDefinition; import org.sonar.server.search.IndexField; import org.sonar.server.search.Indexable; import org.sonar.server.search.es.ListUpdate; +import org.sonar.server.text.MacroInterpreter; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class RuleNormalizer extends BaseNormalizer { + private final MacroInterpreter macroInterpreter; + public static final class RuleParamField extends Indexable { public static final IndexField NAME = add(IndexField.Type.STRING, "name"); @@ -91,6 +90,7 @@ public class RuleNormalizer extends BaseNormalizer { public static final IndexField CREATED_AT = addSortable(IndexField.Type.DATE, "createdAt"); public static final IndexField UPDATED_AT = addSortable(IndexField.Type.DATE, UPDATED_AT_FIELD); public static final IndexField HTML_DESCRIPTION = addSearchable(IndexField.Type.TEXT, "htmlDesc"); + public static final IndexField MARKDOWN_DESCRIPTION = add(IndexField.Type.TEXT, "mdDesc"); public static final IndexField SEVERITY = add(IndexField.Type.STRING, "severity"); public static final IndexField STATUS = add(IndexField.Type.STRING, "status"); public static final IndexField FIX_DESCRIPTION = add(IndexField.Type.STRING, "effortToFix"); @@ -143,8 +143,9 @@ public class RuleNormalizer extends BaseNormalizer { } } - public RuleNormalizer(DbClient db) { + public RuleNormalizer(DbClient db, MacroInterpreter macroInterpreter) { super(IndexDefinition.RULE, db); + this.macroInterpreter = macroInterpreter; } @Override @@ -182,7 +183,15 @@ public class RuleNormalizer extends BaseNormalizer { update.put(RuleField.NAME.field(), rule.getName()); update.put(RuleField.CREATED_AT.field(), rule.getCreatedAt()); update.put(RuleField.UPDATED_AT.field(), rule.getUpdatedAt()); - update.put(RuleField.HTML_DESCRIPTION.field(), rule.getDescription()); + + if (RuleDto.Format.HTML.equals(rule.getDescriptionFormat())) { + update.put(RuleField.HTML_DESCRIPTION.field(), rule.getDescription()); + update.put(RuleField.MARKDOWN_DESCRIPTION.field(), null); + } else { + update.put(RuleField.HTML_DESCRIPTION.field(), rule.getDescription() == null ? null : Markdown.convertToHtml(rule.getDescription())); + update.put(RuleField.MARKDOWN_DESCRIPTION.field(), rule.getDescription()); + } + update.put(RuleField.FIX_DESCRIPTION.field(), rule.getEffortToFixDescription()); update.put(RuleField.SEVERITY.field(), rule.getSeverityString()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/ws/CreateAction.java b/server/sonar-server/src/main/java/org/sonar/server/rule/ws/CreateAction.java index fde8fd299c5..9ad736f922a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/ws/CreateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/ws/CreateAction.java @@ -130,7 +130,7 @@ public class CreateAction implements RequestHandler { if (!Strings.isNullOrEmpty(customKey)) { NewRule newRule = NewRule.createForCustomRule(customKey, RuleKey.parse(request.mandatoryParam(PARAM_TEMPLATE_KEY))) .setName(request.mandatoryParam(PARAM_NAME)) - .setHtmlDescription(request.mandatoryParam(PARAM_DESCRIPTION)) + .setMarkdownDescription(request.mandatoryParam(PARAM_DESCRIPTION)) .setSeverity(request.mandatoryParam(PARAM_SEVERITY)) .setStatus(RuleStatus.valueOf(request.mandatoryParam(PARAM_STATUS))) .setPreventReactivation(request.paramAsBoolean(PARAM_PREVENT_REACTIVATION)); @@ -144,7 +144,7 @@ public class CreateAction implements RequestHandler { if (!Strings.isNullOrEmpty(manualKey)) { NewRule newRule = NewRule.createForManualRule(manualKey) .setName(request.mandatoryParam(PARAM_NAME)) - .setHtmlDescription(request.mandatoryParam(PARAM_DESCRIPTION)) + .setMarkdownDescription(request.mandatoryParam(PARAM_DESCRIPTION)) .setSeverity(request.param(PARAM_SEVERITY)) .setPreventReactivation(request.paramAsBoolean(PARAM_PREVENT_REACTIVATION)); writeResponse(response, service.create(newRule)); diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapping.java b/server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapping.java index ecb1f2233a3..68d4891f938 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapping.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapping.java @@ -20,7 +20,6 @@ package org.sonar.server.rule.ws; import com.google.common.collect.Maps; -import org.apache.commons.lang.StringEscapeUtils; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; import org.sonar.api.server.debt.DebtCharacteristic; @@ -37,6 +36,7 @@ import org.sonar.server.text.MacroInterpreter; import javax.annotation.CheckForNull; import javax.annotation.Nullable; + import java.util.Collection; import java.util.Map; @@ -81,18 +81,14 @@ public class RuleMapping extends BaseMapping { map("htmlDesc", new Mapper() { @Override public void write(JsonWriter json, RuleDoc rule, RuleMappingContext context) { - String html = rule.htmlDescription(); - if (html != null) { - if (rule.isManual() || rule.templateKey() != null) { - String desc = StringEscapeUtils.escapeHtml(html); - desc = desc.replaceAll("\\n", "
"); - json.prop("htmlDesc", desc); - } else { - json.prop("htmlDesc", macroInterpreter.interpret(html)); - } + if (rule.markdownDescription() != null) { + json.prop("htmlDesc", macroInterpreter.interpret(Markdown.convertToHtml(rule.markdownDescription()))); + } else { + json.prop("htmlDesc", macroInterpreter.interpret(rule.htmlDescription())); } } }); + map("mdDesc", RuleNormalizer.RuleField.MARKDOWN_DESCRIPTION.field()); map("noteLogin", RuleNormalizer.RuleField.NOTE_LOGIN.field()); map("mdNote", RuleNormalizer.RuleField.NOTE.field()); map("htmlNote", new IndexMapper(RuleNormalizer.RuleField.NOTE.field()) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleCreatorMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleCreatorMediumTest.java index 9cc91b2c0b2..8751f33e0c2 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleCreatorMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleCreatorMediumTest.java @@ -32,6 +32,7 @@ import org.sonar.api.rule.Severity; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.core.persistence.DbSession; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import org.sonar.core.rule.RuleParamDto; import org.sonar.server.db.DbClient; import org.sonar.server.rule.db.RuleDao; @@ -73,7 +74,7 @@ public class RuleCreatorMediumTest { // Create custom rule NewRule newRule = NewRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey()) .setName("My custom") - .setHtmlDescription("Some description") + .setMarkdownDescription("Some description") .setSeverity(Severity.MAJOR) .setStatus(RuleStatus.READY) .setParameters(ImmutableMap.of("regex", "a.*")); @@ -124,6 +125,7 @@ public class RuleCreatorMediumTest { .setStatus(RuleStatus.REMOVED) .setName("Old name") .setDescription("Old description") + .setDescriptionFormat(Format.MARKDOWN) .setSeverity(Severity.INFO)); dao.addRuleParam(dbSession, rule, dao.findRuleParamsByRuleKey(dbSession, templateRule.getKey()).get(0).setDefaultValue("a.*")); dbSession.commit(); @@ -132,7 +134,7 @@ public class RuleCreatorMediumTest { // Create custom rule with same key, but with different values NewRule newRule = NewRule.createForCustomRule(key, templateRule.getKey()) .setName("New name") - .setHtmlDescription("New description") + .setMarkdownDescription("New description") .setSeverity(Severity.MAJOR) .setStatus(RuleStatus.READY) .setParameters(ImmutableMap.of("regex", "c.*")); @@ -146,7 +148,7 @@ public class RuleCreatorMediumTest { // These values should be the same than before assertThat(result.name()).isEqualTo("Old name"); - assertThat(result.htmlDescription()).isEqualTo("Old description"); + assertThat(result.markdownDescription()).isEqualTo("Old description"); assertThat(result.severity()).isEqualTo(Severity.INFO); assertThat(result.param("regex").defaultValue()).isEqualTo("a.*"); @@ -399,7 +401,7 @@ public class RuleCreatorMediumTest { public void create_manual_rule() throws Exception { NewRule newRule = NewRule.createForManualRule("MANUAL_RULE") .setName("My manual") - .setHtmlDescription("Some description"); + .setMarkdownDescription("Some description"); RuleKey ruleKey = creator.create(newRule); dbSession.clearCache(); @@ -408,7 +410,7 @@ public class RuleCreatorMediumTest { assertThat(rule).isNotNull(); assertThat(rule.key()).isEqualTo(RuleKey.of("manual", "MANUAL_RULE")); assertThat(rule.name()).isEqualTo("My manual"); - assertThat(rule.htmlDescription()).isEqualTo("Some description"); + assertThat(rule.markdownDescription()).isEqualTo("Some description"); assertThat(rule.severity()).isNull(); assertThat(rule.status()).isEqualTo(RuleStatus.READY); assertThat(rule.language()).isNull(); @@ -424,7 +426,7 @@ public class RuleCreatorMediumTest { public void create_manual_rule_with_severity() throws Exception { NewRule newRule = NewRule.createForManualRule("MANUAL_RULE") .setName("My manual") - .setHtmlDescription("Some description") + .setMarkdownDescription("Some description") .setSeverity(Severity.BLOCKER); RuleKey ruleKey = creator.create(newRule); @@ -434,7 +436,7 @@ public class RuleCreatorMediumTest { assertThat(rule).isNotNull(); assertThat(rule.key()).isEqualTo(RuleKey.of("manual", "MANUAL_RULE")); assertThat(rule.name()).isEqualTo("My manual"); - assertThat(rule.htmlDescription()).isEqualTo("Some description"); + assertThat(rule.markdownDescription()).isEqualTo("Some description"); assertThat(rule.severity()).isEqualTo(Severity.BLOCKER); assertThat(rule.status()).isEqualTo(RuleStatus.READY); assertThat(rule.language()).isNull(); @@ -462,7 +464,7 @@ public class RuleCreatorMediumTest { // Create a rule with the same key and with another name, description and severity NewRule newRule = NewRule.createForManualRule(key) .setName("New name") - .setHtmlDescription("New description"); + .setMarkdownDescription("New description"); RuleKey ruleKey = creator.create(newRule); dbSession.clearCache(); @@ -473,7 +475,7 @@ public class RuleCreatorMediumTest { // Name, description and severity should be the same than before assertThat(result.name()).isEqualTo("Old name"); - assertThat(result.htmlDescription()).isEqualTo("Old description"); + assertThat(result.markdownDescription()).isEqualTo("Old description"); assertThat(result.severity()).isEqualTo(Severity.INFO); // Check that the id is the same diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleServiceMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleServiceMediumTest.java index 9c6f692fae2..01e77776e4a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleServiceMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleServiceMediumTest.java @@ -120,7 +120,7 @@ public class RuleServiceMediumTest { Rule rule = service.getByKey(manualRule.getKey()); assertThat(rule).isNotNull(); - assertThat(rule.htmlDescription()).isEqualTo("
Manual rule desc
"); + assertThat(rule.htmlDescription()).isEqualTo("<div>Manual rule desc</div>"); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleTesting.java b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleTesting.java index 38693b7a455..d86c17e1ff8 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/RuleTesting.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/RuleTesting.java @@ -25,6 +25,7 @@ import org.sonar.api.rule.RuleStatus; import org.sonar.api.rule.Severity; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import java.util.Date; @@ -67,6 +68,7 @@ public class RuleTesting { .setRepositoryKey(ruleKey.repository()) .setName("Rule " + ruleKey.rule()) .setDescription("Description " + ruleKey.rule()) + .setDescriptionFormat(Format.HTML) .setStatus(RuleStatus.READY) .setConfigKey("InternalKey" + ruleKey.rule()) .setSeverity(Severity.INFO) diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/db/RuleDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/db/RuleDaoTest.java index 8412ee5b049..013f0a77dec 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/db/RuleDaoTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/db/RuleDaoTest.java @@ -32,6 +32,7 @@ import org.sonar.api.utils.System2; import org.sonar.core.persistence.AbstractDaoTestCase; import org.sonar.core.persistence.DbSession; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import org.sonar.core.rule.RuleParamDto; import java.util.List; @@ -69,7 +70,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { assertThat(ruleDto.getId()).isEqualTo(1); assertThat(ruleDto.getName()).isEqualTo("Avoid Null"); assertThat(ruleDto.getDescription()).isEqualTo("Should avoid NULL"); - assertThat(ruleDto.getDescriptionFormat()).isEqualTo("HTML"); + assertThat(ruleDto.getDescriptionFormat()).isEqualTo(Format.HTML); assertThat(ruleDto.getStatus()).isEqualTo(RuleStatus.READY); assertThat(ruleDto.getRepositoryKey()).isEqualTo("checkstyle"); assertThat(ruleDto.getNoteData()).isEqualTo("Rule note with accents \u00e9\u00e8\u00e0"); @@ -94,7 +95,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { assertThat(ruleDto.getId()).isEqualTo(1); assertThat(ruleDto.getName()).isEqualTo("Avoid Null"); assertThat(ruleDto.getDescription()).isEqualTo("Should avoid NULL"); - assertThat(ruleDto.getDescriptionFormat()).isEqualTo("HTML"); + assertThat(ruleDto.getDescriptionFormat()).isEqualTo(Format.HTML); assertThat(ruleDto.getStatus()).isEqualTo(RuleStatus.READY); assertThat(ruleDto.getRepositoryKey()).isEqualTo("checkstyle"); assertThat(ruleDto.getNoteData()).isEqualTo("Rule note with accents \u00e9\u00e8\u00e0"); @@ -117,7 +118,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { assertThat(ruleDto.getId()).isEqualTo(2); assertThat(ruleDto.getName()).isEqualTo("Avoid Null"); assertThat(ruleDto.getDescription()).isEqualTo("Should avoid NULL"); - assertThat(ruleDto.getDescriptionFormat()).isEqualTo("HTML"); + assertThat(ruleDto.getDescriptionFormat()).isEqualTo(Format.HTML); assertThat(ruleDto.getStatus()).isEqualTo(RuleStatus.READY); assertThat(ruleDto.getRepositoryKey()).isEqualTo("checkstyle"); } @@ -173,7 +174,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { .setRepositoryKey("plugin") .setName("new name") .setDescription("new description") - .setDescriptionFormat("MARKDOWN") + .setDescriptionFormat(Format.MARKDOWN) .setStatus(RuleStatus.DEPRECATED) .setConfigKey("NewConfigKey") .setSeverity(Severity.INFO) @@ -214,7 +215,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { .setRepositoryKey("plugin") .setName("new name") .setDescription("new description") - .setDescriptionFormat("MARKDOWN") + .setDescriptionFormat(Format.MARKDOWN) .setStatus(RuleStatus.DEPRECATED) .setConfigKey("NewConfigKey") .setSeverity(Severity.INFO) @@ -249,7 +250,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { .setRepositoryKey("plugin") .setName("new name") .setDescription("new description") - .setDescriptionFormat("HTML") + .setDescriptionFormat(Format.HTML) .setStatus(RuleStatus.DEPRECATED) .setConfigKey("NewConfigKey") .setSeverity(Severity.INFO) @@ -272,7 +273,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { .setRepositoryKey("plugin2") .setName("new name2") .setDescription("new description2") - .setDescriptionFormat("MARKDOWN") + .setDescriptionFormat(Format.MARKDOWN) .setStatus(RuleStatus.BETA) .setConfigKey("NewConfigKey2") .setSeverity(Severity.MAJOR) diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java index 7e2185547e1..c5f1c0312e7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java @@ -30,6 +30,7 @@ import org.sonar.api.rule.Severity; import org.sonar.core.permission.GlobalPermissions; import org.sonar.core.persistence.DbSession; import org.sonar.core.rule.RuleDto; +import org.sonar.core.rule.RuleDto.Format; import org.sonar.core.rule.RuleParamDto; import org.sonar.core.technicaldebt.db.CharacteristicDao; import org.sonar.core.technicaldebt.db.CharacteristicDto; @@ -79,6 +80,7 @@ public class ShowActionMediumTest { RuleTesting.newDto(RuleKey.of("java", "S001")) .setName("Rule S001") .setDescription("Rule S001 description") + .setDescriptionFormat(Format.HTML) .setSeverity(Severity.MINOR) .setStatus(RuleStatus.BETA) .setConfigKey("InternalKeyS001") @@ -221,6 +223,7 @@ public class ShowActionMediumTest { RuleTesting.newDto(RuleKey.of("java", "S001")) .setName("Rule S001") .setDescription("Rule S001 description") + .setDescriptionFormat(Format.HTML) .setSeverity(Severity.MINOR) .setStatus(RuleStatus.BETA) .setConfigKey("InternalKeyS001") @@ -288,7 +291,7 @@ public class ShowActionMediumTest { .setName("My custom") .setSeverity(Severity.MINOR) .setStatus(RuleStatus.READY) - .setHtmlDescription("
line1\nline2
"); + .setMarkdownDescription("
line1\nline2
"); RuleKey customRuleKey = ruleService.create(customRule); session.clearCache(); @@ -307,7 +310,7 @@ public class ShowActionMediumTest { NewRule manualRule = NewRule.createForManualRule("MY_MANUAL") .setName("My manual") .setSeverity(Severity.MINOR) - .setHtmlDescription("
line1\nline2
"); + .setMarkdownDescription("
line1\nline2
"); RuleKey customRuleKey = ruleService.create(manualRule); session.clearCache(); diff --git a/sonar-core/src/main/java/org/sonar/core/rule/RuleDto.java b/sonar-core/src/main/java/org/sonar/core/rule/RuleDto.java index 3f8ce61f936..c7a3fa9de4d 100644 --- a/sonar-core/src/main/java/org/sonar/core/rule/RuleDto.java +++ b/sonar-core/src/main/java/org/sonar/core/rule/RuleDto.java @@ -37,11 +37,15 @@ public final class RuleDto extends Dto { public static final Integer DISABLED_CHARACTERISTIC_ID = -1; + public enum Format { + HTML, MARKDOWN + } + private Integer id; private String repositoryKey; private String ruleKey; private String description; - private String descriptionFormat; + private Format descriptionFormat; private RuleStatus status; private String name; private String configKey; @@ -111,11 +115,11 @@ public final class RuleDto extends Dto { return this; } - public String getDescriptionFormat() { + public Format getDescriptionFormat() { return descriptionFormat; } - public RuleDto setDescriptionFormat(String descriptionFormat) { + public RuleDto setDescriptionFormat(Format descriptionFormat) { this.descriptionFormat = descriptionFormat; return this; } diff --git a/sonar-core/src/test/java/org/sonar/core/rule/RuleDaoTest.java b/sonar-core/src/test/java/org/sonar/core/rule/RuleDaoTest.java index 5151ad8a633..0c5d788048e 100644 --- a/sonar-core/src/test/java/org/sonar/core/rule/RuleDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/rule/RuleDaoTest.java @@ -47,7 +47,7 @@ public class RuleDaoTest extends AbstractDaoTestCase { assertThat(ruleDto.getId()).isEqualTo(1); assertThat(ruleDto.getName()).isEqualTo("Avoid Null"); assertThat(ruleDto.getDescription()).isEqualTo("Should avoid NULL"); - assertThat(ruleDto.getDescriptionFormat()).isEqualTo("HTML"); + assertThat(ruleDto.getDescriptionFormat()).isEqualTo(RuleDto.Format.HTML); assertThat(ruleDto.getStatus()).isEqualTo(RuleStatus.READY); assertThat(ruleDto.getRepositoryKey()).isEqualTo("checkstyle"); assertThat(ruleDto.getNoteData()).isEqualTo("Rule note with accents \u00e9\u00e8\u00e0"); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java index 885c2d5265f..2076fd07b52 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java @@ -20,13 +20,7 @@ package org.sonar.api.server.rule; import com.google.common.base.Strings; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; +import com.google.common.collect.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; @@ -519,7 +513,7 @@ public interface RulesDefinition extends ServerExtension { class NewRule { private final String repoKey, key; - private String name, htmlDescription, internalKey, severity = Severity.MAJOR; + private String name, htmlDescription, markdownDescription, internalKey, severity = Severity.MAJOR; private boolean template; private RuleStatus status = RuleStatus.defaultStatus(); private String debtSubCharacteristic; @@ -561,6 +555,9 @@ public interface RulesDefinition extends ServerExtension { } public NewRule setHtmlDescription(@Nullable String s) { + if (markdownDescription != null) { + throw new IllegalStateException(String.format("Rule '%s' already has a Markdown description", this)); + } this.htmlDescription = StringUtils.trimToNull(s); return this; } @@ -581,6 +578,30 @@ public interface RulesDefinition extends ServerExtension { return this; } + public NewRule setMarkdownDescription(@Nullable String s) { + if (htmlDescription != null) { + throw new IllegalStateException(String.format("Rule '%s' already has an HTML description", this)); + } + this.markdownDescription = StringUtils.trimToNull(s); + return this; + } + + /** + * Load description from a file available in classpath. Example : setMarkdownDescription(getClass().getResource("/myrepo/Rule1234.md") + */ + public NewRule setMarkdownDescription(@Nullable URL classpathUrl) { + if (classpathUrl != null) { + try { + setMarkdownDescription(IOUtils.toString(classpathUrl)); + } catch (IOException e) { + throw new IllegalStateException("Fail to read: " + classpathUrl, e); + } + } else { + this.markdownDescription = null; + } + return this; + } + /** * Default value is {@link org.sonar.api.rule.RuleStatus#READY}. The value * {@link org.sonar.api.rule.RuleStatus#REMOVED} is not accepted and raises an @@ -702,7 +723,7 @@ public interface RulesDefinition extends ServerExtension { @Immutable class Rule { private final Repository repository; - private final String repoKey, key, name, htmlDescription, internalKey, severity; + private final String repoKey, key, name, htmlDescription, markdownDescription, internalKey, severity; private final boolean template; private final String debtSubCharacteristic; private final DebtRemediationFunction debtRemediationFunction; @@ -717,6 +738,7 @@ public interface RulesDefinition extends ServerExtension { this.key = newRule.key; this.name = newRule.name; this.htmlDescription = newRule.htmlDescription; + this.markdownDescription = newRule.markdownDescription; this.internalKey = newRule.internalKey; this.severity = newRule.severity; this.template = newRule.template; @@ -753,6 +775,11 @@ public interface RulesDefinition extends ServerExtension { return htmlDescription; } + @CheckForNull + public String markdownDescription() { + return markdownDescription; + } + public boolean template() { return template; } -- 2.39.5