All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.vcloud.api.rest.client.VcdClientImpl Maven / Gradle / Ivy

There is a newer version: 0.10.0
Show newest version
/* ***************************************************************************
 * api-extension-template-vcloud-director
 * Copyright 2018 VMware, Inc.
 * SPDX-License-Identifier: BSD-2-Clause
 * **************************************************************************/

package com.vmware.vcloud.api.rest.client;

import java.net.HttpURLConnection;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.jaxrs.client.Client;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.provider.JAXBElementProvider;
import org.apache.cxf.jaxrs.provider.JAXBElementTypedProvider;

import com.vmware.cxfrestclient.CxfClientSecurityContext;
import com.vmware.vcloud.api.rest.client.constants.RelationType;
import com.vmware.vcloud.api.rest.client.constants.RestAdminConstants;
import com.vmware.vcloud.api.rest.client.constants.RestConstants;
import com.vmware.vcloud.api.rest.client.constants.RestConstants.HttpStatusCodes;
import com.vmware.vcloud.api.rest.client.filters.MultisiteAuthorizationFilter;
import com.vmware.vcloud.api.rest.client.impl.EventViewerImpl;
import com.vmware.vcloud.api.rest.client.impl.tasks.VcdTaskMonitorImpl;
import com.vmware.vcloud.api.rest.schema.versioning.SupportedVersionsType;
import com.vmware.vcloud.api.rest.schema.versioning.VersionInfoType;
import com.vmware.vcloud.api.rest.schema_v1_5.AdminOrgType;
import com.vmware.vcloud.api.rest.schema_v1_5.ApiExtensibilityType;
import com.vmware.vcloud.api.rest.schema_v1_5.ContainerType;
import com.vmware.vcloud.api.rest.schema_v1_5.EntityType;
import com.vmware.vcloud.api.rest.schema_v1_5.ErrorType;
import com.vmware.vcloud.api.rest.schema_v1_5.LinkType;
import com.vmware.vcloud.api.rest.schema_v1_5.MultisiteSessionUserInfoType;
import com.vmware.vcloud.api.rest.schema_v1_5.OrgListType;
import com.vmware.vcloud.api.rest.schema_v1_5.OrgType;
import com.vmware.vcloud.api.rest.schema_v1_5.OrganizationReferenceType;
import com.vmware.vcloud.api.rest.schema_v1_5.QueryListType;
import com.vmware.vcloud.api.rest.schema_v1_5.QueryResultRecordType;
import com.vmware.vcloud.api.rest.schema_v1_5.QueryResultRecordsType;
import com.vmware.vcloud.api.rest.schema_v1_5.ReferenceType;
import com.vmware.vcloud.api.rest.schema_v1_5.ReferencesType;
import com.vmware.vcloud.api.rest.schema_v1_5.ResourceType;
import com.vmware.vcloud.api.rest.schema_v1_5.SessionType;
import com.vmware.vcloud.api.rest.schema_v1_5.VCloudType;
import com.vmware.vcloud.api.rest.schema_v1_5.extension.VMWExtensionType;
import com.vmware.vcloud.api.rest.version.ApiVersion;

public class VcdClientImpl extends AbstractVcdClientBase implements VcdClient {

    private volatile String authenticationToken;
    private volatile String jwtToken;
    private volatile ClientCredentials clientCredentials;
    private volatile MultivaluedMap responseHeaders;
    private final Map cookies = new LinkedHashMap();
    private final VcdTaskMonitor taskMonitor = new VcdTaskMonitorImpl(this);
    private final EventViewer eventViewer = new EventViewerImpl(this);
    private Map queryListMap = null;

    private static final String BEARER = "Bearer";

    private final String apiVersion;
    private boolean federateRequests = false;
    private String orgContext;
    private String orgSecurityContext;

    private URI sessionHref = null;

    private URI loggedInAdminOrgEndpoint = null;

    private Map sessionEndpoints;

    private static final Class[] OBJECT_FACTORIES = new Class[] {
            com.vmware.vcloud.api.rest.schema.versioning.ObjectFactory.class,
            com.vmware.vcloud.api.rest.schema_v1_5.ObjectFactory.class,
            com.vmware.vcloud.api.rest.schema_v1_5.extension.ObjectFactory.class,
            com.vmware.vcloud.api.rest.schema.ovf.vmware.ObjectFactory.class,
            com.vmware.vcloud.api.rest.schema.ovf.ObjectFactory.class,
            com.vmware.vcloud.api.rest.schema.ovf.environment.ObjectFactory.class
    };

    /**
     * Implementation of {@link SessionToken} to represent a current session with vCD. It can be retrieved
     * from one {@link VcdClient} and used by another {@link VcdClient} to re-use an existing session.
     */
    public static final class SessionTokenImpl implements SessionToken {

        /**
         * The authentication token from a previous login.
         */
        private final String authenticationToken;

        /**
         * The vcloud-token cookie from a previous login.
         */
        private final String vCloudToken;

        public SessionTokenImpl(final String authenticationToken, final String vCloudToken) {
            this.authenticationToken = authenticationToken;
            this.vCloudToken = vCloudToken;
        }

        @Override
        public String getAuthenticationToken() {
            return authenticationToken;
        }

        public String getVCloudToken() {
            return vCloudToken;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            SessionTokenImpl other = (SessionTokenImpl) obj;
            if (authenticationToken == null) {
                if (other.authenticationToken != null) {
                    return false;
                }
            } else if (!authenticationToken.equals(other.authenticationToken)) {
                return false;
            }
            if (vCloudToken == null) {
                if (other.vCloudToken != null) {
                    return false;
                }
            } else if (!vCloudToken.equals(other.vCloudToken)) {
                return false;
            }
            return true;
        }
    }

