/******************************************************************************* * Copyright (c) 2012-2016 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.ext.java.client.refactoring; import com.google.common.base.Predicate; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.web.bindery.event.shared.EventBus; 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.ide.api.editor.EditorAgent; import org.eclipse.che.ide.api.editor.EditorPartPresenter; import org.eclipse.che.ide.api.event.FileContentUpdateEvent; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.api.project.node.HasStorablePath.StorablePath; import org.eclipse.che.ide.api.project.node.Node; import org.eclipse.che.ide.api.project.tree.VirtualFile; import org.eclipse.che.ide.ext.java.client.JavaLocalizationConstant; import org.eclipse.che.ide.ext.java.client.project.node.PackageNode; import org.eclipse.che.ide.ext.java.shared.dto.refactoring.ChangeInfo; import org.eclipse.che.ide.part.explorer.project.ProjectExplorerPresenter; import org.eclipse.che.ide.project.node.FileReferenceNode; import java.util.Iterator; import java.util.List; import static com.google.common.base.Predicates.not; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Iterables.filter; import static org.eclipse.che.api.promises.client.js.Promises.resolve; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; /** * Utility class for the refactoring operations. * It is needed for refreshing the project tree, updating content of the opening editors. * * @author Valeriy Svydenko * @author Vlad Zhukovskyi */ @Singleton public class RefactoringUpdater { private final EditorAgent editorAgent; private final EventBus eventBus; private final ProjectExplorerPresenter projectExplorer; private final JavaLocalizationConstant locale; private final NotificationManager notificationManager; private Predicate<ChangeInfo> UPDATE_ONLY = new Predicate<ChangeInfo>() { @Override public boolean apply(ChangeInfo input) { return ChangeInfo.ChangeName.UPDATE.equals(input.getName()); } }; @Inject public RefactoringUpdater(EditorAgent editorAgent, EventBus eventBus, NotificationManager notificationManager, ProjectExplorerPresenter projectExplorer, JavaLocalizationConstant locale) { this.editorAgent = editorAgent; this.notificationManager = notificationManager; this.eventBus = eventBus; this.projectExplorer = projectExplorer; this.locale = locale; } /** * Iterates over each refactoring change and according to change type performs specific update operation. * i.e. for {@code ChangeName#UPDATE} updates only opened editors, for {@code ChangeName#MOVE or ChangeName#RENAME_COMPILATION_UNIT} * updates only new paths and opened editors, for {@code ChangeName#RENAME_PACKAGE} reloads package structure and restore expansion. * * @param changes * applied changes */ public void updateAfterRefactoring(RefactorInfo refactoringInfo, List<ChangeInfo> changes) { if (changes == null || changes.isEmpty()) { return; } final Iterable<ChangeInfo> changesExceptUpdates = filter(changes, not(UPDATE_ONLY)); final Iterable<ChangeInfo> updateChangesOnly = filter(changes, UPDATE_ONLY); Promise<Void> promise = resolve(null); promise = proceedGeneralChanges(promise, changesExceptUpdates.iterator(), refactoringInfo); proceedUpdateChanges(promise, updateChangesOnly.iterator()); } /** Iterate over changes except update changes. Refresh tree according to change type. */ private Promise<Void> proceedGeneralChanges(Promise<Void> promise, Iterator<ChangeInfo> iterator, final RefactorInfo refactorInfo) { if (!iterator.hasNext()) { return promise; } final ChangeInfo changeInfo = iterator.next(); if (changeInfo == null || changeInfo.getName() == null) { return proceedGeneralChanges(promise, iterator, refactorInfo); } final Promise<Void> derivedPromise; switch (changeInfo.getName()) { case MOVE: case RENAME_COMPILATION_UNIT: if (refactorInfo != null && refactorInfo.getSelectedItems() != null) { removeNodeFor(changeInfo, refactorInfo.getSelectedItems()); } derivedPromise = promise.thenPromise(proceedRefactoringMove(changeInfo)); break; case RENAME_PACKAGE: derivedPromise = promise.thenPromise(proceedRefactoringRenamePackage(changeInfo, refactorInfo)); break; default: return proceedGeneralChanges(promise, iterator, refactorInfo); } return proceedGeneralChanges(derivedPromise, iterator, refactorInfo); } /** Iterate over changes that has UPDATE mode. In this case we try to update opened editors only. */ private Promise<Void> proceedUpdateChanges(Promise<Void> promise, Iterator<ChangeInfo> iterator) { if (!iterator.hasNext()) { return promise; } final ChangeInfo changeInfo = iterator.next(); //iterate over opened files in editor and find those file that matches ours final FileReferenceNode editorFile = getOpenedFileOrNull(!isNullOrEmpty(changeInfo.getOldPath()) ? changeInfo.getOldPath() : changeInfo.getPath()); //if no one file were found, than it means that we shouldn't update anything if (editorFile == null) { return proceedUpdateChanges(promise, iterator); } final Promise<Void> derivedPromise = promise.thenPromise(new Function<Void, Promise<Void>>() { @Override public Promise<Void> apply(Void arg) throws FunctionException { return projectExplorer.getNodeByPath(new StorablePath(changeInfo.getPath()), true, false) .thenPromise(updateEditorContent(editorFile)) .catchError(onNodeNotFound()); } }); return proceedUpdateChanges(derivedPromise, iterator); } /** Iterates over opened editors and fetch file with specified path or returns null. */ private FileReferenceNode getOpenedFileOrNull(String path) { VirtualFile vFile = null; for (EditorPartPresenter editor : editorAgent.getOpenedEditors().values()) { if (editor.getEditorInput().getFile().getPath().equals(path)) { vFile = editor.getEditorInput().getFile(); break; } } if (vFile == null || !(vFile instanceof FileReferenceNode)) { return null; } return (FileReferenceNode)vFile; } /** Takes input file, provide into ones new data object and notifies editors to re-read file content. */ private Function<Node, Promise<Void>> updateEditorContent(final FileReferenceNode editorFileToUpdate) { return new Function<Node, Promise<Void>>() { @Override public Promise<Void> apply(Node node) throws FunctionException { //here we consume node and if it is file node than we set data from one file into other if (node instanceof FileReferenceNode) { setFileDataObject((FileReferenceNode)node, editorFileToUpdate); } return resolve(null); } }; } /** Set data object from one file into other and notify editor to re-read file content if such opened. */ private void setFileDataObject(FileReferenceNode from, FileReferenceNode to) { if (from == null || to == null) { return; } String tempPath = to.getPath(); to.setData(from.getData()); to.setParent(from.getParent()); editorAgent.updateEditorNode(tempPath, to); eventBus.fireEvent(new FileContentUpdateEvent(from.getPath())); } /** Removes from Project Tree node which matches old path from Refactoring change info object. */ private void removeNodeFor(ChangeInfo changeInfo, List<?> proceedItems) { for (Object proceedItem : proceedItems) { if (proceedItem instanceof FileReferenceNode && ((FileReferenceNode)proceedItem).getStorablePath().equals(changeInfo.getOldPath())) { projectExplorer.removeNode((FileReferenceNode)proceedItem, false); } } } /** Find node by new path and notify editors to re-read files if need. */ private Function<Void, Promise<Void>> proceedRefactoringMove(final ChangeInfo changeInfo) { return new Function<Void, Promise<Void>>() { @Override public Promise<Void> apply(Void arg) throws FunctionException { FileReferenceNode editorFileToUpdate = getOpenedFileOrNull(changeInfo.getOldPath()); return projectExplorer.getNodeByPath(new StorablePath(changeInfo.getPath()), true, false) .thenPromise(updateEditorContent(editorFileToUpdate)) .catchError(onNodeNotFound()); } }; } /** Find new package node and restore expansion if need. */ private Function<Void, Promise<Void>> proceedRefactoringRenamePackage(final ChangeInfo changeInfo, final RefactorInfo refactorInfo) { return new Function<Void, Promise<Void>>() { @Override public Promise<Void> apply(Void arg) throws FunctionException { //according to Rename package action it can be enabled if we have only one selected Package in selection agent Object refItem = refactorInfo.getSelectedItems().get(0); final boolean wasPackageExpanded = refItem instanceof PackageNode && projectExplorer.isExpanded((Node)refItem); return projectExplorer.getNodeByPath(new StorablePath(changeInfo.getPath()), true, false) .thenPromise(new Function<Node, Promise<Void>>() { @Override public Promise<Void> apply(Node node) throws FunctionException { //restore expand state if (wasPackageExpanded) { projectExplorer.setExpanded(node, true); } return resolve(null); } }) .catchError(onNodeNotFound()); } }; } /** Simply notify user in any failed cases. */ private Operation<PromiseError> onNodeNotFound() { return new Operation<PromiseError>() { @Override public void apply(PromiseError arg) throws OperationException { notificationManager.notify(locale.failedToProcessRefactoringOperation(), arg.getMessage(), FAIL, true); } }; } }