org.wso2.carbon.appfactory.s4.integration.DomainMappingManagementService.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.carbon.appfactory.s4.integration.DomainMappingManagementService.java

Source

/*
 * Copyright 2005-2014 WSO2, Inc. (http://wso2.com)
 * 
 * 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.wso2.carbon.appfactory.s4.integration;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.appfactory.common.AppFactoryConstants;
import org.wso2.carbon.appfactory.common.AppFactoryException;
import org.wso2.carbon.appfactory.core.dao.ApplicationDAO;
import org.wso2.carbon.appfactory.core.internal.ServiceHolder;
import org.wso2.carbon.appfactory.eventing.AppFactoryEventException;
import org.wso2.carbon.appfactory.eventing.Event;
import org.wso2.carbon.appfactory.eventing.EventNotifier;
import org.wso2.carbon.appfactory.eventing.builder.utils.AppInfoUpdateEventBuilderUtil;
import org.wso2.carbon.appfactory.s4.integration.internal.ServiceReferenceHolder;
import org.wso2.carbon.appfactory.s4.integration.utils.DomainMappingAction;
import org.wso2.carbon.appfactory.s4.integration.utils.DomainMappingResponse;
import org.wso2.carbon.appfactory.s4.integration.utils.DomainMappingUtils;
import org.wso2.carbon.context.CarbonContext;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Random;

/**
 * Domain mapping management service. Handles the domain mapping requests.
 * <p/>
 * TODO once update domain mapping is available from Stratos side, use that api for update.
 */
public class DomainMappingManagementService {
    private static final Log log = LogFactory.getLog(DomainMappingManagementService.class);

    // DNS records
    public static final String DNS_A_RECORD = "A";
    public static final String DNS_CNAME_RECORD = "CNAME";

    // App wall messages
    private static final String AF_APPWALL_SUCCESS_MSG = "Production URL updated successfully.";
    private static final String AF_APPWALL_REMOVE_SUCCESS_MSG = "Production URL removed successfully.";
    private static final String AF_APPWALL_ERROR_MSG = "Failed to update the domain.";
    private static final String AF_APPWALL_CUSTOM_URL_INVALID_MSG = "Fail to verify custom URL.";
    private static final String AF_APPWALL_URL = "URL: %s";
    private static final String JNDI_KEY_NAMING_FACTORY_INITIAL = "java.naming.factory.initial";
    private static final String JNDI_KEY_DNS_TIMEOUT = "com.sun.jndi.dns.timeout.initial";
    private static final String JDNI_KEY_DNS_RETRIES = "com.sun.jndi.dns.timeout.retries";

