/******************************************************************************* * 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.resources; import com.google.gwt.core.client.JsArrayMixed; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.che.api.promises.client.Function; import org.eclipse.che.api.promises.client.FunctionException; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.api.promises.client.PromiseProvider; import org.eclipse.che.api.promises.client.callback.AsyncPromiseHelper.RequestCall; import org.eclipse.che.api.promises.client.js.JsPromiseError; import org.eclipse.che.ide.CoreLocalizationConstant; import org.eclipse.che.ide.api.dialogs.CancelCallback; import org.eclipse.che.ide.api.dialogs.ConfirmCallback; import org.eclipse.che.ide.api.dialogs.DialogFactory; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.api.notification.StatusNotification; import org.eclipse.che.ide.api.resources.Folder; import org.eclipse.che.ide.api.resources.Resource; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Lists.newArrayList; import static org.eclipse.che.api.promises.client.callback.AsyncPromiseHelper.createFromAsyncRequest; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; import static org.eclipse.che.ide.api.resources.Resource.FILE; import static org.eclipse.che.ide.api.resources.Resource.FOLDER; import static org.eclipse.che.ide.api.resources.Resource.PROJECT; /** * Manager that performs removing resources. Support confirmation for removing. * * @author Vlad Zhukovskiy */ @Singleton public class DeleteResourceManager { private final CoreLocalizationConstant localization; private final DialogFactory dialogFactory; private final PromiseProvider promiseProvider; private final NotificationManager notificationManager; @Inject public DeleteResourceManager(CoreLocalizationConstant localization, DialogFactory dialogFactory, PromiseProvider promiseProvider, NotificationManager notificationManager) { this.localization = localization; this.dialogFactory = dialogFactory; this.promiseProvider = promiseProvider; this.notificationManager = notificationManager; } /** * Deletes the given resources and its descendants in the standard manner from file system. * Method doesn't require a confirmation from the user and removes resources silently. * * @param resources * the resources to delete * @return the {@link Promise} with void if removal has successfully completed * @see #delete(boolean, Resource...) */ public Promise<Void> delete(Resource... resources) { return delete(false, resources); } /** * Deletes the given resources and its descendants in the standard manner from file system. * Method requires a confirmation from the user before resource will be removed. * * @param needConfirmation * true if confirmation is need * @param resources * the resources to delete * @return the {@link Promise} with void if removal has successfully completed * @see #delete(Resource...) */ public Promise<Void> delete(boolean needConfirmation, Resource... resources) { checkArgument(resources != null, "Null resource occurred"); checkArgument(resources.length > 0, "No resources were provided to remove"); final Resource[] filtered = filterDescendants(resources); if (!needConfirmation) { Promise<?>[] deleteAll = new Promise<?>[resources.length]; for (int i = 0; i < resources.length; i++) { deleteAll[i] = resources[i].delete(); } return promiseProvider.all(deleteAll).then(new Function<JsArrayMixed, Void>() { @Override public Void apply(JsArrayMixed arg) throws FunctionException { return null; } }); } List<Resource> projectsList = newArrayList(); for (Resource resource : filtered) { if (resource.getResourceType() == PROJECT) { projectsList.add(resource); } } Resource[] projects = projectsList.toArray(new Resource[projectsList.size()]); if (projectsList.isEmpty()) { //if no project were found in nodes list return promptUserToDelete(filtered); } else if (projects.length < filtered.length) { //inform user that we can't delete mixed list of the nodes return promiseProvider.reject(JsPromiseError.create(localization.mixedProjectDeleteMessage())); } else { //delete only project nodes return promptUserToDelete(projects); } } private Promise<Void> promptUserToDelete(final Resource[] resources) { return createFromAsyncRequest(new RequestCall<Void>() { @Override public void makeCall(AsyncCallback<Void> callback) { String warningMessage = generateWarningMessage(resources); boolean anyDirectories = false; String directoryName = null; for (Resource resource : resources) { if (resource instanceof Folder) { anyDirectories = true; directoryName = resource.getName(); break; } } if (anyDirectories) { warningMessage += resources.length == 1 ? localization.deleteAllFilesAndSubdirectories(directoryName) : localization.deleteFilesAndSubdirectoriesInTheSelectedDirectory(); } dialogFactory.createConfirmDialog(localization.deleteDialogTitle(), warningMessage, onConfirm(resources, callback), onCancel(callback)).show(); } }); } private String generateWarningMessage(Resource[] resources) { if (resources.length == 1) { String name = resources[0].getName(); String type = getDisplayType(resources[0]); return "Delete " + type + " \"" + name + "\"?"; } Map<String, Integer> pluralToSingular = new HashMap<>(); for (Resource resource : resources) { final String type = getDisplayType(resource); if (!pluralToSingular.containsKey(type)) { pluralToSingular.put(type, 1); } else { Integer count = pluralToSingular.get(type); count++; pluralToSingular.put(type, count); } } StringBuilder buffer = new StringBuilder("Delete "); Iterator<Map.Entry<String, Integer>> iterator = pluralToSingular.entrySet().iterator(); if (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); buffer.append(entry.getValue()) .append(" ") .append(entry.getKey()); if (entry.getValue() > 1) { buffer.append("s"); } while (iterator.hasNext()) { Map.Entry<String, Integer> e = iterator.next(); buffer.append(" and ") .append(e.getValue()) .append(" ") .append(e.getKey()); if (e.getValue() > 1) { buffer.append("s"); } } } buffer.append("?"); return buffer.toString(); } private String getDisplayType(Resource resource) { if (resource.getResourceType() == PROJECT) { return "project"; } else if (resource.getResourceType() == FOLDER) { return "folder"; } else if (resource.getResourceType() == FILE) { return "file"; } else { return "resource"; } } private Resource[] filterDescendants(Resource[] resources) { List<Resource> filteredElements = newArrayList(resources); int previousSize; do { previousSize = filteredElements.size(); outer: for (Resource element : filteredElements) { for (Resource element2 : filteredElements) { if (element == element2) { continue; } //compare only paths to increase performance, don't operation in this case with parents if (element.getLocation().isPrefixOf(element2.getLocation())) { filteredElements.remove(element2); break outer; } } } } while (filteredElements.size() != previousSize); return filteredElements.toArray(new Resource[filteredElements.size()]); } private ConfirmCallback onConfirm(final Resource[] resources, final AsyncCallback<Void> callback) { return new ConfirmCallback() { @Override public void accepted() { if (resources == null) { //sometimes we may occur NPE here while reading length callback.onFailure(new IllegalStateException()); return; } Promise<?>[] deleteAll = new Promise<?>[resources.length]; for (int i = 0; i < resources.length; i++) { final Resource resource = resources[i]; deleteAll[i] = resource.delete().catchError(new Operation<PromiseError>() { @Override public void apply(PromiseError error) throws OperationException { notificationManager.notify("Failed to delete '" + resource.getName() + "'", error.getMessage(), FAIL, StatusNotification.DisplayMode.FLOAT_MODE); } }); } promiseProvider.all(deleteAll).then(new Operation<JsArrayMixed>() { @Override public void apply(JsArrayMixed arg) throws OperationException { callback.onSuccess(null); } }); } }; } private CancelCallback onCancel(final AsyncCallback<Void> callback) { return new CancelCallback() { @Override public void cancelled() { callback.onFailure(new Exception("Cancelled")); } }; } }