Add common interface for formatters

Having a common interface for formatters makes it easier to add
additional formatters. Now to add a new formatter one only needs to
implement the Formatter interface and register the implementation in a
DynamicMap.

Change-Id: I4cbbf6cff89b18a277f8d828486ae9cc023c2dca
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/ConfigSection.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/ConfigSection.java
new file mode 100644
index 0000000..7da04a0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/ConfigSection.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs;
+
+import com.google.common.base.MoreObjects;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Set;
+
+public class ConfigSection {
+  private final Config cfg;
+  private final String section;
+  private final String subsection;
+
+  ConfigSection(Config cfg, String section) {
+    this(cfg, section, null);
+  }
+
+  ConfigSection(Config cfg, String section, String subsection) {
+    this.cfg = cfg;
+    this.section = section;
+    this.subsection = subsection;
+  }
+
+  public String getString(String name) {
+    return cfg.getString(section, subsection, name);
+  }
+
+  public String getString(String name, String defaultValue) {
+    if (defaultValue == null) {
+      return cfg.getString(section, subsection, name);
+    } else {
+      return MoreObjects.firstNonNull(cfg.getString(section, subsection, name),
+          defaultValue);
+    }
+  }
+
+  public String[] getStringList(String name) {
+    return cfg.getStringList(section, subsection, name);
+  }
+
+  public int getInt(String name, int defaultValue) {
+    return cfg.getInt(section, subsection, name, defaultValue);
+  }
+
+  public long getLong(String name, long defaultValue) {
+    return cfg.getLong(section, subsection, name, defaultValue);
+  }
+
+  public boolean getBoolean(String name, boolean defaultValue) {
+    return cfg.getBoolean(section, subsection, name, defaultValue);
+  }
+
+  public <T extends Enum<?>> T getEnum(String name, T defaultValue) {
+    return cfg.getEnum(section, subsection, name, defaultValue);
+  }
+
+  public <T extends Enum<?>> T getEnum(T[] all, String name, T defaultValue) {
+    return cfg.getEnum(all, section, subsection, name, defaultValue);
+  }
+
+  public Set<String> getNames() {
+    return cfg.getNames(section, subsection);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
index c313f4d..1f3dac5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
@@ -15,7 +15,9 @@
 package com.googlesource.gerrit.plugins.xdocs;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
@@ -25,6 +27,10 @@
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.inject.Inject;
 
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
+
 import java.util.List;
 
 public class Module extends FactoryModule {
@@ -40,6 +46,14 @@
     install(new XDocLoader.Module());
     factory(XDocProjectConfig.Factory.class);
 
+    DynamicMap.mapOf(binder(), Formatter.class);
+    bind(Formatter.class)
+        .annotatedWith(Exports.named(MarkdownFormatter.NAME))
+        .to(MarkdownFormatter.class);
+    bind(Formatter.class)
+        .annotatedWith(Exports.named(PlainTextFormatter.NAME))
+        .to(PlainTextFormatter.class);
+
     DynamicSet.bind(binder(), ProjectWebLink.class)
         .to(XDocWebLink.class);
     DynamicSet.bind(binder(), BranchWebLink.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocFileWebLink.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocFileWebLink.java
index cd5d676..934e9b1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocFileWebLink.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocFileWebLink.java
@@ -18,13 +18,13 @@
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.httpd.resources.Resource;
-import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
 
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
+
 public class XDocFileWebLink extends XDocWebLink implements FileWebLink {
 
   @Inject
@@ -34,10 +34,8 @@
       @Named(XDocLoader.Module.X_DOC_RESOURCES) LoadingCache<String, Resource> cache,
       XDocProjectConfig.Factory cfgFactory,
       ProjectCache projectCache,
-      FileTypeRegistry fileTypeRegistry,
-      PluginConfigFactory pluginCfgFactory) {
-    super(pluginName, repoManager, cache, cfgFactory, projectCache,
-        fileTypeRegistry, pluginCfgFactory);
+      Formatters formatters) {
+    super(pluginName, repoManager, cache, cfgFactory, projectCache, formatters);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
index 6fbce26..b7fadf4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
@@ -14,48 +14,31 @@
 
 package com.googlesource.gerrit.plugins.xdocs;
 
-import eu.medsea.mimeutil.MimeType;
+import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
 
 import org.eclipse.jgit.lib.Config;
 
-import java.util.HashMap;
-import java.util.Map;
-
 public class XDocGlobalConfig {
   private static final String SECTION_FORMATTER = "formatter";
-  private static final String KEY_ALLOW_HTML = "allowHtml";
-  private static final String KEY_MIME_TYPE = "mimeType";
 
-  enum Formatter {
-    MARKDOWN, PLAIN_TEXT;
-  }
+  public static final String KEY_ALLOW_HTML = "allowHtml";
+  public static final String KEY_MIME_TYPE = "mimeType";
 
   private final Config cfg;
 
-  XDocGlobalConfig(Config cfg) {
+  public XDocGlobalConfig(Config cfg) {
     this.cfg = cfg;
   }
 
-  boolean isHtmlAllowed(Formatter formatter) {
-    return cfg.getBoolean(SECTION_FORMATTER, formatter.name(),
-        KEY_ALLOW_HTML, false);
-  }
-
-  Map<MimeType, Formatter> getMimeTypes() {
-    Map<MimeType, Formatter> mimeTypes = new HashMap<>();
-    for (Formatter f : Formatter.values()) {
-      for (String mimeType :
-          cfg.getStringList(SECTION_FORMATTER, f.name(), KEY_MIME_TYPE)) {
-        mimeTypes.put(new MimeType(mimeType), f);
-      }
-    }
-    return mimeTypes;
+  public ConfigSection getFormatterConfig(String formatterName) {
+    return new ConfigSection(cfg, SECTION_FORMATTER, formatterName);
   }
 
   static void initialize(Config cfg) {
-    cfg.setString(SECTION_FORMATTER, Formatter.MARKDOWN.name(), KEY_MIME_TYPE,
+    cfg.setString(SECTION_FORMATTER, MarkdownFormatter.NAME, KEY_MIME_TYPE,
         "text/x-markdown");
-    cfg.setString(SECTION_FORMATTER, Formatter.PLAIN_TEXT.name(), KEY_MIME_TYPE,
+    cfg.setString(SECTION_FORMATTER, PlainTextFormatter.NAME, KEY_MIME_TYPE,
         "text/plain");
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
index ebfb54d..9fd8ac1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.xdocs;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
 
 import com.google.common.base.Strings;
 import com.google.common.cache.CacheLoader;
@@ -28,13 +27,13 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.documentation.MarkdownFormatter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.Formatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
@@ -45,7 +44,6 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 
-import java.io.IOException;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -58,22 +56,31 @@
   private final Provider<String> webUrl;
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
+  private final Formatters formatters;
 
   @Inject
   XDocLoader(GitRepositoryManager repoManager,
       @CanonicalWebUrl Provider<String> webUrl,
       @PluginName String pluginName,
-      PluginConfigFactory cfgFactory) {
+      PluginConfigFactory cfgFactory,
+      Formatters formatters) {
     this.repoManager = repoManager;
     this.webUrl = webUrl;
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
+    this.formatters = formatters;
   }
 
   @Override
   public Resource load(String strKey) throws Exception {
     XDocResourceKey key = XDocResourceKey.fromString(strKey);
-    XDocGlobalConfig cfg = new XDocGlobalConfig(cfgFactory.getGlobalPluginConfig(pluginName));
+    XDocGlobalConfig cfg =
+        new XDocGlobalConfig(cfgFactory.getGlobalPluginConfig(pluginName));
+    FormatterProvider formatter = formatters.getByName(key.getFormatter());
+    if (formatter == null) {
+      return Resource.NOT_FOUND;
+    }
+    ConfigSection formatterCfg = cfg.getFormatterConfig(formatter.getName());
     Repository repo = repoManager.openRepository(key.getProject());
     try {
       RevWalk rw = new RevWalk(repo);
@@ -91,8 +98,9 @@
           ObjectId objectId = tw.getObjectId(0);
           ObjectLoader loader = repo.open(objectId);
           byte[] raw = loader.getBytes(Integer.MAX_VALUE);
-          byte[] html = formatAsHtml(cfg, key.getFormatter(),
-              replaceMacros(key.getProject(), new String(raw, UTF_8)));
+          String html =
+              formatter.get().format(formatterCfg,
+                  replaceMacros(key.getProject(), raw));
           return getAsHtmlResource(html, commit.getCommitTime());
         } finally {
           tw.release();
@@ -105,7 +113,7 @@
     }
   }
 
-  private String replaceMacros(Project.NameKey project, String raw) {
+  private String replaceMacros(Project.NameKey project, byte[] raw) {
     Map<String, String> macros = Maps.newHashMap();
 
     String url = webUrl.get();
@@ -117,7 +125,8 @@
     macros.put("PROJECT", project.get());
     macros.put("PROJECT_URL", url + "#/admin/projects/" + project.get());
 
-    Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(raw);
+    Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@")
+        .matcher(new String(raw, UTF_8));
     StringBuffer sb = new StringBuffer();
     while (m.find()) {
       String key = m.group(2);
@@ -132,35 +141,8 @@
     return sb.toString();
   }
 
-  private byte[] formatAsHtml(XDocGlobalConfig cfg, Formatter formatter,
-      String raw) throws IOException {
-    switch (formatter) {
-      case MARKDOWN:
-        return formatMarkdownAsHtml(cfg, raw);
-      case PLAIN_TEXT:
-        return formatTxtAsHtml(raw);
-      default:
-        throw new IllegalStateException("Unsupported formatter: "
-            + formatter.name());
-    }
-  }
-
-  private byte[] formatMarkdownAsHtml(XDocGlobalConfig cfg, String md)
-      throws IOException {
-    MarkdownFormatter f = new MarkdownFormatter();
-    if (!cfg.isHtmlAllowed(Formatter.MARKDOWN)) {
-      f.suppressHtml();
-    }
-    return f.markdownToDocHtml(md, UTF_8.name());
-  }
-
-  private byte[] formatTxtAsHtml(String txt) {
-    String html = "<pre>" + escapeHtml(txt) + "</pre>";
-    return html.getBytes(UTF_8);
-  }
-
-  private Resource getAsHtmlResource(byte[] html, int lastModified) {
-    return new SmallResource(html)
+  private Resource getAsHtmlResource(String html, int lastModified) {
+    return new SmallResource(html.getBytes(UTF_8))
         .setContentType("text/html")
         .setCharacterEncoding(UTF_8.name())
         .setLastModified(lastModified);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocResourceKey.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocResourceKey.java
index 09bc287..2407490 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocResourceKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocResourceKey.java
@@ -18,17 +18,15 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.reviewdb.client.Project;
 
-import com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.Formatter;
-
 import org.eclipse.jgit.lib.ObjectId;
 
 public class XDocResourceKey {
-  private final Formatter formatter;
+  private final String formatter;
   private final Project.NameKey project;
   private final String resource;
   private final ObjectId revId;
 
-  XDocResourceKey(Formatter formatter, Project.NameKey project, String r,
+  XDocResourceKey(String formatter, Project.NameKey project, String r,
       ObjectId revId) {
     this.formatter = formatter;
     this.project = project;
@@ -36,7 +34,7 @@
     this.revId = revId;
   }
 
-  public Formatter getFormatter() {
+  public String getFormatter() {
     return formatter;
   }
 
@@ -69,7 +67,7 @@
 
   public String asString() {
     StringBuilder b = new StringBuilder();
-    b.append(formatter.name());
+    b.append(IdString.fromDecoded(formatter).encoded());
     b.append("/");
     b.append(IdString.fromDecoded(project.get()).encoded());
     b.append("/");
@@ -81,12 +79,12 @@
 
   public static XDocResourceKey fromString(String str) {
     String[] s = str.split("/");
-    Formatter formatter = null;
+    String formatter = null;
     String project = null;
     String file = null;
     String revision = null;
     if (s.length > 0) {
-      formatter = Formatter.valueOf(s[0]);
+      formatter = IdString.fromUrl(s[0]).get();
     }
     if (s.length > 1) {
       project = IdString.fromUrl(s[1]).get();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
index 2befa05..786a646 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
@@ -22,7 +22,6 @@
 import com.google.common.cache.LoadingCache;
 import com.google.common.hash.Hashing;
 import com.google.common.net.HttpHeaders;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -31,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.GetHead;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -45,7 +43,8 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
-import com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.Formatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;
 
 import eu.medsea.mimeutil.MimeType;
 
@@ -62,7 +61,6 @@
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 
 import java.io.IOException;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.http.HttpServlet;
@@ -83,8 +81,7 @@
   private final LoadingCache<String, Resource> docCache;
   private final FileTypeRegistry fileTypeRegistry;
   private final XDocProjectConfig.Factory cfgFactory;
-  private final String pluginName;
-  private final PluginConfigFactory pluginCfgFactory;
+  private final Formatters formatters;
 
   @Inject
   XDocServlet(
@@ -96,8 +93,7 @@
       @Named(XDocLoader.Module.X_DOC_RESOURCES) LoadingCache<String, Resource> cache,
       FileTypeRegistry fileTypeRegistry,
       XDocProjectConfig.Factory cfgFactory,
-      @PluginName String pluginName,
-      PluginConfigFactory pluginCfgFactory) {
+      Formatters formatters) {
     this.db = db;
     this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
@@ -106,8 +102,7 @@
     this.docCache = cache;
     this.fileTypeRegistry = fileTypeRegistry;
     this.cfgFactory = cfgFactory;
-    this.pluginName = pluginName;
-    this.pluginCfgFactory = pluginCfgFactory;
+    this.formatters = formatters;
   }
 
   @Override
@@ -130,11 +125,9 @@
       res.sendRedirect(getRedirectUrl(req, key, cfg));
       return;
     }
-    XDocGlobalConfig pluginCfg =
-        new XDocGlobalConfig(pluginCfgFactory.getGlobalPluginConfig(pluginName));
     MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
-    Map<MimeType, Formatter> mimeTypes = pluginCfg.getMimeTypes();
-    if (!mimeTypes.keySet().contains(mimeType)
+    FormatterProvider formatter = formatters.get(key.file);
+    if (formatter == null
         && !("image".equals(mimeType.getMediaType())
             && fileTypeRegistry.isSafeInline(mimeType))) {
       Resource.NOT_FOUND.send(req, res);
@@ -191,10 +184,9 @@
         }
 
         Resource rsc;
-        Formatter formatter = mimeTypes.get(mimeType);
         if (formatter != null) {
           rsc = docCache.getUnchecked(
-              (new XDocResourceKey(formatter, key.project, key.file, revId)).asString());
+              (new XDocResourceKey(formatter.getName(), key.project, key.file, revId)).asString());
         } else if ("image".equals(mimeType.getMediaType())) {
           rsc = getImageResource(repo, revId, key.file);
         } else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocWebLink.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocWebLink.java
index 2c25095..e0b7a60 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocWebLink.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocWebLink.java
@@ -21,8 +21,6 @@
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -30,9 +28,8 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
-import com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.Formatter;
-
-import eu.medsea.mimeutil.MimeType;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -52,8 +49,7 @@
   private final LoadingCache<String, Resource> docCache;
   private final XDocProjectConfig.Factory cfgFactory;
   private final ProjectCache projectCache;
-  private final FileTypeRegistry fileTypeRegistry;
-  private final PluginConfigFactory pluginCfgFactory;
+  private final Formatters formatters;
 
   @Inject
   XDocWebLink(
@@ -62,15 +58,13 @@
       @Named(XDocLoader.Module.X_DOC_RESOURCES) LoadingCache<String, Resource> cache,
       XDocProjectConfig.Factory cfgFactory,
       ProjectCache projectCache,
-      FileTypeRegistry fileTypeRegistry,
-      PluginConfigFactory pluginCfgFactory) {
+      Formatters formatters) {
     this.pluginName = pluginName;
     this.repoManager = repoManager;
     this.docCache = cache;
     this.cfgFactory = cfgFactory;
     this.projectCache = projectCache;
-    this.fileTypeRegistry = fileTypeRegistry;
-    this.pluginCfgFactory = pluginCfgFactory;
+    this.formatters = formatters;
   }
 
   @Override
@@ -96,10 +90,7 @@
 
   public String getPatchUrl(String projectName, String revision,
       String fileName) {
-    XDocGlobalConfig pluginCfg =
-        new XDocGlobalConfig(pluginCfgFactory.getGlobalPluginConfig(pluginName));
-    MimeType mimeType = fileTypeRegistry.getMimeType(fileName, null);
-    Formatter formatter = pluginCfg.getMimeTypes().get(mimeType);
+    FormatterProvider formatter = formatters.get(fileName);
     if (formatter == null) {
       return null;
     }
@@ -113,7 +104,7 @@
           return null;
         }
         Resource rsc = docCache.getUnchecked(
-           (new XDocResourceKey(formatter, p, fileName, revId)).asString());
+           (new XDocResourceKey(formatter.getName(), p, fileName, revId)).asString());
         if (rsc != Resource.NOT_FOUND) {
           StringBuilder url = new StringBuilder();
           url.append("plugins/");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
new file mode 100644
index 0000000..e934c0d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+import java.io.IOException;
+
+@ExtensionPoint
+public interface Formatter {
+
+  /**
+   * Formats the given raw text as html.
+   *
+   * @param cfg the configuration for this formatter
+   * @param raw the raw text
+   * @return the given text formatted as html
+   * @throws IOException thrown if the formatting fails
+   */
+  public String format(ConfigSection cfg, String raw) throws IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java
new file mode 100644
index 0000000..b338027
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_MIME_TYPE;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.FileTypeRegistry;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig;
+
+import eu.medsea.mimeutil.MimeType;
+
+import java.util.Map.Entry;
+
+@Singleton
+public class Formatters {
+  private final String pluginName;
+  private final PluginConfigFactory pluginCfgFactory;
+  private final FileTypeRegistry fileTypeRegistry;
+  private final DynamicMap<Formatter> formatters;
+
+  @Inject
+  Formatters(
+      @PluginName String pluginName,
+      PluginConfigFactory pluginCfgFactory,
+      FileTypeRegistry fileTypeRegistry,
+      DynamicMap<Formatter> formatters) {
+    this.pluginName = pluginName;
+    this.pluginCfgFactory = pluginCfgFactory;
+    this.fileTypeRegistry = fileTypeRegistry;
+    this.formatters = formatters;
+  }
+
+  public FormatterProvider get(String fileName) {
+    XDocGlobalConfig pluginCfg =
+        new XDocGlobalConfig(pluginCfgFactory.getGlobalPluginConfig(pluginName));
+    MimeType mimeType = fileTypeRegistry.getMimeType(fileName, null);
+    for (String pluginName : formatters.plugins()) {
+      for (Entry<String, Provider<Formatter>> e :
+          formatters.byPlugin(pluginName).entrySet()) {
+        for (String configuredMimeType :
+          pluginCfg.getFormatterConfig(e.getKey()).getStringList(KEY_MIME_TYPE)) {
+          if (mimeType.equals(new MimeType(configuredMimeType))) {
+            return new FormatterProvider(e.getKey(), e.getValue());
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  public FormatterProvider getByName(String formatterName) {
+    for (String pluginName : formatters.plugins()) {
+      for (Entry<String, Provider<Formatter>> e :
+          formatters.byPlugin(pluginName).entrySet()) {
+        if (formatterName.equals(e.getKey())) {
+          return new FormatterProvider(formatterName, e.getValue());
+        }
+      }
+    }
+    return null;
+  }
+
+  public static class FormatterProvider {
+    private final String name;
+    private final Provider<Formatter> formatter;
+
+    FormatterProvider(String name,
+        Provider<Formatter> formatter) {
+      this.name = name;
+      this.formatter = formatter;
+    }
+
+    public Formatter get() {
+      return formatter.get();
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
new file mode 100644
index 0000000..6a3a896
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_ALLOW_HTML;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+import java.io.UnsupportedEncodingException;
+
+public class MarkdownFormatter implements Formatter {
+  public final static String NAME = "MARKDOWN";
+
+  @Override
+  public String format(ConfigSection cfg, String raw)
+      throws UnsupportedEncodingException {
+    com.google.gerrit.server.documentation.MarkdownFormatter f =
+        new com.google.gerrit.server.documentation.MarkdownFormatter();
+    if (!cfg.getBoolean(KEY_ALLOW_HTML, false)) {
+      f.suppressHtml();
+    }
+    byte[] b = f.markdownToDocHtml(raw, UTF_8.name());
+    return new String(b, UTF_8);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
new file mode 100644
index 0000000..d6ad0cd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+public class PlainTextFormatter implements Formatter {
+  public final static String NAME = "PLAIN_TEXT";
+
+  @Override
+  public String format(ConfigSection cfg, String raw) {
+    return "<pre>" + escapeHtml(raw) + "</pre>";
+  }
+}