    private final static List PROVIDER_LIST = createJAXBElementProviderFromObjectFactories();

    /**
     * Creates a {@link JAXBElementProvider} that will return {@link JAXBContext}s that handle all
     * the JAXB-generated types using object factories supplied to this method.
     */
    protected static List> createJAXBElementProviderFromObjectFactories() {
        List> list = new ArrayList>();

        JAXBElementTypedProvider jaxbProvider = new JAXBElementTypedProvider();
        jaxbProvider.setExtraClass(OBJECT_FACTORIES);
        list.add(jaxbProvider);

        JAXBElementProvider objProvider = new JAXBElementProvider();
        objProvider.setExtraClass(OBJECT_FACTORIES);
        list.add(objProvider);

        return list;
    }

    @Override
    protected List getCxfProviders() {
        final List providers = PROVIDER_LIST.stream().collect(Collectors.toList());
        if (clientCredentials instanceof VcdMultisiteLoginCredentials) {
            providers.add(new MultisiteAuthorizationFilter(
                    (VcdMultisiteLoginCredentials) clientCredentials));
        }
        return providers;
    }

    /**
     * Creates an instance of VcdClientImpl with given endpoint, API version and cxfClientSecurityContext.
     */
    public VcdClientImpl(final URI endpoint, final String apiVersion, final CxfClientSecurityContext cxfClientSecurityContext) {
        super(endpoint, cxfClientSecurityContext);
        this.apiVersion = apiVersion;
    }

    /**
     * Creates an instance of VcdClientImpl with given endpoint, cxfClientSecurityContext and
     * determines an {@link ApiVersion} supported by both given vCD {@code endpoint}, and given list
     * of preferred API versions in {@code candidateVersions}
     *
     * @param endpoint
     *            URI of vCD
     * @param candidateVersions
     *            a list of {@code ApiVersion}s in order of most to least preferred by client
     * @param cxfClientSecurityContext
     *            security context to use when communicating with given HTTPS vCD {@code endpoint}
     */
    public VcdClientImpl(final URI endpoint, final List candidateVersions,
            final CxfClientSecurityContext cxfClientSecurityContext) {
        super(endpoint, cxfClientSecurityContext);

        final SupportedVersionsType supportedVersions = getVersions();

        final Set cloudSupportedApiVersions = new HashSet<>();

        for (final VersionInfoType versionType : supportedVersions.getVersionInfo()) {
            cloudSupportedApiVersions.add(versionType.getVersion());
        }

        for (final ApiVersion candidateVersion : candidateVersions) {
            if (cloudSupportedApiVersions.contains(candidateVersion.value())) {
                apiVersion = candidateVersion.value();
                return;
            }
        }

        throw new RuntimeException(
                "No preferred API Version is supported by cloud. Preferred versions: "
                        + candidateVersions.toString() + "; vCD supported versions:"
                        + cloudSupportedApiVersions.toString() + "; Cloud URL:" + endpoint);

    }

    private VcdClientImpl(VcdClientImpl vcdClient) {
        super(vcdClient);
        this.apiVersion = vcdClient.apiVersion;
    }

    private URI getEndpoint(WellKnownEndpoint endpoint) {
        return sessionEndpoints == null ? null : sessionEndpoints.get(endpoint);
    }

    /**
     * Gets the openApi endpoint.
     */
    protected URI getOpenApiEndpoint() {
        UriBuilder builder = UriBuilder.fromUri(this.getEndpoint()).replacePath("cloudapi");
        final URI openApiEndPoint = builder.build();
        return openApiEndPoint;
    }

    @Override
    public List getOrganizations() {
        final OrgListType orgList = createWebClient(validateEndpoint(WellKnownEndpoint.ORG_LIST)).get(OrgListType.class);
        return Collections.unmodifiableList(orgList.getOrg());
    }

    @Override
    public SessionType getSession() {
        validateEndpoint(sessionHref);
        return getResource(sessionHref, SessionType.class);
    }

    @Override
    public MultisiteSessionUserInfoType getSessionUserInfo() {
        validateEndpoint(sessionHref);
        return getResource(UriBuilder.fromUri(sessionHref).path("userInfo").build(),
                MultisiteSessionUserInfoType.class);
    }

    @Override
    public SupportedVersionsType getVersions() {
        return getResource(UriBuilder.fromUri(endpoint).path(RestConstants.Uri.VERSIONS).build(),
                SupportedVersionsType.class);
    }

    @Override
    public String getClientApiVersion() {
        return apiVersion;
    }

    private  ResponseClass getResource(WellKnownEndpoint endpoint, Class resourceClass) {
        return getResource(validateEndpoint(endpoint), resourceClass);
    }

    @Override
    public VCloudType getAdmin() {
        return getResource(WellKnownEndpoint.ADMIN, VCloudType.class);
    }

    @Override
    public VMWExtensionType getExtension() {
        return getResource(WellKnownEndpoint.EXTENSION, VMWExtensionType.class);
    }

    @Override
    public QueryListType getQueryList() {
        return getResource(WellKnownEndpoint.QUERY_LIST, QueryListType.class);
    }

    @Override
    public ApiExtensibilityType getApiExtensibility() {
        return getResource(WellKnownEndpoint.API_EXTENSIBILITY, ApiExtensibilityType.class);
    }

    @Override
    public OrgType getLoggedInOrg() {
        return getResource(WellKnownEndpoint.LOGGED_IN_ORG, OrgType.class);
    }

    @Override
    public EntityType resolveEntityById(String id) {
        return getResource(UriBuilder.fromUri(validateEndpoint(WellKnownEndpoint.ENTITY_RESOLVER))
                .path(id).build(), EntityType.class);
    }

