com.microsoft.alm.plugin.context.ServerContextManager.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.alm.plugin.context.ServerContextManager.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.plugin.context;

import com.microsoft.alm.common.utils.UrlHelper;
import com.microsoft.alm.plugin.TeamServicesException;
import com.microsoft.alm.plugin.authentication.AuthHelper;
import com.microsoft.alm.plugin.authentication.AuthenticationInfo;
import com.microsoft.alm.plugin.authentication.AuthenticationProvider;
import com.microsoft.alm.plugin.authentication.TfsAuthenticationProvider;
import com.microsoft.alm.plugin.authentication.VsoAuthenticationProvider;
import com.microsoft.alm.plugin.context.rest.ConnectionData;
import com.microsoft.alm.plugin.context.rest.ServiceDefinition;
import com.microsoft.alm.plugin.context.rest.VstsHttpClient;
import com.microsoft.alm.plugin.context.rest.VstsInfo;
import com.microsoft.alm.plugin.services.PluginServiceProvider;
import com.microsoft.alm.plugin.services.PropertyService;
import com.microsoft.alm.plugin.services.ServerContextStore;
import com.microsoft.alm.plugin.telemetry.TfsTelemetryHelper;
import com.microsoft.alm.core.webapi.CoreHttpClient;
import com.microsoft.alm.core.webapi.model.TeamProjectCollection;
import com.microsoft.alm.core.webapi.model.TeamProjectCollectionReference;
import com.microsoft.alm.sourcecontrol.webapi.GitHttpClient;
import com.microsoft.alm.sourcecontrol.webapi.model.GitRepository;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Singleton class used to manage ServerContext objects.
 */
public class ServerContextManager {
    private static final Logger logger = LoggerFactory.getLogger(ServerContextManager.class);

    private Map<String, ServerContext> contextMap = new HashMap<String, ServerContext>();

    private static class Holder {
        private static final ServerContextManager INSTANCE = new ServerContextManager(true);
    }

    /**
     * The constructor is protected for tests.
     */
    protected ServerContextManager() {
        this(false);
    }

    private ServerContextManager(final boolean restore) {
        if (!restore) {
            return;
        }

        try {
            restoreFromSavedState();
        } catch (Throwable t) {
            // being careful here
            logger.error("constructor", t);
        }
    }

    public static ServerContextManager getInstance() {
        return Holder.INSTANCE;
    }

    public synchronized ServerContext getLastUsedContext() {
        final ServerContext context = get(getLastUsedContextKey());
        return context;
    }

    private void setLastUsedContextKey(String key) {
        PluginServiceProvider.getInstance().getPropertyService().setProperty(PropertyService.PROP_LAST_CONTEXT_KEY,
                key);
    }

    private String getLastUsedContextKey() {
        return PluginServiceProvider.getInstance().getPropertyService()
                .getProperty(PropertyService.PROP_LAST_CONTEXT_KEY);
    }

    public synchronized void clearLastUsedContext() {
        setLastUsedContextKey(null);
    }

    public synchronized boolean lastUsedContextIsEmpty() {
        final ServerContext lastUsed = getLastUsedContext();
        return lastUsed == null;
    }

    public synchronized boolean lastUsedContextIsTFS() {
        final ServerContext lastUsed = getLastUsedContext();
        return lastUsed != null && lastUsed.getType() == ServerContext.Type.TFS;
    }

    public synchronized void add(final ServerContext context) {
        add(context, true);
    }

    public synchronized void add(final ServerContext context, boolean updateLastUsedContext) {
        if (context != null) {
            final String key = context.getKey();
            contextMap.put(key, context);
            getStore().saveServerContext(context);
            if (updateLastUsedContext) {
                setLastUsedContextKey(key);
            }
        }
    }

    public synchronized ServerContext get(final String uri) {
        if (!StringUtils.isEmpty(uri)) {
            final ServerContext context = contextMap.get(ServerContext.getKey(uri));
            return context;
        }

        return null;
    }

    public synchronized void remove(final String serverUri) {
        if (StringUtils.isEmpty(serverUri)) {
            return;
        }

        final String key = ServerContext.getKey(serverUri);
        final ServerContext context = get(key);

        if (context != null) {
            getStore().forgetServerContext(key);
            contextMap.remove(key);
            if (StringUtils.equalsIgnoreCase(key, getLastUsedContextKey())) {
                clearLastUsedContext();
            }
        }
    }

