You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2018/01/11 17:39:19 UTC

[52/53] [abbrv] knox git commit: Merge branch 'master' into KNOX-998-Package_Restructuring

Merge branch 'master' into KNOX-998-Package_Restructuring

# Conflicts:
#	gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java


Project: http://git-wip-us.apache.org/repos/asf/knox/repo
Commit: http://git-wip-us.apache.org/repos/asf/knox/commit/e5fd0622
Tree: http://git-wip-us.apache.org/repos/asf/knox/tree/e5fd0622
Diff: http://git-wip-us.apache.org/repos/asf/knox/diff/e5fd0622

Branch: refs/heads/master
Commit: e5fd0622493a7e3c62811ea03ff7931c979dd87a
Parents: e766b3b 99e6a54
Author: Sandeep More <mo...@apache.org>
Authored: Tue Jan 9 14:25:16 2018 -0500
Committer: Sandeep More <mo...@apache.org>
Committed: Tue Jan 9 14:25:16 2018 -0500

----------------------------------------------------------------------
 LICENSE                                         |  40 ++++-
 NOTICE                                          |   4 +-
 .../discovery/ambari/ServiceURLCreator.java     |  32 ++++
 .../discovery/ambari/ServiceURLFactory.java     |  75 +++++++++
 .../discovery/ambari/WebHdfsUrlCreator.java     |  84 ++++++++++
 .../discovery/ambari/AmbariClientCommon.java    |  14 +-
 .../discovery/ambari/AmbariCluster.java         |   6 +-
 .../ambari/AmbariConfigurationMonitor.java      |  52 +++++--
 .../ambari/AmbariDynamicServiceURLCreator.java  |   4 +-
 .../discovery/ambari/PropertyEqualsHandler.java |  20 ++-
 .../ambari/ServiceURLPropertyConfig.java        |   7 +-
 .../ambari-service-discovery-url-mappings.xml   |  24 +--
 .../AmbariDynamicServiceURLCreatorTest.java     | 116 +++++++++-----
 gateway-release/src/assembly.xml                |   1 +
 gateway-server/pom.xml                          |   2 +-
 .../filter/PortMappingHelperHandler.java        |   2 +-
 .../topology/impl/DefaultTopologyService.java   |  40 +++--
 .../DefaultRemoteConfigurationMonitor.java      |  22 ++-
 .../org/apache/knox/gateway/util/KnoxCLI.java   | 153 ++++++++++++-------
 .../ZooKeeperConfigurationMonitorTest.java      |  17 ++-
 .../apache/knox/gateway/util/KnoxCLITest.java   |  16 ++
 gateway-service-remoteconfig/pom.xml            |   5 -
 gateway-test-release/pom.xml                    |  72 ++++++++-
 pom.xml                                         |  32 +++-
 24 files changed, 690 insertions(+), 150 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariClientCommon.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariClientCommon.java
index 9e5dcb3,0000000..1314305
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariClientCommon.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariClientCommon.java
@@@ -1,102 -1,0 +1,108 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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
 + * <p>
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * <p>
 + * 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +import net.minidev.json.JSONArray;
 +import net.minidev.json.JSONObject;
 +import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 +import org.apache.knox.gateway.services.security.AliasService;
 +import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
 +
 +import java.util.HashMap;
 +import java.util.Map;
 +
 +class AmbariClientCommon {
 +
 +    static final String AMBARI_CLUSTERS_URI = "/api/v1/clusters";
 +
 +    static final String AMBARI_HOSTROLES_URI =
 +                                    AMBARI_CLUSTERS_URI + "/%s/services?fields=components/host_components/HostRoles";
 +
 +    static final String AMBARI_SERVICECONFIGS_URI =
 +                                    AMBARI_CLUSTERS_URI + "/%s/configurations/service_config_versions?is_current=true";
 +
 +    private static final AmbariServiceDiscoveryMessages log = MessagesFactory.get(AmbariServiceDiscoveryMessages.class);
 +
 +    private RESTInvoker restClient;
 +
 +
 +    AmbariClientCommon(AliasService aliasService) {
 +        this(new RESTInvoker(aliasService));
 +    }
 +
 +
 +    AmbariClientCommon(RESTInvoker restInvoker) {
 +        this.restClient = restInvoker;
 +    }
 +
 +
 +
 +    Map<String, Map<String, AmbariCluster.ServiceConfiguration>> getActiveServiceConfigurations(String clusterName,
 +                                                                                                ServiceDiscoveryConfig config) {
-         return getActiveServiceConfigurations(config.getAddress(),
-                                               clusterName,
-                                               config.getUser(),
-                                               config.getPasswordAlias());
++        Map<String, Map<String, AmbariCluster.ServiceConfiguration>> activeConfigs = null;
++
++        if (config != null) {
++            activeConfigs = getActiveServiceConfigurations(config.getAddress(),
++                                                           clusterName,
++                                                           config.getUser(),
++                                                           config.getPasswordAlias());
++        }
++
++        return activeConfigs;
 +    }
 +
 +
 +    Map<String, Map<String, AmbariCluster.ServiceConfiguration>> getActiveServiceConfigurations(String discoveryAddress,
 +                                                                                                String clusterName,
 +                                                                                                String discoveryUser,
 +                                                                                                String discoveryPwdAlias) {
 +        Map<String, Map<String, AmbariCluster.ServiceConfiguration>> serviceConfigurations = new HashMap<>();
 +
 +        String serviceConfigsURL = String.format("%s" + AMBARI_SERVICECONFIGS_URI, discoveryAddress, clusterName);
 +
 +        JSONObject serviceConfigsJSON = restClient.invoke(serviceConfigsURL, discoveryUser, discoveryPwdAlias);
 +        if (serviceConfigsJSON != null) {
 +            // Process the service configurations
 +            JSONArray serviceConfigs = (JSONArray) serviceConfigsJSON.get("items");
 +            for (Object serviceConfig : serviceConfigs) {
 +                String serviceName = (String) ((JSONObject) serviceConfig).get("service_name");
 +                JSONArray configurations = (JSONArray) ((JSONObject) serviceConfig).get("configurations");
 +                for (Object configuration : configurations) {
 +                    String configType = (String) ((JSONObject) configuration).get("type");
 +                    String configVersion = String.valueOf(((JSONObject) configuration).get("version"));
 +
 +                    Map<String, String> configProps = new HashMap<>();
 +                    JSONObject configProperties = (JSONObject) ((JSONObject) configuration).get("properties");
 +                    for (String propertyName : configProperties.keySet()) {
 +                        configProps.put(propertyName, String.valueOf(((JSONObject) configProperties).get(propertyName)));
 +                    }
 +                    if (!serviceConfigurations.containsKey(serviceName)) {
 +                        serviceConfigurations.put(serviceName, new HashMap<>());
 +                    }
 +                    serviceConfigurations.get(serviceName).put(configType,
 +                                                               new AmbariCluster.ServiceConfiguration(configType,
 +                                                                                                      configVersion,
 +                                                                                                      configProps));
 +                }
 +            }
 +        }
 +
 +        return serviceConfigurations;
 +    }
 +
 +
 +}