    @Override
    public AdminOrgType getLoggedInAdminOrg() {

        OrgType org = getLoggedInOrg();
        LinkType link = VcdUtils.findLink(org, RelationType.ALTERNATE,
                                          RestAdminConstants.MediaType.ORGANIZATIONM, false);
        if (link != null) { // link is only present in newer versions.
            return getResource(link.getHref(), AdminOrgType.class);
        } else {
            // We indicate lack of admin access by throwing a missing link exception
            // for the 'admin' link as that would be pre-requisite to getting admin org link
            if(loggedInAdminOrgEndpoint == null){
                loggedInAdminOrgEndpoint = getLoggedInOrgAdminURI();
            }
            validateEndpoint(loggedInAdminOrgEndpoint, RelationType.DOWN,
                             RestAdminConstants.MediaType.VCLOUDM);
            return getResource(loggedInAdminOrgEndpoint, AdminOrgType.class);
        }
    }

    @Override
    public OpenApiClient getOpenApiClient() {
        return new OpenApiClientImpl(this);
    }

    @Override
    public NsxProxyApiClient getNsxProxyApiClient() {
        return new NsxProxyApiClient(this);
    }

    private void validateEndpoint(URI endpoint) {
        if (endpoint == null) {
            throw new IllegalStateException("Can't perform operation when not logged in");
        }
    }

    /**
     * Validates the endpoint to make sure it is accessible by the user
     *
     * @param endpoint
     *            {@link URI} of the endpoint to validate
     * @param rel
     *            {@link RelationType} enum value describing the relation of the endpoint to the
     *            <Session> element in the schema. Used to generate
     *            {@link MissingLinkException} if the endpoint is unavailable
     * @param mediaType
     *            {@code MediaType} of the object pointed to by the relationship above. Used to
     *            generate {@link MissingLinkException} if the endpoint is unavailable
     * @throws MissingLinkException
     *             to indicate link is unavailable
     */
    private void validateEndpoint(URI endpoint, RelationType rel, String mediaType) {
        if (endpoint == null) {
            throw new MissingLinkException(sessionHref.toASCIIString(), rel, mediaType);
        }
    }

    /**
     * Validates the well-known endpoint to make sure it is accessible by the user and
     * return its corresponding URI if it is.
     * @param endpoint the well-known endpoint to validate
     * @throws MissingLinkException to indicate link is unavailable
     */
    private URI validateEndpoint(WellKnownEndpoint endpoint) {
        URI uri = getEndpoint(endpoint);
        validateEndpoint(uri, endpoint.rel, endpoint.mediaType);
        return uri;
    }

    @Override
    protected String[] getAcceptHeaders() {
        StringBuffer acceptHeader = new StringBuffer("application/*+xml");

        if (apiVersion != null) {
            acceptHeader.append(";" + RestConstants.API_VERSION_ATTR);
            acceptHeader.append(apiVersion);
        }
        if (federateRequests) {
            acceptHeader.append(";" + RestConstants.MULTISITE_ATTR + "global");
        }
        final String accept = acceptHeader.toString();
        return new String[] { accept };
    }

    @Override
    public void setAuthenticationHeader(final Client client) {
        if (jwtToken != null) {
            client.header("Authorization", BEARER + " " + jwtToken);
            if (orgSecurityContext != null) {
                client.header(RestConstants.VCLOUD_AUTH_CONTEXT_HEADER, orgSecurityContext);
            }
        } else if (hasSessionlessClientCredentials()) {
            client.header(clientCredentials.getHeaderName(), clientCredentials.getHeaderValue());
        } else if (authenticationToken != null) {
            client.header(RestConstants.VCLOUD_AUTHENTICATION_HEADER, authenticationToken);
        }
    }

    @Override
    protected void setAuthenticationHeaders(final Client client) {
        setAuthenticationHeader(client);
        if (cookies.containsKey(RestConstants.JWT_COOKIE_NAME)) {
            addCookie(RestConstants.JWT_COOKIE_NAME, client);
        }

        if (cookies.containsKey(RestConstants.SESSION_COOKIE_NAME)) {
            addCookie(RestConstants.SESSION_COOKIE_NAME, client);
        } else if (cookies.containsKey(RestConstants.VCLOUD_COOKIE_NAME)) {
            addCookie(RestConstants.VCLOUD_COOKIE_NAME, client);
        }
    }

    private void addCookie(final String cookieName, final Client client) {
        final String rawCookie = cookies.get(cookieName);
        final String cookieValue = rawCookie.substring(rawCookie.indexOf("=") + 1);
        final Cookie cookie = new Cookie(cookieName, cookieValue);
        client.cookie(cookie);
    }

    @Override
    public String getOrgContextHeader() {
        return orgContext;
    }

    @Override
    public void setOrgContextHeader(String orgContext) {
        this.orgContext = orgContext;
    }

    @Override
    public void login(ClientCredentials credentials) {
        dologinInternal(credentials);
    }

    @Override
    public void relogin() {
        if (this.clientCredentials == null) {
            throw new RuntimeException("Expected client credentials to not be null");
        }
        dologinInternal(this.clientCredentials);
    }

    @Override
    public void loginWithJwt(final String jwt, final String orgSecurityContext) {
        this.orgSecurityContext = orgSecurityContext;

        clientCredentials = null;
        jwtToken = jwt;

        doInitClient();
    }

    @Override
    public void loginWithToken(final SessionToken sessionToken) {

        setCredentialsInternal(null);

        // Get the values from the session token necessary to initialize this client
        if (!(sessionToken instanceof SessionTokenImpl)) {
            throw new AssertionError("Invalid session token.");
        }
        final SessionTokenImpl sessionTokenImpl = (SessionTokenImpl) sessionToken;
        this.authenticationToken = sessionTokenImpl.getAuthenticationToken();
        final String vCloudTokenCookie = sessionTokenImpl.getVCloudToken();
        if (vCloudTokenCookie != null) {
            this.cookies.put(RestConstants.VCLOUD_COOKIE_NAME, vCloudTokenCookie);
        }

        doInitClient();
    }

