/** * <copyright> * </copyright> * * */ package org.emftext.language.java.resource.java.ui; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel; import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IMemento; import org.eclipse.ui.IPartListener2; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartReference; import org.eclipse.ui.XMLMemento; import org.osgi.framework.Bundle; /** * This manager adds new projection annotations for the code folding and deletes * old projection annotations with lines < 3. It is needed to hold the toggle * states. It provides the ability to restore the toggle states between Eclipse * sessions and after closing, opening as well. */ public class JavaCodeFoldingManager { private class EditorOnCloseListener implements IPartListener2 { private org.emftext.language.java.resource.java.ui.JavaEditor editor; public EditorOnCloseListener(org.emftext.language.java.resource.java.ui.JavaEditor editor) { this.editor = editor; } public void partActivated(IWorkbenchPartReference partRef) { } public void partBroughtToTop(IWorkbenchPartReference partRef) { } public void partClosed(IWorkbenchPartReference partRef) { if (partRef.isDirty()) { return; } IWorkbenchPart workbenchPart = partRef.getPart(false); if (workbenchPart instanceof org.emftext.language.java.resource.java.ui.JavaEditor) { org.emftext.language.java.resource.java.ui.JavaEditor editor = (org.emftext.language.java.resource.java.ui.JavaEditor) workbenchPart; Resource editorResource = editor.getResource(); if (editorResource == null) { return; } String uri = editorResource.getURI().toString(); Resource thisEditorResource = this.editor.getResource(); URI thisEditorResourceURI = thisEditorResource.getURI(); if (uri.equals(thisEditorResourceURI.toString())) { saveCodeFoldingStateFile(uri); editor.getSite().getPage().removePartListener(this); } } } public void partDeactivated(IWorkbenchPartReference partRef) { } public void partHidden(IWorkbenchPartReference partRef) { } public void partInputChanged(IWorkbenchPartReference partRef) { } public void partOpened(IWorkbenchPartReference partRef) { } public void partVisible(IWorkbenchPartReference partRef) { } } private static final String VERIFY_KEY = "verify_key"; private static final String ANNOTATION = "ANNOTATION"; private static final String IS_COLLAPSED = "IS_COLLAPSED"; private static final String OFFSET = "OFFSET"; private static final String LENGTH = "LENGTH"; private static final String MODEL = "MODEL"; protected List<ProjectionAnnotation> oldAnnotations = new ArrayList<ProjectionAnnotation>(); protected Map<ProjectionAnnotation, Position> additions = new LinkedHashMap<ProjectionAnnotation, Position>(); protected ProjectionAnnotationModel projectionAnnotationModel; protected ProjectionViewer sourceViewer; protected org.emftext.language.java.resource.java.ui.JavaEditor editor; /** * <p> * Creates a code folding manager to handle the <code>ProjectionAnnotation</code>. * </p> * * @param sourceViewer the source viewer to calculate the element lines */ public JavaCodeFoldingManager(ProjectionViewer sourceViewer,org.emftext.language.java.resource.java.ui.JavaEditor textEditor) { this.projectionAnnotationModel = sourceViewer.getProjectionAnnotationModel(); this.sourceViewer = sourceViewer; this.editor = textEditor; addCloseListener(textEditor); try { restoreCodeFoldingStateFromFile(editor.getResource().getURI().toString()); } catch (Exception e) { calculatePositions(); } } private void addCloseListener(final org.emftext.language.java.resource.java.ui.JavaEditor editor) { editor.getSite().getPage().addPartListener(new EditorOnCloseListener(editor)); } /** * <p> * Checks whether the given positions are in the * <code>ProjectionAnnotationModel</code> or in the addition set. If not it tries * to add into <code>additions</code>. Deletes old ProjectionAnnotation with line * count less than 2. * </p> * * @param positions a list of available foldable positions */ public void updateCodefolding(List<Position> positions) { IDocument document = sourceViewer.getDocument(); if (document == null) { return; } oldAnnotations.clear(); Iterator<?> annotationIterator = projectionAnnotationModel.getAnnotationIterator(); while (annotationIterator.hasNext()) { oldAnnotations.add((ProjectionAnnotation) annotationIterator.next()); } // Add new Position with a unique line offset for (Position position : positions) { if (!isInAdditions(position)) { addPosition(position); } } projectionAnnotationModel.modifyAnnotations(oldAnnotations.toArray(new Annotation[0]), additions, null); additions.clear(); } /** * <p> * Checks the offset of the given <code>Position</code> against the * <code>Position</code>s in <code>additions</code> to determine the existence * whether the given position is contained in the additions set. * </p> * * @param position the position to check * * @return <code>true</code> if it is in the <code>additions</code> */ private boolean isInAdditions(Position position) { for (Annotation addition : additions.keySet()) { Position additionPosition = additions.get(addition); if (position.offset == additionPosition.offset && position.length == additionPosition.length) { return true; } } return false; } /** * <p> * Tries to add this position into the model. Only positions with more than 3 * lines can be taken in. If multiple positions exist on the same line, the * longest will be chosen. The shorter ones will be deleted. * </p> * * @param position the position to be added. */ private void addPosition(Position position) { IDocument document = sourceViewer.getDocument(); int lines = 0; try { lines = document.getNumberOfLines(position.offset, position.length); } catch (BadLocationException e) { e.printStackTrace(); return; } if (lines < 3) { return; } // if a position to add existed on the same line, the longest one will be chosen try { for (ProjectionAnnotation annotationToAdd : additions.keySet()) { Position positionToAdd = additions.get(annotationToAdd); if (document.getLineOfOffset(position.offset) == document.getLineOfOffset(positionToAdd.offset)) { if (positionToAdd.length < position.length) { additions.remove(annotationToAdd); } else { return; } } } } catch (BadLocationException e) { return; } for (ProjectionAnnotation annotationInModel : oldAnnotations) { Position positionInModel = projectionAnnotationModel.getPosition(annotationInModel); if (position.offset == positionInModel.offset && position.length == positionInModel.length) { oldAnnotations.remove(annotationInModel); return; } } additions.put(new ProjectionAnnotation(), position); } /** * Saves the code folding state into the given memento. */ public void saveCodeFolding(IMemento memento) { // The annotation model might be null if the editor opened an storage input // instead of a file input. if (projectionAnnotationModel == null) { return; } Iterator<?> annotationIt = projectionAnnotationModel.getAnnotationIterator(); while (annotationIt.hasNext()) { ProjectionAnnotation annotation = (ProjectionAnnotation) annotationIt.next(); IMemento annotationMemento = memento.createChild(ANNOTATION); Position position = projectionAnnotationModel.getPosition(annotation); annotationMemento.putBoolean(IS_COLLAPSED, annotation.isCollapsed()); annotationMemento.putInteger(OFFSET, position.offset); annotationMemento.putInteger(LENGTH, position.length); } } /** * Restores the code folding state information from the given memento. */ public void restoreCodeFolding(IMemento memento) { if (memento == null) { return; } IMemento[] annotationMementos = memento.getChildren(ANNOTATION); if (annotationMementos == null) { return; } Map<ProjectionAnnotation, Boolean> collapsedStates = new LinkedHashMap<ProjectionAnnotation, Boolean>(); for (IMemento annotationMemento : annotationMementos) { ProjectionAnnotation annotation = new ProjectionAnnotation(); collapsedStates.put(annotation, annotationMemento.getBoolean(IS_COLLAPSED)); int offset = annotationMemento.getInteger(OFFSET); int length = annotationMemento.getInteger(LENGTH); Position position = new Position(offset, length); projectionAnnotationModel.addAnnotation(annotation, position); } // postset collapse state to prevent wrong displaying folding code. for (ProjectionAnnotation annotation : collapsedStates.keySet()) { Boolean isCollapsed = collapsedStates.get(annotation); if (isCollapsed != null && isCollapsed.booleanValue()) { projectionAnnotationModel.collapse(annotation); } } } /** * <p> * Restores the code folding state from a XML file in the state location. * </p> * * @param uriString the key to determine the file to load the state from */ public void restoreCodeFoldingStateFromFile(String uriString) { final File stateFile = getCodeFoldingStateFile(uriString); if (stateFile == null || !stateFile.exists()) { calculatePositions(); return; } SafeRunner.run(new SafeRunnable("Unable to read code folding state. The state will be reset.") { public void run() throws Exception { FileInputStream input = new FileInputStream(stateFile); BufferedReader reader = new BufferedReader(new InputStreamReader(input, "utf-8")); IMemento memento = XMLMemento.createReadRoot(reader); reader.close(); String sourceText = sourceViewer.getDocument().get(); if (memento.getString(VERIFY_KEY).equals(makeMD5(sourceText))) { restoreCodeFolding(memento); } else { calculatePositions(); } } }); } /** * <p> * Saves the code folding state to a XML file in the state location. * </p> * * @param uriString the key to determine the file to save to */ public void saveCodeFoldingStateFile(String uriString) { IDocument document = sourceViewer.getDocument(); if (document == null) { return; } XMLMemento codeFoldingMemento = XMLMemento.createWriteRoot(MODEL); codeFoldingMemento.putString(VERIFY_KEY, makeMD5(document.get())); saveCodeFolding(codeFoldingMemento); File stateFile = getCodeFoldingStateFile(uriString); if (stateFile == null) { return; } try { FileOutputStream stream = new FileOutputStream(stateFile); OutputStreamWriter writer = new OutputStreamWriter(stream, "utf-8"); codeFoldingMemento.save(writer); writer.close(); } catch (IOException e) { stateFile.delete(); MessageDialog.openError((Shell) null, "Saving Problems", "Unable to save code folding state."); } } private File getCodeFoldingStateFile(String uriString) { Bundle bundle = Platform.getBundle(org.emftext.language.java.resource.java.ui.JavaUIPlugin.PLUGIN_ID); IPath path = Platform.getStateLocation(bundle); if (path == null) { return null; } path = path.append(makeMD5(uriString) + ".xml"); return path.toFile(); } private String makeMD5(String text) { MessageDigest md = null; byte[] encryptMsg = null; try { md = MessageDigest.getInstance("MD5"); encryptMsg = md.digest(text.getBytes()); } catch (NoSuchAlgorithmException e) { org.emftext.language.java.resource.java.ui.JavaUIPlugin.logError("NoSuchAlgorithmException while creating MD5 checksum.", e); return ""; } String swap = ""; String byteStr = ""; StringBuffer strBuf = new StringBuffer(); for (int i = 0; i <= encryptMsg.length - 1; i++) { byteStr = Integer.toHexString(encryptMsg[i]); switch (byteStr.length()) { case 1: // if hex-number length is 1, add a '0' before swap = "0" + Integer.toHexString(encryptMsg[i]); break; case 2: // correct hex-letter swap = Integer.toHexString(encryptMsg[i]); break; case 8: // get the correct substring swap = (Integer.toHexString(encryptMsg[i])).substring(6, 8); break; } strBuf.append(swap); // appending swap to get complete hash-key } return strBuf.toString(); } protected void calculatePositions() { org.emftext.language.java.resource.java.mopp.JavaResource textResource = (org.emftext.language.java.resource.java.mopp.JavaResource) editor.getResource(); IDocument document = sourceViewer.getDocument(); if (textResource == null || document == null) { return; } if (textResource.hasErrors()) { return; } final List<Position> positions = new ArrayList<Position>(); org.emftext.language.java.resource.java.IJavaLocationMap locationMap = textResource.getLocationMap(); EClass[] foldableClasses = textResource.getMetaInformation().getFoldableClasses(); if (foldableClasses == null) { return; } if (foldableClasses.length < 1) { return; } List<EObject> contents = textResource.getContents(); EObject[] contentArray = contents.toArray(new EObject[0]); List<EObject> allContents = getAllContents(contentArray); for (EObject nextObject : allContents) { boolean isFoldable = false; for (EClass eClass : foldableClasses) { if (nextObject.eClass().equals(eClass)) { isFoldable = true; break; } } if (!isFoldable) { continue; } int offset = locationMap.getCharStart(nextObject); int length = locationMap.getCharEnd(nextObject) + 1 - offset; try { int lines = document.getNumberOfLines(offset, length); if (lines < 2) { continue; } } catch (BadLocationException e) { continue; } length = getOffsetOfNextLine(document, length + offset) - offset; if (offset >= 0 && length > 0) { positions.add(new Position(offset, length)); } } Display.getDefault().asyncExec(new Runnable() { public void run() { updateCodefolding(positions); } }); } private List<EObject> getAllContents(EObject[] contentArray) { List<EObject> result = new ArrayList<EObject>(); for (EObject eObject : contentArray) { if (eObject == null) { continue; } result.add(eObject); List<EObject> contents = eObject.eContents(); if (contents == null) { continue; } result.addAll(getAllContents(contents.toArray(new EObject[0]))); } return result; } private int getOffsetOfNextLine(IDocument document, int offset) { int end = document.getLength(); int nextLineOffset = offset; if (offset < 0 || offset > end) { return -1; } while (nextLineOffset < end) { String charAtOffset = ""; try { charAtOffset += document.getChar(nextLineOffset); } catch (BadLocationException e) { return -1; } if (charAtOffset.matches("\\S")) { return nextLineOffset; } if (charAtOffset.equals("\n")) { return nextLineOffset + 1; } nextLineOffset++; } return offset; } }