/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.factory.utils; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.inject.Inject; import org.eclipse.che.api.core.model.project.SourceStorage; import org.eclipse.che.api.factory.shared.dto.FactoryDto; import org.eclipse.che.api.git.shared.GitCheckoutEvent; import org.eclipse.che.api.promises.client.Function; import org.eclipse.che.api.promises.client.FunctionException; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.api.promises.client.js.Promises; import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; import org.eclipse.che.ide.CoreLocalizationConstant; import org.eclipse.che.ide.api.app.AppContext; import org.eclipse.che.ide.api.dialogs.DialogFactory; import org.eclipse.che.ide.api.importer.AbstractImporter; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.api.notification.StatusNotification; import org.eclipse.che.ide.api.oauth.OAuth2Authenticator; import org.eclipse.che.ide.api.oauth.OAuth2AuthenticatorRegistry; import org.eclipse.che.ide.api.oauth.OAuth2AuthenticatorUrlProvider; import org.eclipse.che.ide.api.project.MutableProjectConfig; import org.eclipse.che.ide.api.project.wizard.ImportProjectNotificationSubscriberFactory; import org.eclipse.che.ide.api.project.wizard.ProjectNotificationSubscriber; import org.eclipse.che.ide.api.resources.Project; import org.eclipse.che.ide.api.user.AskCredentialsDialog; import org.eclipse.che.ide.api.user.Credentials; import org.eclipse.che.ide.resource.Path; import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; import org.eclipse.che.ide.rest.RestContext; import org.eclipse.che.ide.util.ExceptionUtils; import org.eclipse.che.ide.util.StringUtils; import org.eclipse.che.ide.websocket.MessageBus; import org.eclipse.che.ide.websocket.MessageBusProvider; import org.eclipse.che.ide.websocket.WebSocketException; import org.eclipse.che.ide.websocket.rest.SubscriptionHandler; import org.eclipse.che.security.oauth.OAuthStatus; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.common.base.MoreObjects.firstNonNull; import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT; import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT_WITH_START_POINT; import static org.eclipse.che.api.core.ErrorCodes.UNABLE_GET_PRIVATE_SSH_KEY; import static org.eclipse.che.api.core.ErrorCodes.UNAUTHORIZED_GIT_OPERATION; import static org.eclipse.che.api.core.ErrorCodes.UNAUTHORIZED_SVN_OPERATION; import static org.eclipse.che.api.git.shared.ProviderInfo.AUTHENTICATE_URL; import static org.eclipse.che.api.git.shared.ProviderInfo.PROVIDER_NAME; import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.PROGRESS; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.SUCCESS; /** * @author Sergii Leschenko * @author Valeriy Svydenko * @author Anton Korneta */ public class FactoryProjectImporter extends AbstractImporter { private static final String CHANNEL = "git:checkout:"; private final MessageBusProvider messageBusProvider; private final AskCredentialsDialog askCredentialsDialog; private final CoreLocalizationConstant locale; private final NotificationManager notificationManager; private final String restContext; private final DialogFactory dialogFactory; private final OAuth2AuthenticatorRegistry oAuth2AuthenticatorRegistry; private final DtoUnmarshallerFactory dtoUnmarshallerFactory; private FactoryDto factory; private AsyncCallback<Void> callback; @Inject public FactoryProjectImporter(AppContext appContext, NotificationManager notificationManager, AskCredentialsDialog askCredentialsDialog, CoreLocalizationConstant locale, ImportProjectNotificationSubscriberFactory subscriberFactory, @RestContext String restContext, DialogFactory dialogFactory, OAuth2AuthenticatorRegistry oAuth2AuthenticatorRegistry, MessageBusProvider messageBusProvider, DtoUnmarshallerFactory dtoUnmarshallerFactory) { super(appContext, subscriberFactory); this.notificationManager = notificationManager; this.askCredentialsDialog = askCredentialsDialog; this.locale = locale; this.restContext = restContext; this.dialogFactory = dialogFactory; this.oAuth2AuthenticatorRegistry = oAuth2AuthenticatorRegistry; this.messageBusProvider = messageBusProvider; this.dtoUnmarshallerFactory = dtoUnmarshallerFactory; } public void startImporting(FactoryDto factory, AsyncCallback<Void> callback) { this.callback = callback; this.factory = factory; importProjects(); } /** * Import source projects */ private void importProjects() { final Project[] projects = appContext.getProjects(); Set<String> projectNames = new HashSet<>(); String createPolicy = factory.getPolicies() != null ? factory.getPolicies().getCreate() : null; for (Project project : projects) { if (project.getSource() == null || project.getSource().getLocation() == null) { continue; } if (project.exists()) { // to prevent warning when reusing same workspace if (!("perUser".equals(createPolicy) || "perAccount".equals(createPolicy))) { notificationManager.notify("Import", locale.projectAlreadyImported(project.getName()), FAIL, FLOAT_MODE); } continue; } projectNames.add(project.getName()); } importProjects(projectNames); } /** * Import source projects and if it's already exist in workspace * then show warning notification * * @param projectsToImport * set of project names that already exist in workspace and will be imported on file system */ private void importProjects(Set<String> projectsToImport) { final List<Promise<Project>> promises = new ArrayList<>(); for (final ProjectConfigDto projectConfig : factory.getWorkspace().getProjects()) { if (projectsToImport.contains(projectConfig.getName())) { promises.add(startImport(Path.valueOf(projectConfig.getPath()), projectConfig.getSource()).thenPromise( new Function<Project, Promise<Project>>() { @Override public Promise<Project> apply(Project project) throws FunctionException { return project.update().withBody(projectConfig).send(); } })); } } Promises.all(promises.toArray(new Promise<?>[promises.size()])) .then(arg -> { callback.onSuccess(null); }) .catchError(promiseError -> { // If it is unable to import any number of projects then factory import status will be success anyway callback.onSuccess(null); }); } @Override protected Promise<Project> importProject(@NotNull final Path pathToProject, @NotNull final SourceStorage sourceStorage) { return doImport(pathToProject, sourceStorage); } private Promise<Project> doImport(@NotNull final Path pathToProject, @NotNull final SourceStorage sourceStorage) { final String projectName = pathToProject.lastSegment(); final StatusNotification notification = notificationManager.notify(locale.cloningSource(projectName), null, PROGRESS, FLOAT_MODE); final ProjectNotificationSubscriber subscriber = subscriberFactory.createSubscriber(); subscriber.subscribe(projectName, notification); String location = sourceStorage.getLocation(); // it's needed for extract repository name from repository url e.g https://github.com/codenvy/che-core.git // lastIndexOf('/') + 1 for not to capture slash and length - 4 for trim .git final String repository = location.substring(location.lastIndexOf('/') + 1).replace(".git", ""); final Map<String, String> parameters = firstNonNull(sourceStorage.getParameters(), Collections.<String, String>emptyMap()); final String branch = parameters.get("branch"); final String startPoint = parameters.get("startPoint"); final MessageBus messageBus = messageBusProvider.getMachineMessageBus(); final String channel = CHANNEL + appContext.getWorkspaceId() + ':' + projectName; final SubscriptionHandler<GitCheckoutEvent> successImportHandler = new SubscriptionHandler<GitCheckoutEvent>( dtoUnmarshallerFactory.newWSUnmarshaller(GitCheckoutEvent.class)) { @Override protected void onMessageReceived(GitCheckoutEvent result) { if (result.isCheckoutOnly()) { notificationManager.notify(locale.clonedSource(projectName), locale.clonedSourceWithCheckout(projectName, repository, result.getBranchRef(), branch), SUCCESS, FLOAT_MODE); } else { notificationManager.notify(locale.clonedSource(projectName), locale.clonedWithCheckoutOnStartPoint(projectName, repository, startPoint, branch), SUCCESS, FLOAT_MODE); } } @Override protected void onErrorReceived(Throwable e) { try { messageBus.unsubscribe(channel, this); } catch (WebSocketException ignore) { } } }; try { messageBus.subscribe(channel, successImportHandler); } catch (WebSocketException ignore) { } MutableProjectConfig importConfig = new MutableProjectConfig(); importConfig.setPath(pathToProject.toString()); importConfig.setSource(sourceStorage); return appContext.getWorkspaceRoot() .importProject() .withBody(importConfig) .send() .then(new Function<Project, Project>() { @Override public Project apply(Project project) throws FunctionException { subscriber.onSuccess(); notification.setContent(locale.clonedSource(projectName)); notification.setStatus(SUCCESS); return project; } }) .catchErrorPromise(new Function<PromiseError, Promise<Project>>() { @Override public Promise<Project> apply(PromiseError err) throws FunctionException { final int errorCode = ExceptionUtils.getErrorCode(err.getCause()); switch (errorCode) { case UNAUTHORIZED_GIT_OPERATION: subscriber.onFailure(err.getMessage()); final Map<String, String> attributes = ExceptionUtils.getAttributes(err.getCause()); final String providerName = attributes.get(PROVIDER_NAME); final String authenticateUrl = attributes.get(AUTHENTICATE_URL); final boolean authenticated = Boolean.parseBoolean(attributes.get("authenticated")); if (!StringUtils.isNullOrEmpty(providerName) && !StringUtils.isNullOrEmpty(authenticateUrl)) { if (!authenticated) { return tryAuthenticateAndRepeatImport(providerName, authenticateUrl, pathToProject, sourceStorage, subscriber); } else { dialogFactory.createMessageDialog(locale.cloningSourceSshKeyUploadFailedTitle(), locale.cloningSourcesSshKeyUploadFailedText(), null) .show(); } } else { dialogFactory.createMessageDialog(locale.oauthFailedToGetAuthenticatorTitle(), locale.oauthFailedToGetAuthenticatorText(), null).show(); } break; case UNAUTHORIZED_SVN_OPERATION: subscriber.onFailure(err.getMessage()); return recallSubversionImportWithCredentials(pathToProject, sourceStorage); case UNABLE_GET_PRIVATE_SSH_KEY: subscriber.onFailure(locale.acceptSshNotFoundText()); break; case FAILED_CHECKOUT: subscriber.onFailure(locale.cloningSourceWithCheckoutFailed(branch, repository)); notification.setTitle(locale.cloningSourceFailedTitle(projectName)); break; case FAILED_CHECKOUT_WITH_START_POINT: subscriber.onFailure(locale.cloningSourceCheckoutFailed(branch, startPoint)); notification.setTitle(locale.cloningSourceFailedTitle(projectName)); break; default: subscriber.onFailure(err.getMessage()); notification.setTitle(locale.cloningSourceFailedTitle(projectName)); notification.setStatus(FAIL); } return Promises.resolve(null); } }); } private Promise<Project> tryAuthenticateAndRepeatImport(@NotNull final String providerName, @NotNull final String authenticateUrl, @NotNull final Path pathToProject, @NotNull final SourceStorage sourceStorage, @NotNull final ProjectNotificationSubscriber subscriber) { OAuth2Authenticator authenticator = oAuth2AuthenticatorRegistry.getAuthenticator(providerName); if (authenticator == null) { authenticator = oAuth2AuthenticatorRegistry.getAuthenticator("default"); } return authenticator.authenticate(OAuth2AuthenticatorUrlProvider.get(restContext, authenticateUrl)).thenPromise( new Function<OAuthStatus, Promise<Project>>() { @Override public Promise<Project> apply(OAuthStatus result) throws FunctionException { if (!result.equals(OAuthStatus.NOT_PERFORMED)) { return doImport(pathToProject, sourceStorage); } else { subscriber.onFailure("Authentication cancelled"); callback.onSuccess(null); } return Promises.resolve(null); } }).catchError(caught -> { callback.onFailure(new Exception(caught.getMessage())); }); } private Promise<Project> recallSubversionImportWithCredentials(final Path path, final SourceStorage sourceStorage) { return askCredentialsDialog.askCredentials() .thenPromise(new Function<Credentials, Promise<Project>>() { @Override public Promise<Project> apply(Credentials credentials) throws FunctionException { sourceStorage.getParameters().put("username", credentials.getUsername()); sourceStorage.getParameters().put("password", credentials.getPassword()); return doImport(path, sourceStorage); } }) .catchError(error -> { callback.onFailure(error.getCause()); }); } }