    /**
     * Initialize this {@link VcdClient}'s internals with a session that will either be created from
     * this {@link WebClient} via login, or with this {@link WebClient} via retrieving an existing
     * session.
     *
     * @param client
     *            The client to create or retrieve the session.
     * @param isLogin
     *            {@code true} if a new session should be created by logging in, {@code false} to
     *            reuse an existing session.
     */
    private void initializeWithSession(final WebClient client, final boolean isLogin) {

        configureHttpRequestHeaders(client);
        configureSSLTrustManager(WebClient.getConfig(client));

        /*
         * Get/create the session and process the response.
         */
        final Response response = isLogin ? client.post(null) : client.get();
        checkResponse(response, HttpURLConnection.HTTP_OK);

        SessionType session = response.readEntity(SessionType.class);
        responseHeaders = response.getMetadata();

        // Processing the headers is only necessary after the login, because if the session
        // is being reused, the authentication token and vcloud token have already been set.
        // In the case of a login (new session) we need to process the headers and reconfigure
        // the client's headers, as the auth token and vcloud token have been retrieved.
        if (isLogin && !hasSessionlessClientCredentials()) {
            processHeaders();
            configureHttpRequestHeaders(client);
        }

        sessionHref = URI.create(session.getHref());
        sessionEndpoints = WellKnownEndpoint.getSessionEndpoints(session);
    }

    /**
     * Checks HTTP status in the specified response.
     *
     * @param response HTTP response to check
     * @param expectedStatus Expected HTTP status
     * @throws {@link VcdErrorResponseException} if HTTP status doesn't match {@code expectedStatus}
     */
    private void checkResponse(final Response response, final int expectedStatus) {
        if (response.getStatus() == expectedStatus) {
            return;
        }

        final int responseStatus = response.getStatus();

        ErrorType error = null;
        if (responseStatus != HttpURLConnection.HTTP_UNAUTHORIZED) {

            try {
                // readEntity fails in scenarios where the response is not of ErrorType
                // for example HTTP Error 301
                error = response.readEntity(ErrorType.class);
            } catch (final Exception e) {
                // ignore
            }
        }

        final String requestId = getRequestId(response);

        throw new VcdErrorResponseException(responseStatus, requestId, error, null);
    }

    private String getRequestId(final Response response) {
        final String requestId =
                response.getHeaderString(RestConstants.VCLOUD_REQUEST_ID_HEADER);
        return requestId;
    }

    @Override
    public SessionToken getSessionToken() {
        if (authenticationToken != null) {
            return new SessionTokenImpl(authenticationToken,
                    cookies.get(RestConstants.VCLOUD_COOKIE_NAME));
        }
        return null;
    }

    @Override
    public String getJwtToken() {
        return jwtToken;
    }

    private void processHeaders() {
        authenticationToken = (String) responseHeaders.getFirst(RestConstants.VCLOUD_AUTHENTICATION_HEADER);
        if (authenticationToken == null) {
            throw new RuntimeException("The login response is missing a " +  RestConstants.VCLOUD_AUTHENTICATION_HEADER + " cookie");
        }

        final String accessToken =
                (String) responseHeaders.getFirst(RestConstants.VCLOUD_ACCESS_TOKEN_HEADER);
        if (accessToken != null) {
            jwtToken = accessToken;
        }

        final List rawCookies = responseHeaders.get("Set-Cookie");
        if (rawCookies == null) {
            return;
        }

        for (Object o : rawCookies) {
            final String rawCookie = (String) o;
            final String name = rawCookie.substring(0, rawCookie.indexOf("="));
            cookies.put(name, rawCookie);
        }
    }

    private enum WellKnownEndpoint {
        // Endpoints that are always present when VCD is not in maintenance mode:
        ENTITY_RESOLVER(RelationType.ENTITY_RESOLVER, RestConstants.MediaType.ENTITY),
        LOGGED_IN_ORG(RelationType.DOWN, RestConstants.MediaType.ORGANIZATION),
        ORG_LIST(RelationType.DOWN, RestConstants.MediaType.ORGANIZATION_LIST),
        QUERY_LIST(RelationType.DOWN, RestConstants.MediaType.QUERY_LIST),

        // Endpoints whose presence depends on the credentials used when establishing the session:
        ADMIN(RelationType.DOWN, RestAdminConstants.MediaType.VCLOUDM),
        API_EXTENSIBILITY(RelationType.API_EXTENSIBILITY, RestConstants.MediaType.API_EXTENSIBILITY),
        EXTENSION(RelationType.DOWN, RestConstants.MediaType.VMW_EXTENSION),
        OPENAPI(RelationType.OPENAPI, RestConstants.MediaType.APPLICATION_JSON),
        ;

        private final String mediaType;
        private final RelationType rel;

        WellKnownEndpoint(RelationType rel, String mediaType) {
            this.rel = rel;
            this.mediaType = mediaType;
        }

        static Map getSessionEndpoints(SessionType session) {
            Map map = new HashMap();
            for (WellKnownEndpoint endpoint : WellKnownEndpoint.values()) {
                LinkType link = VcdUtils.findLink(session, endpoint.rel, endpoint.mediaType, false);
                if (link != null) {
                    map.put(endpoint, URI.create(link.getHref()));
                }
            }
            return map;
        }
    }