    public synchronized Collection<ServerContext> getAllServerContexts() {
        //copy values from HashMap to a new List make sure the list is immutable
        return Collections.unmodifiableCollection(new ArrayList<ServerContext>(contextMap.values()));
    }

    private ServerContextStore getStore() {
        return PluginServiceProvider.getInstance().getServerContextStore();
    }

    /**
     * Called once from constructor restore the state from disk between sessions.
     */
    private synchronized void restoreFromSavedState() {
        final List<ServerContext> contexts = getStore().restoreServerContexts();
        for (final ServerContext sc : contexts) {
            add(sc, false);
        }
    }

    /**
     * Validates a provided server context and if validation succeeds saves a server context with the user's team foundation Id
     *
     * @param context
     */
    public ServerContext validateServerConnection(final ServerContext context) {
        ServerContext contextToValidate = context;

        //If context.uri is remote git repo url, try to parse it if needed
        if (UrlHelper.isGitRemoteUrl(context.getUri().toString()) && (context.getServerUri() == null
                || context.getTeamProjectCollectionReference() == null || context.getGitRepository() == null)) {
            //parse url
            Validator validator = new Validator(context);
            if (validator.validate(context.getUri().toString())) {
                contextToValidate = new ServerContextBuilder(context).serverUri(validator.getServerUrl())
                        .collection(validator.getCollection()).repository(validator.getRepository()).build();
            } else {
                //failed to parse
                contextToValidate = null;
            }
        }

        if (context.getType() == ServerContext.Type.TFS) {
            return checkTfsVersionAndConnection(contextToValidate);
        } else {
            return checkVstsConnection(contextToValidate);
        }
    }

    private ServerContext checkVstsConnection(final ServerContext context) throws TeamServicesException {
        final String CONNECTION_DATA_REST_API_PATH = "/_apis/connectionData?connectOptions=lastChangeId=-1&lastChangeId64=-1&api-version=1.0";

        if (context == null || context.getServerUri() == null) {
            throw new TeamServicesException(TeamServicesException.KEY_VSO_AUTH_FAILED);
        }

        final ConnectionData data = VstsHttpClient.sendRequest(context.getClient(),
                context.getServerUri().toString().concat(CONNECTION_DATA_REST_API_PATH), ConnectionData.class);

        if (data == null || data.getAuthenticatedUser() == null) {
            throw new TeamServicesException(TeamServicesException.KEY_VSO_AUTH_FAILED);
        }

        //connection is verified, save the context
        final ServerContext contextWithUserId = new ServerContextBuilder(context)
                .userId(data.getAuthenticatedUser().getId()).build();
        add(contextWithUserId);

        return contextWithUserId;
    }

