]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5001 Update API, ES and WS to support Markdown in rule descriptions
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 16 Jul 2014 08:52:00 +0000 (10:52 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 16 Jul 2014 13:47:03 +0000 (15:47 +0200)
17 files changed:
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/rule/NewRule.java
server/sonar-server/src/main/java/org/sonar/server/rule/RegisterRules.java
server/sonar-server/src/main/java/org/sonar/server/rule/Rule.java
server/sonar-server/src/main/java/org/sonar/server/rule/RuleCreator.java
server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleDoc.java
server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleNormalizer.java
server/sonar-server/src/main/java/org/sonar/server/rule/ws/CreateAction.java
server/sonar-server/src/main/java/org/sonar/server/rule/ws/RuleMapping.java
server/sonar-server/src/test/java/org/sonar/server/rule/RuleCreatorMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/RuleServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/RuleTesting.java
server/sonar-server/src/test/java/org/sonar/server/rule/db/RuleDaoTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/ws/ShowActionMediumTest.java
sonar-core/src/main/java/org/sonar/core/rule/RuleDto.java
sonar-core/src/test/java/org/sonar/core/rule/RuleDaoTest.java
sonar-plugin-api/src/main/java/org/sonar/api/server/rule/RulesDefinition.java

index d711892df8dc54bcd23274b1ca3f684196e304c8..d03e457c44934cd940b10b2edc8306786fdeecf8 100644 (file)
@@ -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
index f6c4cabaee5221e9278070468724cd2a0bdcc29f..6af6fa8f369667e35ef08392a1de10d6a653c1b3 100644 (file)
@@ -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<String, String> 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;
index 2fac09447af125234532ca5bd70ebfbcda8d70fa..5e09f77dd0624df52a941b4b23836b1e9ac40af3 100644 (file)
@@ -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;
index c27d9c967367df5a34d537c94e09077d9058ddcf..076e6c6dd5703d87e7887998361aee6f8fc2058a 100644 (file)
@@ -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();
 
   /**
index 30dd0f2c058817ab0462ea6e4a64c9511a9440ae..0a60c46eca996d661d6c97c649603e5bf26318a3 100644 (file)
@@ -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);
index 963bba036fb7a0514a524bb76ace1cf0ee4512c1..05211a508112f3114dc14cd3fb25b4bb2ba23e81 100644 (file)
@@ -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() {
index e31c4c85b0b1e1e2404bb847db045c76a5b7e18c..fa01035ae0899ebd56b74c824657a89bb0a6a906 100644 (file)
@@ -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<RuleDto, RuleKey> {
 
+  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<RuleDto, RuleKey> {
     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<RuleDto, RuleKey> {
     }
   }
 
-  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<RuleDto, RuleKey> {
       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());
 
index fde8fd299c5b4fe321e01e9cabdc60e4ec436e9a..9ad736f922a710d4f4c75063ca31c14dd913090d 100644 (file)
@@ -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));
index ecb1f2233a3bc742b3b98f005796c6a33b23ad55..68d4891f9384c5ad6d7db04c73e608789ed22a2d 100644 (file)
@@ -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<RuleDoc, RuleMappingContext> {
     map("htmlDesc", new Mapper<RuleDoc, RuleMappingContext>() {
       @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", "<br/>");
-            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<RuleDoc, RuleMappingContext>(RuleNormalizer.RuleField.NOTE.field()) {
index 9cc91b2c0b214a7c036876d59f0db1df3022ef10..8751f33e0c2df57631e0b67f2d49acd9e1da0eb3 100644 (file)
@@ -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
index 9c6f692fae2ba9d60603d60e3c8462a32698066c..01e77776e4a4b0b0b8f16d98f9d9d5ced2cf3a9f 100644 (file)
@@ -120,7 +120,7 @@ public class RuleServiceMediumTest {
 
     Rule rule = service.getByKey(manualRule.getKey());
     assertThat(rule).isNotNull();
-    assertThat(rule.htmlDescription()).isEqualTo("<div>Manual rule desc</div>");
+    assertThat(rule.htmlDescription()).isEqualTo("&lt;div&gt;Manual rule desc&lt;/div&gt;");
   }
 
   @Test
index 38693b7a455fd262b3492f095b93e58ce7897d27..d86c17e1ff80b772b959e0573c7ac2d6332e96d2 100644 (file)
@@ -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)
index 8412ee5b0494485ada063dbe8a6a0cea7e104d29..013f0a77decf91b93ff09ab5dd01367a3ce066cb 100644 (file)
@@ -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)
index 7e2185547e19b0924891ca05396c0e0cc4e8b609..c5f1c0312e78bc234a61ed10b2767109f5855ebf 100644 (file)
@@ -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 <b>description</b>")
+        .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 <b>description</b>")
+        .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("<div>line1\nline2</div>");
+      .setMarkdownDescription("<div>line1\nline2</div>");
     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("<div>line1\nline2</div>");
+      .setMarkdownDescription("<div>line1\nline2</div>");
     RuleKey customRuleKey = ruleService.create(manualRule);
     session.clearCache();
 
index 3f8ce61f936724fecc94aa2aa7e7cca6ef51279a..c7a3fa9de4df2fb94f99425a450b483bcee5495d 100644 (file)
@@ -37,11 +37,15 @@ public final class RuleDto extends Dto<RuleKey> {
 
   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<RuleKey> {
     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;
   }
index 5151ad8a633845eb10bdce070ee984508dd1e4db..0c5d788048e1c19aa92e26d8cbecf7b527e57ae9 100644 (file)
@@ -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");
index 885c2d5265fd224748c7a89ff54461f8524aed7a..2076fd07b523266805f029ecf56cbe135a3397ec 100644 (file)
 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 : <code>setMarkdownDescription(getClass().getResource("/myrepo/Rule1234.md")</code>
+     */
+    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;
     }