    private URI getLoggedInOrgAdminURI() {
        if (getEndpoint(WellKnownEndpoint.ADMIN) == null) {
            // If no access to admin resource, just return now.
            return null;
        }

        final OrgType loggedInOrg = getLoggedInOrg();
        final String loggedInOrgId = loggedInOrg.getId();

        final List adminOrganizationReferences =
                getAdmin().getOrganizationReferences().getOrganizationReference();

        for (final OrganizationReferenceType adminOrganizationReference : adminOrganizationReferences) {
            // Unfortunately adminOrganizationReference is not going to have an 'id'
            final AdminOrgType adminOrgType = getResource(adminOrganizationReference, AdminOrgType.class);

            if (loggedInOrgId.equals(adminOrgType.getId())) {
                return URI.create(adminOrgType.getHref());
            }
        }

        throw new AssertionError("Admin org reference not found despite user having access to admin resources!!");
    }

    @Override
    public void logout() {
        try {
            final Response response =
                    createWebClient(
                            UriBuilder.fromUri(endpoint).path(RestConstants.Uri.SESSION).build())
                            .delete();

            checkResponse(response, HttpURLConnection.HTTP_NO_CONTENT);

        } finally {
            clearSessionData();
        }
    }

    @Override
    public VcdClient duplicate(boolean newSession) throws VcdErrorException {
        VcdClientImpl duplicateClient = new VcdClientImpl(this);
        if (newSession) {
            duplicateClient.setCredentials(clientCredentials);
        } else if (jwtToken != null) {
            duplicateClient.loginWithJwt(jwtToken, orgSecurityContext);
        } else {
            duplicateClient.loginWithToken(getSessionToken());
        }
        return duplicateClient;
    }

    private void clearSessionData() {
        cookies.clear();
        sessionEndpoints = null;
        loggedInAdminOrgEndpoint = null;
        authenticationToken = null;
        sessionHref = null;
        jwtToken = null;
    }

    @Override
    public WebClient createWebClient(URI uri, String type) {
        WebClient client = super.createWebClient(uri, type);

        /*
          WebClient will create a Content-Type header (oddly, even in the case of GETs) that in some cases
          might not be acceptable to the server.  The problem seems limited to requests to vCloud API
          extensions and/or the HCS extension in particular.  In any case perhaps the server being too picky.
           */
        client.type(type != null ? type : "application/*+xml");

        return client;
    }

    @Override
    public Response getServerStatus() {
        final WebClient webClient = createWebClient(UriBuilder.fromUri(endpoint).path("server_status").build());
        webClient.accept("*/*");
        return webClient.get();
    }

    @Override
    public  ResourceClass getResource(ResourceType resource, RelationType rel, String mediaType, Class resourceClass) {
        LinkType link = VcdUtils.findLink(resource, rel, mediaType);
        return getResource(link.getHref(), resourceClass);
    }

    @Override
    public  ResourceClass getResource(ResourceType resourceType, Class resourceClass) {
        return getResource(resourceType.getHref(), resourceClass);
    }

    @Override
    public  ResourceClass getResource(ReferenceType reference, Class resourceClass) {
        return getResource(reference.getHref(), resourceClass);
    }

    private  ResourceClass getResource(String href, Class resourceClass) {
        return super.getResource(URI.create(href), resourceClass);
    }

    @Override
    public  ResponseClass getEntity(String entityId, String mediaType, Class responseClass) {
        EntityType resolvedEntity = resolveEntityById(entityId);
        return getResource(
                        resolvedEntity,
                        RelationType.ALTERNATE,
                        mediaType,
                        responseClass);
    }


    @Override
    public  T getEntity(
            ReferenceType referenceType,
            Class resourceClass,
            ReferenceTypeChangedCallBack referenceTypeChangedCallBack) {

        /*
         * Get entity using current href if not null, discard result if href is stale
         */
        RuntimeException savedException = null;
        try {
            String href = referenceType.getHref();
            if (href != null) {
                final T result = getResource(URI.create(href), resourceClass);
                if (referenceType.getId().equals(result.getId())) {
                    return result;
                }
            }
        } catch (ProcessingException e) {
            // href is invalid (example: host not reachable), fall through
            savedException = e;
        } catch (VcdErrorException e) {
            if (e.getHttpStatusCode() == HttpStatusCodes.SC_UNAUTHORIZED) {
                throw e;
            }
            savedException = e;
            // href is invalid (example: 404 RESOURCE_NOT_FOUND), fall through
        }
        /*
         * href is invalid, attempt to use vcd entity resolver with
         * the API endpoint associated with this VcdClient (which presumably is not
         * a prefix of the invalid href).
         */
        final T result;
        try {
            result = getEntity(referenceType.getId(), referenceType.getType(), resourceClass);
        } catch (RuntimeException e) {
            if (savedException != null) {
                throw savedException;
            }
            throw e;
        }
        /*
         * Success, let caller know of this repaired href
         */
        referenceTypeChangedCallBack.hrefChanged(result.getHref());

        return result;
    }

    @Override
    public  ResponseClass putResource(ResourceType resource, RelationType rel, String mediaType, JAXBElement contents, Class responseClass) {
        LinkType link = VcdUtils.findLink(resource, rel, mediaType);
        return putResource(link.getHref(), mediaType, contents, responseClass);
    }

    @Override
    public  ResponseClass putResource(ReferenceType reference, String mediaType, JAXBElement contents, Class responseClass) {
        return putResource(reference.getHref(), mediaType, contents, responseClass);
    }

    @Override
    public  ResponseClass putResource(String mediaType, JAXBElement contents, Class responseClass) {
        return putResource(contents.getValue().getHref(), mediaType, contents, responseClass);
    }

    private  ResponseClass putResource(String href, String mediaType, JAXBElement contents, Class responseClass) throws VcdErrorException {
        return super.putResource(URI.create(href), mediaType, contents, responseClass);
    }