http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariCluster.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariCluster.java
index bcf3adc,0000000..9d3fa74
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariCluster.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariCluster.java
@@@ -1,120 -1,0 +1,120 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +import org.apache.knox.gateway.topology.discovery.ServiceDiscovery;
 +
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +class AmbariCluster implements ServiceDiscovery.Cluster {
 +
 +    private String name = null;
 +
-     private AmbariDynamicServiceURLCreator urlCreator;
++    private ServiceURLFactory urlFactory;
 +
 +    private Map<String, Map<String, ServiceConfiguration>> serviceConfigurations = new HashMap<>();
 +
 +    private Map<String, AmbariComponent> components = null;
 +
 +
 +    AmbariCluster(String name) {
 +        this.name = name;
 +        components = new HashMap<>();
-         urlCreator = new AmbariDynamicServiceURLCreator(this);
++        urlFactory = ServiceURLFactory.newInstance(this);
 +    }
 +
 +    void addServiceConfiguration(String serviceName, String configurationType, ServiceConfiguration serviceConfig) {
 +        if (!serviceConfigurations.keySet().contains(serviceName)) {
 +            serviceConfigurations.put(serviceName, new HashMap<>());
 +        }
 +        serviceConfigurations.get(serviceName).put(configurationType, serviceConfig);
 +    }
 +
 +
 +    void addComponent(AmbariComponent component) {
 +        components.put(component.getName(), component);
 +    }
 +
 +
 +    ServiceConfiguration getServiceConfiguration(String serviceName, String configurationType) {
 +        ServiceConfiguration sc = null;
 +        Map<String, ServiceConfiguration> configs = serviceConfigurations.get(serviceName);
 +        if (configs != null) {
 +            sc = configs.get(configurationType);
 +        }
 +        return sc;
 +    }
 +
 +
 +    Map<String, Map<String, ServiceConfiguration>> getServiceConfigurations() {
 +        return serviceConfigurations;
 +    }
 +
 +
 +    Map<String, AmbariComponent> getComponents() {
 +        return components;
 +    }
 +
 +
 +    AmbariComponent getComponent(String name) {
 +        return components.get(name);
 +    }
 +
 +
 +    @Override
 +    public String getName() {
 +        return name;
 +    }
 +
 +
 +    @Override
 +    public List<String> getServiceURLs(String serviceName) {
 +        List<String> urls = new ArrayList<>();
-         urls.addAll(urlCreator.create(serviceName));
++        urls.addAll(urlFactory.create(serviceName));
 +        return urls;
 +    }
 +
 +
 +    static class ServiceConfiguration {
 +
 +        private String type;
 +        private String version;
 +        private Map<String, String> props;
 +
 +        ServiceConfiguration(String type, String version, Map<String, String> properties) {
 +            this.type = type;
 +            this.version = version;
 +            this.props = properties;
 +        }
 +
 +        public String getVersion() {
 +            return version;
 +        }
 +
 +        public String getType() {
 +            return type;
 +        }
 +
 +        public Map<String, String> getProperties() {
 +            return props;
 +        }
 +    }
 +
 +}

