// 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 com.cloud.hypervisor.vmware.util;

import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;


import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.cloudstack.utils.security.SecureSSLSocketFactory;

import com.cloud.utils.StringUtils;

import org.w3c.dom.Element;

import com.vmware.pbm.PbmPortType;
import com.vmware.pbm.PbmService;
import com.vmware.pbm.PbmServiceInstanceContent;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import com.vmware.vim25.DynamicProperty;
import com.vmware.vim25.InvalidCollectorVersionFaultMsg;
import com.vmware.vim25.InvalidPropertyFaultMsg;
import com.vmware.vim25.LocalizedMethodFault;
import com.vmware.vim25.ManagedObjectReference;
import com.vmware.vim25.MethodFault;
import com.vmware.vim25.ObjectContent;
import com.vmware.vim25.ObjectSpec;
import com.vmware.vim25.ObjectUpdate;
import com.vmware.vim25.ObjectUpdateKind;
import com.vmware.vim25.PropertyChange;
import com.vmware.vim25.PropertyChangeOp;
import com.vmware.vim25.PropertyFilterSpec;
import com.vmware.vim25.PropertyFilterUpdate;
import com.vmware.vim25.PropertySpec;
import com.vmware.vim25.RequestCanceled;
import com.vmware.vim25.RuntimeFaultFaultMsg;
import com.vmware.vim25.SelectionSpec;
import com.vmware.vim25.ServiceContent;
import com.vmware.vim25.TaskInfo;
import com.vmware.vim25.TaskInfoState;
import com.vmware.vim25.TraversalSpec;
import com.vmware.vim25.UpdateSet;
import com.vmware.vim25.VimPortType;
import com.vmware.vim25.VimService;
import com.vmware.vim25.WaitOptions;

/**
 * A wrapper class to handle Vmware vsphere connection and disconnection.
 *
 * DISCLAIMER: This code is partly copied from sample codes that come along with Vmware web service 5.1 SDK.
 *
 */
public class VmwareClient {
    protected static Logger LOGGER = LogManager.getLogger(VmwareClient.class);