    @Override
    public  ResponseClass postResource(ResourceType resource, RelationType rel, String mediaType, JAXBElement contents, Class responseClass) {
        LinkType link = VcdUtils.findLink(resource, rel, mediaType);
        return postResource(link.getHref(), mediaType, contents, responseClass);
    }

    @Override
    public  ResponseClass postResource(ReferenceType reference, String mediaType, JAXBElement contents, Class responseClass) {
        return postResource(reference.getHref(), mediaType, contents, responseClass);
    }

    private  ResponseClass postResource(String href, String mediaType, JAXBElement contents, Class responseClass) throws VcdErrorException {
        return super.postResource(URI.create(href), mediaType, contents, responseClass);
    }

    @Override
    protected VcdErrorException makeException(WebApplicationException webApplicationException) {
        final Response response = webApplicationException.getResponse();
        final int responseStatus = response.getStatus();
        try {
            final String requestId = getRequestId(response);
            final String contentType = response.getHeaderString(HttpHeaders.CONTENT_TYPE);
            if (StringUtils.contains(contentType, ErrorType.CONTENT_TYPE)) {
                final ErrorType error = response.readEntity(ErrorType.class);
                return new VcdErrorResponseException(responseStatus, requestId, error, webApplicationException);
            } else if (StringUtils.contains(contentType, MediaType.TEXT_PLAIN)
                    || StringUtils.contains(contentType, MediaType.TEXT_XML)) {
                final String errorMsg = response.readEntity(String.class);
                return new VcdErrorResponseException(responseStatus, requestId, errorMsg, webApplicationException);
            }
            return new VcdErrorResponseProcessingException(responseStatus, null, webApplicationException);

        } catch (ProcessingException pe) {
            return new VcdErrorResponseProcessingException(responseStatus, pe, webApplicationException);
        } catch (IllegalStateException ise) {
            return new VcdErrorResponseProcessingException(responseStatus, ise, webApplicationException);
        }
    }

    @Override
    public Response removeResource(ResourceType resource) {
        return removeResource(resource, null, null, Response.class);
    }

    @Override
    public  ResponseClass removeResource(ResourceType resource, Boolean force, Boolean recursive, Class responseClass) {
        try {
            final LinkType link = VcdUtils.findLink(resource, RelationType.REMOVE, null);
            return deleteResource(VcdUtils.buildDeleteUri(URI.create(link.getHref()), force, recursive), responseClass);
        } catch (MissingLinkException e) {
            throw new UnsupportedOperationException("Unable to delete " + resource.getHref(), e);
        }
    }

    @Override
    public  ResponseClass deleteResource(ReferenceType ref, Class responseClass) {
        return this.deleteResource(ref, null, null, responseClass);
    }

    @Override
    public  ResponseClass deleteResource(ResourceType ref, Class responseClass) {
        return this.deleteResource(ref, null, null, responseClass);
    }

    @Override
    public  ResponseClass deleteResource(ResourceType resource, Boolean force, Boolean recursive, Class responseClass) {
        return super.deleteResource(VcdUtils.buildDeleteUri(URI.create(resource.getHref()), force, recursive), responseClass);
    }

    @Override
    public  ResponseClass deleteResource(ReferenceType ref, Boolean force, Boolean recursive, Class responseClass) {
        return super.deleteResource(VcdUtils.buildDeleteUri(URI.create(ref.getHref()), force, recursive), responseClass);
    }

    @Override
    public com.vmware.vcloud.api.rest.schema_v1_5.ObjectFactory getVCloudObjectFactory() {
        return new com.vmware.vcloud.api.rest.schema_v1_5.ObjectFactory();
    }

    @Override
    public com.vmware.vcloud.api.rest.schema_v1_5.extension.ObjectFactory getVCloudExtensionObjectFactory() {
        return new com.vmware.vcloud.api.rest.schema_v1_5.extension.ObjectFactory();
    }

    @Override
    public com.vmware.vcloud.api.rest.schema.ovf.ObjectFactory getOvfObjectFactory() {
        return new com.vmware.vcloud.api.rest.schema.ovf.ObjectFactory();
    }

    @Override
    public com.vmware.vcloud.api.rest.schema.ovf.vmware.ObjectFactory getOvfVmwareObjectFactory() {
        return new com.vmware.vcloud.api.rest.schema.ovf.vmware.ObjectFactory();
    }

    @Override
    public VcdTaskMonitor getTaskMonitor() {
        return taskMonitor;
    }

    @Override
    public EventViewer getEventViewer() {
        return eventViewer;
    }

    @Override
    public void setMultisiteRequests(boolean federateRequests) {
        this.federateRequests = federateRequests;
    }

    @Override
    public void setClientRequestIdProvider(ClientRequestIdProvider clientRequestIdGenerator) {
        super.setClientRequestIdProvider(clientRequestIdGenerator);
    }

    private Map getQueryListMap() {
        if (queryListMap == null) {
            queryListMap = new HashMap();
            for (final LinkType link : getQueryList().getLink()) {
                final String queryListKey = makeQueryListMapKey(link.getType(), link.getName());
                queryListMap.put(queryListKey, URI.create(link.getHref()));
            }
        }

        return queryListMap;
    }

    private final class QueryResultPageIterator implements ListIterator> {

        private ContainerType container;
        private final Class queryResultClass;
        // boolean indicating whether 'container' has the results which can be returned from next()
        // or whether next() should fetch results by following next-page link in the container
        private boolean nextPageAvailable;

        public QueryResultPageIterator(final ContainerType containerType, final Class queryResultClass) {
            this.container = containerType;
            this.queryResultClass = queryResultClass;
            this.nextPageAvailable = true;
        }

        @Override
        public boolean hasNext() {
            if (nextPageAvailable && container.getTotal() > 0) {
                return true;
            }

            return getNextPageUri() != null;
        }

