Skip to content

Content of file NotesView.java

/*******************************************************************************
 * Copyright (c) 2008, 2023 SAP AG and IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    SAP AG - initial API and implementation
 *    Andrew Johnson - improved undo
 *******************************************************************************/
package org.eclipse.mat.ui.internal.views;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.CRC32;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.TextViewerUndoManager;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.DefaultHyperlinkPresenter;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.mat.SnapshotException;
import org.eclipse.mat.query.ContextProvider;
import org.eclipse.mat.query.IContextObject;
import org.eclipse.mat.query.IQueryContext;
import org.eclipse.mat.snapshot.ISnapshot;
import org.eclipse.mat.ui.editor.MultiPaneEditor;
import org.eclipse.mat.ui.snapshot.editor.HeapEditor;
import org.eclipse.mat.ui.snapshot.editor.ISnapshotEditorInput;
import org.eclipse.mat.ui.snapshot.editor.ISnapshotEditorInput.IChangeListener;
import org.eclipse.mat.ui.util.PopupMenu;
import org.eclipse.mat.ui.util.QueryContextMenu;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISaveablePart2;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.part.ViewPart;

public class NotesView extends ViewPart implements IPartListener, ISaveablePart, ISaveablePart2
{
    private static final String NOTES_ENCODING = "UTF8"; //$NON-NLS-1$

    private static final int UNDO_LEVEL = 25;

    private File resource;
    private File prefix;

    private Action undo;
    private Action redo;

    private Menu menu;
    private Font font;
    private Color hyperlinkColor;

    private MultiPaneEditor editor;

    private TextViewer textViewer;
    private TextViewerUndoManager undoManager;
    private Map<String, NotesViewAction> actions = new HashMap<String, NotesViewAction>();
    long hash;
    boolean modified;

    @Override
    public void createPartControl(Composite parent)
    {
        parent.setLayout(new FillLayout());
        // No need for a dispose listener - the SaveablePart will save it

        textViewer = new TextViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.LEFT | SWT.H_SCROLL);
        textViewer.setDocument(new Document());
        textViewer.getControl().setEnabled(false);
        textViewer.getTextWidget().setWordWrap(false);
        font = JFaceResources.getFont("org.eclipse.mat.ui.notesfont"); //$NON-NLS-1$
        textViewer.getControl().setFont(font);

        hyperlinkColor = JFaceResources.getColorRegistry().get(JFacePreferences.HYPERLINK_COLOR);

        getSite().getPage().addPartListener(this);

        int undolevel = Platform.getPreferencesService().getInt("org.eclipse.ui.editors", "undoHistorySize", UNDO_LEVEL, null); //$NON-NLS-1$ //$NON-NLS-2$
        undoManager = new TextViewerUndoManager(undolevel);
        undoManager.connect(textViewer);
        textViewer.setUndoManager(undoManager);

        textViewer.addSelectionChangedListener(new ISelectionChangedListener()
        {
            public void selectionChanged(SelectionChangedEvent event)
            {
                updateActions();
            }
        });

        textViewer.addTextListener(new ITextListener()
        {
            public void textChanged(TextEvent event)
            {
                modified = true;
                searchForHyperlinks(textViewer.getDocument().get(), 0);
                firePropertyChange(PROP_DIRTY); 
            }
        });

        textViewer.setHyperlinkPresenter(new DefaultHyperlinkPresenter(hyperlinkColor));
        textViewer.setHyperlinkDetectors(new IHyperlinkDetector[] { new ObjectAddressHyperlinkDetector() }, SWT.MOD1);