    private ServerContext checkTfsVersionAndConnection(final ServerContext context) throws TeamServicesException {
        final String CONNECTION_DATA_REST_API_PATH = "/_apis/connectionData?connectOptions=IncludeServices&lastChangeId=-1&lastChangeId64=-1&api-version=1.0";
        final String TFS2015_NEW_SERVICE = "distributedtask";
        final String TELEMETRY_CONNECTION_EVENT = "TfsConnection";
        final String TELEMETRY_TFS_VERSION = "TFS.Version";
        final String TELEMETRY_TFS2012_OR_OLDER = "TFS2012_or_older";
        final String TELEMETRY_TFS2013 = "TFS2013";
        final String TELEMETRY_TFS2015_OR_LATER = "TFS2015_or_later";

        if (context == null || context.getServerUri() == null) {
            throw new TeamServicesException(TeamServicesException.KEY_TFS_AUTH_FAILED);
        }

        try {
            final String urlForConnectionData;
            if (context.getCollectionURI() != null) {
                urlForConnectionData = context.getCollectionURI().toString();
            } else {
                urlForConnectionData = context.getServerUri().toString();
            }
            final ConnectionData data = VstsHttpClient.sendRequest(context.getClient(),
                    urlForConnectionData.concat(UrlHelper.URL_SEPARATOR).concat(CONNECTION_DATA_REST_API_PATH),
                    ConnectionData.class);

            if (data == null || data.getAuthenticatedUser() == null) {
                throw new TeamServicesException(TeamServicesException.KEY_TFS_AUTH_FAILED);
            }

            if (data.getLocationServiceData() != null
                    && data.getLocationServiceData().getServiceDefinitions() != null) {
                for (final ServiceDefinition s : data.getLocationServiceData().getServiceDefinitions()) {
                    if (StringUtils.equalsIgnoreCase(s.getServiceType(), TFS2015_NEW_SERVICE)) {
                        //TFS 2015 or higher, save the context with userId
                        final ServerContext contextWithUserId = new ServerContextBuilder(context)
                                .userId(data.getAuthenticatedUser().getId()).build();
                        add(contextWithUserId);
                        final ServerContext lastUsedTfsContext = new ServerContextBuilder(contextWithUserId)
                                .uri(TfsAuthenticationProvider.TFS_LAST_USED_URL).build();
                        add(lastUsedTfsContext);

                        TfsTelemetryHelper.getInstance().sendEvent(TELEMETRY_CONNECTION_EVENT,
                                new TfsTelemetryHelper.PropertyMapBuilder().success(true)
                                        .pair(TELEMETRY_TFS_VERSION, TELEMETRY_TFS2015_OR_LATER).build());

                        return contextWithUserId;
                    }
                }

                //This is TFS 2013
                logger.warn("checkTfsVersionAndConnection: Detected an attempt to connect to a TFS 2013 server");
                TfsTelemetryHelper.getInstance().sendEvent(TELEMETRY_CONNECTION_EVENT,
                        new TfsTelemetryHelper.PropertyMapBuilder().success(false)
                                .pair(TELEMETRY_TFS_VERSION, TELEMETRY_TFS2013).build());

                throw new TeamServicesException(TeamServicesException.KEY_TFS_UNSUPPORTED_VERSION);
            }
        } catch (com.microsoft.alm.plugin.context.rest.VstsHttpClient.VstsHttpClientException e) {
            if (e.getStatusCode() == 404) {
                //HTTP not found, so server does not have this endpoint i.e. TFS 2012 or older
                logger.warn(
                        "checkTfsVersionAndConnection: Detected an attempt to connect to a TFS 2012 or older version server");
                TfsTelemetryHelper.getInstance().sendEvent(TELEMETRY_CONNECTION_EVENT,
                        new TfsTelemetryHelper.PropertyMapBuilder().success(false)
                                .pair(TELEMETRY_TFS_VERSION, TELEMETRY_TFS2012_OR_OLDER).build());
                throw new TeamServicesException(TeamServicesException.KEY_TFS_UNSUPPORTED_VERSION);
            } else {
                throw new RuntimeException(e);
            }
        }

        //unexpected case
        logger.warn(
                "checkTfsVersionAndConnection: Didn't match TFS 2015 or later, TFS 2013 or TFS 2012 or older server check: {}",
                context.getUri());
        throw new TeamServicesException(TeamServicesException.KEY_TFS_AUTH_FAILED);
    }

    /**
     * Get a fully authenticated context from the provided git remote url.
     * Note that if a context does not exist, one will be created and the user will be prompted if necessary.
     * Run this on a background thread, will hang if run on the UI thread
     */
    public ServerContext getAuthenticatedContext(final String gitRemoteUrl, final boolean setAsActiveContext) {
        try {
            // get context from builder, create PAT if needed, and store in active context
            final ServerContext context = createContextFromRemoteUrl(gitRemoteUrl);
            if (context != null && setAsActiveContext) {
                //nothing to do
                //context is already added to the manager if it is valid
            }
            return context;
        } catch (Throwable t) {
            logger.warn("getAuthenticatedContext unexpected exception", t);
        }
        return null;
    }

    /**
     * Use this method to create a ServerContext from a remote git url.
     * Note that this will require server calls and should be done on a background thread.
     *
     * @param gitRemoteUrl
     * @return
     */
    public ServerContext createContextFromRemoteUrl(final String gitRemoteUrl) {
        return createContextFromRemoteUrl(gitRemoteUrl, true);
    }

    public ServerContext createContextFromRemoteUrl(final String gitRemoteUrl, final boolean prompt) {
        assert !StringUtils.isEmpty(gitRemoteUrl);

        // Get matching context from manager
        ServerContext context = get(gitRemoteUrl);
        if (context == null || context.getGitRepository() == null || context.getServerUri() == null
                || !StringUtils.equalsIgnoreCase(context.getUsableGitUrl(), gitRemoteUrl)) {
            context = null;
        }

        if (context == null) {
            // Manager didn't have a matching context, so try to look up the auth info
            final AuthenticationInfo authenticationInfo = getAuthenticationInfo(gitRemoteUrl, prompt);
            if (authenticationInfo != null) {
                final ServerContext.Type type = UrlHelper.isTeamServicesUrl(gitRemoteUrl) ? ServerContext.Type.VSO
                        : ServerContext.Type.TFS;
                final ServerContext contextToValidate = new ServerContextBuilder().type(type).uri(gitRemoteUrl)
                        .authentication(authenticationInfo).build();
                context = validateServerConnection(contextToValidate);
            }
        }

        if (context != null && context.getUserId() == null) {
            //validate the context and save it with userId
            context = validateServerConnection(context);
        }

        return context;
    }