        @Override
        public QueryListPage next() {

            if (nextPageAvailable) {
                nextPageAvailable = false;
            } else {
                container = getResource(getNextPageUri(), container.getClass());
            }

            return getResultsPage();
        }

        @Override
        public boolean hasPrevious() {
            if (nextPageAvailable) {
                return false;
            }

            return getPrevPageUri() != null;
        }

        @Override
        public QueryListPage previous() {

            container = getResource(getPrevPageUri(), container.getClass());

            return getResultsPage();
        }

        @Override
        public int nextIndex() {
            return container.getPage();
        }

        @Override
        public int previousIndex() {
            // index is zero based, but page numbers start with 1; current index is page-1; therefore previous index is page -2
            return container.getPage()-2;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        @Override
        public void set(QueryListPage e) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void add(QueryListPage e) {
            throw new UnsupportedOperationException();
        }

        private QueryListPage getResultsPage() {
            if (container instanceof QueryResultRecordsType) {
                return extractQueryResults((QueryResultRecordsType)container, queryResultClass);
            } else {
                return extractQueryResults((ReferencesType)container, queryResultClass);
            }
        }

        private  QueryListPage extractQueryResults(
                final QueryResultRecordsType queryResultRecordsType,
                Class queryResultClass) {

            final List queryResultRecordList =
                    new ArrayList();

            for (JAXBElement element : queryResultRecordsType
                    .getRecord()) {
                final QueryResultRecordType queryResultRecord = element.getValue();
                assert queryResultRecord != null : "Record is present but value is null";
                queryResultRecordList.add(queryResultClass.cast(queryResultRecord));
            }

            return new QueryListPage(queryResultRecordList,
                    queryResultRecordsType.getTotal());
        }

        private  QueryListPage extractQueryResults(
                ReferencesType referencesType, Class queryResultClass) {
            final List referenceList =
                    new ArrayList();

            for (JAXBElement ref : referencesType.getReference()) {
                @SuppressWarnings("unchecked")
                final QueryResultClass referenceType = (QueryResultClass) ref.getValue();

                assert referencesType != null : "Reference present but value is null";
                referenceList.add(referenceType);
            }

            return new QueryListPage(referenceList,
                    referencesType.getTotal());
        }

        private LinkType getNextPageUri() {
            return VcdUtils.findLink(container, RelationType.NEXT_PAGE, container.getType(), false);
        }


        private LinkType getPrevPageUri() {
            return VcdUtils.findLink(container, RelationType.PREVIOUS_PAGE, container.getType(), false);
        }
    }

    private static String makeQueryListMapKey(String mediaType, String queryTypeName) {
        return mediaType + "!" + queryTypeName;
    }

    private final class TypedQuery extends AbstractQuery {
        private final String queryTypeName;

        public TypedQuery(String queryTypeName, Class queryResultClass) {
            super(queryResultClass);
            this.queryTypeName = queryTypeName;
        }

        @Override
        protected URI findQueryUri(final QueryResultFormat format) {
            final String queryKey = makeQueryListMapKey(format.getMediaType(), queryTypeName);

            final URI queryHref = getQueryListMap().get(queryKey);
            if (queryHref == null) {
                throw new IllegalArgumentException(
                        "No query found with given query format '"
                        + format.getApiString() + "' and name '" + queryTypeName + "'");
            }
            return queryHref;
        }
    }

    private abstract class AbstractQuery implements Query {

        final int DEFAULT_PAGE_SIZE = -1;

        private final Class queryResultClass;
        private QueryResultFormat queryResultFormat;
        private int page = 1;
        private int pageSize = DEFAULT_PAGE_SIZE;
        private boolean includeLinks = false;
        private String filter;
        private String fields;
        private String sortDesc;
        private String sortAsc;

        public AbstractQuery(final Class queryResultClass) {
            this.queryResultClass = queryResultClass;

            if (ReferenceType.class.isAssignableFrom(queryResultClass)) {
                queryResultFormat = QueryResultFormat.REFERENCES;
            } else if (QueryResultRecordType.class.isAssignableFrom(queryResultClass)) {
                queryResultFormat = QueryResultFormat.RECORDS;
            } else {
                throw new IllegalArgumentException("queryResultClass");
            }
        }

        @Override
        public Query setQueryResultFormat(QueryResultFormat queryResultFormat) {
            switch (queryResultFormat) {
            case REFERENCES:
                if (!ReferenceType.class.isAssignableFrom(queryResultClass)) {
                    throw new IllegalArgumentException("queryResultFormat");
                }
                break;
            case RECORDS:
            case ID_RECORDS:
                if (!QueryResultRecordType.class.isAssignableFrom(queryResultClass)) {
                    throw new IllegalArgumentException("queryResultFormat");
                }
                break;
            }

            this.queryResultFormat = queryResultFormat;
            return this;
        }

        @Override
        public Query setPage(int page) {
            this.page = page;
            return this;
        }

        @Override
        public Query setPageSize(int pageSize) {
            this.pageSize = pageSize;
            return this;
        }

        @Override
        public Query setFilter(String filter) {
            this.filter = filter;
            return this;
        }

        @Override
        public Query setFields(Collection fields) {
            this.fields = fields.stream().collect(Collectors.joining(","));
            return this;
        }

        @Override
        public Query setEqualityFilter(String name, String value) {
            final StringBuffer sb = new StringBuffer();

            if (this.filter != null) {
                sb.append(this.filter);
                sb.append(";");
            }
            sb.append(name);
            sb.append("==");
            sb.append(value);

            this.filter = sb.toString();

            return this;
        }

        @Override
        public Query setSortDesc(String fieldName) {
            this.sortDesc = fieldName;
            return this;
        }

        @Override
        public Query setSortAsc(String fieldName) {
            this.sortAsc = fieldName;
            return this;
        }