    private static class TrustAllTrustManager implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {

        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
        }

        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
        }
    }

    static {
        try {
            trustAllHttpsCertificates();
            HostnameVerifier hv = new HostnameVerifier() {
                @Override
                public boolean verify(String urlHostName, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(hv);

            vimService = new VimService();
            pbmService = new PbmService();
        } catch (Exception e) {
            LOGGER.info("[ignored]"
                    + "failed to trust all certificates blindly: ", e);
        }
    }

    private static void trustAllHttpsCertificates() throws Exception {
        // Create a trust manager that does not validate certificate chains:
        javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
        javax.net.ssl.TrustManager tm = new TrustAllTrustManager();
        trustAllCerts[0] = tm;
        javax.net.ssl.SSLContext sc = SSLUtils.getSSLContext();
        javax.net.ssl.SSLSessionContext sslsc = sc.getServerSessionContext();
        sslsc.setSessionTimeout(0);
        sc.init(null, trustAllCerts, null);
        javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(new SecureSSLSocketFactory(sc));
    }

    private final ManagedObjectReference svcInstRef = new ManagedObjectReference();
    private final ManagedObjectReference pbmSvcInstRef = new ManagedObjectReference();

    private static VimService vimService;
    private static PbmService pbmService;
    private PbmServiceInstanceContent pbmServiceContent;
    private VimPortType vimPort;
    private PbmPortType pbmPort;
    private static final String PBM_SERVICE_INSTANCE_TYPE = "PbmServiceInstance";
    private static final String PBM_SERVICE_INSTANCE_VALUE = "ServiceInstance";

    private String serviceCookie;
    private final static String SVC_INST_NAME = "ServiceInstance";
    private int vCenterSessionTimeout = 1200000; // Timeout in milliseconds

    private boolean isConnected = false;

    public VmwareClient(String name) {
    }

    /**
     * Establishes session with the virtual center server.
     *
     * @throws Exception
     *             the exception
     */
    public void connect(String url, String userName, String password) throws Exception {
        svcInstRef.setType(SVC_INST_NAME);
        svcInstRef.setValue(SVC_INST_NAME);

        vimPort = vimService.getVimPort();
        Map<String, Object> ctxt = ((BindingProvider)vimPort).getRequestContext();

        ctxt.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url);
        ctxt.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true);

        ctxt.put("com.sun.xml.internal.ws.request.timeout", vCenterSessionTimeout);
        ctxt.put("com.sun.xml.internal.ws.connect.timeout", vCenterSessionTimeout);

        ServiceContent serviceContent = vimPort.retrieveServiceContent(svcInstRef);

        // Extract a cookie. See vmware sample program com.vmware.httpfileaccess.GetVMFiles
        @SuppressWarnings("unchecked")
        Map<String, List<String>> headers = (Map<String, List<String>>)((BindingProvider)vimPort).getResponseContext().get(MessageContext.HTTP_RESPONSE_HEADERS);
        List<String> cookies = headers.get("Set-cookie");

        vimPort.login(serviceContent.getSessionManager(), userName, password, null);

        if (cookies == null) {
            // Get the cookie from the response header. See vmware sample program com.vmware.httpfileaccess.GetVMFiles
            @SuppressWarnings("unchecked")
            Map<String, List<String>> responseHeaders = (Map<String, List<String>>)((BindingProvider)vimPort).getResponseContext().get(MessageContext.HTTP_RESPONSE_HEADERS);
            cookies = responseHeaders.get("Set-cookie");
            if (cookies == null) {
                String msg = "Login successful, but failed to get server cookies from url :[" + url + "]";
                LOGGER.error(msg);
                throw new Exception(msg);
            }
        }

        String cookieValue = cookies.get(0);
        StringTokenizer tokenizer = new StringTokenizer(cookieValue, ";");
        cookieValue = tokenizer.nextToken();
        String pathData = "$" + tokenizer.nextToken();
        serviceCookie = "$Version=\"1\"; " + cookieValue + "; " + pathData;
        Map<String, List<String>> map = new HashMap<>();
        map.put("Cookie", Collections.singletonList(serviceCookie));
        ((BindingProvider)vimPort).getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS, map);
        pbmConnect(url, cookieValue);
        isConnected = true;
    }

    private void pbmConnect(String url, String cookieValue) throws Exception {
        URI uri = new URI(url);
        String pbmurl = "https://" + uri.getHost() + "/pbm";
        String[] tokens = cookieValue.split("=");
        String extractedCookie = tokens[1];

        HandlerResolver soapHandlerResolver = new HandlerResolver() {
            @Override
            public List<Handler> getHandlerChain(PortInfo portInfo) {
                VcenterSessionHandler VcSessionHandler = new VcenterSessionHandler(extractedCookie);
                List<Handler> handlerChain = new ArrayList<>();
                handlerChain.add(VcSessionHandler);
                return handlerChain;
            }
        };
        pbmService.setHandlerResolver(soapHandlerResolver);

        pbmSvcInstRef.setType(PBM_SERVICE_INSTANCE_TYPE);
        pbmSvcInstRef.setValue(PBM_SERVICE_INSTANCE_VALUE);
        pbmPort = pbmService.getPbmPort();
        Map<String, Object> pbmCtxt = ((BindingProvider)pbmPort).getRequestContext();
        pbmCtxt.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true);
        pbmCtxt.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, pbmurl);
    }

    /**
     * Disconnects the user session.
     *
     * @throws Exception
     */
    public void disconnect() throws Exception {
        if (isConnected) {
            vimPort.logout(getServiceContent().getSessionManager());
        }
        isConnected = false;
    }

    /**
     * @return Service instance
     */
    public VimPortType getService() {
        return vimPort;
    }

    /**
     * @return Service instance content
     */
    public ServiceContent getServiceContent() {

        try {
            return vimPort.retrieveServiceContent(svcInstRef);
        } catch (RuntimeFaultFaultMsg e) {
            // ignored
        }
        return null;
    }

    /**
     * @return PBM service instance
     */
    public PbmPortType getPbmService() {
        return pbmPort;
    }

    /**
     * @return Service instance content
     */
    public PbmServiceInstanceContent getPbmServiceContent() {
        try {
            return pbmPort.pbmRetrieveServiceContent(pbmSvcInstRef);
        } catch (com.vmware.pbm.RuntimeFaultFaultMsg e) {
            // ignored
        }
        return null;
    }

    /**
     * @return cookie used in service connection
     */
    public String getServiceCookie() {
        return serviceCookie;
    }

    /**
     * @return Service property collector
     */
    public ManagedObjectReference getPropCol() {
        return getServiceContent().getPropertyCollector();
    }

    /**
     * @return Root folder
     */
    public ManagedObjectReference getRootFolder() {
        return getServiceContent().getRootFolder();
    }

    public boolean validate() {
        //
        // There is no official API to validate an open vCenter API session. This is hacking way to tell if
        // an open vCenter API session is still valid for making calls.
        //
        // It will give false result if there really does not exist data-center in the inventory, however, I consider
        // this really is not possible in production deployment
        //

        // Create PropertySpecs
        PropertySpec pSpec = new PropertySpec();
        pSpec.setType("Datacenter");
        pSpec.setAll(false);
        pSpec.getPathSet().add("name");

        ObjectSpec oSpec = new ObjectSpec();
        oSpec.setObj(getRootFolder());
        oSpec.setSkip(false);
        oSpec.getSelectSet().addAll(constructCompleteTraversalSpec());

        PropertyFilterSpec spec = new PropertyFilterSpec();
        spec.getPropSet().add(pSpec);
        spec.getObjectSet().add(oSpec);
        List<PropertyFilterSpec> specArr = new ArrayList<>();
        specArr.add(spec);

        try {
            List<ObjectContent> ocary = vimPort.retrieveProperties(getPropCol(), specArr);
            if (ocary != null && !ocary.isEmpty())
                return true;
        } catch (Exception e) {
            return false;
        }

        return false;
    }

    /**
     * Get the property value of a managed object.
     *
     * @param mor
     *            managed object reference
     * @param propertyName
     *            property name.
     * @return property value.
     */
    @SuppressWarnings("unchecked")
    public <T> T getDynamicProperty(ManagedObjectReference mor, String propertyName) throws Exception {
        List<String> props = new ArrayList<String>();
        props.add(propertyName);
        List<ObjectContent> objContent = retrieveMoRefProperties(mor, props);

        Object propertyValue = null;
        if (objContent != null && !objContent.isEmpty()) {
            List<DynamicProperty> dynamicProperty = objContent.get(0).getPropSet();
            if (dynamicProperty != null && !dynamicProperty.isEmpty()) {
                DynamicProperty dp = dynamicProperty.get(0);
                propertyValue = dp.getVal();
                /*
                 * If object is ArrayOfXXX object, then get the XXX[] by
                 * invoking getXXX() on the object.
                 * For Ex:
                 * ArrayOfManagedObjectReference.getManagedObjectReference()
                 * returns ManagedObjectReference[] array.
                 */
                Class dpCls = propertyValue.getClass();
                String dynamicPropertyName = dpCls.getName();
                if (dynamicPropertyName.contains("ArrayOf")) {
                    String methodName = "get" + dynamicPropertyName.substring(dynamicPropertyName.indexOf("ArrayOf") + "ArrayOf".length(), dynamicPropertyName.length());

                    Method getMorMethod = dpCls.getDeclaredMethod(methodName, null);
                    propertyValue = getMorMethod.invoke(propertyValue, (Object[])null);
                }
            }
        }
        return (T)propertyValue;
    }

    private List<ObjectContent> retrieveMoRefProperties(ManagedObjectReference mObj, List<String> props) throws Exception {
        PropertySpec pSpec = new PropertySpec();
        pSpec.setAll(false);
        pSpec.setType(mObj.getType());
        pSpec.getPathSet().addAll(props);

        ObjectSpec oSpec = new ObjectSpec();
        oSpec.setObj(mObj);
        oSpec.setSkip(false);
        PropertyFilterSpec spec = new PropertyFilterSpec();
        spec.getPropSet().add(pSpec);
        spec.getObjectSet().add(oSpec);
        List<PropertyFilterSpec> specArr = new ArrayList<>();
        specArr.add(spec);

        return vimPort.retrieveProperties(getPropCol(), specArr);
    }

    /**
     * This method returns a boolean value specifying whether the Task is
     * succeeded or failed.
     *
     * @param task
     *            ManagedObjectReference representing the Task.
     *
     * @return boolean value representing the Task result.
     * @throws InvalidCollectorVersionFaultMsg
     * @throws RuntimeFaultFaultMsg
     * @throws InvalidPropertyFaultMsg
     */
    public boolean waitForTask(ManagedObjectReference task) throws InvalidPropertyFaultMsg, RuntimeFaultFaultMsg, InvalidCollectorVersionFaultMsg, Exception {

        boolean retVal = false;

        try {
            // info has a property - state for state of the task
            Object[] result = waitForValues(task, new String[] { "info.state", "info.error" }, new String[] { "state" }, new Object[][] { new Object[] {
                    TaskInfoState.SUCCESS, TaskInfoState.ERROR } });

            if (result != null && result.length == 2) { //result for 2 properties: info.state, info.error
                if (result[0].equals(TaskInfoState.SUCCESS)) {
                    retVal = true;
                }
                if (result[1] instanceof LocalizedMethodFault) {
                    throw new RuntimeException(((LocalizedMethodFault)result[1]).getLocalizedMessage());
                }
            }
        } catch (WebServiceException we) {
            LOGGER.warn("Session to vCenter failed with: " + we.getLocalizedMessage());

            TaskInfo taskInfo = (TaskInfo)getDynamicProperty(task, "info");
            if (!taskInfo.isCancelable()) {
                LOGGER.warn("vCenter task: " + taskInfo.getName() + "(" + taskInfo.getKey() + ")" + " will continue to run on vCenter because the task cannot be cancelled");
                throw new RuntimeException(we.getLocalizedMessage());
            }

            LOGGER.debug("Cancelling vCenter task: " + taskInfo.getName() + "(" + taskInfo.getKey() + ")");
            getService().cancelTask(task);

            // Since task cancellation is asynchronous, wait for the task to be cancelled
            Object[] result = waitForValues(task, new String[] {"info.state", "info.error"}, new String[] {"state"},
                    new Object[][] {new Object[] {TaskInfoState.SUCCESS, TaskInfoState.ERROR}});

            if (result != null && result.length == 2) { //result for 2 properties: info.state, info.error
                if (result[0].equals(TaskInfoState.SUCCESS)) {
                    LOGGER.warn("Failed to cancel vCenter task: " + taskInfo.getName() + "(" + taskInfo.getKey() + ")" + " and the task successfully completed");
                    retVal = true;
                }

                if (result[1] instanceof LocalizedMethodFault) {
                    MethodFault fault = ((LocalizedMethodFault)result[1]).getFault();
                    if (fault instanceof RequestCanceled) {
                        LOGGER.debug("vCenter task " + taskInfo.getName() + "(" + taskInfo.getKey() + ")" + " was successfully cancelled");
                        throw new RuntimeException(we.getLocalizedMessage());
                    }
                } else {
                    throw new RuntimeException(((LocalizedMethodFault)result[1]).getLocalizedMessage());
                }
            }
        }
        return retVal;
    }

    /**
     * Handle Updates for a single object. waits till expected values of
     * properties to check are reached Destroys the ObjectFilter when done.
     *
     * @param objmor
     *            MOR of the Object to wait for
     * @param filterProps
     *            Properties list to filter
     * @param endWaitProps
     *            Properties list to check for expected values these be
     *            properties of a property in the filter properties list
     * @param expectedVals
     *            values for properties to end the wait
     * @return true indicating expected values were met, and false otherwise
     * @throws RuntimeFaultFaultMsg
     * @throws InvalidPropertyFaultMsg
     * @throws InvalidCollectorVersionFaultMsg
     */
    private synchronized Object[] waitForValues(ManagedObjectReference objmor, String[] filterProps, String[] endWaitProps, Object[][] expectedVals) throws InvalidPropertyFaultMsg,
    RuntimeFaultFaultMsg, InvalidCollectorVersionFaultMsg {
        // version string is initially null
        String version = "";
        Object[] endVals = new Object[endWaitProps.length];
        Object[] filterVals = new Object[filterProps.length];
        String stateVal = null;

        PropertyFilterSpec spec = new PropertyFilterSpec();
        ObjectSpec oSpec = new ObjectSpec();
        oSpec.setObj(objmor);
        oSpec.setSkip(Boolean.FALSE);
        spec.getObjectSet().add(oSpec);

        PropertySpec pSpec = new PropertySpec();
        pSpec.getPathSet().addAll(Arrays.asList(filterProps));
        pSpec.setType(objmor.getType());
        spec.getPropSet().add(pSpec);

        ManagedObjectReference propertyCollector = getPropCol();
        ManagedObjectReference filterSpecRef = vimPort.createFilter(propertyCollector, spec, true);

        boolean reached = false;

        UpdateSet updateset = null;
        List<PropertyFilterUpdate> filtupary = null;
        List<ObjectUpdate> objupary = null;
        List<PropertyChange> propchgary = null;
        while (!reached) {
            updateset = vimPort.waitForUpdatesEx(propertyCollector, version, new WaitOptions());
            if (updateset == null || updateset.getFilterSet() == null) {
                continue;
            }
            version = updateset.getVersion();

            // Make this code more general purpose when PropCol changes later.
            filtupary = updateset.getFilterSet();

            for (PropertyFilterUpdate filtup : filtupary) {
                objupary = filtup.getObjectSet();
                for (ObjectUpdate objup : objupary) {
                    if (objup.getKind() == ObjectUpdateKind.MODIFY || objup.getKind() == ObjectUpdateKind.ENTER || objup.getKind() == ObjectUpdateKind.LEAVE) {
                        propchgary = objup.getChangeSet();
                        for (PropertyChange propchg : propchgary) {
                            updateValues(endWaitProps, endVals, propchg);
                            updateValues(filterProps, filterVals, propchg);
                        }
                    }
                }
            }

            Object expctdval = null;
            // Check if the expected values have been reached and exit the loop if done.
            // Also exit the WaitForUpdates loop if this is the case.
            for (int chgi = 0; chgi < endVals.length && !reached; chgi++) {
                for (int vali = 0; vali < expectedVals[chgi].length && !reached; vali++) {
                    expctdval = expectedVals[chgi][vali];

                    if (endVals[chgi] == null) {
                        // Do nothing
                    } else if (endVals[chgi].toString().contains("val: null")) {
                        // Handle JAX-WS De-serialization issue, by parsing nodes
                        Element stateElement = (Element) endVals[chgi];
                        if (stateElement != null && stateElement.getFirstChild() != null) {
                            stateVal = stateElement.getFirstChild().getTextContent();
                            reached = expctdval.toString().equalsIgnoreCase(stateVal) || reached;
                        }
                    } else {
                        reached = expctdval.equals(endVals[chgi]) || reached;
                        stateVal = "filtervals";
                    }
                }
            }
        }

        // Destroy the filter when we are done.
        vimPort.destroyPropertyFilter(filterSpecRef);

        Object[] retVal = filterVals;
        if (stateVal != null && stateVal.equalsIgnoreCase("success")) {
            retVal = new Object[] { TaskInfoState.SUCCESS, null };
        }

        return retVal;
    }

    private void updateValues(String[] props, Object[] vals, PropertyChange propchg) {
        for (int findi = 0; findi < props.length; findi++) {
            if (propchg.getName().lastIndexOf(props[findi]) >= 0) {
                if (propchg.getOp() == PropertyChangeOp.REMOVE) {
                    vals[findi] = "";
                } else {
                    vals[findi] = propchg.getVal();
                }
            }
        }
    }

    private SelectionSpec getSelectionSpec(String name) {
        SelectionSpec genericSpec = new SelectionSpec();
        genericSpec.setName(name);
        return genericSpec;
    }

    /*
     * @return An array of SelectionSpec covering VM, Host, Resource pool,
     * Cluster Compute Resource and Datastore.
     */
    private List<SelectionSpec> constructCompleteTraversalSpec() {
        // ResourcePools to VM: RP -> VM
        TraversalSpec rpToVm = new TraversalSpec();
        rpToVm.setName("rpToVm");
        rpToVm.setType("ResourcePool");
        rpToVm.setPath("vm");
        rpToVm.setSkip(Boolean.FALSE);

        // VirtualApp to VM: vApp -> VM
        TraversalSpec vAppToVM = new TraversalSpec();
        vAppToVM.setName("vAppToVM");
        vAppToVM.setType("VirtualApp");
        vAppToVM.setPath("vm");

        // Host to VM: HostSystem -> VM
        TraversalSpec hToVm = new TraversalSpec();
        hToVm.setType("HostSystem");
        hToVm.setPath("vm");
        hToVm.setName("hToVm");
        hToVm.getSelectSet().add(getSelectionSpec("VisitFolders"));
        hToVm.setSkip(Boolean.FALSE);

        // DataCenter to DataStore: DC -> DS
        TraversalSpec dcToDs = new TraversalSpec();
        dcToDs.setType("Datacenter");
        dcToDs.setPath("datastore");
        dcToDs.setName("dcToDs");
        dcToDs.setSkip(Boolean.FALSE);

        // Recurse through all ResourcePools
        TraversalSpec rpToRp = new TraversalSpec();
        rpToRp.setType("ResourcePool");
        rpToRp.setPath("resourcePool");
        rpToRp.setSkip(Boolean.FALSE);
        rpToRp.setName("rpToRp");
        rpToRp.getSelectSet().add(getSelectionSpec("rpToRp"));

        TraversalSpec crToRp = new TraversalSpec();
        crToRp.setType("ComputeResource");
        crToRp.setPath("resourcePool");
        crToRp.setSkip(Boolean.FALSE);
        crToRp.setName("crToRp");
        crToRp.getSelectSet().add(getSelectionSpec("rpToRp"));

        TraversalSpec crToH = new TraversalSpec();
        crToH.setSkip(Boolean.FALSE);
        crToH.setType("ComputeResource");
        crToH.setPath("host");
        crToH.setName("crToH");

        TraversalSpec dcToHf = new TraversalSpec();
        dcToHf.setSkip(Boolean.FALSE);
        dcToHf.setType("Datacenter");
        dcToHf.setPath("hostFolder");
        dcToHf.setName("dcToHf");
        dcToHf.getSelectSet().add(getSelectionSpec("VisitFolders"));

        TraversalSpec vAppToRp = new TraversalSpec();
        vAppToRp.setName("vAppToRp");
        vAppToRp.setType("VirtualApp");
        vAppToRp.setPath("resourcePool");
        vAppToRp.getSelectSet().add(getSelectionSpec("rpToRp"));

        TraversalSpec dcToVmf = new TraversalSpec();
        dcToVmf.setType("Datacenter");
        dcToVmf.setSkip(Boolean.FALSE);
        dcToVmf.setPath("vmFolder");
        dcToVmf.setName("dcToVmf");
        dcToVmf.getSelectSet().add(getSelectionSpec("VisitFolders"));

        // For Folder -> Folder recursion
        TraversalSpec visitFolders = new TraversalSpec();
        visitFolders.setType("Folder");
        visitFolders.setPath("childEntity");
        visitFolders.setSkip(Boolean.FALSE);
        visitFolders.setName("VisitFolders");
        List<SelectionSpec> sspecarrvf = new ArrayList<>();
        sspecarrvf.add(getSelectionSpec("crToRp"));
        sspecarrvf.add(getSelectionSpec("crToH"));
        sspecarrvf.add(getSelectionSpec("dcToVmf"));
        sspecarrvf.add(getSelectionSpec("dcToHf"));
        sspecarrvf.add(getSelectionSpec("vAppToRp"));
        sspecarrvf.add(getSelectionSpec("vAppToVM"));
        sspecarrvf.add(getSelectionSpec("dcToDs"));
        sspecarrvf.add(getSelectionSpec("hToVm"));
        sspecarrvf.add(getSelectionSpec("rpToVm"));
        sspecarrvf.add(getSelectionSpec("VisitFolders"));

        visitFolders.getSelectSet().addAll(sspecarrvf);

        List<SelectionSpec> resultspec = new ArrayList<>();
        resultspec.add(visitFolders);
        resultspec.add(crToRp);
        resultspec.add(crToH);
        resultspec.add(dcToVmf);
        resultspec.add(dcToHf);
        resultspec.add(vAppToRp);
        resultspec.add(vAppToVM);
        resultspec.add(dcToDs);
        resultspec.add(hToVm);
        resultspec.add(rpToVm);
        resultspec.add(rpToRp);

        return resultspec;
    }

    /**
     * Get the ManagedObjectReference for an item under the
     * specified root folder that has the type and name specified.
     *
     * @param root a root folder if available, or null for default
     * @param type type of the managed object
     * @param name name to match
     *
     * @return First ManagedObjectReference of the type / name pair found
     */
    public ManagedObjectReference getDecendentMoRef(ManagedObjectReference root, String type, String name) throws Exception {
        if (name == null || name.isEmpty()) {
            return null;
        }

        try {
            // Create PropertySpecs
            PropertySpec pSpec = new PropertySpec();
            pSpec.setType(type);
            pSpec.setAll(false);
            pSpec.getPathSet().add("name");

            ObjectSpec oSpec = new ObjectSpec();
            oSpec.setObj(root);
            oSpec.setSkip(false);
            oSpec.getSelectSet().addAll(constructCompleteTraversalSpec());

            PropertyFilterSpec spec = new PropertyFilterSpec();
            spec.getPropSet().add(pSpec);
            spec.getObjectSet().add(oSpec);
            List<PropertyFilterSpec> specArr = new ArrayList<>();
            specArr.add(spec);

            ManagedObjectReference propCollector = getPropCol();
            List<ObjectContent> ocary = vimPort.retrieveProperties(propCollector, specArr);

            if (ocary == null || ocary.isEmpty()) {
                return null;
            }

            // filter through retrieved objects to get the first match.
            for (ObjectContent oc : ocary) {
                ManagedObjectReference mor = oc.getObj();
                List<DynamicProperty> propary = oc.getPropSet();
                if (type == null || type.equals(mor.getType())) {
                    if (!propary.isEmpty()) {
                        String propval = (String)propary.get(0).getVal();
                        if (name.equalsIgnoreCase(propval))
                            return mor;
                    }
                }
            }
        } catch (InvalidPropertyFaultMsg | RuntimeFaultFaultMsg invalidPropertyException) {
            LOGGER.debug("Failed to get Vmware ManagedObjectReference for name: " + name + " and type: " + type + " due to " + invalidPropertyException.getMessage());
            throw invalidPropertyException;
        }

        return null;
    }

    /**
     * Get a MORef from the property returned.
     *
     * @param objMor Object to get a reference property from
     * @param propName name of the property that is the MORef
     * @return the ManagedObjectReference for that property.
     */
    public ManagedObjectReference getMoRefProp(ManagedObjectReference objMor, String propName) throws Exception {
        Object props = getDynamicProperty(objMor, propName);
        ManagedObjectReference propmor = null;
        if (!props.getClass().isArray()) {
            propmor = (ManagedObjectReference)props;
        }
        return propmor;
    }

    public void setVcenterSessionTimeout(int vCenterSessionTimeout) {
        this.vCenterSessionTimeout = vCenterSessionTimeout;
    }

    public int getVcenterSessionTimeout() {
        return vCenterSessionTimeout;
    }

    public void cancelTask(ManagedObjectReference task) throws Exception {
        TaskInfo info = (TaskInfo)(getDynamicProperty(task, "info"));
        if (info == null) {
            LOGGER.warn("Unable to get the task info, so couldn't cancel the task");
            return;
        }

        String taskName = StringUtils.isNotBlank(info.getName()) ? info.getName() : "Unknown";
        taskName += "(" + info.getKey() + ")";

        String entityName = StringUtils.isNotBlank(info.getEntityName()) ? info.getEntityName() : "";

        if (info.getState().equals(TaskInfoState.SUCCESS)) {
            LOGGER.debug(taskName + " task successfully completed for the entity " + entityName + ", can't cancel it");
            return;
        }

        if (info.getState().equals(TaskInfoState.ERROR)) {
            LOGGER.debug(taskName + " task execution failed for the entity " + entityName + ", can't cancel it");
            return;
        }

        LOGGER.debug(taskName + " task pending for the entity " + entityName + ", trying to cancel");
        if (!info.isCancelable()) {
            LOGGER.warn(taskName + " task will continue to run on vCenter because it can't be cancelled");
            return;
        }

        LOGGER.debug("Cancelling task " + taskName + " of the entity " + entityName);
        getService().cancelTask(task);

        // Since task cancellation is asynchronous, wait for the task to be cancelled
        Object[] result = waitForValues(task, new String[] {"info.state", "info.error"}, new String[] {"state"},
                new Object[][] {new Object[] {TaskInfoState.SUCCESS, TaskInfoState.ERROR}});

        if (result != null && result.length == 2) { //result for 2 properties: info.state, info.error
            if (result[0].equals(TaskInfoState.SUCCESS)) {
                LOGGER.warn("Failed to cancel" + taskName + " task of the entity " + entityName + ", the task successfully completed");
            }

            if (result[1] instanceof LocalizedMethodFault) {
                MethodFault fault = ((LocalizedMethodFault)result[1]).getFault();
                if (fault instanceof RequestCanceled) {
                    LOGGER.debug(taskName + " task of the entity " + entityName + " was successfully cancelled");
                }
            } else {
                LOGGER.warn("Couldn't cancel " + taskName + " task of the entity " + entityName + " due to " + ((LocalizedMethodFault)result[1]).getLocalizedMessage());
            }
        }
    }
}