    /**
     * This method tries to find existing authentication info for a given git url.
     * If the auth info cannot be found and the prompt flag is true, the user will be prompted.
     */
    public AuthenticationInfo getBestAuthenticationInfo(final String url, final boolean prompt) {
        final ServerContext context = get(url);
        final AuthenticationInfo info;
        if (context != null) {
            // return exact match
            info = context.getAuthenticationInfo();
        } else {
            // look for a good enough match
            info = getAuthenticationInfo(url, prompt);
        }
        return info;
    }

    /**
     * This method tries to find existing authentication info for a given git url.
     * If the auth info cannot be found and the prompt flag is true, the user will be prompted.
     */
    public AuthenticationInfo getAuthenticationInfo(final String gitRemoteUrl, final boolean prompt) {
        AuthenticationInfo authenticationInfo = null;

        // For now I will just do a linear search for an appropriate context info to copy the auth info from
        final URI remoteUri = UrlHelper.createUri(gitRemoteUrl);
        for (final ServerContext context : getAllServerContexts()) {
            if (UrlHelper.haveSameAuthority(remoteUri, context.getUri())) {
                authenticationInfo = context.getAuthenticationInfo();
                break;
            }
        }

        // If the auth info wasn't found and we are ok to prompt, then prompt
        if (authenticationInfo == null && prompt) {
            final AuthenticationProvider authenticationProvider = getAuthenticationProvider(gitRemoteUrl);
            authenticationInfo = AuthHelper.getAuthenticationInfoSynchronously(authenticationProvider,
                    gitRemoteUrl);
        }

        return authenticationInfo;
    }

    /**
     * Updates all contexts with matching authority in URI with new authentication info, will prompt the user
     * Has to be called on a background thread, will hang if called on UI thread
     *
     * @param remoteUrl
     */
    public ServerContext updateAuthenticationInfo(final String remoteUrl) {
        AuthenticationInfo newAuthenticationInfo = null;
        boolean promptUser = true;
        final URI remoteUri = UrlHelper.createUri(remoteUrl);
        ServerContext matchingContext = null;

        //Linear search through all contexts to find the ones with same authority as remoteUrl
        for (final ServerContext context : getAllServerContexts()) {
            if (UrlHelper.haveSameAuthority(remoteUri, context.getUri())) {
                //remove the context with old credentials
                remove(context.getKey());

                //get new credentials by prompting the user one time only
                if (promptUser) {
                    //prompt user
                    final AuthenticationProvider authenticationProvider = getAuthenticationProvider(remoteUrl);
                    newAuthenticationInfo = AuthHelper.getAuthenticationInfoSynchronously(authenticationProvider,
                            remoteUrl);
                    promptUser = false;
                }

                if (newAuthenticationInfo != null) {
                    //build a context with new authentication info and add
                    final ServerContextBuilder builder = new ServerContextBuilder(context);
                    builder.authentication(newAuthenticationInfo);
                    final ServerContext newContext = builder.build();
                    if (StringUtils.equalsIgnoreCase(context.getUri().toString(), remoteUrl)) {
                        add(newContext, true);
                        matchingContext = newContext;
                    } else {
                        add(newContext, false);
                    }
                }
            }
        }
        return matchingContext;
    }

    /**
     * Use this method to get the appropriate AuthenticationProvider based on an url.
     *
     * @param url
     * @return
     */
    public AuthenticationProvider getAuthenticationProvider(final String url) {
        if (UrlHelper.isTeamServicesUrl(url)) {
            return VsoAuthenticationProvider.getInstance();
        }

        return TfsAuthenticationProvider.getInstance();
    }

    private static class Validator implements UrlHelper.ParseResultValidator {
        private final static String REPO_INFO_URL_PATH = "/vsts/info";
        private String serverUrl;
        private final ServerContext context;
        private GitRepository repository;
        private TeamProjectCollection collection;

        public Validator(final ServerContext context) {
            this.context = context;
        }

        public String getServerUrl() {
            return serverUrl;
        }

