Code formatting of configuration framework
[sdc.git] / common / onap-common-configuration-management / onap-configuration-management-core / src / main / java / org / onap / config / impl / ConfigurationChangeNotifier.java
index 88c71c1..cd6481f 100644 (file)
@@ -1,11 +1,21 @@
+/*
+ * Copyright © 2016-2018 European Support Limited
+ *
+ * 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 org.onap.config.impl;
 
-import org.onap.config.ConfigurationUtils;
-import org.onap.config.Constants;
-import org.onap.config.api.ConfigurationChangeListener;
-import org.onap.config.api.ConfigurationManager;
-import org.onap.config.api.Hint;
 import java.io.File;
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
@@ -35,441 +45,349 @@ import java.util.concurrent.TimeUnit;
 import javax.management.JMX;
 import javax.management.MBeanServerConnection;
 import javax.management.ObjectName;
+import org.onap.config.ConfigurationUtils;
+import org.onap.config.Constants;
+import org.onap.config.api.ConfigurationChangeListener;
+import org.onap.config.api.ConfigurationManager;
+import org.onap.config.api.Hint;
 
 
-
-/**
- * The type Configuration change notifier.
- */
 public final class ConfigurationChangeNotifier {
 
-  private final HashMap<String, List<NotificationData>> store = new HashMap<>();
-  private final ScheduledExecutorService executor =
-      Executors.newScheduledThreadPool(5, ConfigurationUtils.getThreadFactory());
-  private final ExecutorService notificationExecutor =
-      Executors.newCachedThreadPool(ConfigurationUtils.getThreadFactory());
-  private final Map<String, WatchService> watchServiceCollection =
-      Collections.synchronizedMap(new HashMap<>());
-
-  static {
-    if (!Thread.currentThread().getStackTrace()[2].getClassName()
-        .equals(ConfigurationImpl.class.getName())) {
-      throw new RuntimeException("Illegal access.");
+    static {
+        if (!Thread.currentThread().getStackTrace()[2].getClassName().equals(ConfigurationImpl.class.getName())) {
+            throw new RuntimeException("Illegal access.");
+        }
     }
-  }
 
-  /**
-   * Instantiates a new Configuration change notifier.
-   *
-   * @param inMemoryConfig the in memory config
-   */
-  public ConfigurationChangeNotifier(Map<String, AggregateConfiguration> inMemoryConfig) {
-    executor.scheduleWithFixedDelay(() -> this
-        .pollFilesystemAndUpdateConfigurationIfRequired(inMemoryConfig,
-            System.getProperty("config.location"), false), 1, 1, TimeUnit.MILLISECONDS);
-    executor.scheduleWithFixedDelay(() -> this
-        .pollFilesystemAndUpdateConfigurationIfRequired(inMemoryConfig,
-            System.getProperty("tenant.config.location"), true), 1, 1, TimeUnit.MILLISECONDS);
-    executor.scheduleWithFixedDelay(() -> this
-        .pollFilesystemAndUpdateNodeSpecificConfigurationIfRequired(
-            System.getProperty("node.config.location")), 1, 1, TimeUnit.MILLISECONDS);
-  }
+    private final HashMap<String, List<NotificationData>> store = new HashMap<>();
+    private final ScheduledExecutorService executor =
+            Executors.newScheduledThreadPool(5, ConfigurationUtils.getThreadFactory());
+    private final ExecutorService notificationExecutor =
+            Executors.newCachedThreadPool(ConfigurationUtils.getThreadFactory());
+    private final Map<String, WatchService> watchServiceCollection = Collections.synchronizedMap(new HashMap<>());
 
-  /**
-   * Shutdown.
-   */
-  public void shutdown() {
-    for (WatchService watch : watchServiceCollection.values()) {
-      try {
-        watch.close();
-      } catch (IOException exception) {
-        //do nothing
-      }
+    public ConfigurationChangeNotifier(Map<String, AggregateConfiguration> inMemoryConfig) {
+        executor.scheduleWithFixedDelay(() -> this.pollFilesystemAndUpdateConfigurationIfRequired(inMemoryConfig,
+                System.getProperty("config.location"), false), 1, 1, TimeUnit.MILLISECONDS);
+        executor.scheduleWithFixedDelay(() -> this.pollFilesystemAndUpdateConfigurationIfRequired(inMemoryConfig,
+                System.getProperty("tenant.config.location"), true), 1, 1, TimeUnit.MILLISECONDS);
+        executor.scheduleWithFixedDelay(() -> this.pollFilesystemAndUpdateNodeSpecificConfigurationIfRequired(
+                System.getProperty("node.config.location")), 1, 1, TimeUnit.MILLISECONDS);
     }
-    executor.shutdownNow();
-  }
 
-  /**
-   * Poll filesystem and update configuration if required.
-   *
-   * @param inMemoryConfig   the in memory config
-   * @param location         the location
-   * @param isTenantLocation the is tenant location
-   */
-  public void pollFilesystemAndUpdateConfigurationIfRequired(
-      Map<String, AggregateConfiguration> inMemoryConfig, String location,
-      boolean isTenantLocation) {
-    try {
-      Set<Path> paths = watchForChange(location);
-      if (paths != null) {
-        for (Path path : paths) {
-          File file = path.toAbsolutePath().toFile();
-          String repositoryKey = null;
-          if (ConfigurationUtils.isConfig(file) && file.isFile()) {
-            if (isTenantLocation) {
-              Collection<File> tenantsRoot =
-                  ConfigurationUtils.getAllFiles(new File(location), false, true);
-              for (File tenantRoot : tenantsRoot) {
-                if (file.getAbsolutePath().startsWith(tenantRoot.getAbsolutePath())) {
-                  repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(
-                      (tenantRoot.getName() + Constants.TENANT_NAMESPACE_SAPERATOR
-                          + ConfigurationUtils.getNamespace(file))
-                          .split(Constants.TENANT_NAMESPACE_SAPERATOR));
+    public void pollFilesystemAndUpdateConfigurationIfRequired(Map<String, AggregateConfiguration> inMemoryConfig,
+            String location, boolean isTenantLocation) {
+        try {
+            Set<Path> paths = watchForChange(location);
+            if (paths != null) {
+                for (Path path : paths) {
+                    File file = path.toAbsolutePath().toFile();
+                    String repositoryKey = null;
+                    if (ConfigurationUtils.isConfig(file) && file.isFile()) {
+                        if (isTenantLocation) {
+                            Collection<File> tenantsRoot =
+                                    ConfigurationUtils.getAllFiles(new File(location), false, true);
+                            for (File tenantRoot : tenantsRoot) {
+                                if (file.getAbsolutePath().startsWith(tenantRoot.getAbsolutePath())) {
+                                    repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(
+                                            (tenantRoot.getName() + Constants.TENANT_NAMESPACE_SEPARATOR
+                                                     + ConfigurationUtils.getNamespace(file))
+                                                    .split(Constants.TENANT_NAMESPACE_SEPARATOR));
+                                }
+                            }
+                        } else {
+                            repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(file);
+                        }
+                        AggregateConfiguration config = inMemoryConfig.get(repositoryKey);
+                        if (config != null) {
+                            LinkedHashMap origConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
+                            config.addConfig(file);
+                            LinkedHashMap latestConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
+                            Map map = ConfigurationUtils.diff(origConfig, latestConfig);
+                            String[] tenantNamespaceArray = repositoryKey.split(Constants.KEY_ELEMENTS_DELIMETER);
+                            updateConfigurationValues(tenantNamespaceArray[0], tenantNamespaceArray[1], map);
+                        }
+                    } else {
+                        for (String configKey : inMemoryConfig.keySet()) {
+                            repositoryKey = configKey;
+                            AggregateConfiguration config = inMemoryConfig.get(repositoryKey);
+                            if (config.containsConfig(file)) {
+                                LinkedHashMap origConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
+                                config.removeConfig(file);
+                                LinkedHashMap latestConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
+                                Map map = ConfigurationUtils.diff(origConfig, latestConfig);
+                                String[] tenantNamespaceArray = repositoryKey.split(Constants.KEY_ELEMENTS_DELIMETER);
+                                updateConfigurationValues(tenantNamespaceArray[0], tenantNamespaceArray[1], map);
+                            }
+                        }
+                    }
                 }
-              }
-            } else {
-              repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(file);
             }
-            AggregateConfiguration config = inMemoryConfig.get(repositoryKey);
-            if (config != null) {
-              LinkedHashMap origConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
-              config.addConfig(file);
-              LinkedHashMap latestConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
-              Map map = ConfigurationUtils.diff(origConfig, latestConfig);
-              String[] tenantNamespaceArray =
-                  repositoryKey.split(Constants.KEY_ELEMENTS_DELEMETER);
-              updateConfigurationValues(tenantNamespaceArray[0], tenantNamespaceArray[1], map);
-            }
-          } else {
-            for (String configKey : inMemoryConfig.keySet()) {
-              repositoryKey = configKey;
-              AggregateConfiguration config = inMemoryConfig.get(repositoryKey);
-              if (config.containsConfig(file)) {
-                LinkedHashMap origConfig = ConfigurationUtils.toMap(config.getFinalConfiguration());
-                config.removeConfig(file);
-                LinkedHashMap latestConfig =
-                        ConfigurationUtils.toMap(config.getFinalConfiguration());
-                Map map = ConfigurationUtils.diff(origConfig, latestConfig);
-                String[] tenantNamespaceArray =
-                        repositoryKey.split(Constants.KEY_ELEMENTS_DELEMETER);
-                updateConfigurationValues(tenantNamespaceArray[0], tenantNamespaceArray[1],
-                        map);
-              }
-            }
-          }
+        } catch (ClosedWatchServiceException exception) {
+            // do nothing.
+        } catch (Exception exception) {
+            exception.printStackTrace();
         }
-      }
-    } catch (ClosedWatchServiceException exception) {
-      // do nothing.
-    } catch (Exception exception) {
-      exception.printStackTrace();
     }
-  }
-
-  private void updateConfigurationValues(String tenant, String namespace, Map map)
-      throws Exception {
-    MBeanServerConnection mbsc = ManagementFactory.getPlatformMBeanServer();
-    ObjectName mbeanName = new ObjectName(Constants.MBEAN_NAME);
-    ConfigurationManager conf =
-        JMX.newMBeanProxy(mbsc, mbeanName, ConfigurationManager.class,
-            true);
-    conf.updateConfigurationValues(tenant, namespace, map);
-  }
 
-  /**
-   * Poll filesystem and update node specific configuration if required.
-   *
-   * @param location the location
-   */
-  public void pollFilesystemAndUpdateNodeSpecificConfigurationIfRequired(String location) {
-    try {
-      Set<Path> paths = watchForChange(location);
-      if (paths != null) {
-        for (Path path : paths) {
-          File file = path.toAbsolutePath().toFile();
+    public void pollFilesystemAndUpdateNodeSpecificConfigurationIfRequired(String location) {
+        try {
+            Set<Path> paths = watchForChange(location);
+            if (paths != null) {
+                for (Path path : paths) {
+                    File file = path.toAbsolutePath().toFile();
 
-          if (ConfigurationUtils.isConfig(file)) {
-            String repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(file);
-            ConfigurationRepository.lookup().populateOverrideConfigurtaion(repositoryKey, file);
-          } else {
-            ConfigurationRepository.lookup().removeOverrideConfigurtaion(file);
-          }
+                    if (ConfigurationUtils.isConfig(file)) {
+                        String repositoryKey = ConfigurationUtils.getConfigurationRepositoryKey(file);
+                        ConfigurationRepository.lookup().populateOverrideConfiguration(repositoryKey, file);
+                    } else {
+                        ConfigurationRepository.lookup().removeOverrideConfiguration(file);
+                    }
+                }
+            }
+        } catch (Exception exception) {
+            exception.printStackTrace();
         }
-      }
-    } catch (Exception exception) {
-      exception.printStackTrace();
     }
-  }
 
-  /**
-   * Notify changes towards.
-   *
-   * @param tenant    the tenant
-   * @param component the component
-   * @param key       the key
-   * @param myself    the myself
-   * @throws Exception the exception
-   */
-  public void notifyChangesTowards(String tenant, String component, String key,
-                                   ConfigurationChangeListener myself) throws Exception {
-    List<NotificationData> notificationList =
-        store.get(tenant + Constants.KEY_ELEMENTS_DELEMETER + component);
-    if (notificationList == null) {
-      notificationList = Collections.synchronizedList(new ArrayList<>());
-      store.put(tenant + Constants.KEY_ELEMENTS_DELEMETER + component, notificationList);
-      executor.scheduleWithFixedDelay(
-          () -> triggerScanning(tenant + Constants.KEY_ELEMENTS_DELEMETER + component), 1, 30000,
-          TimeUnit.MILLISECONDS);
+    private Set<Path> watchForChange(String location) throws Exception {
+        if (location == null || location.trim().length() == 0) {
+            return Collections.emptySet();
+        }
+        File file = new File(location);
+        if (!file.exists()) {
+            return Collections.emptySet();
+        }
+        Path path = file.toPath();
+        Set<Path> toReturn = new HashSet<>();
+        try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
+            watchServiceCollection.put(location, watchService);
+            path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE,
+                    StandardWatchEventKinds.ENTRY_DELETE);
+            for (File dir : ConfigurationUtils.getAllFiles(file, true, true)) {
+                dir.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
+                        StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
+            }
+            while (true) {
+                final WatchKey wk = watchService.take();
+                Thread.sleep(ConfigurationRepository.lookup()
+                                     .getConfigurationFor(Constants.DEFAULT_TENANT, Constants.DB_NAMESPACE)
+                                     .getLong("event.fetch.delay"));
+                for (WatchEvent<?> event : wk.pollEvents()) {
+                    Object context = event.context();
+                    if (context instanceof Path) {
+                        File newFile = new File(((Path) wk.watchable()).toFile(), context.toString());
+                        if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
+                            if (newFile.isDirectory()) {
+                                newFile.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
+                                        StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
+                                continue;
+                            }
+                        } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
+                            if (newFile.isDirectory()) {
+                                continue;
+                            }
+                        }
+                        toReturn.add(newFile.toPath());
+                    }
+                }
+                if (toReturn.isEmpty()) {
+                    continue;
+                }
+                break;
+            }
+        }
+        return toReturn;
     }
-    notificationList.add(new NotificationData(tenant, component, key, myself));
-  }
 
-  /**
-   * Stop notification towards.
-   *
-   * @param tenant    the tenant
-   * @param component the component
-   * @param key       the key
-   * @param myself    the myself
-   * @throws Exception the exception
-   */
-  public void stopNotificationTowards(String tenant, String component, String key,
-                                      ConfigurationChangeListener myself) throws Exception {
-    List<NotificationData> notificationList =
-        store.get(tenant + Constants.KEY_ELEMENTS_DELEMETER + component);
-    if (notificationList != null) {
-      boolean removed =
-          notificationList.remove(new NotificationData(tenant, component, key, myself));
-      if (removed && notificationList.isEmpty()) {
-        store.remove(tenant + Constants.KEY_ELEMENTS_DELEMETER + component);
-      }
+    private void updateConfigurationValues(String tenant, String namespace, Map map) throws Exception {
+        MBeanServerConnection mbsc = ManagementFactory.getPlatformMBeanServer();
+        ObjectName mbeanName = new ObjectName(Constants.MBEAN_NAME);
+        ConfigurationManager conf = JMX.newMBeanProxy(mbsc, mbeanName, ConfigurationManager.class, true);
+        conf.updateConfigurationValues(tenant, namespace, map);
     }
 
-  }
-
-  private void triggerScanning(String key) {
-    if (store.get(key) != null) {
-      notificationExecutor.submit(() -> scanForChanges(key));
-    } else {
-      throw new IllegalArgumentException("Notification service for " + key + " is suspended.");
+    public void shutdown() {
+        for (WatchService watch : watchServiceCollection.values()) {
+            try {
+                watch.close();
+            } catch (IOException exception) {
+                //do nothing
+            }
+        }
+        executor.shutdownNow();
     }
-  }
 
-  private void scanForChanges(String key) {
-    List<NotificationData> list = store.get(key);
-    if (list != null) {
-      list.stream()
-              .filter(NotificationData::isChanged)
-              .forEach(notificationData -> notificationExecutor.submit(() -> sendNotification(notificationData)));
+    public void notifyChangesTowards(String tenant, String component, String key, ConfigurationChangeListener myself)
+            throws Exception {
+        List<NotificationData> notificationList = store.get(tenant + Constants.KEY_ELEMENTS_DELIMETER + component);
+        if (notificationList == null) {
+            notificationList = Collections.synchronizedList(new ArrayList<>());
+            store.put(tenant + Constants.KEY_ELEMENTS_DELIMETER + component, notificationList);
+            executor.scheduleWithFixedDelay(
+                    () -> triggerScanning(tenant + Constants.KEY_ELEMENTS_DELIMETER + component), 1, 30000,
+                    TimeUnit.MILLISECONDS);
+        }
+        notificationList.add(new NotificationData(tenant, component, key, myself));
     }
-  }
 
-  private void sendNotification(NotificationData notificationData) {
-    try {
-      notificationData.dispatchNotification();
-    } catch (Exception exception) {
-      exception.printStackTrace();
+    private void triggerScanning(String key) {
+        if (store.get(key) != null) {
+            notificationExecutor.submit(() -> scanForChanges(key));
+        } else {
+            throw new IllegalArgumentException("Notification service for " + key + " is suspended.");
+        }
     }
-  }
 
-  private Set<Path> watchForChange(String location) throws Exception {
-    if (location == null || location.trim().length() == 0) {
-      return Collections.emptySet();
+    private void scanForChanges(String key) {
+        List<NotificationData> list = store.get(key);
+        if (list != null) {
+            list.stream().filter(NotificationData::isChanged)
+                    .forEach(notificationData -> notificationExecutor.submit(() -> sendNotification(notificationData)));
+        }
     }
-    File file = new File(location);
-    if (!file.exists()) {
-      return Collections.emptySet();
+
+    private void sendNotification(NotificationData notificationData) {
+        try {
+            notificationData.dispatchNotification();
+        } catch (Exception exception) {
+            exception.printStackTrace();
+        }
     }
-    Path path = file.toPath();
-    Set<Path> toReturn = new HashSet<>();
-    try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
-      watchServiceCollection.put(location, watchService);
-      path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
-          StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
-      for (File dir : ConfigurationUtils.getAllFiles(file, true, true)) {
-        dir.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
-            StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
-      }
-      while (true) {
-        final WatchKey wk = watchService.take();
-        Thread.sleep(ConfigurationRepository.lookup()
-            .getConfigurationFor(Constants.DEFAULT_TENANT, Constants.DB_NAMESPACE)
-            .getLong("event.fetch.delay"));
-        for (WatchEvent<?> event : wk.pollEvents()) {
-          Object context = event.context();
-          if (context instanceof Path) {
-            File newFile = new File(((Path) wk.watchable()).toFile(), context.toString());
-            if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
-              if (newFile.isDirectory()) {
-                newFile.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
-                    StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
-                continue;
-              }
-            } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
-              if (newFile.isDirectory()) {
-                continue;
-              }
+
+    public void stopNotificationTowards(String tenant, String component, String key, ConfigurationChangeListener myself)
+            throws Exception {
+        List<NotificationData> notificationList = store.get(tenant + Constants.KEY_ELEMENTS_DELIMETER + component);
+        if (notificationList != null) {
+            boolean removed = notificationList.remove(new NotificationData(tenant, component, key, myself));
+            if (removed && notificationList.isEmpty()) {
+                store.remove(tenant + Constants.KEY_ELEMENTS_DELIMETER + component);
             }
-            toReturn.add(newFile.toPath());
-          }
         }
-        if (toReturn.isEmpty()) {
-          continue;
-        }
-        break;
-      }
+
     }
-    return toReturn;
-  }
 
-  /**
-   * The type Notification data.
-   */
-  static class NotificationData {
+    static class NotificationData {
 
-    /**
-     * The Tenant.
-     */
-    final String tenant;
-    /**
-     * The Namespace.
-     */
-    final String namespace;
-    /**
-     * The Key.
-     */
-    final String key;
-    /**
-     * The Myself.
-     */
-    final ConfigurationChangeListener myself;
-    /**
-     * The Current value.
-     */
-    Object currentValue;
-    /**
-     * The Is array.
-     */
-    boolean isArray;
+        final String tenant;
 
-    /**
-     * Instantiates a new Notification data.
-     *
-     * @param tenant    the tenant
-     * @param component the component
-     * @param key       the key
-     * @param myself    the myself
-     * @throws Exception the exception
-     */
-    public NotificationData(String tenant, String component, String key,
-                            ConfigurationChangeListener myself) throws Exception {
-      this.tenant = tenant;
-      this.namespace = component;
-      this.key = key;
-      this.myself = myself;
-      if (!ConfigurationRepository.lookup().getConfigurationFor(tenant, component)
-          .containsKey(key)) {
-        throw new RuntimeException("Key[" + key + "] not found.");
-      }
-      isArray = ConfigurationUtils.isArray(tenant, component, key, Hint.DEFAULT.value());
-      if (isArray) {
-        currentValue = ConfigurationManager.lookup().getAsStringValues(tenant, component, key);
-      } else {
-        currentValue = ConfigurationManager.lookup().getAsString(tenant, component, key);
-      }
-    }
+        final String namespace;
 
-    @Override
-    public boolean equals(Object obj) {
-      if (!(obj instanceof NotificationData)) {
-        return false;
-      }
-      NotificationData nd = (NotificationData) obj;
-      return Objects.equals(tenant, nd.tenant)
-              && Objects.equals(namespace, nd.namespace)
-              && Objects.equals(key, nd.key)
-              && Objects.equals(myself, nd.myself)
-              && Objects.equals(currentValue, nd.currentValue) // it's either String or List<String>
-              && isArray == nd.isArray;
-    }
+        final String key;
 
-    @Override
-    public int hashCode() {
-      return Objects.hash(tenant, namespace, key, myself, currentValue, isArray);
-    }
+        final ConfigurationChangeListener myself;
 
-    /**
-     * Is changed boolean.
-     *
-     * @return the boolean
-     */
-    public boolean isChanged() {
-      Object latestValue;
-      try {
-        if (isArray) {
-          latestValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
-        } else {
-          latestValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
-        }
-        if (!isArray) {
-          return !currentValue.equals(latestValue);
-        } else {
-          Collection<String> oldCollection = (Collection<String>) currentValue;
-          Collection<String> newCollection = (Collection<String>) latestValue;
-          for (String val : oldCollection) {
-            if (!newCollection.remove(val)) {
-              return true;
+        Object currentValue;
+
+        boolean isArray;
+
+        public NotificationData(String tenant, String component, String key, ConfigurationChangeListener myself)
+                throws Exception {
+            this.tenant = tenant;
+            this.namespace = component;
+            this.key = key;
+            this.myself = myself;
+            if (!ConfigurationRepository.lookup().getConfigurationFor(tenant, component).containsKey(key)) {
+                throw new RuntimeException("Key[" + key + "] not found.");
+            }
+            isArray = ConfigurationUtils.isArray(tenant, component, key, Hint.DEFAULT.value());
+            if (isArray) {
+                currentValue = ConfigurationManager.lookup().getAsStringValues(tenant, component, key);
+            } else {
+                currentValue = ConfigurationManager.lookup().getAsString(tenant, component, key);
             }
-          }
-          return !newCollection.isEmpty();
         }
-      } catch (Exception exception) {
-        return false;
-      }
-    }
 
-    /**
-     * Dispatch notification.
-     *
-     * @throws Exception the exception
-     */
-    public void dispatchNotification() throws Exception {
-      Method method = null;
-      Vector<Object> parameters = null;
-      try {
-        Object latestValue;
-        if (isArray) {
-          latestValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
-        } else {
-          latestValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
+        @Override
+        public int hashCode() {
+            return Objects.hash(tenant, namespace, key, myself, currentValue, isArray);
         }
-        Method[] methods = myself.getClass().getDeclaredMethods();
-        if (methods != null && methods.length > 0) {
-          method = methods[0];
-          int paramCount = method.getParameterCount();
-          parameters = new Vector<>();
-          if (paramCount > 4) {
-            if (tenant.equals(Constants.DEFAULT_TENANT)) {
-              parameters.add(null);
-            } else {
-              parameters.add(tenant);
-            }
-          }
-          if (paramCount > 3) {
-            if (namespace.equals(Constants.DEFAULT_NAMESPACE)) {
-              parameters.add(null);
-            } else {
-              parameters.add(namespace);
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof NotificationData)) {
+                return false;
             }
-          }
-          parameters.add(key);
-          parameters.add(currentValue);
-          parameters.add(latestValue);
-          method.setAccessible(true);
+            NotificationData nd = (NotificationData) obj;
+            return Objects.equals(tenant, nd.tenant) && Objects.equals(namespace, nd.namespace) && Objects.equals(key,
+                    nd.key) && Objects.equals(myself, nd.myself) && Objects.equals(currentValue, nd.currentValue)
+                           // it's either String or List<String>
+                           && isArray == nd.isArray;
         }
-      } catch (Exception exception) {
-        exception.printStackTrace();
-      } finally {
-        isArray = ConfigurationUtils.isArray(tenant, namespace, key, Hint.DEFAULT.value());
-        if (isArray) {
-          currentValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
-        } else {
-          currentValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
+
+        public boolean isChanged() {
+            Object latestValue;
+            try {
+                if (isArray) {
+                    latestValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
+                } else {
+                    latestValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
+                }
+                if (!isArray) {
+                    return !currentValue.equals(latestValue);
+                } else {
+                    Collection<String> oldCollection = (Collection<String>) currentValue;
+                    Collection<String> newCollection = (Collection<String>) latestValue;
+                    for (String val : oldCollection) {
+                        if (!newCollection.remove(val)) {
+                            return true;
+                        }
+                    }
+                    return !newCollection.isEmpty();
+                }
+            } catch (Exception exception) {
+                return false;
+            }
         }
-        if (method != null && parameters != null) {
-          method.invoke(myself, parameters.toArray());
+
+        public void dispatchNotification() throws Exception {
+            Method method = null;
+            Vector<Object> parameters = null;
+            try {
+                Object latestValue;
+                if (isArray) {
+                    latestValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
+                } else {
+                    latestValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
+                }
+                Method[] methods = myself.getClass().getDeclaredMethods();
+                if (methods != null && methods.length > 0) {
+                    method = methods[0];
+                    int paramCount = method.getParameterCount();
+                    parameters = new Vector<>();
+                    if (paramCount > 4) {
+                        if (tenant.equals(Constants.DEFAULT_TENANT)) {
+                            parameters.add(null);
+                        } else {
+                            parameters.add(tenant);
+                        }
+                    }
+                    if (paramCount > 3) {
+                        if (namespace.equals(Constants.DEFAULT_NAMESPACE)) {
+                            parameters.add(null);
+                        } else {
+                            parameters.add(namespace);
+                        }
+                    }
+                    parameters.add(key);
+                    parameters.add(currentValue);
+                    parameters.add(latestValue);
+                    method.setAccessible(true);
+                }
+            } catch (Exception exception) {
+                exception.printStackTrace();
+            } finally {
+                isArray = ConfigurationUtils.isArray(tenant, namespace, key, Hint.DEFAULT.value());
+                if (isArray) {
+                    currentValue = ConfigurationManager.lookup().getAsStringValues(tenant, namespace, key);
+                } else {
+                    currentValue = ConfigurationManager.lookup().getAsString(tenant, namespace, key);
+                }
+                if (method != null && parameters != null) {
+                    method.invoke(myself, parameters.toArray());
+                }
+            }
         }
-      }
     }
-  }
 }