Merge remote-tracking branch 'origin/github/rinrinne/dev-3.0' into stable-2.10

* origin/github/rinrinne/dev-3.0:
  Remove unused import and annotation
  Put exception message to logger
  Rename ChangeWorker to EventWorker
  Apply multibinding to Solver
  Rename RabbitMQManager to Manager
  Apply Provider pattern to SessionFactory
  Fix shutdown channel/connection logic
  Fix close logic for session
  Fix shutdown session
  Apply Guice Multibinder to Section injection
  Add Guice multibinding extension library
  Update gradle-shadow plugin to 1.2.1 then fix build.gradle
  Update git ignore pattern
  Some improvements
  Change Solver structure
  Decoupled normalize() from fromConfig()
  Structured configuration classes
  Cleanup codes
  Create some packages then move classes
  Roughly support multi config
  Add MessagePublishers to listen ChangeEvent
  Add dependency: commons-io
  Get plugin name then assume config filename
  Remove unnecessary codes

Change-Id: If41294c035f5bc3cdd658c686134acc30f36e184
diff --git a/.gitignore b/.gitignore
index 5c43274..931312c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,11 @@
-/work
-/build
-/bin
-/target
-/.gradle
-/.classpath
-/.checkstyle
-/.project
-/.settings
-/.idea
-/gerrit
+work/
+build/
+bin/
+target/
+.gradle/
+.classpath
+.checkstyle
+.project
+.settings/
+.idea/
 *.iml
diff --git a/build.gradle b/build.gradle
index 6792639..f8b83bf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,14 +7,14 @@
     jcenter()
   }
   dependencies {
-    classpath 'com.github.jengelman.gradle.plugins:shadow:0.8'
+    classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.1'
   }
 }
 
 apply plugin: 'java'
 apply plugin: 'eclipse'
 apply plugin: 'idea'
-apply plugin: 'shadow'
+apply plugin: 'com.github.johnrengelman.shadow'
 
 sourceCompatibility = 1.7
 targetCompatibility = 1.7
@@ -35,15 +35,18 @@
       'Implementation-Version' : version,
       'Created-By': 'rinrinne (rinrin.ne@gmail.com)'
     )
-  } 
+  }
 }
 
