eu.numberfour.n4js.ui.organize.imports.N4JSOrganizeImportsHandler.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.ui.organize.imports.N4JSOrganizeImportsHandler.java

Source

/**
 * Copyright (c) 2016 NumberFour AG.
 * 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:
 *   NumberFour AG - Initial API and implementation
 */
package eu.numberfour.n4js.ui.organize.imports;

import static eu.numberfour.n4js.N4JSGlobals.JS_FILE_EXTENSION;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Logger;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.xtext.resource.FileExtensionProvider;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.XtextEditor;
import org.eclipse.xtext.ui.editor.model.IXtextDocument;
import org.eclipse.xtext.ui.editor.model.XtextDocumentProvider;
import org.eclipse.xtext.ui.editor.utils.EditorUtils;
import org.eclipse.xtext.util.TextRegion;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;

import eu.numberfour.n4js.resource.N4JSResource;
import eu.numberfour.n4js.ui.changes.ChangeManager;
import eu.numberfour.n4js.ui.changes.ChangeProvider;
import eu.numberfour.n4js.ui.changes.IAtomicChange;
import eu.numberfour.n4js.ui.changes.IChange;
import eu.numberfour.n4js.ui.changes.Replacement;

/**
 * Handler used for two cases: Mass updates on files/folders in selection or organizing the current N4JS Editor.
 */
public class N4JSOrganizeImportsHandler extends AbstractHandler {

    private static final Logger LOGGER = Logger.getLogger(N4JSOrganizeImportsHandler.class);

    // list of FileExtensions to exclude:
    private static final List<String> RAW_JS_FILES = ImmutableList.of(JS_FILE_EXTENSION);

    @Inject
    private N4JSOrganizeImports organizeImports;

    @Inject
    private ChangeManager changeManager;

    @Inject
    private XtextDocumentProvider docProvider;

    // IDEBUG-239 we have to limit here to N4JS & N4JSD only. (see above of things to remove.)
    @Inject
    private FileExtensionProvider fileExtensionsProvider;

    // cleaned version of extensions got from fileExtensions
    private Collection<String> n4FileExtensions;

    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {

        Collection<?> callingMenus = HandlerUtil.getActiveMenus(event);
        // "#TextEditorContext" is the defined plugin.xml
        boolean fromTextContext = (callingMenus != null && callingMenus.contains("#TextEditorContext"));
        boolean fromShortCut = (callingMenus == null || callingMenus.isEmpty());

        XtextEditor editor = EditorUtils.getActiveXtextEditor(event);
        boolean haveActiveEditor = editor != null;

        ISelection selection = HandlerUtil.getCurrentSelection(event);
        boolean nonEmptyStructuredSelection = (selection != null && selection instanceof IStructuredSelection
                && !selection.isEmpty());

        if (haveActiveEditor && (fromTextContext || fromShortCut)) {
            organizeEditor(editor);
        } else if (nonEmptyStructuredSelection) {
            // probably called on a tree-selection in the package-manager or whatever view shows the project-structure:
            // organize files and folders:
            // for each selection entry collect files:
            Multimap<IProject, IFile> projectFiles = collectFiles((IStructuredSelection) selection);

            HashSet<IFile> filesInSet = new HashSet<>(projectFiles.values());
            List<IFile> filesAsList = new ArrayList<>(filesInSet);

            if (filesAsList.isEmpty()) {
                return null;
            }

            // Query unsaved
            IWorkbench wbench = PlatformUI.getWorkbench();
            IWorkbenchWindow activeWorkbenchWindow = wbench.getActiveWorkbenchWindow();
            boolean allSaved = wbench.saveAll(activeWorkbenchWindow, activeWorkbenchWindow, null, true);
            if (!allSaved) {
                return null;
            }

            Shell shell = HandlerUtil.getActiveShell(event);

            IRunnableWithProgress op = new IRunnableWithProgress() {
                @Override
                public void run(IProgressMonitor mon) throws InvocationTargetException, InterruptedException {
                    int totalWork = filesAsList.size();
                    mon.beginTask("Organize imports.", totalWork);
                    for (int i = 0; !mon.isCanceled() && i < filesAsList.size(); i++) {
                        IFile currentFile = filesAsList.get(i);
                        mon.setTaskName("Organize imports." + " - File (" + (i + 1) + " of " + totalWork + ")");
                        try {
                            mon.subTask(currentFile.getName());
                            doOrganizeImports(currentFile, new SubProgressMonitor(mon, 1));

                        } catch (CoreException | RuntimeException e) {
                            String msg = "Exception in file " + currentFile.getFullPath().toString() + ".";
                            LOGGER.error(msg, e);
                            if (errorDialogWithStackTrace(msg + " Hit OK to continue.", e)) {
                                // - logged anyway
                            } else {
                                throw new InvocationTargetException(e);
                            }
                        }
                    }
                    if (mon.isCanceled()) {
                        throw new InterruptedException();
                    }
                }
            };

            try {
                new ProgressMonitorDialog(shell).run(true, true, op);
            } catch (InvocationTargetException e) {
                throw new ExecutionException("Error during organizing imports", e);
            } catch (InterruptedException e) {
                // user cancelled, ok
            }

        }

        return null;
    }