        @Override
        public Query setIncludeLinks(boolean includeLinks) {
            this.includeLinks = includeLinks;
            return this;
        }

        @Override
        public ListIterator> execute() {
            final URI queryUri =
                    buildQueryUri(findQueryUri(queryResultFormat), page, pageSize,
                            filter, includeLinks);
            if (QueryResultRecordType.class.isAssignableFrom(queryResultClass)) {
                final QueryResultRecordsType queryResultRecordsType =
                        getResource(queryUri, QueryResultRecordsType.class);
                return new QueryResultPageIterator(
                        queryResultRecordsType, queryResultClass);
            }

            if (ReferenceType.class.isAssignableFrom(queryResultClass)) {
                final ReferencesType referencesType =
                        getResource(queryUri, ReferencesType.class);
                return new QueryResultPageIterator(referencesType,
                        queryResultClass);
            }

            assert false : "Unexpected type of query results: " + queryResultClass;
            return null;
        }

        @Override
        public Stream stream() {
            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(execute(), Spliterator.ORDERED), false)
                                .flatMap(page -> page.getPage().stream());
        }

        abstract protected URI findQueryUri(final QueryResultFormat format);

        private URI buildQueryUri(final URI baseQueryHref, final int page,
                final int pageSize, final String filter, final boolean includeLinks) {
            final UriBuilder builder = UriBuilder.fromUri(baseQueryHref);
            final AtomicInteger index = new AtomicInteger(0);
            final Map paramArgs = new HashMap<>();

            final Function wrapArgInTemplate = argValue -> {
                final String arg = String.format("arg%d", index.getAndIncrement());
                paramArgs.put(arg, ObjectUtils.defaultIfNull(argValue, "").toString().replaceAll("\\+", "%2B"));
                return String.format("{%s}", arg);
            };

            builder.queryParam("page", wrapArgInTemplate.apply(page));

            if (pageSize != DEFAULT_PAGE_SIZE) {
                builder.queryParam("pageSize", pageSize);
            }

            if (!StringUtils.isEmpty(fields)) {
                builder.queryParam(RestConstants.QueryParams.FIELDS, wrapArgInTemplate.apply(fields));
            }

            if (!StringUtils.isEmpty(filter)) {
                // filterEncoded=true allows VCD to properly parse encoded '==' in the filter parameter.
                builder.queryParam("filterEncoded", wrapArgInTemplate.apply(true));
                builder.queryParam(RestConstants.QueryParams.FILTER, wrapArgInTemplate.apply(filter));
            }

            if (!StringUtils.isEmpty(sortAsc)) {
                builder.queryParam("sortAsc", wrapArgInTemplate.apply(sortAsc));
            }

            if (includeLinks) {
                builder.queryParam("links", wrapArgInTemplate.apply(true));
            }

            if (!StringUtils.isEmpty(sortDesc)) {
                builder.queryParam("sortDesc", wrapArgInTemplate.apply(sortDesc));
            }

            return builder.buildFromMap(paramArgs);
        }

        @Override
        public QueryResultClass findUnique() {
            final ListIterator> result = execute();
            if (! result.hasNext()) {
                throw new MissingRecordException(toString());
            }
            final QueryListPage page = result.next();
            final List pageContents = page.getPage();
            final int size = pageContents.size();
            if (size != 1) {
                throw (size > 1) ? new MultipleRecordsException(toString()) : new MissingRecordException(toString());
            }
            return pageContents.get(0);
        }

        @Override
        public String toString() {
            URI u = buildQueryUri(findQueryUri(queryResultFormat), page, pageSize, filter, includeLinks);
            return u.toASCIIString();
        }

    }

    @Override
    public  Query getQuery(String queryTypeName, Class queryResultClass) {
        return new TypedQuery(queryTypeName, queryResultClass);
    }

    private final class PackagedQuery extends AbstractQuery {
        private final String queryPath;

        public PackagedQuery(String queryPath,
                Class queryResultClass) {
            super(queryResultClass);
            this.queryPath = queryPath;
        }

        @Override
        protected URI findQueryUri(QueryResultFormat format) {
            return UriBuilder.fromUri(endpoint).path(queryPath)
                    .queryParam("format", format.getApiString()).build();
        }
    }

    @Override
    public  Query getPackagedQuery(String queryPath, Class queryResultClass) {
        return new PackagedQuery(queryPath, queryResultClass);
    }

    private boolean hasSessionlessClientCredentials() {
        return clientCredentials != null && clientCredentials.supportsSessionless();
    }

    @Override
    public void setCredentials(ClientCredentials credentials) {
        setCredentialsInternal(credentials);
        if (credentials.supportsSessionless()) {
            doInitClient();
        } else {
            dologinInternal(credentials);
        }
    }

    ClientCredentials getCredentials() {
        return clientCredentials;
    }

    private void setCredentialsInternal(ClientCredentials credentials) {
        this.clientCredentials = credentials;
        this.jwtToken = null;
    }

    private void dologinInternal(ClientCredentials credentials) {
        setCredentialsInternal(credentials);
        WebClient client =
                createWebClient(UriBuilder.fromUri(endpoint).path(RestConstants.Uri.SESSIONS)
                        .build());
        client.header(credentials.getHeaderName(), credentials.getHeaderValue());

        // Initialize this client with the newly constructed session
        initializeWithSession(client, true); /*is login*/
    }

    private void doInitClient() {
        // Construct a web client to retrieve a session based on ClientCredentials or authentication token
        WebClient client =
                createWebClient(UriBuilder.fromUri(endpoint).path(RestConstants.Uri.SESSION)
                        .build());

        // Initialize this client using the existing session
        initializeWithSession(client, false /*do not login*/);
    }
}