        public GitRepository getRepository() {
            return repository;
        }

        public TeamProjectCollection getCollection() {
            return collection;
        }

        /**
         * This method queries the server with the given Git remote URL for repository, project and collection information
         * If unable to get the info, it parses the Git remote url and tries to verify it by querying the server again
         *
         * @param gitRemoteUrl
         * @return true if server information is determined
         */
        public boolean validate(final String gitRemoteUrl) {
            try {
                final String gitUrlToParse;

                //handle SSH Git urls
                if (UrlHelper.isSshGitRemoteUrl(gitRemoteUrl)) {
                    gitUrlToParse = UrlHelper.getHttpsGitUrlFromSshUrl(gitRemoteUrl);
                } else {
                    gitUrlToParse = gitRemoteUrl;
                }

                //query the server endpoint for VSTS repo, project and collection info
                if (getVstsInfo(gitUrlToParse)) {
                    return true;
                }
                //server endpoint query was not successful, try to parse the url
                final UrlHelper.ParseResult uriParseResult = UrlHelper.tryParse(gitUrlToParse, this);
                if (uriParseResult.isSuccess()) {
                    return true;
                }
            } catch (Throwable t) {
                logger.warn("validate: {} of git remote url failed", gitRemoteUrl);
                logger.warn("validate: unexpected exception ", t);
            }

            //failed to get VSTS repo, project and collection info
            return false;
        }

        private boolean getVstsInfo(final String gitRemoteUrl) {
            try {
                //Try to query the server endpoint gitRemoteUrl/vsts/info
                final VstsInfo vstsInfo = VstsHttpClient.sendRequest(context.getClient(),
                        gitRemoteUrl.concat(REPO_INFO_URL_PATH), VstsInfo.class);
                if (vstsInfo == null || vstsInfo.getCollectionReference() == null
                        || vstsInfo.getRepository() == null
                        || vstsInfo.getRepository().getProjectReference() == null) {
                    //information received from the server is not sufficient
                    return false;
                }

                serverUrl = vstsInfo.getServerUrl();

                collection = new TeamProjectCollection();
                collection.setId(vstsInfo.getCollectionReference().getId());
                collection.setName(vstsInfo.getCollectionReference().getName());
                collection.setUrl(vstsInfo.getCollectionReference().getUrl());
                repository = vstsInfo.getRepository();
                return true;

            } catch (Throwable throwable) {
                //failed to get VSTS information, endpoint may not be available on the server
                logger.warn("validate: failed for Git remote url: {}", gitRemoteUrl);
                logger.warn("validate", throwable);
                if (AuthHelper.isNotAuthorizedError(throwable)) {
                    throw new TeamServicesException(TeamServicesException.KEY_VSO_AUTH_FAILED, throwable);
                }
                return false;
            }
        }

        /**
         * This method gets all the info we need from the server given the parse results.
         * If some call fails we simply return false and ignore the results.
         *
         * @param parseResult
         * @return
         */
        @Override
        public boolean validate(final UrlHelper.ParseResult parseResult) {
            try {
                serverUrl = parseResult.getServerUrl();
                final URI collectionUri = URI
                        .create(UrlHelper.getCmdLineFriendlyUrl(parseResult.getCollectionUrl()));
                final GitHttpClient gitClient = new GitHttpClient(context.getClient(), collectionUri);
                // Get the repository object and team project
                repository = gitClient.getRepository(parseResult.getProjectName(), parseResult.getRepoName());
                // Get the collection object
                final URI serverUri = URI.create(parseResult.getServerUrl());
                if (UrlHelper.isTeamServicesUrl(parseResult.getServerUrl())) {
                    final CoreHttpClient coreClient = new CoreHttpClient(context.getClient(), serverUri);
                    collection = coreClient.getProjectCollection(parseResult.getCollectionName());
                } else {
                    final ServerContext contextToValidate = new ServerContextBuilder(context).serverUri(serverUrl)
                            .build();
                    final TeamProjectCollectionReference ref = contextToValidate.getSoapServices()
                            .getCatalogService().getProjectCollection(parseResult.getCollectionName());
                    collection = new TeamProjectCollection();
                    collection.setId(ref.getId());
                    collection.setName(ref.getName());
                    collection.setUrl(ref.getUrl());
                }
            } catch (Throwable throwable) {
                logger.error("validate: failed for parseResult " + parseResult.toString());
                logger.warn("validate", throwable);
                return false;
            }

            return true;
        }
    }
}