        makeActions();
        hookContextMenu();
        showBootstrapPart();
        updateActions();
    }

    private void updateActions()
    {
        for (NotesViewAction a : actions.values())
            a.setEnabled(textViewer.canDoOperation(a.actionId));
    }

    private void hookContextMenu()
    {
        MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener()
        {
            public void menuAboutToShow(IMenuManager manager)
            {
                textEditorContextMenuAboutToShow(manager);
            }
        });
        Menu menu = menuMgr.createContextMenu(textViewer.getControl());
        textViewer.getControl().setMenu(menu);
    }

    private void textEditorContextMenuAboutToShow(IMenuManager manager)
    {
        if (textViewer != null)
        {
            undo.setEnabled(undoManager.undoable());
            redo.setEnabled(undoManager.redoable());
            manager.add(undo);
            manager.add(redo);
            manager.add(new Separator());

            manager.add(getAction(ActionFactory.CUT.getId()));
            manager.add(getAction(ActionFactory.COPY.getId()));
            manager.add(getAction(ActionFactory.PASTE.getId()));
            manager.add(new Separator());
            manager.add(getAction(ActionFactory.DELETE.getId()));
            manager.add(getAction(ActionFactory.SELECT_ALL.getId()));
        }
    }

    private NotesViewAction getAction(String actionID)
    {
        return actions.get(actionID);
    }

    private Action addAction(ActionFactory actionFactory, int textOperation, String actionDefinitionId)
    {
        IWorkbenchWindow window = getViewSite().getWorkbenchWindow();
        IWorkbenchAction globalAction = actionFactory.create(window);

        // Create our text action.
        NotesViewAction action = new NotesViewAction(textOperation, actionDefinitionId);
        actions.put(actionFactory.getId(), action);
        // Copy its properties from the global action.
        action.setText(globalAction.getText());
        action.setToolTipText(globalAction.getToolTipText());
        action.setDescription(globalAction.getDescription());
        action.setImageDescriptor(globalAction.getImageDescriptor());

        action.setDisabledImageDescriptor(globalAction.getDisabledImageDescriptor());
        action.setAccelerator(globalAction.getAccelerator());

        // Register our text action with the global action handler.
        IActionBars actionBars = getViewSite().getActionBars();
        actionBars.setGlobalActionHandler(actionFactory.getId(), action);
        return action;
    }

    @Override
    public void setFocus()
    {
        textViewer.getControl().setFocus();
    }

    @Override
    public void partActivated(IWorkbenchPart part)
    {
        if (!supportsNotes(part))
            return;

        textViewer.getControl().setEnabled(true);
        editor = (MultiPaneEditor) part;
        File path = editor.getResourceFile();

        if (path != null && !path.equals(resource))
        {
            if (isDirty())
                saveNotes();
            resource = path;
            IQueryContext icq = editor.getQueryContext();
            if (icq != null)
            {
                prefix = new File(icq.getPrefix());
            }
            else
            {
                prefix = null;
                if (editor instanceof HeapEditor)
                {
                    HeapEditor heapEditor = (HeapEditor) editor;
                    ISnapshotEditorInput input = heapEditor.getSnapshotInput();
                    if (input != null && input.hasSnapshot())
                    {
                        prefix = new File(input.getSnapshot().getSnapshotInfo().getPrefix());
                    }
                    else
                    {
                        IChangeListener l = new IChangeListener()
                        {
                            @Override
                            public void onSnapshotLoaded(ISnapshot snapshot)
                            {
                                if (!textViewer.getControl().isDisposed() && prefix == null && resource != null
                                                && resource.getPath().equals(snapshot.getSnapshotInfo().getPath()))
                                {
                                    File prefix1 = new File(snapshot.getSnapshotInfo().getPrefix());
                                    File prefixNotesFile = getDefaultNotesFile(prefix1);
                                    if (!getDefaultNotesFile(resource).equals(prefixNotesFile))
                                    {
                                        /*
                                         * Change in notes file, and there is an
                                         * existing notes file. Save the old
                                         * file, use the new file. Otherwise,
                                         * keep the data read, and use it for
                                         * the new file.
                                         */
                                        if (prefixNotesFile.canRead() && prefixNotesFile.isFile()
                                                        && prefixNotesFile.length() > 0)
                                        {
                                            // Save the old file
                                            if (isDirty())
                                                saveNotes();
                                            // Use the new file
                                            prefix = prefix1;
                                            updateTextViewer();
                                        }
                                        else
                                        {
                                            // The file to save to is empty /
                                            // doesn't exist.
                                            String text = textViewer.getDocument().get();
                                            prefix = prefix1;
                                            if (text != null && !text.isEmpty())
                                            {
                                                /*
                                                 * The old file had some data,
                                                 * so make it to be saved in the
                                                 * new file.
                                                 */
                                                modified = true;
                                                hash = 0;
                                                firePropertyChange(PROP_DIRTY);
                                            }
                                        }
                                    }
                                }
                                input.removeChangeListener(this);
                            }

                            @Override
                            public void onBaselineLoaded(ISnapshot snapshot)
                            {}
                        };
                        input.addChangeListener(l);
Possible null pointer dereference of input in org.eclipse.mat.ui.internal.views.NotesView.partActivated(IWorkbenchPart)

There is a branch of statement that, if executed, guarantees that a null value will be dereferenced, which would generate a NullPointerException when the code is executed. Of course, the problem might be that the branch or statement is infeasible and that the null pointer exception can't ever be executed; deciding that is beyond the ability of SpotBugs.

} } } updateTextViewer(); } } @Override public void partBroughtToTop(IWorkbenchPart part) { partActivated(part); } @Override public void partClosed(IWorkbenchPart part) { if (!supportsNotes(part)) { return; } MultiPaneEditor editor = (MultiPaneEditor) part; File resource = editor.getResourceFile(); if (resource.equals(this.resource)) { // Saving usually done as SaveablePart except when snapshot is closed if (isDirty()) saveNotes(); this.prefix = null; this.resource = null; this.editor = null; this.updateTextViewer(); } } @Override public void partDeactivated(IWorkbenchPart part) {} @Override public void partOpened(IWorkbenchPart part) {} private void showBootstrapPart() { IWorkbenchPage page = getSite().getPage(); if (page != null) partActivated(page.getActiveEditor()); } private void makeActions() { // Install the standard text actions. addAction(ActionFactory.CUT, ITextOperationTarget.CUT, "org.eclipse.ui.edit.cut");//$NON-NLS-1$ addAction(ActionFactory.COPY, ITextOperationTarget.COPY, "org.eclipse.ui.edit.copy");//$NON-NLS-1$ addAction(ActionFactory.PASTE, ITextOperationTarget.PASTE, "org.eclipse.ui.edit.paste");//$NON-NLS-1$ addAction(ActionFactory.DELETE, ITextOperationTarget.DELETE, "org.eclipse.ui.edit.delete");//$NON-NLS-1$ addAction(ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL, "org.eclipse.ui.edit.selectAll");//$NON-NLS-1$ undo = addAction(ActionFactory.UNDO, ITextOperationTarget.UNDO, "org.eclipse.ui.edit.undo");//$NON-NLS-1$ redo = addAction(ActionFactory.REDO, ITextOperationTarget.REDO, "org.eclipse.ui.edit.redo");//$NON-NLS-1$ } private long hash() { // Used to detect if document has changed. CRC32 crc = new CRC32(); try { crc.update(textViewer.getDocument().get().getBytes(NOTES_ENCODING)); } catch (UnsupportedEncodingException e) { // Won't happen return textViewer.getDocument().get().hashCode(); } return crc.getValue(); } private void updateTextViewer() { // get notes.txt and if it's not null set is as input if (resource != null) { String buffer = readNotes(prefix != null ? prefix : resource); if (buffer != null) { Document document = new Document(buffer); textViewer.setDocument(document); revealEndOfDocument(); } else { textViewer.setDocument(new Document(""));//$NON-NLS-1$ } } else { textViewer.setDocument(new Document(""));//$NON-NLS-1$ textViewer.getControl().setEnabled(false); } hash = hash(); modified = false; firePropertyChange(PROP_DIRTY); } private void searchForHyperlinks(String allText, int offset) { if (resource == null) return; Pattern addressPattern = Pattern.compile("0x\\p{XDigit}+");//$NON-NLS-1$ String[] fields = allText.split("\\W", 0);//$NON-NLS-1$ List<IdHyperlink> hyperlinks = new ArrayList<IdHyperlink>(); for (String field : fields) { if (addressPattern.matcher(field).matches()) { IRegion idRegion = new Region(offset, field.length()); IdHyperlink hyperlink = new IdHyperlink(field, editor, idRegion); hyperlinks.add(hyperlink); } offset = offset + field.length() + 1; // length of the splitter } if (!hyperlinks.isEmpty()) highlightHyperlinks(hyperlinks); } private void highlightHyperlinks(List<IdHyperlink> hyperlinks) { TextPresentation style = new TextPresentation(); for (IHyperlink hyperlink : hyperlinks) { int startIndex = hyperlink.getHyperlinkRegion().getOffset(); int length = hyperlink.getHyperlinkRegion().getLength(); StyleRange styleRange = new StyleRange(startIndex, length, hyperlinkColor, null, SWT.ITALIC); styleRange.underline = true; style.addStyleRange(styleRange); } textViewer.changeTextPresentation(style, true); } private void saveNotes() { if (resource != null) { String text = textViewer.getDocument().get(); if (text != null) saveNotes(prefix != null ? prefix : resource, text); resetUndoManager(); hash = hash(); modified = false; } } @Override public void dispose() { undoManager.disconnect(); getSite().getPage().removePartListener(this); // The parent composite has been disposed, so there is no need to remove the disposeListener. if (menu != null) menu.dispose(); super.dispose(); } private boolean supportsNotes(IWorkbenchPart part) { if (part instanceof MultiPaneEditor) { return true; } return false; } protected void revealEndOfDocument() { IDocument doc = textViewer.getDocument(); int docLength = doc.getLength(); if (docLength > 0) { textViewer.revealRange(docLength - 1, 1); StyledText widget = textViewer.getTextWidget(); widget.setCaretOffset(docLength); } } public void resetUndoManager() { undoManager.reset(); } private final class IdHyperlink implements IHyperlink { String id; MultiPaneEditor editor; IRegion region; public IdHyperlink(String id, MultiPaneEditor editor, IRegion region) { this.id = id; this.editor = editor; this.region = region; } @Override public IRegion getHyperlinkRegion() { return region; } @Override public String getHyperlinkText() { return null; } @Override public String getTypeLabel() { return null; } @Override public void open() { try { final int objectId = editor.getQueryContext().mapToObjectId(id); if (objectId < 0) return; QueryContextMenu contextMenu = new QueryContextMenu(editor, new ContextProvider((String) null) { @Override public IContextObject getContext(final Object row) { return new IContextObject() { public int getObjectId() { return (Integer) row; } }; } }); PopupMenu popupMenu = new PopupMenu(); contextMenu.addContextActions(popupMenu, new StructuredSelection(objectId), null); if (menu != null && !menu.isDisposed()) menu.dispose(); menu = popupMenu.createMenu(getViewSite().getActionBars().getStatusLineManager(), PlatformUI .getWorkbench().getDisplay().getActiveShell()); menu.setVisible(true); } catch (NumberFormatException ignore) { // $JL-EXC$ // shouldn't happen and if it does nothing can be done } catch (SnapshotException ignore) { // $JL-EXC$ // mapToAddress throws exception on illegal values } } } private class ObjectAddressHyperlinkDetector extends AbstractHyperlinkDetector { public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) { if (region == null || textViewer == null) return null; IDocument document = textViewer.getDocument(); int offset = region.getOffset(); if (document == null) return null; IRegion lineInfo; String text; try { lineInfo = document.getLineInformationOfOffset(offset); text = document.get(lineInfo.getOffset(), lineInfo.getLength()); } catch (BadLocationException ex) { return null; } int index = offset - lineInfo.getOffset(); // to the left from offset char ch; do { index--; ch = ' '; if (index > -1) ch = text.charAt(index); } while (Character.isLetterOrDigit(ch)); int startIndex = index + 1; // to the right from offset index = offset - lineInfo.getOffset() - 1; do { index++; if (index >= text.length()) { break; } ch = text.charAt(index); } while (Character.isLetterOrDigit(ch)); Pattern addressPattern = Pattern.compile("0x\\p{XDigit}+");//$NON-NLS-1$ String address = text.substring(startIndex, index); if (address != null && addressPattern.matcher(address).matches()) { IRegion idRegion = new Region(startIndex, address.length()); return new IHyperlink[] { new IdHyperlink(address, editor, idRegion) }; } else return null; } } private class NotesViewAction extends Action { private int actionId; NotesViewAction(int actionId, String actionDefinitionId) { this.actionId = actionId; this.setActionDefinitionId(actionDefinitionId); } @Override public boolean isEnabled() { return textViewer.canDoOperation(actionId); } @Override public void run() { textViewer.doOperation(actionId); } } // ////////////////////////////////////////////////////////////// // notes management // ////////////////////////////////////////////////////////////// /** * Read the contents of the notes file, based on the * snapshot resource. * @param resourcePath The editor file (snapshot or index file). * @return The contents of the notes file, lines separated by \n. * @throws UncheckedIOException for some IO errors. */ public static String readNotes(File resourcePath) { if (resourcePath != null) { File notesFile = getDefaultNotesFile(resourcePath); if (notesFile.canRead() && notesFile.isFile()) { try (FileInputStream fileInput = new FileInputStream(notesFile)) { BufferedReader myInput = new BufferedReader(new InputStreamReader(fileInput, NOTES_ENCODING)); try { String s; StringBuffer b = new StringBuffer(); while ((s = myInput.readLine()) != null) { b.append(s); b.append("\n");//$NON-NLS-1$ } return b.toString(); } finally { try { myInput.close(); } catch (IOException ignore) {} } } catch (FileNotFoundException e) { throw new UncheckedIOException(notesFile.getPath(), e); } catch (IOException e) { throw new UncheckedIOException(notesFile.getPath(), e); } } } return null; } private static void saveNotes(File resource, String notes) { OutputStream fout = null; try { File notesFile = getDefaultNotesFile(resource); fout = new FileOutputStream(notesFile); OutputStreamWriter out = new OutputStreamWriter(new BufferedOutputStream(fout), NOTES_ENCODING); try { out.write(notes); out.flush(); } finally { out.close(); } } catch (FileNotFoundException e) { throw new UncheckedIOException(e); } catch (IOException e) { throw new UncheckedIOException(e); } finally { try { if (fout != null) fout.close(); } catch (IOException ignore) {} } } private static File getDefaultNotesFile(File resource) { String filename = resource.getName(); int p = filename.lastIndexOf('.'); if (p >= 0) filename = filename.substring(0, p); return new File(resource.getParentFile(), filename + ".notes.txt");//$NON-NLS-1$ } @Override public void doSave(IProgressMonitor monitor) { saveNotes(); firePropertyChange(PROP_DIRTY); } @Override public void doSaveAs() { } @Override public boolean isDirty() { return undoManager.undoable() || modified && hash != hash(); } @Override public boolean isSaveAsAllowed() { return false; } @Override public boolean isSaveOnCloseNeeded() { return true; } @Override public int promptToSaveOnClose() { return ISaveablePart2.YES; } }