    /**
     * Add new domain mapping entry to stratos.
     *
     * @param stage          mapping stage
     * @param domain         e.g: some.organization.org this doesn't require the protocol such as http/https
     * @param appKey         application key
     * @param version        version to be mapped, null if map to default page
     * @param isCustomDomain whether {@code domain} is custom domain or not
     * @throws AppFactoryException                if an error occurred during the operation
     * @throws DomainMappingVerificationException if the requested {@code domain} is not verified, given that it is a
     *                                            custom domain({@code isCustomDomain} is true). To find more info on
     *                                            verification method, please refer to {@link
     *                                            #verifyCustomUrlForApplication}
     */
    public void addSubscriptionDomain(String stage, String domain, String appKey, String version,
            boolean isCustomDomain) throws AppFactoryException, DomainMappingVerificationException {
        String appType = ApplicationDAO.getInstance().getApplicationInfo(appKey).getType();
        String addSubscriptionDomainEndPoint = DomainMappingUtils.getAddDomainEndPoint(stage, appType);
        synchronized (domain.intern()) {

            // Check domain availability
            boolean domainAvailable = isDomainAvailable(stage, domain, appType);
            if (!domainAvailable) {
                // Throws an exception, if requested domain is not available.
                log.error("Requested domain: " + domain + " does not available for application id :" + appKey
                        + " for stage :" + stage);
                throw new AppFactoryException(
                        String.format(DomainMappingUtils.AF_DOMAIN_NOT_AVAILABLE_MSG, domain));
            }

            // if the given domain is custom domain, check whether domain is verified
            if (isCustomDomain && !verifyCustomUrlForApplication(domain, appKey)) {
                // if custom utl is not verified, throw an exception. Here we are throwing DomainMappingVerificationException therefore
                // we could identify the error occurred due to unsuccessful verification
                notifyAppWall(appKey, AF_APPWALL_CUSTOM_URL_INVALID_MSG, String.format(AF_APPWALL_URL, domain),
                        Event.Category.ERROR);
                log.warn("Requested custom domain :" + domain + " is not verified for application id :" + appKey
                        + " for stage :" + stage);
                throw new DomainMappingVerificationException(
                        String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, domain));
            }

            DomainMappingResponse response;
            try {
                String body;
                if (StringUtils.isNotBlank(version)) { // if the domain is to be mapped to a version
                    body = DomainMappingUtils.generateAddSubscriptionDomainJSON(domain, appKey, version, stage);
                } else { // map the domain to initial url
                    body = DomainMappingUtils.generateInitialSubscriptionDomainJSON(domain);
                }
                response = DomainMappingUtils.sendPostRequest(stage, body, addSubscriptionDomainEndPoint);
            } catch (AppFactoryException e) {
                log.error("Error occurred adding domain mappings to appkey " + appKey + " version " + version
                        + " domain " + domain, e);
                //Notifying the domain mapping failure to app wall
                notifyAppWall(appKey, AF_APPWALL_ERROR_MSG, "", Event.Category.ERROR);
                throw new AppFactoryException(String.format(DomainMappingUtils.AF_ERROR_ADD_DOMAIN_MSG, domain));
            }

            if (response.statusCode == HttpStatus.SC_OK) {
                log.info("Successfully added domain mapping for application: " + appKey + " domain:" + domain
                        + (StringUtils.isNotBlank(version) ? (" to version: " + version) : ""));
                if (log.isDebugEnabled()) {
                    log.debug("Stratos response status: " + response.statusCode + " Stratos Response message: "
                            + response.getResponse());
                }
                DomainMappingUtils.publishToDomainMappingEventHandlers(domain,
                        DomainMappingAction.ADD_DOMAIN_MAPPING);
                //Notifying the domain mapping success to app wall
                notifyAppWall(appKey, AF_APPWALL_SUCCESS_MSG, String.format(AF_APPWALL_URL, domain),
                        Event.Category.INFO);
            } else {
                notifyAppWall(appKey, AF_APPWALL_ERROR_MSG, "", Event.Category.ERROR);
                log.error(String.format(DomainMappingUtils.AF_ERROR_ADD_DOMAIN_MSG, domain)
                        + " Stratos response status: " + response.statusCode + " Stratos Response message: "
                        + response.getResponse());
                throw new AppFactoryException(String.format(DomainMappingUtils.AF_ERROR_ADD_DOMAIN_MSG, domain));
            }
        } // end of synchronized
    }

    /**
     * Add default production url, if its failed during app creation time
     *
     * @param appKey    application key
     * @param version   version of the application
     * @throws AppFactoryException
     */
    public void addDefaultProdUrl(String appKey, String version) throws AppFactoryException {
        String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
        String defaultHostName = ServiceReferenceHolder.getInstance().getAppFactoryConfiguration()
                .getFirstProperty(AppFactoryConstants.DOMAIN_NAME);
        String defaultUrl = DomainMappingUtils.generateDefaultProdUrl(
                appKey + AppFactoryConstants.MINUS + (new Random()).nextInt(1000), tenantDomain.replace(".", ""),
                defaultHostName);
        try {
            addSubscriptionDomain(
                    ServiceReferenceHolder.getInstance().getAppFactoryConfiguration()
                            .getFirstProperty(AppFactoryConstants.FINE_GRAINED_DOMAIN_MAPPING_ALLOWED_STAGE),
                    defaultUrl, appKey, version, false);
        } catch (DomainMappingVerificationException e) {
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, defaultUrl), e);
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, defaultUrl));
        }
        DomainMappingUtils.updateMetadata(appKey, defaultUrl, version, false);
    }

    /**
     * Add new domain mapping entry by mapping it to initial url
     *
     * @param stage          mapping stage
     * @param domain         e.g: some.organization.org this doesn't require the protocol such as http/https
     * @param appKey         application key
     * @param version        version to be mapped
     * @param isCustomDomain whether {@code domain} is custom domain or not
     */
    public void addNewSubscriptionDomain(String stage, String domain, String appKey, String version,
            boolean isCustomDomain) throws AppFactoryException {
        try {
            addSubscriptionDomain(stage, domain, appKey, version, isCustomDomain);
        } catch (DomainMappingVerificationException e) {
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, domain), e);
            // update the custom url values of the rxt.
            DomainMappingUtils.updateCustomUrlMetadata(appKey, domain,
                    DomainMappingUtils.UNVERIFIED_VERIFICATION_CODE);
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, domain));
        }
        DomainMappingUtils.updateMetadata(appKey, domain, version, isCustomDomain);
    }

    /**
     * Remap given domain to given {@code version}
     * <p/>
     * Example
     * before
     * domain   -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_1.0.0
     * <p/>
     * after
     * domain   -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_2.0.0
     *
     * @param stage           mapping stage
     * @param domain          default domain.e.g: some.organization1.org this doesn't require the protocol such as http/https
     * @param appKey          application key
     * @param newVersion      version to be mapped
     * @param previousVersion previously mapped version
     * @param isCustomDomain  whether the given domains are related to custom domains or not
     * @throws AppFactoryException
     */
    public void remapDomainToContext(String stage, String domain, String appKey, String newVersion,
            String previousVersion, boolean isCustomDomain) throws AppFactoryException {
        // remove existing mapping
        removeSubscriptionDomain(stage, domain, appKey);
        if (StringUtils.isNotBlank(previousVersion)) { // if the domain is already mapped, remove the domain from previousVersion
            DomainMappingUtils.updateVersionMetadataWithMappedDomain(appKey, previousVersion, "");
        }
        try {
            addSubscriptionDomain(stage, domain, appKey, newVersion, isCustomDomain);
        } catch (DomainMappingVerificationException e) {
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, domain), e);
            // update the custom url values of the rxt.
            if (isCustomDomain) {
                DomainMappingUtils.updateCustomUrlMetadata(appKey, domain,
                        DomainMappingUtils.UNVERIFIED_VERIFICATION_CODE);
            }
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, domain));
        }
        DomainMappingUtils.updateMetadata(appKey, domain, newVersion, isCustomDomain);

    }

    /**
     * Remap given existing production urls(custom and default) to given {@code version}
     * <p/>
     * Example
     * before
     * domain   -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_1.0.0
     * <p/>
     * after
     * domain   -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_2.0.0
     *
     * @param stage           mapping stage
     * @param appKey          application key
     * @param newVersion      version to be mapped
     * @param previousVersion previously mapped version
     * @throws AppFactoryException
     */
    public void remapDomainToContext(String stage, String appKey, String newVersion, String previousVersion)
            throws AppFactoryException {
        // remap default domain
        String defaultDomain = DomainMappingUtils.getDefaultDomain(appKey);
        if (StringUtils.isNotBlank(defaultDomain)) {
            remapDomainToContext(stage, defaultDomain, appKey, newVersion, previousVersion, false);
        }

        // remap custom domains
        String customDomain = DomainMappingUtils.getCustomDomain(appKey);
        if (StringUtils.isNotBlank(customDomain)) {
            remapDomainToContext(stage, customDomain, appKey, newVersion, previousVersion, true);
        }
    }

    /**
     * Remap given version from existing mapped domain to {@code newDomain}.<br>
     * If existing mapped domain is null, then it will just map {@code version} to {@code newDomain}.<br>
     * <p/>
     * Example
     * [before]
     * previousDomain   -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_1.0.0
     * newDomain        -> null
     * <p/>
     * [after]
     * previousDomain   -> null
     * newDomain        -> appserver.cloud.wso2.com/t/mytenant/webapps/abc_1.0.0
     *
     * @param stage          mapping stage
     * @param newDomain      new domain name. e.g: some.organization1.org this doesn't require the protocol such as
     *                       http/https
     * @param appKey         application key
     * @param version        version to be mapped
     * @param isCustomDomain whether the given domains are related to custom domains or not
     * @throws AppFactoryException
     */
    public void remapContextToDomain(String stage, String newDomain, String appKey, String version,
            boolean isCustomDomain) throws AppFactoryException {

        // Get previously mapped domain, since we have to remove the previous domain mapping
        String previousDomain;
        if (isCustomDomain) {
            previousDomain = DomainMappingUtils.getCustomDomain(appKey);
        } else {
            previousDomain = DomainMappingUtils.getDefaultDomain(appKey);
        }

        // First add the new domain
        try {
            addSubscriptionDomain(stage, newDomain, appKey, version, isCustomDomain);
        } catch (DomainMappingVerificationException e) { //validation unsuccessful for custom urls
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, newDomain), e);
            // update the custom url values of the rxt with new values.
            DomainMappingUtils.updateCustomUrlMetadata(appKey, newDomain,
                    DomainMappingUtils.UNVERIFIED_VERIFICATION_CODE);

            // Since we are going to keep the new domain, as the custom url
            // remove existing domain mapping for previous domain
            if (StringUtils.isNotBlank(previousDomain)) {
                try {
                    removeSubscriptionDomain(stage, previousDomain, appKey);
                } catch (AppFactoryException e1) {
                    String msg = "Domain validation unsuccessful for domain : " + newDomain
                            + " and error occurred removing domain mapping for previous domain " + previousDomain
                            + " for application id: " + appKey;
                    // Removing previous domain should not interrupt the operation. therefore we are not going to throw an exception here
                    log.error(msg, e1);
                }
            }
            // throw an exception with not validated message to display in UI
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_CUSTOM_URL_NOT_VERIFIED, newDomain));
        }
        DomainMappingUtils.updateMetadata(appKey, newDomain, version, isCustomDomain);

        // Since we are going to keep the new domain details, remove previous domain mapping
        if (StringUtils.isNotBlank(previousDomain) && !previousDomain.equalsIgnoreCase(newDomain)) {
            try {
                removeSubscriptionDomain(stage, previousDomain, appKey);
            } catch (AppFactoryException e) {
                // if mapping to newDomain is successful it should continue,
                // therefore not throwing an exception here
                String msg = "Successfully added domain mapping for domain : " + newDomain + " version : " + version
                        + " Error occurred removing domain mapping for " + previousDomain + " when remapping"
                        + " for application id: " + appKey;
                log.error(msg, e);
            }
        }
    }

    /**
     * Remove domain mapping from application.
     *
     * @param stage          mapping stage
     * @param appKey         application key
     * @param version        version to be removed
     * @param isCustomDomain whether to remove custom domain or not
     * @throws AppFactoryException
     */
    public void removeDomainMappingFromApplication(String stage, String appKey, String version,
            boolean isCustomDomain) throws AppFactoryException {
        String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
        try {
            if (isCustomDomain) {
                String customDomain = DomainMappingUtils.getCustomDomain(appKey);
                if (StringUtils.isNotBlank(customDomain)) {
                    removeSubscriptionDomain(stage, customDomain, appKey);
                    DomainMappingUtils.updateCustomUrlMetadata(appKey, DomainMappingUtils.UNDEFINED_URL_RXT_VALUE,
                            DomainMappingUtils.UNVERIFIED_VERIFICATION_CODE);
                    // NOTE: need not update the appversion rxt as removed since default prod url has not removed
                    log.info("Successfully removed custom domain : " + customDomain + " from application id : "
                            + appKey + " of tenant domain : " + tenantDomain);
                    notifyAppWall(appKey, AF_APPWALL_REMOVE_SUCCESS_MSG,
                            String.format(AF_APPWALL_URL, customDomain), Event.Category.INFO);
                }
            } else {
                String defaultDomain = DomainMappingUtils.getDefaultDomain(appKey);
                if (StringUtils.isNotBlank(defaultDomain)) {
                    removeSubscriptionDomain(stage, defaultDomain, appKey);
                    DomainMappingUtils.updateApplicationMetaDataMappedDomain(appKey,
                            DomainMappingUtils.UNDEFINED_URL_RXT_VALUE);
                    if (StringUtils.isNotBlank(version)) { // update the appversion rxt as removed
                        DomainMappingUtils.updateVersionMetadataWithMappedDomain(appKey, version,
                                DomainMappingUtils.UNDEFINED_URL_RXT_VALUE);
                    }
                    log.info("Successfully removed default domain : " + defaultDomain + " from application id : "
                            + appKey + " of tenant domain : " + tenantDomain);
                    notifyAppWall(appKey, AF_APPWALL_REMOVE_SUCCESS_MSG,
                            String.format(AF_APPWALL_URL, defaultDomain), Event.Category.INFO);
                }
            }
        } catch (AppFactoryException e) {
            log.error("Error occurred while removing domain mapping for application id: " + appKey + " in stage: "
                    + stage + " for tenant domain : " + tenantDomain, e);
            notifyAppWall(appKey, AF_APPWALL_ERROR_MSG, "", Event.Category.ERROR);
            throw new AppFactoryException(DomainMappingUtils.AF_ERROR_REMOVE_DOMAIN_MSG, e);
        }
    }

    /**
     * Remove existing domain mapping entries from Stratos
     *
     * @param stage  mapping stage
     * @param domain e.g: some.organization.org this doesn't require the protocol such as http/https
     * @param appKey application key
     * @throws AppFactoryException
     */
    public void removeSubscriptionDomain(String stage, String domain, String appKey) throws AppFactoryException {
        String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
        String appType = ApplicationDAO.getInstance().getApplicationInfo(appKey).getType();
        String removeSubscriptionDomainEndPoint = DomainMappingUtils.getRemoveDomainEndPoint(stage, domain,
                appType);
        DomainMappingResponse deleteResponse;
        try {
            deleteResponse = DomainMappingUtils.sendDeleteRequest(stage, removeSubscriptionDomainEndPoint);
        } catch (AppFactoryException e) {
            log.error("Error occurred removing domain mapping : " + domain + " from tenant domain : " + tenantDomain
                    + " in stage :" + stage, e);
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_ERROR_GENARIC_MSG, domain));
        }

        if (deleteResponse.statusCode == HttpStatus.SC_OK) {
            DomainMappingUtils.publishToDomainMappingEventHandlers(domain,
                    DomainMappingAction.REMOVE_DOMAIN_MAPPING);
            log.info("Successfully removed custom domain : " + domain + " from tenant domain : " + tenantDomain);
            if (log.isDebugEnabled()) {
                log.debug("Stratos response status: " + deleteResponse.statusCode + " Stratos Response message: "
                        + deleteResponse.getResponse());
            }
        } else {
            log.error("Error occurred while removing domain mapping : " + domain + " from tenant domain : "
                    + tenantDomain + " in stage :" + stage + " [Stratos Response Status: "
                    + deleteResponse.statusCode + " , Stratos Response Message: " + deleteResponse.getResponse()
                    + "]");
            throw new AppFactoryException(String.format(DomainMappingUtils.AF_ERROR_GENARIC_MSG, domain));
        }
    }

    /**
     * Verify ownership of the custom url by using {@link org.wso2.carbon.appfactory.s4.integration.DomainMappingManagementService#verifyCustomUrlByCname(String, String)} method.
     * Will check whether {@code customUrl} has CNAME entry to {@code defaultProdUrl}.
     *
     * @param customUrl custom url to be verified
     * @param appKey    application key
     * @return whether verification success or not, false if an error occurred while performing the operation
     */
    public boolean verifyCustomUrlForApplication(String customUrl, String appKey) {
        if (StringUtils.isNotBlank(DomainMappingUtils.getSubDomain(customUrl))) { // if custom url has default host
            return true;
        }
        boolean validation = false;
        try {
            String defaultProdUrl = DomainMappingUtils.getDefaultDomain(appKey);
            if (StringUtils.isNotBlank(defaultProdUrl)) {
                validation = verifyCustomUrlByCname(defaultProdUrl, customUrl);
                if (validation) {
                    log.info("Successfully verified domain: " + customUrl + " for application " + appKey);
                } else {
                    log.warn("Failed to verify domain: " + customUrl + " for application: " + appKey);
                }
            } else {
                log.error("Failed to verify custom domain: " + customUrl + " for application id: " + appKey
                        + " since default production url is empty");
                validation = false;
            }
        } catch (AppFactoryException e) {
            // We are not throwing an error here, since we will return false if verification fails
            log.error("Error occurred while checking domain validation for application: " + appKey + " domain:"
                    + customUrl, e);
        }
        return validation;
    }

    /**
     * Check whether there is a CNAME entry from {@code customUrl} to {@code pointedUrl}.
     *
     * @param pointedUrl url that is pointed by the CNAME entry of {@code customUrl}
     * @param customUrl  custom url.
     * @return success whether there is a CNAME entry from {@code customUrl} to {@code pointedUrl}
     * @throws AppFactoryException
     */
    public boolean verifyCustomUrlByCname(String pointedUrl, String customUrl) throws AppFactoryException {
        Hashtable<String, String> env = new Hashtable<String, String>();
        boolean success;
        // set environment configurations
        env.put(JNDI_KEY_NAMING_FACTORY_INITIAL,
                ServiceHolder.getAppFactoryConfiguration().getFirstProperty("JNDI.DomainMapping.FactoryInitial"));
        env.put(JNDI_KEY_DNS_TIMEOUT,
                ServiceHolder.getAppFactoryConfiguration().getFirstProperty("JNDI.DomainMapping.DnsTimeOut"));
        env.put(JDNI_KEY_DNS_RETRIES,
                ServiceHolder.getAppFactoryConfiguration().getFirstProperty("JNDI.DomainMapping.DnsRetries"));
        try {
            Multimap<String, String> resolvedHosts = resolveDNS(customUrl, env);
            Collection<String> resolvedCnames = resolvedHosts.get(DNS_CNAME_RECORD);
            if (!resolvedCnames.isEmpty() && resolvedCnames.contains(pointedUrl)) {
                if (log.isDebugEnabled()) {
                    log.debug(pointedUrl + " can be reached from: " + customUrl + " via CNAME records");
                }
                success = true;
            } else {
                if (log.isDebugEnabled()) {
                    log.debug(pointedUrl + " cannot be reached from: " + customUrl + " via CNAME records");
                }
                success = false;
            }
        } catch (AppFactoryException e) {
            log.error("Error occurred while resolving dns for: " + customUrl, e);
            throw new AppFactoryException("Error occurred while resolving dns for: " + customUrl, e);
        } catch (DomainMappingVerificationException e) {
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(pointedUrl + " cannot be reached from: " + customUrl + " via CNAME records. Provided custom"
                    + " url: " + customUrl + " might not a valid url.", e);
            success = false;
        }
        return success;
    }

    /**
     * Resolve CNAME and A records for the given {@code hostname}.
     *
     * @param domain             hostname to be resolved.
     * @param environmentConfigs environment configuration
     * @return {@link com.google.common.collect.Multimap} of resolved dns entries. This {@link com.google.common.collect.Multimap} will contain the resolved
     * "CNAME" and "A" records from the given {@code hostname}
     * @throws AppFactoryException if error occurred while the operation
     */
    public Multimap<String, String> resolveDNS(String domain, Hashtable<String, String> environmentConfigs)
            throws AppFactoryException, DomainMappingVerificationException {
        // result mutimap of dns records. Contains the cname and records resolved by the given hostname
        // ex:  CNAME   => foo.com,bar.com
        //      A       => 192.1.2.3 , 192.3.4.5
        Multimap<String, String> dnsRecordsResult = ArrayListMultimap.create();
        Attributes dnsRecords;
        boolean isARecordFound = false;
        boolean isCNAMEFound = false;

        try {
            if (log.isDebugEnabled()) {
                log.debug("DNS validation: resolving DNS for " + domain + " " + "(A/CNAME)");
            }
            DirContext context = new InitialDirContext(environmentConfigs);
            String[] dnsRecordsToCheck = new String[] { DNS_A_RECORD, DNS_CNAME_RECORD };
            dnsRecords = context.getAttributes(domain, dnsRecordsToCheck);
        } catch (NamingException e) {
            String msg = "DNS validation: DNS query failed for: " + domain + ". Error occurred while configuring "
                    + "directory context.";
            log.error(msg, e);
            throw new AppFactoryException(msg, e);
        }

        try {
            // looking for for A records
            Attribute aRecords = dnsRecords.get(DNS_A_RECORD);
            if (aRecords != null && aRecords.size() > 0) { // if an A record exists
                NamingEnumeration aRecordHosts = aRecords.getAll(); // get all resolved A entries
                String aHost;
                while (aRecordHosts.hasMore()) {
                    isARecordFound = true;
                    aHost = (String) aRecordHosts.next();
                    dnsRecordsResult.put(DNS_A_RECORD, aHost);
                    if (log.isDebugEnabled()) {
                        log.debug("DNS validation: A record found: " + aHost);
                    }
                }
            }

            // looking for CNAME records
            Attribute cnameRecords = dnsRecords.get(DNS_CNAME_RECORD);
            if (cnameRecords != null && cnameRecords.size() > 0) { // if CNAME record exists
                NamingEnumeration cnameRecordHosts = cnameRecords.getAll(); // get all resolved CNAME entries for hostname
                String cnameHost;
                while (cnameRecordHosts.hasMore()) {
                    isCNAMEFound = true;
                    cnameHost = (String) cnameRecordHosts.next();
                    if (cnameHost.endsWith(".")) {
                        // Since DNS records are end with "." we are removing it.
                        // For example real dns entry for www.google.com is www.google.com.
                        cnameHost = cnameHost.substring(0, cnameHost.lastIndexOf('.'));
                    }
                    dnsRecordsResult.put(DNS_CNAME_RECORD, cnameHost);
                    if (log.isDebugEnabled()) {
                        log.debug("DNS validation: recurring on CNAME record towards host " + cnameHost);
                    }
                    dnsRecordsResult.putAll(resolveDNS(cnameHost, environmentConfigs)); // recursively resolve cnameHost
                }
            }

            if (!isARecordFound && !isCNAMEFound && log.isDebugEnabled()) {
                log.debug("DNS validation: No CNAME or A record found for domain: '" + domain);
            }
            return dnsRecordsResult;
        } catch (NamingException ne) {
            String msg = "DNS validation: DNS query failed for: " + domain + ". Provided domain: " + domain
                    + " might be a " + "non existing domain.";
            // we are logging this as warn messages since this is caused, due to an user error. For example if the
            // user entered a rubbish custom url(Or a url which is, CNAME record is not propagated at the
            // time of adding the url), then url validation will fail but it is not an system error
            log.warn(msg, ne);
            throw new DomainMappingVerificationException(msg, ne);
        }
    }

    /**
     * Check whether domain is available
     *
     * @param stage  mapping stage
     * @param domain e.g: some.organization1.org this doesn't require the protocol such as http/https
     * @param appType application type
     * @return true if domain is available
     */
    private boolean isDomainAvailable(String stage, String domain, String appType) throws AppFactoryException {
        boolean isAvailable;
        String validateSubscriptionDomainEndPoint = DomainMappingUtils.getDomainAvailableEndPoint(stage, domain,
                appType);

        DomainMappingResponse response;
        try {
            // sending GET request for with domain.
            // if the requested domain is mapped it will send a response with 200 OK , else 404 Not found
            response = DomainMappingUtils.sendGetRequest(stage, validateSubscriptionDomainEndPoint);
        } catch (AppFactoryException e) {
            log.error("Error occurred while checking domain availability from Stratos side for domain:" + domain,
                    e);
            throw new AppFactoryException(
                    String.format(DomainMappingUtils.AF_DOMAIN_AVAILABILITY_ERROR_MSG, domain));
        }

        if (response.statusCode == HttpStatus.SC_NOT_FOUND) { // there is no existing mapping found for requested domain
            isAvailable = true;
        } else if (response.statusCode == HttpStatus.SC_OK) {
            isAvailable = false;
        } else {
            log.error(
                    "Error occurred while checking availability for domain:" + domain + " Stratos response status: "
                            + response.statusCode + " Stratos Response message: " + response.getResponse());
            throw new AppFactoryException(
                    String.format(DomainMappingUtils.AF_DOMAIN_AVAILABILITY_ERROR_MSG, domain));
        }
        return isAvailable;
    }

    /**
     * Notify the app wall status of the domain update
     *
     * @param appKey      application key
     * @param message     message to send
     * @param description message description
     * @param msgType     message type
     */
    private void notifyAppWall(String appKey, String message, String description, Event.Category msgType) {
        try {
            EventNotifier.getInstance().notify(AppInfoUpdateEventBuilderUtil
                    .createDomainMappingCompletedEvent(appKey, message, description, msgType));
        } catch (AppFactoryEventException e) {
            // need not to throw an exception since failure of appwall notification should not interrupt the current operation
            log.error("Failed notifying custom url update event", e);
        }
    }

}