    private Multimap<IProject, IFile> collectFiles(IStructuredSelection structuredSelection) {
        Multimap<IProject, IFile> result = HashMultimap.create();
        for (Object object : structuredSelection.toList()) {
            collectRelevantFiles(object, result);
        }
        return result;
    }

    private void collectRelevantFiles(Object element, Multimap<IProject, IFile> result) {
        if (element instanceof IWorkingSet) {
            IWorkingSet workingSet = (IWorkingSet) element;
            IAdaptable[] elements = workingSet.getElements();
            for (int j = 0; j < elements.length; j++) {
                collectRelevantFiles(elements[j], result);
            }
        } else if (element instanceof IContainer) {
            IContainer container = (IContainer) element;
            try {
                for (IResource child : container.members(IContainer.EXCLUDE_DERIVED)) {
                    collectRelevantFiles(child, result);
                }
            } catch (CoreException c) {
                LOGGER.warn("Error while collecting files", c);
            }
        } else if (element instanceof IFile) {
            collectIFiles(result, new Object[] { element });
        }
    }

    private void collectIFiles(Multimap<IProject, IFile> result, Object[] nonJavaResources) {
        for (Object object : nonJavaResources) {
            if (object instanceof IFile) {
                IFile iFile = (IFile) object;
                if (shouldHandleFile(iFile))
                    result.put(iFile.getProject(), iFile);
            }
        }
    }

    /**
     * Checking the file type by getting the known extensions from the FileExtensionProvider
     *
     * @param object
     *            file to judge
     * @return true if the file is a valid file for organize import
     */
    private boolean shouldHandleFile(IFile object) {
        String fileExtension = object.getFileExtension();
        return fileExtension != null && getN4FileExtensions().contains(fileExtension);
    }

    /**
     * Access with lazy init to the desired file extensions to organize.
     *
     * @return Set of extensions for files on which organization should be applied
     */
    private Collection<String> getN4FileExtensions() {
        // Lazily obtain list of valid extensions:
        if (n4FileExtensions == null) {
            n4FileExtensions = new HashSet<>(fileExtensionsProvider.getFileExtensions());
            n4FileExtensions.removeAll(RAW_JS_FILES);
        }
        return n4FileExtensions;
    }

    private void organizeEditor(XtextEditor editor) {
        try {
            IXtextDocument document = editor.getDocument();
            doOrganizeImports(document, Interaction.queryUser);
        } catch (RuntimeException re) {
            if (re.getCause() instanceof BreakException) {
                LOGGER.debug("user canceled");
            } else {
                LOGGER.warn("Unrecognized RT-exception", re);
            }

        }
    }

    private void doOrganizeImports(IFile file, IProgressMonitor mon) throws CoreException {

        mon.beginTask("Organizing " + file.getName(), IProgressMonitor.UNKNOWN);

        FileEditorInput fei = new FileEditorInput(file);

        docProvider.connect(fei); // without connecting no document will be provided
        IXtextDocument document = (IXtextDocument) docProvider.getDocument(fei);

        docProvider.aboutToChange(fei);

        doOrganizeImports(document, Interaction.breakBuild);

        mon.subTask("Saving " + file.getName());

        docProvider.saveDocument(new SubProgressMonitor(mon, 0), fei, document, true);

        docProvider.changed(fei);
        docProvider.disconnect(fei);

        mon.done();
    }

    /**
     * Organize the imports in the N4JS document.
     *
     * @param document
     *            N4JS document
     * @throws RuntimeException
     *             wrapping a BreakException in case of user-abortion ({@link Interaction#queryUser}) or
     *             resolution-failure({@link Interaction#breakBuild} )
     */
    public void doOrganizeImports(final IXtextDocument document, final Interaction interaction) {
        // trigger Linking
        document.readOnly((XtextResource p) -> {
            N4JSResource.postProcess(p);
            return null;
        });

        List<IChange> result = document.readOnly(new IUnitOfWork<List<IChange>, XtextResource>() {
            @Override
            public List<IChange> exec(XtextResource xtextResource) throws Exception {
                // Position, length 0
                TextRegion importRegion = organizeImports.getImportRegion(xtextResource);

                if (importRegion != null) {
                    List<IChange> changes = new ArrayList<>();
                    try {
                        final String NL = ChangeProvider.lineDelimiter(document, importRegion.getOffset());

                        final String organizedImportSection = organizeImports
                                .getOrganizedImportSection(xtextResource, NL, interaction);
                        if (organizedImportSection != null) { // remove old imports
                            changes.addAll(organizeImports.getCleanupChanges(xtextResource, document));
                            if (!organizedImportSection.isEmpty()) {
                                // advance ImportRegion-offset if not nil:
                                int offset = importRegion.getOffset();
                                if (offset != 0) {
                                    offset += NL.length();
                                }
                                changes.add(ChangeProvider.insertLineAbove(document, offset, organizedImportSection,
                                        true));
                            }
                            return changes;
                        }
                    } catch (BreakException e) {
                        LOGGER.warn("Organize imports broke:", e);
                        throw new RuntimeException(e);
                    }
                }
                return null;
            }
        });

        if (result != null && !result.isEmpty()) {
            // do the changes really modify anything?
            ChangeAnalysis changeAnalysis = condense(result);
            if (changeAnalysis.noRealChanges) {
                // verify again:
                String del = document.get().substring(changeAnalysis.deletion.getOffset(),
                        changeAnalysis.deletion.getOffset() + changeAnalysis.deletion.getLength());
                if (changeAnalysis.newText.getText().equals(del)) {
                    return;
                }
            }
            document.modify(new IUnitOfWork.Void<XtextResource>() {
                @Override
                public void process(XtextResource state) throws Exception {
                    try {
                        EcoreUtil.resolveAll(state);
                        changeManager.applyAllInSameDocument(changeAnalysis.changes, document);
                    } catch (BadLocationException e) {
                        LOGGER.error(e);
                    }
                }
            });

        }

    }