http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
index c3aa27a,0000000..920b05c7
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariConfigurationMonitor.java
@@@ -1,525 -1,0 +1,559 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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
 + * <p>
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * <p>
 + * 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +import org.apache.commons.io.FileUtils;
 +import org.apache.knox.gateway.config.GatewayConfig;
 +import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 +import org.apache.knox.gateway.services.security.AliasService;
 +import org.apache.knox.gateway.topology.discovery.ClusterConfigurationMonitor;
 +import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig;
 +
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.FileOutputStream;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Properties;
 +import java.util.concurrent.locks.ReadWriteLock;
 +import java.util.concurrent.locks.ReentrantReadWriteLock;
 +
 +
 +class AmbariConfigurationMonitor implements ClusterConfigurationMonitor {
 +
 +    private static final String TYPE = "Ambari";
 +
 +    private static final String CLUSTERS_DATA_DIR_NAME = "clusters";
 +
 +    private static final String PERSISTED_FILE_COMMENT = "Generated File. Do Not Edit!";
 +
 +    private static final String PROP_CLUSTER_PREFIX = "cluster.";
 +    private static final String PROP_CLUSTER_SOURCE = PROP_CLUSTER_PREFIX + "source";
 +    private static final String PROP_CLUSTER_NAME   = PROP_CLUSTER_PREFIX + "name";
 +    private static final String PROP_CLUSTER_USER   = PROP_CLUSTER_PREFIX + "user";
 +    private static final String PROP_CLUSTER_ALIAS  = PROP_CLUSTER_PREFIX + "pwd.alias";
 +
 +    static final String INTERVAL_PROPERTY_NAME = "org.apache.knox.gateway.topology.discovery.ambari.monitor.interval";
 +
 +
 +    private static final AmbariServiceDiscoveryMessages log = MessagesFactory.get(AmbariServiceDiscoveryMessages.class);
 +
 +    // Ambari address
 +    //    clusterName -> ServiceDiscoveryConfig
 +    //
 +    Map<String, Map<String, ServiceDiscoveryConfig>> clusterMonitorConfigurations = new HashMap<>();
 +
 +    // Ambari address
 +    //    clusterName
 +    //        configType -> version
 +    //
 +    Map<String, Map<String, Map<String, String>>> ambariClusterConfigVersions = new HashMap<>();
 +
 +    ReadWriteLock configVersionsLock = new ReentrantReadWriteLock();
 +
 +    private List<ConfigurationChangeListener> changeListeners = new ArrayList<>();
 +
 +    private AmbariClientCommon ambariClient;
 +
 +    PollingConfigAnalyzer internalMonitor;
 +
 +    GatewayConfig gatewayConfig = null;
 +
 +    static String getType() {
 +        return TYPE;
 +    }
 +
 +    AmbariConfigurationMonitor(GatewayConfig config, AliasService aliasService) {
 +        this.gatewayConfig   = config;
 +        this.ambariClient    = new AmbariClientCommon(aliasService);
 +        this.internalMonitor = new PollingConfigAnalyzer(this);
 +
 +        // Override the default polling interval if it has been configured
 +        int interval = config.getClusterMonitorPollingInterval(getType());
 +        if (interval > 0) {
 +            setPollingInterval(interval);
 +        }
 +
 +        init();
 +    }
 +
 +    @Override
 +    public void setPollingInterval(int interval) {
 +        internalMonitor.setInterval(interval);
 +    }
 +
 +    private void init() {
 +        loadDiscoveryConfiguration();
 +        loadClusterVersionData();
 +    }
 +
 +    /**
 +     * Load any previously-persisted service discovery configurations.
 +     * This is necessary for checking previously-deployed topologies.
 +     */
 +    private void loadDiscoveryConfiguration() {
 +        File persistenceDir = getPersistenceDir();
 +        if (persistenceDir != null) {
 +            Collection<File> persistedConfigs = FileUtils.listFiles(persistenceDir, new String[]{"conf"}, false);
 +            for (File persisted : persistedConfigs) {
 +                Properties props = new Properties();
++                FileInputStream in = null;
 +                try {
-                     props.load(new FileInputStream(persisted));
++                    in = new FileInputStream(persisted);
++                    props.load(in);
 +
 +                    addDiscoveryConfig(props.getProperty(PROP_CLUSTER_NAME), new ServiceDiscoveryConfig() {
 +                                                            public String getAddress() {
 +                                                                return props.getProperty(PROP_CLUSTER_SOURCE);
 +                                                            }
 +
 +                                                            public String getUser() {
 +                                                                return props.getProperty(PROP_CLUSTER_USER);
 +                                                            }
 +
 +                                                            public String getPasswordAlias() {
 +                                                                return props.getProperty(PROP_CLUSTER_ALIAS);
 +                                                            }
 +                                                        });
 +                } catch (IOException e) {
 +                    log.failedToLoadClusterMonitorServiceDiscoveryConfig(getType(), e);
++                } finally {
++                    if (in != null) {
++                        try {
++                            in.close();
++                        } catch (IOException e) {
++                            //
++                        }
++                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Load any previously-persisted cluster configuration version records, so the monitor will check
 +     * previously-deployed topologies against the current cluster configuration.
 +     */
 +    private void loadClusterVersionData() {
 +        File persistenceDir = getPersistenceDir();
 +        if (persistenceDir != null) {
-             Collection<File> persistedConfigs = FileUtils.listFiles(getPersistenceDir(), new String[]{"ver"}, false);
++            Collection<File> persistedConfigs = FileUtils.listFiles(persistenceDir, new String[]{"ver"}, false);
 +            for (File persisted : persistedConfigs) {
 +                Properties props = new Properties();
++                FileInputStream in = null;
 +                try {
-                     props.load(new FileInputStream(persisted));
++                    in = new FileInputStream(persisted);
++                    props.load(in);
 +
 +                    String source = props.getProperty(PROP_CLUSTER_SOURCE);
 +                    String clusterName = props.getProperty(PROP_CLUSTER_NAME);
 +
 +                    Map<String, String> configVersions = new HashMap<>();
 +                    for (String name : props.stringPropertyNames()) {
 +                        if (!name.startsWith(PROP_CLUSTER_PREFIX)) { // Ignore implementation-specific properties
 +                            configVersions.put(name, props.getProperty(name));
 +                        }
 +                    }
 +
 +                    // Map the config versions to the cluster name
 +                    addClusterConfigVersions(source, clusterName, configVersions);
 +
 +                } catch (IOException e) {
 +                    log.failedToLoadClusterMonitorConfigVersions(getType(), e);
++                } finally {
++                    if (in != null) {
++                        try {
++                            in.close();
++                        } catch (IOException e) {
++                            //
++                        }
++                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    private void persistDiscoveryConfiguration(String clusterName, ServiceDiscoveryConfig sdc) {
 +        File persistenceDir = getPersistenceDir();
 +        if (persistenceDir != null) {
 +
 +            Properties props = new Properties();
 +            props.setProperty(PROP_CLUSTER_NAME, clusterName);
 +            props.setProperty(PROP_CLUSTER_SOURCE, sdc.getAddress());
 +
 +            String username = sdc.getUser();
 +            if (username != null) {
 +                props.setProperty(PROP_CLUSTER_USER, username);
 +            }
 +            String pwdAlias = sdc.getPasswordAlias();
 +            if (pwdAlias != null) {
 +                props.setProperty(PROP_CLUSTER_ALIAS, pwdAlias);
 +            }
 +
 +            persist(props, getDiscoveryConfigPersistenceFile(sdc.getAddress(), clusterName));
 +        }
 +    }
 +
 +    private void persistClusterVersionData(String address, String clusterName, Map<String, String> configVersions) {
 +        File persistenceDir = getPersistenceDir();
 +        if (persistenceDir != null) {
 +            Properties props = new Properties();
 +            props.setProperty(PROP_CLUSTER_NAME, clusterName);
 +            props.setProperty(PROP_CLUSTER_SOURCE, address);
 +            for (String name : configVersions.keySet()) {
 +                props.setProperty(name, configVersions.get(name));
 +            }
 +
 +            persist(props, getConfigVersionsPersistenceFile(address, clusterName));
 +        }
 +    }
 +
 +    private void persist(Properties props, File dest) {
++        FileOutputStream out = null;
 +        try {
-             props.store(new FileOutputStream(dest), PERSISTED_FILE_COMMENT);
++            out = new FileOutputStream(dest);
++            props.store(out, PERSISTED_FILE_COMMENT);
++            out.flush();
 +        } catch (Exception e) {
 +            log.failedToPersistClusterMonitorData(getType(), dest.getAbsolutePath(), e);
++        } finally {
++            if (out != null) {
++                try {
++                    out.close();
++                } catch (IOException e) {
++                    //
++                }
++            }
 +        }
 +    }
 +
 +    private File getPersistenceDir() {
 +        File persistenceDir = null;
 +
 +        File dataDir = new File(gatewayConfig.getGatewayDataDir());
 +        if (dataDir.exists()) {
 +            File clustersDir = new File(dataDir, CLUSTERS_DATA_DIR_NAME);
 +            if (!clustersDir.exists()) {
 +                clustersDir.mkdirs();
 +            }
 +            persistenceDir = clustersDir;
 +        }
 +
 +        return persistenceDir;
 +    }
 +
 +    private File getDiscoveryConfigPersistenceFile(String address, String clusterName) {
 +        return getPersistenceFile(address, clusterName, "conf");
 +    }
 +
 +    private File getConfigVersionsPersistenceFile(String address, String clusterName) {
 +        return getPersistenceFile(address, clusterName, "ver");
 +    }
 +
 +    private File getPersistenceFile(String address, String clusterName, String ext) {
 +        String fileName = address.replace(":", "_").replace("/", "_") + "-" + clusterName + "." + ext;
 +        return new File(getPersistenceDir(), fileName);
 +    }
 +
 +    /**
 +     * Add cluster configuration details to the monitor's in-memory record.
 +     *
 +     * @param address        An Ambari instance address.
 +     * @param clusterName    The name of a cluster associated with the Ambari instance.
 +     * @param configVersions A Map of configuration types and their corresponding versions.
 +     */
 +    private void addClusterConfigVersions(String address, String clusterName, Map<String, String> configVersions) {
 +        configVersionsLock.writeLock().lock();
 +        try {
 +            ambariClusterConfigVersions.computeIfAbsent(address, k -> new HashMap<>())
 +                                       .put(clusterName, configVersions);
 +        } finally {
 +            configVersionsLock.writeLock().unlock();
 +        }
 +    }
 +
 +    public void start() {
 +        (new Thread(internalMonitor, "AmbariConfigurationMonitor")).start();
 +    }
 +
 +    public void stop() {
 +        internalMonitor.stop();
 +    }
 +
 +    @Override
 +    public void addListener(ConfigurationChangeListener listener) {
 +        changeListeners.add(listener);
 +    }
 +
 +    /**
 +     * Add discovery configuration details for the specified cluster, so the monitor knows how to connect to check for
 +     * changes.
 +     *
 +     * @param clusterName The name of the cluster.
 +     * @param config      The associated service discovery configuration.
 +     */
 +    void addDiscoveryConfig(String clusterName, ServiceDiscoveryConfig config) {
 +        clusterMonitorConfigurations.computeIfAbsent(config.getAddress(), k -> new HashMap<>()).put(clusterName, config);
 +    }
 +
 +
 +    /**
 +     * Get the service discovery configuration associated with the specified Ambari instance and cluster.
 +     *
 +     * @param address     An Ambari instance address.
 +     * @param clusterName The name of a cluster associated with the Ambari instance.
 +     *
 +     * @return The associated ServiceDiscoveryConfig object.
 +     */
 +    ServiceDiscoveryConfig getDiscoveryConfig(String address, String clusterName) {
 +        ServiceDiscoveryConfig config = null;
 +        if (clusterMonitorConfigurations.containsKey(address)) {
 +            config = clusterMonitorConfigurations.get(address).get(clusterName);
 +        }
 +        return config;
 +    }
 +
 +
 +    /**
 +     * Add cluster configuration data to the monitor, which it will use when determining if configuration has changed.
 +     *
 +     * @param cluster         An AmbariCluster object.
 +     * @param discoveryConfig The discovery configuration associated with the cluster.
 +     */
 +    void addClusterConfigVersions(AmbariCluster cluster, ServiceDiscoveryConfig discoveryConfig) {
 +
 +        String clusterName = cluster.getName();
 +
 +        // Register the cluster discovery configuration for the monitor connections
 +        persistDiscoveryConfiguration(clusterName, discoveryConfig);
 +        addDiscoveryConfig(clusterName, discoveryConfig);
 +
 +        // Build the set of configuration versions
 +        Map<String, String> configVersions = new HashMap<>();
 +        Map<String, Map<String, AmbariCluster.ServiceConfiguration>> serviceConfigs = cluster.getServiceConfigurations();
 +        for (String serviceName : serviceConfigs.keySet()) {
 +            Map<String, AmbariCluster.ServiceConfiguration> configTypeVersionMap = serviceConfigs.get(serviceName);
 +            for (AmbariCluster.ServiceConfiguration config : configTypeVersionMap.values()) {
 +                String configType = config.getType();
 +                String version = config.getVersion();
 +                configVersions.put(configType, version);
 +            }
 +        }
 +
 +        persistClusterVersionData(discoveryConfig.getAddress(), clusterName, configVersions);
 +        addClusterConfigVersions(discoveryConfig.getAddress(), clusterName, configVersions);
 +    }
 +
 +
 +    /**
 +     * Remove the configuration record for the specified Ambari instance and cluster name.
 +     *
 +     * @param address     An Ambari instance address.
 +     * @param clusterName The name of a cluster associated with the Ambari instance.
 +     *
 +     * @return The removed data; A Map of configuration types and their corresponding versions.
 +     */
 +    Map<String, String> removeClusterConfigVersions(String address, String clusterName) {
 +        Map<String, String> result = new HashMap<>();
 +
 +        configVersionsLock.writeLock().lock();
 +        try {
 +            if (ambariClusterConfigVersions.containsKey(address)) {
 +                result.putAll(ambariClusterConfigVersions.get(address).remove(clusterName));
 +            }
 +        } finally {
 +            configVersionsLock.writeLock().unlock();
 +        }
 +
 +        // Delete the associated persisted record
 +        File persisted = getConfigVersionsPersistenceFile(address, clusterName);
 +        if (persisted.exists()) {
 +            persisted.delete();
 +        }
 +
 +        return result;
 +    }
 +
 +    /**
 +     * Get the cluster configuration details for the specified cluster and Ambari instance.
 +     *
 +     * @param address     An Ambari instance address.
 +     * @param clusterName The name of a cluster associated with the Ambari instance.
 +     *
 +     * @return A Map of configuration types and their corresponding versions.
 +     */
 +    Map<String, String> getClusterConfigVersions(String address, String clusterName) {
 +        Map<String, String> result = new HashMap<>();
 +
 +        configVersionsLock.readLock().lock();
 +        try {
 +            if (ambariClusterConfigVersions.containsKey(address)) {
 +                result.putAll(ambariClusterConfigVersions.get(address).get(clusterName));
 +            }
 +        } finally {
 +            configVersionsLock.readLock().unlock();
 +        }
 +
 +        return result;
 +    }
 +
 +
 +    /**
 +     * Get all the clusters the monitor knows about.
 +     *
 +     * @return A Map of Ambari instance addresses to associated cluster names.
 +     */
 +    Map<String, List<String>> getClusterNames() {
 +        Map<String, List<String>> result = new HashMap<>();
 +
 +        configVersionsLock.readLock().lock();
 +        try {
 +            for (String address : ambariClusterConfigVersions.keySet()) {
 +                List<String> clusterNames = new ArrayList<>();
 +                clusterNames.addAll(ambariClusterConfigVersions.get(address).keySet());
 +                result.put(address, clusterNames);
 +            }
 +        } finally {
 +            configVersionsLock.readLock().unlock();
 +        }
 +
 +        return result;
 +
 +    }
 +
 +
 +    /**
 +     * Notify registered change listeners.
 +     *
 +     * @param source      The address of the Ambari instance from which the cluster details were determined.
 +     * @param clusterName The name of the cluster whose configuration details have changed.
 +     */
 +    void notifyChangeListeners(String source, String clusterName) {
 +        for (ConfigurationChangeListener listener : changeListeners) {
 +            listener.onConfigurationChange(source, clusterName);
 +        }
 +    }
 +
 +
 +    /**
 +     * Request the current active configuration version info from Ambari.
 +     *
 +     * @param address     The Ambari instance address.
 +     * @param clusterName The name of the cluster for which the details are desired.
 +     *
 +     * @return A Map of service configuration types and their corresponding versions.
 +     */
 +    Map<String, String> getUpdatedConfigVersions(String address, String clusterName) {
 +        Map<String, String> configVersions = new HashMap<>();
 +
-         Map<String, Map<String, AmbariCluster.ServiceConfiguration>> serviceConfigs =
-                     ambariClient.getActiveServiceConfigurations(clusterName, getDiscoveryConfig(address, clusterName));
++        ServiceDiscoveryConfig sdc = getDiscoveryConfig(address, clusterName);
++        if (sdc != null) {
++            Map<String, Map<String, AmbariCluster.ServiceConfiguration>> serviceConfigs =
++                                                       ambariClient.getActiveServiceConfigurations(clusterName, sdc);
 +
-         for (Map<String, AmbariCluster.ServiceConfiguration> serviceConfig : serviceConfigs.values()) {
-             for (AmbariCluster.ServiceConfiguration config : serviceConfig.values()) {
-                 configVersions.put(config.getType(), config.getVersion());
++            for (Map<String, AmbariCluster.ServiceConfiguration> serviceConfig : serviceConfigs.values()) {
++                for (AmbariCluster.ServiceConfiguration config : serviceConfig.values()) {
++                    configVersions.put(config.getType(), config.getVersion());
++                }
 +            }
 +        }
 +
 +        return configVersions;
 +    }
 +
 +
 +    /**
 +     * The thread that polls Ambari for configuration details for clusters associated with discovered topologies,
 +     * compares them with the current recorded values, and notifies any listeners when differences are discovered.
 +     */
 +    static final class PollingConfigAnalyzer implements Runnable {
 +
 +        private static final int DEFAULT_POLLING_INTERVAL = 60;
 +
 +        // Polling interval in seconds
 +        private int interval = DEFAULT_POLLING_INTERVAL;
 +
 +        private AmbariConfigurationMonitor delegate;
 +
 +        private boolean isActive = false;
 +
 +        PollingConfigAnalyzer(AmbariConfigurationMonitor delegate) {
 +            this.delegate = delegate;
 +            this.interval = Integer.getInteger(INTERVAL_PROPERTY_NAME, PollingConfigAnalyzer.DEFAULT_POLLING_INTERVAL);
 +        }
 +
 +        void setInterval(int interval) {
 +            this.interval = interval;
 +        }
 +
 +
 +        void stop() {
 +            isActive = false;
 +        }
 +
 +        @Override
 +        public void run() {
 +            isActive = true;
 +
 +            log.startedAmbariConfigMonitor(interval);
 +
 +            while (isActive) {
 +                for (Map.Entry<String, List<String>> entry : delegate.getClusterNames().entrySet()) {
 +                    String address = entry.getKey();
 +                    for (String clusterName : entry.getValue()) {
 +                        Map<String, String> configVersions = delegate.getClusterConfigVersions(address, clusterName);
 +                        if (configVersions != null && !configVersions.isEmpty()) {
 +                            Map<String, String> updatedVersions = delegate.getUpdatedConfigVersions(address, clusterName);
 +                            if (updatedVersions != null && !updatedVersions.isEmpty()) {
 +                                boolean configHasChanged = false;
 +
 +                                // If the config sets don't match in size, then something has changed
 +                                if (updatedVersions.size() != configVersions.size()) {
 +                                    configHasChanged = true;
 +                                } else {
 +                                    // Perform the comparison of all the config versions
 +                                    for (Map.Entry<String, String> configVersion : configVersions.entrySet()) {
 +                                        if (!updatedVersions.get(configVersion.getKey()).equals(configVersion.getValue())) {
 +                                            configHasChanged = true;
 +                                            break;
 +                                        }
 +                                    }
 +                                }
 +
 +                                // If a change has occurred, notify the listeners
 +                                if (configHasChanged) {
 +                                    delegate.notifyChangeListeners(address, clusterName);
 +                                }
 +                            }
 +                        }
 +                    }
 +                }
 +
 +                try {
 +                    Thread.sleep(interval * 1000);
 +                } catch (InterruptedException e) {
 +                    // Ignore
 +                }
 +            }
 +        }
 +    }
 +
 +}

http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariDynamicServiceURLCreator.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariDynamicServiceURLCreator.java
index 3c2269d,0000000..dc4ac49
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariDynamicServiceURLCreator.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariDynamicServiceURLCreator.java
@@@ -1,151 -1,0 +1,151 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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
 + * <p>
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * <p>
 + * 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 +
 +import java.io.ByteArrayInputStream;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +
- class AmbariDynamicServiceURLCreator {
++class AmbariDynamicServiceURLCreator implements ServiceURLCreator {
 +
 +    static final String MAPPING_CONFIG_OVERRIDE_PROPERTY = "org.apache.gateway.topology.discovery.ambari.config";
 +
 +    private AmbariServiceDiscoveryMessages log = MessagesFactory.get(AmbariServiceDiscoveryMessages.class);
 +
 +    private AmbariCluster cluster = null;
 +    private ServiceURLPropertyConfig config;
 +
 +    AmbariDynamicServiceURLCreator(AmbariCluster cluster) {
 +        this.cluster = cluster;
 +
 +        String mappingConfiguration = System.getProperty(MAPPING_CONFIG_OVERRIDE_PROPERTY);
 +        if (mappingConfiguration != null) {
 +            File mappingConfigFile = new File(mappingConfiguration);
 +            if (mappingConfigFile.exists()) {
 +                try {
 +                    config = new ServiceURLPropertyConfig(mappingConfigFile);
 +                    log.loadedComponentConfigMappings(mappingConfigFile.getAbsolutePath());
 +                } catch (Exception e) {
 +                    log.failedToLoadComponentConfigMappings(mappingConfigFile.getAbsolutePath(), e);
 +                }
 +            }
 +        }
 +
 +        // If there is no valid override configured, fall-back to the internal mapping configuration
 +        if (config == null) {
 +            config = new ServiceURLPropertyConfig();
 +        }
 +    }
 +
 +    AmbariDynamicServiceURLCreator(AmbariCluster cluster, File mappingConfiguration) throws IOException {
 +        this.cluster = cluster;
 +        config = new ServiceURLPropertyConfig(new FileInputStream(mappingConfiguration));
 +    }
 +
 +    AmbariDynamicServiceURLCreator(AmbariCluster cluster, String mappings) {
 +        this.cluster = cluster;
 +        config = new ServiceURLPropertyConfig(new ByteArrayInputStream(mappings.getBytes()));
 +    }
 +
-     List<String> create(String serviceName) {
++    public List<String> create(String serviceName) {
 +        List<String> urls = new ArrayList<>();
 +
 +        Map<String, String> placeholderValues = new HashMap<>();
 +        List<String> componentHostnames = new ArrayList<>();
 +        String hostNamePlaceholder = null;
 +
 +        ServiceURLPropertyConfig.URLPattern pattern = config.getURLPattern(serviceName);
 +        if (pattern != null) {
 +            for (String propertyName : pattern.getPlaceholders()) {
 +                ServiceURLPropertyConfig.Property configProperty = config.getConfigProperty(serviceName, propertyName);
 +
 +                String propertyValue = null;
 +                String propertyType = configProperty.getType();
 +                if (ServiceURLPropertyConfig.Property.TYPE_SERVICE.equals(propertyType)) {
 +                    log.lookingUpServiceConfigProperty(configProperty.getService(), configProperty.getServiceConfig(), configProperty.getValue());
 +                    AmbariCluster.ServiceConfiguration svcConfig =
 +                        cluster.getServiceConfiguration(configProperty.getService(), configProperty.getServiceConfig());
 +                    if (svcConfig != null) {
 +                        propertyValue = svcConfig.getProperties().get(configProperty.getValue());
 +                    }
 +                } else if (ServiceURLPropertyConfig.Property.TYPE_COMPONENT.equals(propertyType)) {
 +                    String compName = configProperty.getComponent();
 +                    if (compName != null) {
 +                        AmbariComponent component = cluster.getComponent(compName);
 +                        if (component != null) {
 +                            if (ServiceURLPropertyConfig.Property.PROP_COMP_HOSTNAME.equals(configProperty.getValue())) {
 +                                log.lookingUpComponentHosts(compName);
 +                                componentHostnames.addAll(component.getHostNames());
 +                                hostNamePlaceholder = propertyName; // Remember the host name placeholder
 +                            } else {
 +                                log.lookingUpComponentConfigProperty(compName, configProperty.getValue());
 +                                propertyValue = component.getConfigProperty(configProperty.getValue());
 +                            }
 +                        }
 +                    }
 +                } else { // Derived property
 +                    log.handlingDerivedProperty(serviceName, configProperty.getType(), configProperty.getName());
 +                    ServiceURLPropertyConfig.Property p = config.getConfigProperty(serviceName, configProperty.getName());
 +                    propertyValue = p.getValue();
 +                    if (propertyValue == null) {
 +                        if (p.getConditionHandler() != null) {
 +                            propertyValue = p.getConditionHandler().evaluate(config, cluster);
 +                        }
 +                    }
 +                }
 +
 +                log.determinedPropertyValue(configProperty.getName(), propertyValue);
 +                placeholderValues.put(configProperty.getName(), propertyValue);
 +            }
 +
 +            // For patterns with a placeholder value for the hostname (e.g., multiple URL scenarios)
 +            if (!componentHostnames.isEmpty()) {
 +                for (String componentHostname : componentHostnames) {
 +                    String url = pattern.get().replace("{" + hostNamePlaceholder + "}", componentHostname);
 +                    urls.add(createURL(url, placeholderValues));
 +                }
 +            } else { // Single URL result case
 +                urls.add(createURL(pattern.get(), placeholderValues));
 +            }
 +        }
 +
 +        return urls;
 +    }
 +
 +    private String createURL(String pattern, Map<String, String> placeholderValues) {
 +        String url = null;
 +        if (pattern != null) {
 +            url = pattern;
 +            for (String placeHolder : placeholderValues.keySet()) {
 +                String value = placeholderValues.get(placeHolder);
 +                if (value != null) {
 +                    url = url.replace("{" + placeHolder + "}", value);
 +                }
 +            }
 +        }
 +        return url;
 +    }
 +
 +}

http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/PropertyEqualsHandler.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/PropertyEqualsHandler.java
index 4044d56,0000000..0dfab36
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/PropertyEqualsHandler.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/PropertyEqualsHandler.java
@@@ -1,76 -1,0 +1,88 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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
 + * <p>
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * <p>
 + * 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +
 +class PropertyEqualsHandler implements ConditionalValueHandler {
 +
 +    private String serviceName                        = null;
 +    private String propertyName                       = null;
 +    private String propertyValue                      = null;
 +    private ConditionalValueHandler affirmativeResult = null;
 +    private ConditionalValueHandler negativeResult    = null;
 +
 +    PropertyEqualsHandler(String                  serviceName,
 +                          String                  propertyName,
 +                          String                  propertyValue,
 +                          ConditionalValueHandler affirmativeResult,
 +                          ConditionalValueHandler negativeResult) {
 +        this.serviceName       = serviceName;
 +        this.propertyName      = propertyName;
 +        this.propertyValue     = propertyValue;
 +        this.affirmativeResult = affirmativeResult;
 +        this.negativeResult    = negativeResult;
 +    }
 +
 +    @Override
 +    public String evaluate(ServiceURLPropertyConfig config, AmbariCluster cluster) {
 +        String result = null;
 +
 +        ServiceURLPropertyConfig.Property p = config.getConfigProperty(serviceName, propertyName);
 +        if (p != null) {
 +            String value = getActualPropertyValue(cluster, p);
-             if (propertyValue.equals(value)) {
-                 result = affirmativeResult.evaluate(config, cluster);
-             } else if (negativeResult != null) {
-                 result = negativeResult.evaluate(config, cluster);
++            if (propertyValue == null) {
++                // If the property value isn't specified, then we're just checking if the property is set with any value
++                if (value != null) {
++                    // So, if there is a value in the config, respond with the affirmative
++                    result = affirmativeResult.evaluate(config, cluster);
++                } else if (negativeResult != null) {
++                    result = negativeResult.evaluate(config, cluster);
++                }
++            }
++
++            if (result == null) {
++                if (propertyValue.equals(value)) {
++                    result = affirmativeResult.evaluate(config, cluster);
++                } else if (negativeResult != null) {
++                    result = negativeResult.evaluate(config, cluster);
++                }
 +            }
 +
 +            // Check if the result is a reference to a local derived property
 +            ServiceURLPropertyConfig.Property derived = config.getConfigProperty(serviceName, result);
 +            if (derived != null) {
 +                result = getActualPropertyValue(cluster, derived);
 +            }
 +        }
 +
 +        return result;
 +    }
 +
 +    private String getActualPropertyValue(AmbariCluster cluster, ServiceURLPropertyConfig.Property property) {
 +        String value = null;
 +        String propertyType = property.getType();
 +        if (ServiceURLPropertyConfig.Property.TYPE_COMPONENT.equals(propertyType)) {
 +            AmbariComponent component = cluster.getComponent(property.getComponent());
 +            if (component != null) {
 +                value = component.getConfigProperty(property.getValue());
 +            }
 +        } else if (ServiceURLPropertyConfig.Property.TYPE_SERVICE.equals(propertyType)) {
 +            value = cluster.getServiceConfiguration(property.getService(), property.getServiceConfig()).getProperties().get(property.getValue());
 +        }
 +        return value;
 +    }
 +}

http://git-wip-us.apache.org/repos/asf/knox/blob/e5fd0622/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/ServiceURLPropertyConfig.java
----------------------------------------------------------------------
diff --cc gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/ServiceURLPropertyConfig.java
index 47b20e9,0000000..9f3da3d
mode 100644,000000..100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/ServiceURLPropertyConfig.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/ServiceURLPropertyConfig.java
@@@ -1,324 -1,0 +1,329 @@@
 +/**
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements. See the NOTICE file distributed with this
 + * work for additional information regarding copyright ownership. The ASF
 + * licenses this file to you 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
 + * <p>
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * <p>
 + * 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.apache.knox.gateway.topology.discovery.ambari;
 +
 +import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 +import org.apache.knox.gateway.util.XmlUtils;
 +import org.w3c.dom.Document;
 +import org.w3c.dom.NamedNodeMap;
 +import org.w3c.dom.Node;
 +import org.w3c.dom.NodeList;
 +
 +import javax.xml.xpath.XPath;
 +import javax.xml.xpath.XPathConstants;
 +import javax.xml.xpath.XPathExpression;
 +import javax.xml.xpath.XPathExpressionException;
 +import javax.xml.xpath.XPathFactory;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.IOException;
 +import java.io.InputStream;
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.regex.Matcher;
 +import java.util.regex.Pattern;
 +
 +/**
 + * Service URL pattern mapping configuration model.
 + */
 +class ServiceURLPropertyConfig {
 +
 +    private static final AmbariServiceDiscoveryMessages log = MessagesFactory.get(AmbariServiceDiscoveryMessages.class);
 +
 +    private static final String ATTR_NAME = "name";
 +
 +    private static XPathExpression SERVICE_URL_PATTERN_MAPPINGS;
 +    private static XPathExpression URL_PATTERN;
 +    private static XPathExpression PROPERTIES;
 +    static {
 +        XPath xpath = XPathFactory.newInstance().newXPath();
 +        try {
 +            SERVICE_URL_PATTERN_MAPPINGS = xpath.compile("/service-discovery-url-mappings/service");
 +            URL_PATTERN                  = xpath.compile("url-pattern/text()");
 +            PROPERTIES                   = xpath.compile("properties/property");
 +        } catch (XPathExpressionException e) {
 +            e.printStackTrace();
 +        }
 +    }
 +
 +    private static final String DEFAULT_SERVICE_URL_MAPPINGS = "ambari-service-discovery-url-mappings.xml";
 +
 +    private Map<String, URLPattern> urlPatterns = new HashMap<>();
 +
 +    private Map<String, Map<String, Property>> properties = new HashMap<>();
 +
 +
 +    /**
 +     * The default service URL pattern to property mapping configuration will be used.
 +     */
 +    ServiceURLPropertyConfig() {
 +        this(ServiceURLPropertyConfig.class.getClassLoader().getResourceAsStream(DEFAULT_SERVICE_URL_MAPPINGS));
 +    }
 +
 +    /**
 +     * The default service URL pattern to property mapping configuration will be used.
 +     */
 +    ServiceURLPropertyConfig(File mappingConfigurationFile) throws Exception {
 +        this(new FileInputStream(mappingConfigurationFile));
 +    }
 +
 +    /**
 +     *
 +     * @param source An InputStream for the XML content
 +     */
 +    ServiceURLPropertyConfig(InputStream source) {
 +        // Parse the XML, and build the model
 +        try {
 +            Document doc = XmlUtils.readXml(source);
 +
 +            NodeList serviceNodes =
 +                    (NodeList) SERVICE_URL_PATTERN_MAPPINGS.evaluate(doc, XPathConstants.NODESET);
 +            for (int i=0; i < serviceNodes.getLength(); i++) {
 +                Node serviceNode = serviceNodes.item(i);
 +                String serviceName = serviceNode.getAttributes().getNamedItem(ATTR_NAME).getNodeValue();
 +                properties.put(serviceName, new HashMap<String, Property>());
 +
 +                Node urlPatternNode = (Node) URL_PATTERN.evaluate(serviceNode, XPathConstants.NODE);
 +                if (urlPatternNode != null) {
 +                    urlPatterns.put(serviceName, new URLPattern(urlPatternNode.getNodeValue()));
 +                }
 +
 +                NodeList propertiesNode = (NodeList) PROPERTIES.evaluate(serviceNode, XPathConstants.NODESET);
 +                if (propertiesNode != null) {
 +                    processProperties(serviceName, propertiesNode);
 +                }
 +            }
 +        } catch (Exception e) {
 +            log.failedToLoadServiceDiscoveryURLDefConfiguration(e);
 +        } finally {
 +            try {
 +                source.close();
 +            } catch (IOException e) {
 +                // Ignore
 +            }
 +        }
 +    }
 +
 +    private void processProperties(String serviceName, NodeList propertyNodes) {
 +        for (int i = 0; i < propertyNodes.getLength(); i++) {
 +            Property p = Property.createProperty(serviceName, propertyNodes.item(i));
 +            properties.get(serviceName).put(p.getName(), p);
 +        }
 +    }
 +
 +    URLPattern getURLPattern(String service) {
 +        return urlPatterns.get(service);
 +    }
 +
 +    Property getConfigProperty(String service, String property) {
 +        return properties.get(service).get(property);
 +    }
 +
 +    static class URLPattern {
 +        String pattern;
 +        List<String> placeholders = new ArrayList<>();
 +
 +        URLPattern(String pattern) {
 +            this.pattern = pattern;
 +
 +            final Pattern regex = Pattern.compile("\\{(.*?)}", Pattern.DOTALL);
 +            final Matcher matcher = regex.matcher(pattern);
 +            while( matcher.find() ){
 +                placeholders.add(matcher.group(1));
 +            }
 +        }
 +
 +        String get() {return pattern; }
 +        List<String> getPlaceholders() {
 +            return placeholders;
 +        }
 +    }
 +
 +    static class Property {
 +        static final String TYPE_SERVICE   = "SERVICE";
 +        static final String TYPE_COMPONENT = "COMPONENT";
 +        static final String TYPE_DERIVED   = "DERIVED";
 +
 +        static final String PROP_COMP_HOSTNAME = "component.host.name";
 +
 +        static final String ATTR_NAME     = "name";
 +        static final String ATTR_PROPERTY = "property";
 +        static final String ATTR_VALUE    = "value";
 +
 +        static XPathExpression HOSTNAME;
 +        static XPathExpression SERVICE_CONFIG;
 +        static XPathExpression COMPONENT;
 +        static XPathExpression CONFIG_PROPERTY;
 +        static XPathExpression IF;
 +        static XPathExpression THEN;
 +        static XPathExpression ELSE;
 +        static XPathExpression TEXT;
 +        static {
 +            XPath xpath = XPathFactory.newInstance().newXPath();
 +            try {
 +                HOSTNAME        = xpath.compile("hostname");
 +                SERVICE_CONFIG  = xpath.compile("service-config");
 +                COMPONENT       = xpath.compile("component");
 +                CONFIG_PROPERTY = xpath.compile("config-property");
 +                IF              = xpath.compile("if");
 +                THEN            = xpath.compile("then");
 +                ELSE            = xpath.compile("else");
 +                TEXT            = xpath.compile("text()");
 +            } catch (XPathExpressionException e) {
 +                e.printStackTrace();
 +            }
 +        }
 +
 +
 +        String type;
 +        String name;
 +        String component;
 +        String service;
 +        String serviceConfig;
 +        String value;
 +        ConditionalValueHandler conditionHandler = null;
 +
 +        private Property(String type,
 +                         String propertyName,
 +                         String component,
 +                         String service,
 +                         String configType,
 +                         String value,
 +                         ConditionalValueHandler pch) {
 +            this.type = type;
 +            this.name = propertyName;
 +            this.service = service;
 +            this.component = component;
 +            this.serviceConfig = configType;
 +            this.value = value;
 +            conditionHandler = pch;
 +        }
 +
 +        static Property createProperty(String serviceName, Node propertyNode) {
 +            String propertyName = propertyNode.getAttributes().getNamedItem(ATTR_NAME).getNodeValue();
 +            String propertyType = null;
 +            String serviceType = null;
 +            String configType = null;
 +            String componentType = null;
 +            String value = null;
 +            ConditionalValueHandler pch = null;
 +
 +            try {
 +                Node hostNameNode = (Node) HOSTNAME.evaluate(propertyNode, XPathConstants.NODE);
 +                if (hostNameNode != null) {
 +                    value = PROP_COMP_HOSTNAME;
 +                }
 +
 +                // Check for a service-config node
 +                Node scNode = (Node) SERVICE_CONFIG.evaluate(propertyNode, XPathConstants.NODE);
 +                if (scNode != null) {
 +                    // Service config property
 +                    propertyType = Property.TYPE_SERVICE;
 +                    serviceType = scNode.getAttributes().getNamedItem(ATTR_NAME).getNodeValue();
 +                    Node scTextNode = (Node) TEXT.evaluate(scNode, XPathConstants.NODE);
 +                    configType = scTextNode.getNodeValue();
 +                } else { // If not service-config node, check for a component config node
 +                    Node cNode = (Node) COMPONENT.evaluate(propertyNode, XPathConstants.NODE);
 +                    if (cNode != null) {
 +                        // Component config property
 +                        propertyType = Property.TYPE_COMPONENT;
 +                        componentType = cNode.getFirstChild().getNodeValue();
 +                        Node cTextNode = (Node) TEXT.evaluate(cNode, XPathConstants.NODE);
 +                        configType = cTextNode.getNodeValue();
 +                        componentType = cTextNode.getNodeValue();
 +                    }
 +                }
 +
 +                // Check for a config property node
 +                Node cpNode = (Node) CONFIG_PROPERTY.evaluate(propertyNode, XPathConstants.NODE);
 +                if (cpNode != null) {
 +                    // Check for a condition element
 +                    Node ifNode = (Node) IF.evaluate(cpNode, XPathConstants.NODE);
 +                    if (ifNode != null) {
 +                        propertyType = TYPE_DERIVED;
 +                        pch = getConditionHandler(serviceName, ifNode);
 +                    } else {
 +                        Node cpTextNode = (Node) TEXT.evaluate(cpNode, XPathConstants.NODE);
 +                        value = cpTextNode.getNodeValue();
 +                    }
 +                }
 +            } catch (Exception e) {
 +                e.printStackTrace();
 +            }
 +
 +            // Create and return the property representation
 +            return new Property(propertyType, propertyName, componentType, serviceType, configType, value, pch);
 +        }
 +
 +        private static ConditionalValueHandler getConditionHandler(String serviceName, Node ifNode) throws Exception {
 +            ConditionalValueHandler result = null;
 +
 +            if (ifNode != null) {
 +                NamedNodeMap attrs = ifNode.getAttributes();
 +                String comparisonPropName = attrs.getNamedItem(ATTR_PROPERTY).getNodeValue();
-                 String comparisonValue = attrs.getNamedItem(ATTR_VALUE).getNodeValue();
++
++                String comparisonValue = null;
++                Node valueNode = attrs.getNamedItem(ATTR_VALUE);
++                if (valueNode != null) {
++                    comparisonValue = attrs.getNamedItem(ATTR_VALUE).getNodeValue();
++                }
 +
 +                ConditionalValueHandler affirmativeResult = null;
 +                Node thenNode = (Node) THEN.evaluate(ifNode, XPathConstants.NODE);
 +                if (thenNode != null) {
 +                    Node subIfNode = (Node) IF.evaluate(thenNode, XPathConstants.NODE);
 +                    if (subIfNode != null) {
 +                        affirmativeResult = getConditionHandler(serviceName, subIfNode);
 +                    } else {
 +                        affirmativeResult = new SimpleValueHandler(thenNode.getFirstChild().getNodeValue());
 +                    }
 +                }
 +
 +                ConditionalValueHandler negativeResult = null;
 +                Node elseNode = (Node) ELSE.evaluate(ifNode, XPathConstants.NODE);
 +                if (elseNode != null) {
 +                    Node subIfNode = (Node) IF.evaluate(elseNode, XPathConstants.NODE);
 +                    if (subIfNode != null) {
 +                        negativeResult = getConditionHandler(serviceName, subIfNode);
 +                    } else {
 +                        negativeResult = new SimpleValueHandler(elseNode.getFirstChild().getNodeValue());
 +                    }
 +                }
 +
 +                result = new PropertyEqualsHandler(serviceName,
 +                        comparisonPropName,
 +                        comparisonValue,
 +                        affirmativeResult,
 +                        negativeResult);
 +            }
 +
 +            return result;
 +        }
 +
 +        String getType() { return type; }
 +        String getName() { return name; }
 +        String getComponent() { return component; }
 +        String getService() { return service; }
 +        String getServiceConfig() { return serviceConfig; }
 +        String getValue() {
 +            return value;
 +        }
 +        ConditionalValueHandler getConditionHandler() { return conditionHandler; }
 +    }
 +}