-shadow {
+shadowJar {
   classifier = apiVersion
-  exclude 'META-INF/*.DSA'
-  exclude 'META-INF/*.RSA'
-  artifactSet {
-    include 'com.rabbitmq'
+  mergeServiceFiles {
+    exclude 'META-INF/*.DSA'
+    exclude 'META-INF/*.RSA'
+  }
+  dependencies {
+    include(dependency('com.rabbitmq:amqp-client'))
+    include(dependency('com.google.inject.extensions:guice-multibindings'))
   }
 }
 
@@ -63,9 +66,11 @@
 dependencies {
   compile(
     [group: 'com.google.gerrit', name: "gerrit-${apiType}-api", version: apiVersion],
+    [group: 'com.google.inject.extensions', name: 'guice-multibindings', version: '4.0-beta5'],
     [group: 'com.google.code.gson', name: 'gson', version: '2.1'],
     [group: 'commons-lang', name: 'commons-lang', version: '2.5'],
     [group: 'commons-codec', name: 'commons-codec', version: '1.4'],
+    [group: 'commons-io', name: 'commons-io', version: '1.4'],
     [group: 'com.rabbitmq', name: 'amqp-client', version: '3.4.4'],
   )
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/AMQPSession.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/AMQPSession.java
deleted file mode 100644
index 4cf2968..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/AMQPSession.java
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright (C) 2013 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.rabbitmq;
-
-import com.google.inject.Inject;
-
-// import com.rabbitmq.client.AMQP.BasicProperties;
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.Connection;
-import com.rabbitmq.client.ConnectionFactory;
-// import com.rabbitmq.client.DefaultConsumer;
-// import com.rabbitmq.client.Envelope;
-import com.rabbitmq.client.ShutdownListener;
-import com.rabbitmq.client.ShutdownSignalException;
-
-import org.apache.commons.codec.CharEncoding;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.URISyntaxException;
-
-public class AMQPSession implements ShutdownListener {
-
-  private static final Logger LOGGER = LoggerFactory.getLogger(AMQPSession.class);
-  private final Properties properties;
-  private volatile Connection connection;
-  private volatile Channel publishChannel;
-  private volatile Channel consumeChannel;
-  private volatile int failureCount = 0;
-
-  @Inject
-  public AMQPSession(Properties properties) {
-    this.properties = properties;
-  }
-
-  public boolean isOpen() {
-    if (connection != null) {
-      return true;
-    }
-    return false;
-  }
-
-  private Channel getChannel() {
-    Channel ch = null;
-    if (connection == null) {
-      connect();
-    }
-    if (connection != null) {
-      try {
-        ch = connection.createChannel();
-        ch.addShutdownListener(this);
-        failureCount = 0;
-      } catch (Exception ex) {
-        LOGGER.warn("Failed to open publish channel.");
-        failureCount++;
-      }
-      if (failureCount > properties.getInt(Keys.MONITOR_FAILURECOUNT)) {
-        LOGGER.warn("Connection has something wrong. So will be disconnected.");
-        disconnect();
-      }
-    }
-    return ch;
-  }
-
-  public void connect() {
-    LOGGER.info("Connect to {}...", properties.getString(Keys.AMQP_URI));
-    ConnectionFactory factory = new ConnectionFactory();
-    try {
-      if (StringUtils.isNotEmpty(properties.getString(Keys.AMQP_URI))) {
-        factory.setUri(properties.getString(Keys.AMQP_URI));
-        if (StringUtils.isNotEmpty(properties.getString(Keys.AMQP_USERNAME))) {
-          factory.setUsername(properties.getString(Keys.AMQP_USERNAME));
-        }
-        if (StringUtils.isNotEmpty(properties.getString(Keys.AMQP_PASSWORD))) {
-          factory.setPassword(properties.getString(Keys.AMQP_PASSWORD));
-        }
-        connection = factory.newConnection();
-        connection.addShutdownListener(this);
-        LOGGER.info("Connection established.");
-      }
-      //TODO: Consume review
-      // setupConsumer();
-    } catch (URISyntaxException ex) {
-      LOGGER.error("URI syntax error: {}", properties.getString(Keys.AMQP_URI));
-    } catch (IOException ex) {
-      LOGGER.error("Connection cannot be opened.");
-    } catch (Exception ex) {
-      LOGGER.warn("Connection has something error. it will be disposed.", ex);
-    }
-  }
-
-//TODO: Consume review
-/*
-  private void setupConsumer() {
-    if (connection != null) {
-      if (properties.getBoolean(Keys.QUEUE_CONSUME)) {
-        if (StringUtils.isNotEmpty(properties.getString(Keys.QUEUE_NAME))) {
-          consumeMessage();
-        }
-      }
-      LOGGER.info("Complete to setup channel.");
-    }
-  }
-*/
-
-  public void disconnect() {
-    LOGGER.info("Disconnecting...");
-    try {
-      if (connection != null) {
-        connection.close();
-      }
-    } catch (Exception ex) {
-      LOGGER.warn("Error when close connection." , ex);
-    } finally {
-      connection = null;
-      publishChannel = null;
-    }
-  }
-
-  public void publishMessage(String message) {
-    if (publishChannel == null || !publishChannel.isOpen()) {
-      publishChannel = getChannel();
-    }
-    if (publishChannel != null && publishChannel.isOpen()) {
-      try {
-        LOGGER.debug("Send message.");
-        publishChannel.basicPublish(properties.getString(Keys.EXCHANGE_NAME),
-            properties.getString(Keys.MESSAGE_ROUTINGKEY),
-            properties.getBasicProperties(),
-            message.getBytes(CharEncoding.UTF_8));
-      } catch (Exception ex) {
-        LOGGER.warn("Error when sending meessage.", ex);
-      }
-    }
-  }
-
-// TODO: Consume review.
-/*
-  public void consumeMessage() {
-    if (consumeChannel == null || !consumeChannel.isOpen()) {
-      consumeChannel = getChannel();
-    }
-    if (consumeChannel != null && consumeChannel.isOpen()) {
-      try {
-        consumeChannel.basicConsume(
-            properties.getString(Keys.QUEUE_NAME),
-            false,
-            new MessageConsumer(consumeChannel));
-        LOGGER.debug("Start consuming message.");
-      } catch (Exception ex) {
-        LOGGER.warn("Error when consuming message.", ex);
-      }
-    }
-  }
-*/
-  @Override
-  public void shutdownCompleted(ShutdownSignalException exception) {
-    Object obj = exception.getReference();
-
-    if (obj instanceof Channel) {
-      Channel ch = (Channel) obj;
-      if (ch.equals(publishChannel)) {
-        LOGGER.info("Publish channel closed.");
-        publishChannel = null;
-      } else if (ch.equals(consumeChannel)) {
-        LOGGER.info("Consume channel closed.");
-        consumeChannel = null;
-      }
-    } else if (obj instanceof Connection) {
-      LOGGER.info("Connection disconnected.");
-      connection = null;
-    }
-  }
-
-// TODO: Consume review.
-/*
-  public class MessageConsumer extends DefaultConsumer {
-
-    public MessageConsumer(Channel channel) {
-        super(channel);
-    }
-
-    @Override
-    public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties props, byte[] body)
-            throws IOException {
-      try {
-        long deliveryTag = envelope.getDeliveryTag();
-
-        if (Properties.APPROVE_APPID.equals(props.getAppId()) &&
-            Properties.CONTENT_TYPE_JSON.equals(props.getContentType())) {
-          // TODO: Get message then input as review. required 2.9 or later.
-        }
-
-        getChannel().basicAck(deliveryTag, false);
-
-      } catch (IOException ex) {
-        throw ex;
-      } catch (RuntimeException ex) {
-        LOGGER.warn("caught exception in delivery handler", ex);
-      }
-    }
-  }
-*/
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Keys.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Keys.java
deleted file mode 100644
index 14de98a..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Keys.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2013 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.rabbitmq;
-
-public enum Keys {
-  AMQP_URI("amqp.uri", null, "amqp://localhost"),
-  AMQP_USERNAME("amqp.username", null, "guest"),
-  AMQP_PASSWORD("amqp.password", null, "guest"),
-  EXCHANGE_NAME("exchange.name", null, "gerrit.publish"),
-  MESSAGE_DELIVERY_MODE("message.deliveryMode", null, 1),
-  MESSAGE_PRIORITY("message.priority", null, 0),
-  MESSAGE_ROUTINGKEY("message.routingKey", null, ""),
-  GERRIT_NAME("gerrit.name", "gerrit-name", ""),
-  GERRIT_HOSTNAME("gerrit.hostname", "gerrit-host", ""),
-  GERRIT_SCHEME("gerrit.scheme", "gerrit-scheme", "ssh"),
-  GERRIT_PORT("gerrit.port", "gerrit-port", 29418),
-  GERRIT_FRONT_URL("gerrit.canonicalWebUrl", "gerrit-front-url", ""),
-  GERRIT_VERSION("gerrit.version", "gerrit-version", null),
-  GERRIT_LISTENAS("gerrit.listenAs", null, null),
-  MONITOR_INTERVAL("monitor.interval", null, 15000),
-  MONITOR_FAILURECOUNT("monitor.failureCount", null, 15);
-
-  public String section;
-  public String name;
-  public String key;
-  public Object defaultVal;
-
-  Keys(String property, String key, Object defaultVal) {
-    String[] part = property.split("\\.");
-    this.section = part[0];
-    this.name = part[1];
-    this.key = key;
-    this.defaultVal = defaultVal;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Manager.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Manager.java
new file mode 100644
index 0000000..84d47cb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Manager.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2013 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.rabbitmq;
+
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.config.PropertiesFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Gerrit;
+import com.googlesource.gerrit.plugins.rabbitmq.message.Publisher;
+import com.googlesource.gerrit.plugins.rabbitmq.message.PublisherFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.solver.Solver;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.EventWorker;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.EventWorkerFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.DefaultEventWorker;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@Singleton
+public class Manager implements LifecycleListener {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(Manager.class);
+
+  public static final String FILE_EXT = ".config";
+  public static final String SITE_DIR = "site";
+
+  private final String pluginName;
+  private final Path pluginDataDir;
+  private final EventWorker defaultEventWorker;
+  private final EventWorker userEventWorker;
+  private final PublisherFactory publisherFactory;
+  private final PropertiesFactory propFactory;
+  private final Set<Solver> solvers;
+  private final List<Publisher> publisherList = new ArrayList<>();
+
+  @Inject
+  public Manager(
+      @PluginName final String pluginName,
+      @PluginData final File pluginData,
+      final DefaultEventWorker defaultEventWorker,
+      final EventWorkerFactory eventWorkerFactory,
+      final PublisherFactory publisherFactory,
+      final PropertiesFactory propFactory,
+      final Set<Solver> solvers) {
+    this.pluginName = pluginName;
+    this.pluginDataDir = pluginData.toPath();
+    this.defaultEventWorker = defaultEventWorker;
+    this.userEventWorker = eventWorkerFactory.create();
+    this.publisherFactory = publisherFactory;
+    this.propFactory = propFactory;
+    this.solvers = solvers;
+  }
+
+  @Override
+  public void start() {
+    for (Solver solver : solvers) {
+      solver.solve();
+    }
+
+    List<Properties> propList = load();
+    for (Properties properties : propList) {
+      Publisher publisher = publisherFactory.create(properties);
+      publisher.start();
+      String listenAs = properties.getSection(Gerrit.class).listenAs;
+      if (!listenAs.isEmpty()) {
+        userEventWorker.addPublisher(publisher, listenAs);
+      } else {
+        defaultEventWorker.addPublisher(publisher);
+      }
+      publisherList.add(publisher);
+    }
+  }
+
+  @Override
+  public void stop() {
+    for (Publisher publisher : publisherList) {
+      publisher.stop();
+      String listenAs = publisher.getProperties().getSection(Gerrit.class).listenAs;
+      if (!listenAs.isEmpty()) {
+        userEventWorker.removePublisher(publisher);
+      } else {
+        defaultEventWorker.removePublisher(publisher);
+      }
+    }
+    publisherList.clear();
+  }
+
+  private List<Properties> load() {
+    List<Properties> propList = new ArrayList<>();
+    // Load base
+    Properties base = propFactory.create(pluginDataDir.resolve(pluginName + FILE_EXT));
+    base.load();
+
+    // Load site
+    try (DirectoryStream<Path> ds = Files.newDirectoryStream(pluginDataDir.resolve(SITE_DIR), "*" + FILE_EXT)) {
+      for (Path configFile : ds) {
+        Properties site = propFactory.create(configFile);
+        if (site.load(base)) {
+          propList.add(site);
+        }
+      }
+    } catch (IOException iex) {
+      LOGGER.warn(iex.getMessage());
+    }
+    return propList;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Module.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Module.java
index 9626063..d19b4c3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Module.java
@@ -18,26 +18,51 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.multibindings.Multibinder;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.PluginProperties;
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.config.PropertiesFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.AMQP;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Exchange;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Gerrit;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Message;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Monitor;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Section;
+import com.googlesource.gerrit.plugins.rabbitmq.message.MessagePublisher;
+import com.googlesource.gerrit.plugins.rabbitmq.message.Publisher;
+import com.googlesource.gerrit.plugins.rabbitmq.message.PublisherFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.session.SessionFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.session.SessionFactoryProvider;
+import com.googlesource.gerrit.plugins.rabbitmq.solver.Solver;
+import com.googlesource.gerrit.plugins.rabbitmq.solver.version.V1;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.EventWorker;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.EventWorkerFactory;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.DefaultEventWorker;
+import com.googlesource.gerrit.plugins.rabbitmq.worker.UserEventWorker;
 
 class Module extends AbstractModule {
 
-  private final Properties properties;
-
-  @Inject
-  public Module(Properties properties) {
-    this.properties = properties;
-  }
-
   @Override
   protected void configure() {
-    bind(AMQPSession.class);
-//    bind(Properties.class);
-    bind(RabbitMQManager.class);
-    if (!properties.hasListenAs()) {
-      // No listenAs to filter events against. Register an unrestricted ChangeListener
-      DynamicSet.bind(binder(), ChangeListener.class).to(RabbitMQManager.class);
-    }
-    DynamicSet.bind(binder(), LifecycleListener.class).to(RabbitMQManager.class);
+    bind(SessionFactory.class).toProvider(SessionFactoryProvider.class);
+
+    Multibinder<Section> sectionBinder = Multibinder.newSetBinder(binder(), Section.class);
+    sectionBinder.addBinding().to(AMQP.class);
+    sectionBinder.addBinding().to(Exchange.class);
+    sectionBinder.addBinding().to(Gerrit.class);
+    sectionBinder.addBinding().to(Message.class);
+    sectionBinder.addBinding().to(Monitor.class);
+
+    Multibinder<Solver> solverBinder = Multibinder.newSetBinder(binder(), Solver.class);
+    solverBinder.addBinding().to(V1.class);
+
+    install(new FactoryModuleBuilder().implement(Publisher.class, MessagePublisher.class).build(PublisherFactory.class));
+    install(new FactoryModuleBuilder().implement(Properties.class, PluginProperties.class).build(PropertiesFactory.class));
+    install(new FactoryModuleBuilder().implement(EventWorker.class, UserEventWorker.class).build(EventWorkerFactory.class));
+
+    DynamicSet.bind(binder(), LifecycleListener.class).to(Manager.class);
+    DynamicSet.bind(binder(), ChangeListener.class).to(DefaultEventWorker.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Properties.java
deleted file mode 100644
index 418453c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/Properties.java
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright (C) 2013 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.rabbitmq;
-
-import com.google.gerrit.common.Version;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import com.rabbitmq.client.AMQP;
-
-import org.apache.commons.codec.CharEncoding;
-import org.apache.commons.lang.StringUtils;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-@Singleton
-public class Properties {
-
-  // TODO: Value will be replaced to "gerrit.event".
-  public final static String EVENT_APPID = "gerrit";
-  public final static String APPROVE_APPID = "gerrit.approve";
-  public final static String CONTENT_TYPE_JSON = "application/json";
-
-  private static final Logger LOGGER = LoggerFactory.getLogger(Properties.class);
-  private final static String CONFIG_FILENAME = "rabbitmq.config";
-
-  private final static int MINIMUM_CONNECTION_MONITOR_INTERVAL = 5000;
-
-  private final Config config;
-  private final Config pluginConfig;
-  private AMQP.BasicProperties properties;
-
-  @Inject
-  public Properties(final SitePaths site, @GerritServerConfig final Config config) {
-    this.config = config;
-    this.pluginConfig = getPluginConfig(new File(site.etc_dir, CONFIG_FILENAME));
-    this.properties = generateBasicProperties();
-  }
-
-  public Config getPluginConfig(File cfgPath) {
-    LOGGER.info("Loading " + cfgPath.toString() + " ...");
-    FileBasedConfig cfg = new FileBasedConfig(cfgPath, FS.DETECTED);
-    if (!cfg.getFile().exists()) {
-      LOGGER.warn("No " + cfg.getFile());
-      return cfg;
-    }
-    if (cfg.getFile().length() == 0) {
-      LOGGER.info("Empty " + cfg.getFile());
-      return cfg;
-    }
-
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      LOGGER.info("Config file " + cfg.getFile() + " is invalid: " + e.getMessage());
-    } catch (IOException e) {
-      LOGGER.info("Cannot read " + cfg.getFile() + ": " + e.getMessage());
-    }
-    return cfg;
-  }
-
-  public String getString(Keys key) {
-    String val = pluginConfig.getString(key.section, null, key.name);
-    if (val == null) {
-      return key.defaultVal.toString();
-    }
-    return val;
-  }
-
-  public int getInt(Keys key) {
-    return pluginConfig.getInt(key.section, key.name, new Integer(key.defaultVal.toString()));
-  }
-
-  public boolean getBoolean(Keys key) {
-    return pluginConfig.getBoolean(key.section, key.name, new Boolean(key.defaultVal.toString()));
-  }
-
-  public String getGerritFrontUrl() {
-    return StringUtils.stripToEmpty(config.getString(Keys.GERRIT_FRONT_URL.section, null, Keys.GERRIT_FRONT_URL.name));
-  }
-
-  public boolean hasListenAs() {
-    return !getListenAs().isEmpty();
-  }
-
-  public String getListenAs() {
-    return StringUtils.stripToEmpty(pluginConfig.getString(
-        Keys.GERRIT_LISTENAS.section, null, Keys.GERRIT_LISTENAS.name));
-  }
-
-  public String getGerritVersion() {
-    return StringUtils.stripToEmpty(Version.getVersion());
-  }
-
-  public int getConnectionMonitorInterval() {
-    int interval = getInt(Keys.MONITOR_INTERVAL);
-    if (interval < MINIMUM_CONNECTION_MONITOR_INTERVAL) {
-      return MINIMUM_CONNECTION_MONITOR_INTERVAL;
-    }
-    return interval;
-  }
-
-  private AMQP.BasicProperties generateBasicProperties() {
-    Map<String, Object> headers = new HashMap<>();
-    headers.put(Keys.GERRIT_NAME.key, getString(Keys.GERRIT_NAME));
-    headers.put(Keys.GERRIT_HOSTNAME.key, getString(Keys.GERRIT_HOSTNAME));
-    headers.put(Keys.GERRIT_SCHEME.key, getString(Keys.GERRIT_SCHEME));
-    headers.put(Keys.GERRIT_PORT.key, String.valueOf(getInt(Keys.GERRIT_PORT)));
-    headers.put(Keys.GERRIT_FRONT_URL.key, getGerritFrontUrl());
-    headers.put(Keys.GERRIT_VERSION.key, getGerritVersion());
-
-    AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
-    builder.appId(EVENT_APPID);
-    builder.contentEncoding(CharEncoding.UTF_8);
-    builder.contentType(CONTENT_TYPE_JSON);
-    builder.deliveryMode(getInt(Keys.MESSAGE_DELIVERY_MODE));
-    builder.priority(getInt(Keys.MESSAGE_PRIORITY));
-    builder.headers(headers);
-
-    return builder.build();
-  }
-
-  public AMQP.BasicProperties getBasicProperties() {
-    return properties;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/RabbitMQManager.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/RabbitMQManager.java
deleted file mode 100644
index 8e69528..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/RabbitMQManager.java
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright (C) 2013 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.rabbitmq;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.ChangeListener;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.events.ChangeEvent;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gson.Gson;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-@Singleton
-public class RabbitMQManager implements ChangeListener, LifecycleListener {
-
-  private static final Logger LOGGER = LoggerFactory
-      .getLogger(RabbitMQManager.class);
-  private final static int MONITOR_FIRSTTIME_DELAY = 15000;
-  private final Properties properties;
-  private final AMQPSession session;
-  private final Gson gson = new Gson();
-  private final Timer monitorTimer = new Timer();
-  private final ChangeHooks hooks;
-  private final AccountResolver accountResolver;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final WorkQueue workQueue;
-  private final ThreadLocalRequestContext threadLocalRequestContext;
-  private final PluginUser pluginUser;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private ReviewDb db;
-  private Account userAccount;
-
-  @Inject
-  public RabbitMQManager(Properties properties,
-      AMQPSession session,
-      ChangeHooks hooks,
-      AccountResolver accountResolver,
-      IdentifiedUser.GenericFactory userFactory,
-      WorkQueue workQueue,
-      ThreadLocalRequestContext threadLocalRequestContext,
-      PluginUser pluginUser,
-      SchemaFactory<ReviewDb> schemaFactory) {
-    this.properties = properties;
-    this.session = session;
-    this.hooks = hooks;
-    this.accountResolver = accountResolver;
-    this.userFactory = userFactory;
-    this.workQueue = workQueue;
-    this.threadLocalRequestContext = threadLocalRequestContext;
-    this.pluginUser = pluginUser;
-    this.schemaFactory = schemaFactory;
-  }
-
-  @Override
-  public void start() {
-    session.connect();
-    monitorTimer.schedule(new TimerTask() {
-      @Override
-      public void run() {
-        if (!session.isOpen()) {
-          LOGGER.info("#start: try to reconnect");
-          session.connect();
-        }
-      }
-    }, MONITOR_FIRSTTIME_DELAY, properties.getInt(Keys.MONITOR_INTERVAL));
-
-    if (properties.hasListenAs()) {
-      final String userName = properties.getListenAs();
-      final ChangeListener changeListener = this;
-      workQueue.getDefaultQueue().submit(new Runnable() {
-        @Override
-        public void run() {
-          RequestContext old = threadLocalRequestContext
-              .setContext(new RequestContext() {
-
-                @Override
-                public CurrentUser getCurrentUser() {
-                  return pluginUser;
-                }
-
-                @Override
-                public Provider<ReviewDb> getReviewDbProvider() {
-                  return new Provider<ReviewDb>() {
-                    @Override
-                    public ReviewDb get() {
-                      if (db == null) {
-                        try {
-                          db = schemaFactory.open();
-                        } catch (OrmException e) {
-                          throw new ProvisionException("Cannot open ReviewDb", e);
-                        }
-                      }
-                      return db;
-                    }
-                  };
-                }
-              });
-          try {
-            userAccount = accountResolver.find(userName);
-            if (userAccount == null) {
-              LOGGER.error("No single user could be found when searching for listenAs: {}", userName);
-              return;
-            }
-
-            IdentifiedUser user = userFactory.create(userAccount.getId());
-            hooks.addChangeListener(changeListener, user);
-            LOGGER.info("Listen events as : {}", userName);
-          } catch (OrmException e) {
-            LOGGER.error("Could not query database for listenAs", e);
-            return;
-          } finally {
-            threadLocalRequestContext.setContext(old);
-            if (db != null) {
-              db.close();
-              db = null;
-            }
-          }
-        }
-      });
-    }
-  }
-
-  @Override
-  public void stop() {
-    monitorTimer.cancel();
-    session.disconnect();
-    hooks.removeChangeListener(this);
-  }
-
-  @Override
-  public void onChangeEvent(ChangeEvent event) {
-    session.publishMessage(gson.toJson(event));
-  }
-
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Default.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Default.java
new file mode 100644
index 0000000..87eacd9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Default.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2015 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.rabbitmq.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Default {
+  String value() default "";
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Limit.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Limit.java
new file mode 100644
index 0000000..4c33219
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/Limit.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 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.rabbitmq.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Limit {
+  int max() default -1;
+  int min() default -1;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/MessageHeader.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/MessageHeader.java
new file mode 100644
index 0000000..401ef49
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/annotation/MessageHeader.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2015 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.rabbitmq.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MessageHeader {
+  String value() default "";
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/AMQProperties.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/AMQProperties.java
new file mode 100644
index 0000000..0fd3a88
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/AMQProperties.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2015 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.rabbitmq.config;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.MessageHeader;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Message;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Section;
+import com.rabbitmq.client.AMQP;
+
+import org.apache.commons.codec.CharEncoding;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AMQProperties {
+
+  public final static String EVENT_APPID = "gerrit";
+  public final static String CONTENT_TYPE_JSON = "application/json";
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(AMQProperties.class);
+
+  private final PluginProperties properties;
+  private AMQP.BasicProperties amqpProperties;
+
+  public AMQProperties(PluginProperties properties) {
+    this.properties = properties;
+  }
+
+  public AMQP.BasicProperties getBasicProperties() {
+    if (amqpProperties == null) {
+      Map<String, Object> headers = new HashMap<>();
+      for (Section section : properties.getSections()) {
+        for (Field f : section.getClass().getFields()) {
+          if (f.isAnnotationPresent(MessageHeader.class)) {
+            MessageHeader mh = f.getAnnotation(MessageHeader.class);
+            try {
+              switch(f.getType().getSimpleName()) {
+                case "String":
+                  headers.put(mh.value(), f.get(section).toString());
+                  break;
+                case "Integer":
+                  headers.put(mh.value(), f.getInt(section));
+                  break;
+                case "Long":
+                  headers.put(mh.value(), f.getLong(section));
+                  break;
+                case "Boolean":
+                  headers.put(mh.value(), f.getBoolean(section));
+                  break;
+                default:
+                  break;
+              }
+            } catch (Exception ex) {
+              LOGGER.info(ex.getMessage());
+            }
+          }
+        }
+      }
+      Message message = properties.getSection(Message.class);
+      AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
+      builder.appId(EVENT_APPID);
+      builder.contentEncoding(CharEncoding.UTF_8);
+      builder.contentType(CONTENT_TYPE_JSON);
+      builder.deliveryMode(message.deliveryMode);
+      builder.priority(message.priority);
+      builder.headers(headers);
+
+      amqpProperties = builder.build();
+    }
+    return amqpProperties;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PluginProperties.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PluginProperties.java
new file mode 100644
index 0000000..9c978b2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PluginProperties.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2015 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.rabbitmq.config;
+
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Gerrit;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Monitor;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Section;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Sections;
+
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
+
+public class PluginProperties implements Properties {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(PluginProperties.class);
+
+  private final static int MINIMUM_CONNECTION_MONITOR_INTERVAL = 5000;
+
+  private final Set<Section> sections;
+  private final Path propertiesFile;
+  private AMQProperties amqProperties;
+
+  @AssistedInject
+  public PluginProperties(Set<Section> sections) {
+    this(sections, null);
+  }
+
+  @AssistedInject
+  public PluginProperties(Set<Section> sections, @Assisted Path propertiesFile) {
+    this.sections = sections;
+    this.propertiesFile = propertiesFile;
+  }
+
+  private void initialize() {
+    for (Section section : sections) {
+      Sections.initialize(section);
+    }
+  }
+
+  @Override
+  public Config toConfig() {
+    Config config = new Config();
+    for (Section section : sections) {
+      config = Sections.toConfig(section, config);
+    }
+    return config;
+  }
+
+  @Override
+  public boolean load() {
+    return load(null);
+  }
+
+  @Override
+  public boolean load(Properties baseProperties) {
+    initialize();
+    LOGGER.info("Loading {} ...", propertiesFile);
+    if (!Files.exists(propertiesFile)) {
+      LOGGER.warn("No {}", propertiesFile);
+      return false;
+    }
+
+    FileBasedConfig cfg = new FileBasedConfig(propertiesFile.toFile(), FS.DETECTED);
+    try {
+      cfg.load();
+    } catch (ConfigInvalidException e) {
+      LOGGER.info("{} has invalid format: {}", propertiesFile, e.getMessage());
+      return false;
+    } catch (IOException e) {
+      LOGGER.info("Cannot read {}: {}", propertiesFile, e.getMessage());
+      return false;
+    }
+    for (Section section : getSections()) {
+      if (baseProperties != null) {
+        Sections.fromConfig(section, baseProperties.toConfig(), cfg);
+      } else {
+        Sections.fromConfig(section, cfg);
+      }
+      Sections.normalize(section);
+    }
+    return true;
+  }
+
+  @Override
+  public Path getPath() {
+    return propertiesFile;
+  }
+
+  @Override
+  public String getName() {
+    if (propertiesFile != null) {
+      return FilenameUtils.removeExtension(propertiesFile.getFileName().toString());
+    }
+    return null;
+  }
+
+  @Override
+  public Set<Section> getSections() {
+    return Collections.unmodifiableSet(sections);
+  }
+
+  @Override
+  public <T extends Section> T getSection(Class<T> clazz) {
+    for (Section section : sections) {
+      if (section.getClass() == clazz) {
+        return clazz.cast(section);
+      }
+    }
+    return null;
+  }
+
+  public String getGerritFrontUrl() {
+    Gerrit gerrit = (Gerrit) getSection(Gerrit.class);
+    if (gerrit != null) {
+      return gerrit.canonicalWebUrl;
+    }
+    return null;
+  }
+
+  public boolean hasListenAs() {
+    Gerrit gerrit = (Gerrit) getSection(Gerrit.class);
+    if (gerrit != null) {
+      return gerrit.listenAs.isEmpty();
+    }
+    return false;
+  }
+
+  public String getListenAs() {
+    Gerrit gerrit = (Gerrit) getSection(Gerrit.class);
+    if (gerrit != null) {
+      return gerrit.listenAs;
+    }
+    return null;
+  }
+
+  public String getGerritVersion() {
+    Gerrit gerrit = (Gerrit) getSection(Gerrit.class);
+    if (gerrit != null) {
+      return gerrit.version;
+    }
+    return null;
+  }
+
+  public int getConnectionMonitorInterval() {
+    Monitor monitor = (Monitor) getSection(Monitor.class);
+    if (monitor != null && monitor.interval < MINIMUM_CONNECTION_MONITOR_INTERVAL) {
+      return monitor.interval;
+    }
+    return MINIMUM_CONNECTION_MONITOR_INTERVAL;
+  }
+
+  public AMQProperties getAMQProperties() {
+    if (amqProperties == null) {
+      amqProperties = new AMQProperties(this);
+    }
+    return amqProperties;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/Properties.java
new file mode 100644
index 0000000..d81379b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/Properties.java
@@ -0,0 +1,19 @@
+package com.googlesource.gerrit.plugins.rabbitmq.config;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Section;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.nio.file.Path;
+import java.util.Set;
+
+public interface Properties extends Cloneable {
+  public Config toConfig();
+  public boolean load();
+  public boolean load(Properties baseProperties);
+  public Path getPath();
+  public String getName();
+  public Set<Section> getSections();
+  public <T extends Section> T getSection(Class<T> clazz);
+  public AMQProperties getAMQProperties();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PropertiesFactory.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PropertiesFactory.java
new file mode 100644
index 0000000..a82ebac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/PropertiesFactory.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 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.rabbitmq.config;
+
+import java.nio.file.Path;
+
+public interface PropertiesFactory {
+  Properties create();
+  Properties create(Path propertiesFile);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/internal/GerritFrontUrl.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/internal/GerritFrontUrl.java
new file mode 100644
index 0000000..233c156
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/internal/GerritFrontUrl.java
@@ -0,0 +1,7 @@
+package com.googlesource.gerrit.plugins.rabbitmq.config.internal;
+
+import org.eclipse.jgit.lib.Config;
+
+public interface GerritFrontUrl {
+  public void setGerritFrontUrlFromConfig(Config config);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/AMQP.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/AMQP.java
new file mode 100644
index 0000000..11446cc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/AMQP.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+
+public class AMQP implements Section {
+
+  @Default("amqp://localhost")
+  public String uri;
+
+  @Default("guest")
+  public String username;
+
+  @Default("guest")
+  public String password;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Exchange.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Exchange.java
new file mode 100644
index 0000000..350a206
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Exchange.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+
+public class Exchange implements Section {
+
+  @Default("gerrit.publish")
+  public String name;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Gerrit.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Gerrit.java
new file mode 100644
index 0000000..5ad4a49
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Gerrit.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.MessageHeader;
+
+import org.eclipse.jgit.lib.Config;
+
+public class Gerrit implements Section {
+
+  @Default
+  @MessageHeader("gerrit-name")
+  public String name;
+
+  @Default
+  @MessageHeader("gerrit-host")
+  public String hostname;
+
+  @Default("ssh")
+  @MessageHeader("gerrit-scheme")
+  public String scheme;
+
+  @Default("29418")
+  @MessageHeader("gerrit-port")
+  public Integer port;
+
+  @MessageHeader("gerrit-front-url")
+  public String canonicalWebUrl;
+
+  @MessageHeader("gerrit-version")
+  public String version;
+
+  @Default
+  public String listenAs;
+
+  @Inject
+  public Gerrit(final @GerritServerConfig Config config) {
+    this.canonicalWebUrl = config.getString("gerrit", null, "canonicalWebUrl");
+    this.version = Version.getVersion();
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Message.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Message.java
new file mode 100644
index 0000000..4760c4c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Message.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+
+public class Message implements Section {
+
+  @Default("1")
+  public Integer deliveryMode;
+
+  @Default("0")
+  public Integer priority;
+
+  @Default
+  public String routingKey;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Monitor.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Monitor.java
new file mode 100644
index 0000000..6f91648
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Monitor.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Limit;
+
+public class Monitor implements Section {
+
+  @Default("15000")
+  @Limit(min=5000)
+  public Integer interval;
+
+  @Default("15")
+  public Integer failureCount;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Section.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Section.java
new file mode 100644
index 0000000..dcb0a9a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Section.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+public interface Section {
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Sections.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Sections.java
new file mode 100644
index 0000000..263f8ba
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/config/section/Sections.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2015 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.rabbitmq.config.section;
+
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Default;
+import com.googlesource.gerrit.plugins.rabbitmq.annotation.Limit;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.util.Set;
+
+public final class Sections {
+  private static final Logger LOGGER = LoggerFactory.getLogger(Sections.class);
+
+  public static final <T extends Section> String getName(T section) {
+    return section.getClass().getSimpleName().toLowerCase();
+  }
+
+  public static final <T extends Section> T initialize(T section) {
+    Field[] fs = section.getClass().getFields();
+    for (Field f : fs) {
+      try {
+        if (f.isAnnotationPresent(Default.class)) {
+          Default a = f.getAnnotation(Default.class);
+          Class<?> type = f.getType();
+          if (type == String.class) {
+            f.set(section, new String(a.value()));
+          } else if (type == Integer.class) {
+            f.set(section, new Integer(a.value()));
+          } else if (type == Long.class) {
+            f.set(section, new Long(a.value()));
+          } else if (type == Boolean.class) {
+            f.set(section, new Boolean(a.value()));
+          }
+        }
+      } catch (Exception ex) {
+        LOGGER.warn("Exception during initialize: {}", f.getName());
+      }
+    }
+    return section;
+  }
+
+  public static final <T extends Section> Config toConfig(T section) {
+    return toConfig(section, new Config());
+  }
+
+  public static final <T extends Section> Config toConfig(T section, Config config) {
+    Field[] fs = section.getClass().getFields();
+    for (Field f : fs) {
+      try {
+        Class<?> type = f.getType();
+        Object obj = f.get(section);
+        if (obj != null) {
+          if (type == String.class) {
+            config.setString(getName(section), null, f.getName(), String.class.cast(obj));
+          } else if (type == Integer.class) {
+            config.setInt(getName(section), null, f.getName(), Integer.class.cast(obj));
+          } else if (type == Long.class) {
+            config.setLong(getName(section), null, f.getName(), Long.class.cast(obj));
+          } else if (type == Boolean.class) {
+            config.setBoolean(getName(section), null, f.getName(), Boolean.class.cast(obj));
+          }
+        }
+      } catch (Exception ex) {
+        LOGGER.warn("Exception during toConfig: {}", f.getName());
+        LOGGER.info("{}", ex.getMessage());
+      }
+    }
+    return config;
+  }
+
+  public static final <T extends Section> Section fromConfig(T section, Config... configs) {
+    for (Config config : configs) {
+      if (config != null) {
+        Set<String> names = config.getNames(getName(section));
+        Field[] fs = section.getClass().getFields();
+
+        for (Field f : fs) {
+          try {
+            if (names.contains(f.getName())) {
+              Class<?> type = f.getType();
+              if (type == String.class) {
+                f.set(section, new String(config.getString(getName(section), null, f.getName())));
+              } else if (type == Integer.class) {
+                f.set(section, new Integer(config.getInt(getName(section), null, f.getName(), 0)));
+              } else if (type == Long.class) {
+                f.set(section, new Long(config.getLong(getName(section), null, f.getName(), 0)));
+              } else if (type == Boolean.class) {
+                f.set(section, new Boolean(config.getBoolean(getName(section), null, f.getName(), false)));
+              }
+            }
+          } catch (Exception ex) {
+            LOGGER.warn("Exception during fromConfig: {}", f.getName());
+          }
+        }
+      }
+    }
+    return section;
+  }
+
+  public static final <T extends Section> T normalize(T section) {
+    Field[] fs = section.getClass().getFields();
+    for (Field f : fs) {
+      try {
+        if (f.getType() == Integer.class && f.isAnnotationPresent(Limit.class)) {
+          Object obj = f.get(section);
+          if (obj != null) {
+            Integer val = Integer.class.cast(obj);
+            Limit a = f.getAnnotation(Limit.class);
+            if (a.min() != -1 && val < a.min()) {
+              val = a.min();
+            }
+            if (a.max() != -1 && val > a.max()) {
+              val = a.max();
+            }
+            f.set(section, val);
+          }
+        }
+      } catch (Exception ex) {
+        LOGGER.warn("Exception during normalize: {}", f.getName());
+        LOGGER.info("{}", ex.getMessage());
+      }
+    }
+    return section;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/MessagePublisher.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/MessagePublisher.java
new file mode 100644
index 0000000..5d31adc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/MessagePublisher.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2015 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.rabbitmq.message;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Monitor;
+import com.googlesource.gerrit.plugins.rabbitmq.session.Session;
+import com.googlesource.gerrit.plugins.rabbitmq.session.SessionFactoryProvider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class MessagePublisher implements Publisher, LifecycleListener {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(MessagePublisher.class);
+
+  private final static int MONITOR_FIRSTTIME_DELAY = 15000;
+
+  private final Session session;
+  private final Properties properties;
+  private final Gson gson;
+  private final Timer monitorTimer = new Timer();
+  private boolean available = true;
+
+  @Inject
+  public MessagePublisher(
+      @Assisted Properties properties,
+      SessionFactoryProvider sessionFactoryProvider,
+      Gson gson) {
+    this.session = sessionFactoryProvider.get().create(properties);
+    this.properties = properties;
+    this.gson = gson;
+  }
+
+  @Override
+  public void start() {
+    if (!session.isOpen()) {
+      session.connect();
+      monitorTimer.schedule(new TimerTask() {
+        @Override
+        public void run() {
+          if (!session.isOpen()) {
+            LOGGER.info("#start: try to reconnect");
+            session.connect();
+          }
+        }
+      }, MONITOR_FIRSTTIME_DELAY, properties.getSection(Monitor.class).interval);
+      available = true;
+    }
+  }
+
+  @Override
+  public void stop() {
+    monitorTimer.cancel();
+    session.disconnect();
+    available = false;
+  }
+
+  @Override
+  public void onChangeEvent(ChangeEvent event) {
+    if (available && session.isOpen()) {
+      session.publish(gson.toJson(event));
+    }
+  }
+
+  @Override
+  public void enable() {
+    available = true;
+  }
+
+  @Override
+  public void disable() {
+    available = false;
+  }
+
+  @Override
+  public boolean isEnable() {
+    return available;
+  }
+
+  @Override
+  public Session getSession() {
+    return session;
+  }
+
+  @Override
+  public Properties getProperties() {
+    return properties;
+  }
+
+  @Override
+  public String getName() {
+    return properties.getName();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/Publisher.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/Publisher.java
new file mode 100644
index 0000000..77d4962
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/Publisher.java
@@ -0,0 +1,17 @@
+package com.googlesource.gerrit.plugins.rabbitmq.message;
+
+import com.google.gerrit.common.ChangeListener;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.session.Session;
+
+public interface Publisher extends ChangeListener {
+  public void start();
+  public void stop();
+  public void enable();
+  public void disable();
+  public boolean isEnable();
+  public Session getSession();
+  public Properties getProperties();
+  public String getName();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/PublisherFactory.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/PublisherFactory.java
new file mode 100644
index 0000000..225f201
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/message/PublisherFactory.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2015 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.rabbitmq.message;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+
+public interface PublisherFactory {
+  Publisher create(Properties properties);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/Session.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/Session.java
new file mode 100644
index 0000000..6cd7e81
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/Session.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2015 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.rabbitmq.session;
+
+public interface Session {
+  public boolean isOpen();
+  public void connect();
+  public void disconnect();
+  public void publish(String message);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactory.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactory.java
new file mode 100644
index 0000000..16bd310
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactory.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2015 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.rabbitmq.session;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.session.type.AMQPSession;
+
+public class SessionFactory {
+  public Session create(Properties properties) {
+    return new AMQPSession(properties);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactoryProvider.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactoryProvider.java
new file mode 100644
index 0000000..07a6552
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/SessionFactoryProvider.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2015 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.rabbitmq.session;
+
+import com.google.inject.Provider;
+
+public class SessionFactoryProvider implements Provider<SessionFactory> {
+
+  @Override
+  public SessionFactory get() {
+    return new SessionFactory();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/type/AMQPSession.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/type/AMQPSession.java
new file mode 100644
index 0000000..f7dfba6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/session/type/AMQPSession.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2013 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.rabbitmq.session.type;
+
+import com.googlesource.gerrit.plugins.rabbitmq.config.Properties;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.AMQP;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Exchange;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Message;
+import com.googlesource.gerrit.plugins.rabbitmq.config.section.Monitor;
+import com.googlesource.gerrit.plugins.rabbitmq.session.Session;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
+import com.rabbitmq.client.ShutdownListener;
+import com.rabbitmq.client.ShutdownNotifier;
+import com.rabbitmq.client.ShutdownSignalException;
+
+import org.apache.commons.codec.CharEncoding;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+public final class AMQPSession implements Session {
+
+  private class ShutdownListenerImpl implements ShutdownListener {
+
+    private final Class<?> clazz;
+
+    public <T extends ShutdownNotifier> ShutdownListenerImpl(Class<T> clazz) {
+      this.clazz = clazz;
+    }
+
+    @Override
+    public void shutdownCompleted(ShutdownSignalException cause) {
+      if (cause != null) {
+        Object obj = cause.getReference();
+        if (Channel.class.isInstance(obj)) {
+          Channel.class.cast(obj).removeShutdownListener(this);
+        } else if (Connection.class.isInstance(obj)) {
+          Connection.class.cast(obj).removeShutdownListener(this);
+        }
+        if (clazz.isInstance(obj)) {
+          if (clazz == Channel.class) {
+            Channel ch = Channel.class.cast(obj);
+            if (cause.isInitiatedByApplication()) {
+              LOGGER.info(MSG("Channel #{} closed."), ch.getChannelNumber());
+            } else {
+              LOGGER.info(MSG("Channel #{} suddenly closed."), ch.getChannelNumber());
+            }
+            if (ch.equals(AMQPSession.this.channel)) {
+              AMQPSession.this.channel = null;
+            }
+          } else if (clazz == Connection.class) {
+            Connection conn = Connection.class.cast(obj);
+            if (cause.isInitiatedByApplication()) {
+              LOGGER.info(MSG("Connection closed."));
+            } else {
+              LOGGER.info(MSG("Connection suddenly closed."));
+            }
+            if (conn.equals(AMQPSession.this.connection)) {
+              AMQPSession.this.connection = null;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(AMQPSession.class);
+  private final Properties properties;
+  private volatile Connection connection;
+  private volatile Channel channel;
+  private volatile int failureCount = 0;
+
+  private final ShutdownListener connectionListener = new ShutdownListenerImpl(Connection.class);
+  private final ShutdownListener channelListener = new ShutdownListenerImpl(Channel.class);
+
+  public AMQPSession(Properties properties) {
+    this.properties = properties;
+  }
+
+  private String MSG(String msg) {
+    return String.format("[%s] %s", properties.getName(), msg);
+  }
+
+  @Override
+  public boolean isOpen() {
+    if (connection != null) {
+      return true;
+    }
+    return false;
+  }
+
+  private Channel getChannel() {
+    Channel ch = null;
+    if (connection == null) {
+      connect();
+    } else {
+      try {
+        ch = connection.createChannel();
+        ch.addShutdownListener(channelListener);
+        failureCount = 0;
+        LOGGER.info(MSG("Channel #{} opened."), ch.getChannelNumber());
+      } catch (Exception ex) {
+        LOGGER.warn(MSG("Failed to open channel."));
+        failureCount++;
+      }
+      if (failureCount > properties.getSection(Monitor.class).failureCount) {
+        LOGGER.warn("Connection has something wrong. So will be disconnected.");
+        disconnect();
+      }
+    }
+    return ch;
+  }
+
+  @Override
+  public void connect() {
+    if (connection != null && connection.isOpen()) {
+      LOGGER.info(MSG("Already connected."));
+      return;
+    }
+    AMQP amqp = properties.getSection(AMQP.class);
+    LOGGER.info(MSG("Connect to {}..."), amqp.uri);
+    ConnectionFactory factory = new ConnectionFactory();
+    try {
+      if (StringUtils.isNotEmpty(amqp.uri)) {
+        factory.setUri(amqp.uri);
+        if (StringUtils.isNotEmpty(amqp.username)) {
+          factory.setUsername(amqp.username);
+        }
+        if (StringUtils.isNotEmpty(amqp.password)) {
+          factory.setPassword(amqp.password);
+        }
+        connection = factory.newConnection();
+        connection.addShutdownListener(connectionListener);
+        LOGGER.info(MSG("Connection established."));
+      }
+    } catch (URISyntaxException ex) {
+      LOGGER.error(MSG("URI syntax error: {}"), amqp.uri);
+    } catch (IOException ex) {
+      LOGGER.error(MSG("Connection cannot be opened."));
+    } catch (Exception ex) {
+      LOGGER.warn(MSG("Connection has something error. it will be disposed."), ex);
+    }
+  }
+
+  @Override
+  public void disconnect() {
+    LOGGER.info(MSG("Disconnecting..."));
+    try {
+      if (channel != null) {
+        LOGGER.info(MSG("Close Channel #{}..."), channel.getChannelNumber());
+        channel.close();
+      }
+    } catch (Exception ex) {
+      LOGGER.warn(MSG("Error when close channel.") , ex);
+    } finally {
+      channel = null;
+    }
+
+    try {
+      if (connection != null) {
+        LOGGER.info(MSG("Close Connection..."));
+        connection.close();
+      }
+    } catch (Exception ex) {
+      LOGGER.warn(MSG("Error when close connection.") , ex);
+    } finally {
+      connection = null;
+    }
+  }
+
+  @Override
+  public void publish(String messageBody) {
+    if (channel == null || !channel.isOpen()) {
+      channel = getChannel();
+    }
+    if (channel != null && channel.isOpen()) {
+      Message message = properties.getSection(Message.class);
+      Exchange exchange = properties.getSection(Exchange.class);
+      try {
+        LOGGER.debug(MSG("Send message."));
+        channel.basicPublish(exchange.name, message.routingKey,
+            properties.getAMQProperties().getBasicProperties(),
+            messageBody.getBytes(CharEncoding.UTF_8));
+      } catch (Exception ex) {
+        LOGGER.warn(MSG("Error when sending meessage."), ex);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/Solver.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/Solver.java
new file mode 100644
index 0000000..98d5cf1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/Solver.java
@@ -0,0 +1,17 @@
+// Copyright (C) 2015 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
+package com.googlesource.gerrit.plugins.rabbitmq.solver;
+
+public interface Solver {
+  public void solve();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/version/V1.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/version/V1.java
new file mode 100644
index 0000000..29d7bb3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/solver/version/V1.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2015 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
+package com.googlesource.gerrit.plugins.rabbitmq.solver.version;
+
+import static com.googlesource.gerrit.plugins.rabbitmq.Manager.FILE_EXT;
+import static com.googlesource.gerrit.plugins.rabbitmq.Manager.SITE_DIR;
+
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.rabbitmq.solver.Solver;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class V1 implements Solver {
+
+  private final static String DEFAULT_SITE_NAME = "default";
+  private static final Logger LOGGER = LoggerFactory.getLogger(V1.class);
+
+  private final String pluginName;
+  private final Path pluginDataDir;
+  private final Path etcDir;
+
+  @Inject
+  public V1(
+      @PluginName final String pluginName,
+      @PluginData final File pluginData,
+      final SitePaths sites
+      ) {
+    this.pluginName = pluginName;
+    this.pluginDataDir = pluginData.toPath();
+    this.etcDir = sites.etc_dir.toPath();
+  }
+
+  /**
+   * old : etc/rabbitmq.config
+   *
+   * new : data/rabbitmq/rabbitmq.config
+   *       data/rabbitmq/site/default.config
+   */
+  public void solve() {
+    try {
+      Path oldFile = etcDir.resolve(pluginName + FILE_EXT);
+      Path newFile = pluginDataDir.resolve(pluginName + FILE_EXT);
+      Path siteDir = pluginDataDir.resolve(SITE_DIR);
+
+      Files.createDirectories(siteDir);
+      Files.move(oldFile, newFile);
+      Files.createFile(siteDir.resolve(DEFAULT_SITE_NAME + FILE_EXT));
+    } catch (Exception ex) {
+      LOGGER.info(ex.getMessage());
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/DefaultEventWorker.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/DefaultEventWorker.java
new file mode 100644
index 0000000..6ebfb0d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/DefaultEventWorker.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2015 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.rabbitmq.worker;
+
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.rabbitmq.message.Publisher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+@Singleton
+public class DefaultEventWorker implements ChangeListener, EventWorker {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(DefaultEventWorker.class);
+
+  private final Set<Publisher> publishers = new CopyOnWriteArraySet<>();
+
+  @Override
+  public void addPublisher(Publisher publisher) {
+    publishers.add(publisher);
+  }
+
+  @Override
+  public void addPublisher(Publisher publisher, String userName) {
+    LOGGER.warn("addPublisher() with username '{}' was called. Hence no operation.", userName);
+  }
+
+  @Override
+  public void removePublisher(Publisher publisher) {
+    publishers.remove(publisher);
+  }
+
+  @Override
+  public void clear() {
+    publishers.clear();
+  }
+
+  @Override
+  public void onChangeEvent(ChangeEvent event) {
+    for (Publisher publisher : publishers) {
+      publisher.onChangeEvent(event);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorker.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorker.java
new file mode 100644
index 0000000..332b9cd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorker.java
@@ -0,0 +1,10 @@
+package com.googlesource.gerrit.plugins.rabbitmq.worker;
+
+import com.googlesource.gerrit.plugins.rabbitmq.message.Publisher;
+
+public interface EventWorker {
+  public void addPublisher(Publisher publisher);
+  public void addPublisher(Publisher publisher, String userName);
+  public void removePublisher(Publisher publisher);
+  public void clear();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorkerFactory.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorkerFactory.java
new file mode 100644
index 0000000..92c36b9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/EventWorkerFactory.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2015 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.rabbitmq.worker;
+
+public interface EventWorkerFactory {
+  public EventWorker create();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/UserEventWorker.java b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/UserEventWorker.java
new file mode 100644
index 0000000..1f7710d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/rabbitmq/worker/UserEventWorker.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2015 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.rabbitmq.worker;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import com.googlesource.gerrit.plugins.rabbitmq.message.Publisher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class UserEventWorker implements EventWorker {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(UserEventWorker.class);
+
+  private final ChangeHooks hooks;
+  private final WorkQueue workQueue;
+  private final AccountResolver accountResolver;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ThreadLocalRequestContext threadLocalRequestContext;
+  private final PluginUser pluginUser;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+
+  @Inject
+  public UserEventWorker(
+      ChangeHooks hooks,
+      WorkQueue workQueue,
+      AccountResolver accountResolver,
+      IdentifiedUser.GenericFactory userFactory,
+      ThreadLocalRequestContext threadLocalRequestContext,
+      PluginUser pluginUser,
+      SchemaFactory<ReviewDb> schemaFactory) {
+    this.hooks = hooks;
+    this.workQueue = workQueue;
+    this.accountResolver = accountResolver;
+    this.userFactory = userFactory;
+    this.threadLocalRequestContext = threadLocalRequestContext;
+    this.pluginUser = pluginUser;
+    this.schemaFactory = schemaFactory;
+  }
+
+  @Override
+  public void addPublisher(final Publisher publisher) {
+    LOGGER.warn("addPublisher() without username was called. Hence no operation.");
+  }
+
+  @Override
+  public void addPublisher(final Publisher publisher, final String userName) {
+    workQueue.getDefaultQueue().submit(new Runnable() {
+      private ReviewDb db;
+      private Account userAccount;
+
+      @Override
+      public void run() {
+        RequestContext old = threadLocalRequestContext
+            .setContext(new RequestContext() {
+
+              @Override
+              public CurrentUser getCurrentUser() {
+                return pluginUser;
+              }
+
+              @Override
+              public Provider<ReviewDb> getReviewDbProvider() {
+                return new Provider<ReviewDb>() {
+                  @Override
+                  public ReviewDb get() {
+                    if (db == null) {
+                      try {
+                        db = schemaFactory.open();
+                      } catch (OrmException e) {
+                        throw new ProvisionException("Cannot open ReviewDb", e);
+                      }
+                    }
+                    return db;
+                  }
+                };
+              }
+            });
+        try {
+          userAccount = accountResolver.find(userName);
+          if (userAccount == null) {
+            LOGGER.error("No single user could be found when searching for listenAs: {}", userName);
+            return;
+          }
+
+          IdentifiedUser user = userFactory.create(userAccount.getId());
+          hooks.addChangeListener(publisher, user);
+          LOGGER.info("Listen events as : {}", userName);
+        } catch (OrmException e) {
+          LOGGER.error("Could not query database for listenAs", e);
+          return;
+        } finally {
+          threadLocalRequestContext.setContext(old);
+          if (db != null) {
+            db.close();
+            db = null;
+          }
+        }
+      }
+    });
+  }
+
+  @Override
+  public void removePublisher(final Publisher publisher) {
+    hooks.removeChangeListener(publisher);
+  }
+
+  @Override
+  public void clear() {
+    // no op.
+  }
+}