    /**
     * Very specific to the generator: One has a text with nonzero length, all others are deletions an have zero-length
     * texts.
     *
     * Find the one with text, try to condense the other into one atomic change.
     *
     *
     * @param changes
     *            list of Changes to process
     * @return Pair of Changes, flag if nothing changes.
     */
    private ChangeAnalysis condense(List<IChange> changes) {
        List<IAtomicChange> atomicResult = changeManager.flattenAndOrganized(changes);
        if (atomicResult.isEmpty()) {
            return new ChangeAnalysis(atomicResult, true);
        }
        // if all are from same uri and type of Replacement, then it will be condensed.
        URI uri = atomicResult.get(0).getURI();
        if (!(atomicResult.get(0) instanceof Replacement)) {
            return new ChangeAnalysis(atomicResult, false);
        }

        // Pre condition: find the one with text !=  && other no test.
        // Pre uris must match.
        Replacement rText = null;
        for (IAtomicChange nxt : atomicResult) {
            if (!(nxt instanceof Replacement) || !uri.equals(nxt.getURI())) {
                return new ChangeAnalysis(atomicResult, false);
            }
            Replacement rplc = (Replacement) nxt;
            if (rplc.getText() != null && rplc.getText().length() > 0) {
                if (rText == null) {
                    rText = rplc;
                } else {
                    return new ChangeAnalysis(atomicResult, false); // more then one text-addition, pre doesn't hold
                }
            }
        }

        Replacement current = null;
        // Back to front
        for (int i = atomicResult.size() - 1; i >= 0; i--) {
            IAtomicChange nxt = atomicResult.get(i);
            if (nxt == rText) {
                continue;
            }
            Replacement rplc = (Replacement) nxt;
            if (current == null) {
                current = rplc;
                continue;
            }
            // all Texts are
            if (current.getOffset() + current.getLength() == rplc.getOffset()) {
                // possible to concatenate.
                current = new Replacement(uri, current.getOffset(), current.getLength() + rplc.getLength(), "");
            } else {
                // cannot merge
                return new ChangeAnalysis(atomicResult, false);
            }
        }
        // compare length:
        if (current == null || rText == null || current.getLength() != rText.getText().length()) {
            return new ChangeAnalysis(atomicResult, false);
        }

        ChangeAnalysis result = new ChangeAnalysis(Arrays.asList(current, rText), true);
        result.deletion = current;
        result.newText = rText;
        return result;
    }

    static class ChangeAnalysis {
        public ChangeAnalysis(List<IAtomicChange> changes, boolean noRealChanges) {
            super();
            this.changes = changes;
            this.noRealChanges = noRealChanges;
        }

        List<IAtomicChange> changes;
        boolean noRealChanges;
        Replacement newText = null;
        Replacement deletion = null;
    }

    /**
     * Shows JFace ErrorDialog but improved by constructing full stack trace in detail area.
     *
     * @return true if OK was pressed
     */
    public static boolean errorDialogWithStackTrace(String msg, Throwable t) {

        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        t.printStackTrace(pw);

        final String trace = sw.toString(); // stack trace as a string

        // Temporary holder of child statuses
        List<Status> childStatuses = new ArrayList<>();

        // Split output by OS-independent new-line
        for (String line : trace.split(System.getProperty("line.separator"))) {
            // build & add status
            childStatuses.add(new Status(IStatus.ERROR, "N4js-plugin-id", line));
        }

        MultiStatus ms = new MultiStatus("N4js-plugin-id", IStatus.ERROR, childStatuses.toArray(new Status[] {}), // convert to array of statuses
                t.getLocalizedMessage(), t);

        final AtomicBoolean result = new AtomicBoolean(true);
        Display.getDefault().syncExec(() -> result
                .set(ErrorDialog.openError(null, "Error occurred while organizing ", msg, ms) == Window.OK));

        return result.get();
    }
}