| /******************************************************************************* |
| * Copyright (c) 2004, 2011 IBM Corporation and others. |
| * 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jst.jsp.core.internal.java; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.ToolFactory; |
| import org.eclipse.jdt.core.formatter.CodeFormatter; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jst.jsp.core.internal.Logger; |
| import org.eclipse.jst.jsp.core.internal.regions.DOMJSPRegionContexts; |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.InsertEdit; |
| import org.eclipse.text.edits.MalformedTreeException; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| import org.eclipse.text.edits.UndoEdit; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionCollection; |
| import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; |
| |
| import com.ibm.icu.util.StringTokenizer; |
| |
| |
| /** |
| * Adds the notion of IDocuments (jsp Document and java Document) Used for |
| * TextEdit translation |
| * |
| * @author pavery |
| */ |
| public class JSPTranslationExtension extends JSPTranslation { |
| |
| // for debugging |
| private static final boolean DEBUG; |
| static { |
| String value = Platform.getDebugOption("org.eclipse.jst.jsp.core/debug/jsptranslation"); //$NON-NLS-1$ |
| DEBUG = value != null && value.equalsIgnoreCase("true"); //$NON-NLS-1$ |
| } |
| |
| // just a convenience data structure |
| // to keep track of java position deltas |
| private class PositionDelta { |
| |
| public boolean isDeleted = false; |
| public int preOffset = 0; |
| public int preLength = 0; |
| public int postOffset = 0; |
| public int postLength = 0; |
| |
| public PositionDelta(int preOffset, int preLength) { |
| this.preOffset = preOffset; |
| this.preLength = preLength; |
| } |
| |
| public void setPostEditData(int postOffset, int postLength, boolean isDeleted) { |
| this.postOffset = postOffset; |
| this.postLength = postLength; |
| this.isDeleted = isDeleted; |
| } |
| } |
| |
| private IDocument fJspDocument = null; |
| private IDocument fJavaDocument = null; |
| private CodeFormatter fCodeFormatter = null; |
| |
| public JSPTranslationExtension(IDocument jspDocument, IDocument javaDocument, IJavaProject javaProj, JSPTranslator translator) { |
| super(javaProj, translator); |
| fJspDocument = jspDocument; |
| fJavaDocument = javaDocument; |
| |
| // make sure positions are added to Java and JSP documents |
| // this is necessary for text edits |
| addPositionsToDocuments(); |
| } |
| |
| public void retranslate(IDocument javaDocument, JSPTranslator translator) { |
| fJavaDocument = javaDocument; |
| retranslate(translator); |
| |
| addPositionsToDocuments(); |
| } |
| |
| public IDocument getJspDocument() { |
| return fJspDocument; |
| } |
| |
| public IDocument getJavaDocument() { |
| return fJavaDocument; |
| } |
| |
| public String getJavaText() { |
| return getJavaDocument() != null ? getJavaDocument().get() : ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns a corresponding TextEdit for the JSP file given a TextEdit for |
| * a Java file. |
| * |
| * @param javaEdit |
| * @return the corresponding JSP edits (not applied to the document yet) |
| */ |
| public TextEdit getJspEdit(TextEdit javaEdit) { |
| |
| if (javaEdit == null) |
| return null; |
| |
| List jspEdits = new ArrayList(); |
| |
| int offset = javaEdit.getOffset(); |
| int length = javaEdit.getLength(); |
| |
| if (javaEdit instanceof MultiTextEdit && javaEdit.getChildren().length > 0) { |
| |
| IRegion r = TextEdit.getCoverage(getAllEdits(javaEdit)); |
| offset = r.getOffset(); |
| length = r.getLength(); |
| } |
| |
| // get java ranges that will be affected by the edit |
| Position[] javaPositions = getJavaRanges(offset, length); |
| |
| // record position data before the change |
| Position[] jspPositions = new Position[javaPositions.length]; |
| PositionDelta[] deltas = new PositionDelta[javaPositions.length]; |
| for (int i = 0; i < javaPositions.length; i++) { |
| deltas[i] = new PositionDelta(javaPositions[i].offset, javaPositions[i].length); |
| // isIndirect means the position doesn't actually exist as exact |
| // text |
| // mapping from java <-> jsp (eg. an import statement) |
| if (!isIndirect(javaPositions[i].offset)) |
| jspPositions[i] = (Position) getJava2JspMap().get(javaPositions[i]); |
| } |
| |
| if (DEBUG) { |
| System.out.println("================================================"); //$NON-NLS-1$ |
| System.out.println("deltas:"); //$NON-NLS-1$ |
| String javaText = getJavaText(); |
| for (int i = 0; i < deltas.length; i++) |
| System.out.println("pos[" + deltas[i].preOffset + ":" + deltas[i].preLength + "]" + javaText.substring(deltas[i].preOffset, deltas[i].preOffset + deltas[i].preLength)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| System.out.println("==============================================="); //$NON-NLS-1$ |
| } |
| UndoEdit undo = null; |
| // apply the edit to the java document |
| try { |
| undo = javaEdit.apply(getJavaDocument()); |
| } |
| catch (MalformedTreeException e) { |
| Logger.logException(e); |
| } |
| catch (BadLocationException e) { |
| Logger.logException(e); |
| } |
| // now at this point Java positions are unreliable since they were |
| // updated after applying java edit. |
| |
| String newJavaText = getJavaDocument().get(); |
| if (DEBUG) |
| System.out.println("java post format text:\n" + newJavaText); //$NON-NLS-1$ |
| |
| // record post edit data |
| for (int i = 0; i < javaPositions.length; i++) |
| deltas[i].setPostEditData(javaPositions[i].offset, javaPositions[i].length, javaPositions[i].isDeleted); |
| |
| // create appropriate text edits for deltas |
| Position jspPos = null; |
| String replaceText = ""; //$NON-NLS-1$ |
| for (int i = 0; i < deltas.length; i++) { |
| jspPos = jspPositions[i]; |
| // can be null if it's an indirect mapping position |
| // or if something was added into java that was not originally in |
| // JSP (like a new import...) |
| |
| if (jspPos != null) { |
| if (deltas[i].isDeleted) { |
| jspEdits.add(new DeleteEdit(jspPos.offset, jspPos.length)); |
| } |
| else { |
| replaceText = newJavaText.substring(deltas[i].postOffset, deltas[i].postOffset + deltas[i].postLength); |
| |
| // get rid of pre and post white space or fine tuned |
| // adjustment later. |
| // fix text here... |
| replaceText = fixJspReplaceText(replaceText, jspPos); |
| if (!(replaceText.length() == 0 && jspPos.length == 0))//Unwanted TextEdit can lead to MalformedTreeException.See: Bug 321977 |
| jspEdits.add(new ReplaceEdit(jspPos.offset, jspPos.length, replaceText)); |
| } |
| if (DEBUG) |
| debugReplace(deltas, jspPos, replaceText, i); |
| } |
| else { |
| // the new Java text has no corresponding JSP position |
| // possible new import? |
| if (isImport(javaPositions[i].getOffset()) && replaceText.lastIndexOf("import ") != -1) { //$NON-NLS-1$ |
| replaceText = newJavaText.substring(deltas[i].postOffset, deltas[i].postOffset + deltas[i].postLength); |
| String importText = replaceText.substring(replaceText.lastIndexOf("import "), replaceText.indexOf(";")); //$NON-NLS-1$ //$NON-NLS-2$ |
| // evenutally need to check if it's XML-JSP |
| importText = "<%@page import=\"" + importText + "\" %>\n"; //$NON-NLS-1$ //$NON-NLS-2$ |
| jspEdits.add(new InsertEdit(0, importText)); |
| } |
| } |
| } |
| TextEdit allJspEdits = createMultiTextEdit((TextEdit[]) jspEdits.toArray(new TextEdit[jspEdits.size()])); |
| |
| // https://bugs.eclipse.org/bugs/show_bug.cgi?id=105632 |
| // undo the java edit |
| // (so the underlying Java document still represents what's in the |
| // editor) |
| if (undo != null) { |
| try { |
| undo.apply(getJavaDocument()); |
| } |
| catch (MalformedTreeException e) { |
| Logger.logException(e); |
| } |
| catch (BadLocationException e) { |
| Logger.logException(e); |
| } |
| } |
| |
| return allJspEdits; |
| } |
| |
| private String fixJspReplaceText(String replaceText, Position jspPos) { |
| |
| // result is the text inbetween the delimiters |
| // eg. |
| // |
| // <% result |
| // %> |
| String result = replaceText.trim(); |
| String preDelimiterWhitespace = ""; //$NON-NLS-1$ |
| |
| IDocument jspDoc = getJspDocument(); |
| if (jspDoc instanceof IStructuredDocument) { |
| IStructuredDocument sDoc = (IStructuredDocument) jspDoc; |
| IStructuredDocumentRegion[] regions = sDoc.getStructuredDocumentRegions(0, jspPos.offset); |
| IStructuredDocumentRegion lastRegion = regions[regions.length - 1]; |
| |
| // only specifically modify scriptlets |
| if (lastRegion != null && lastRegion.getType() == DOMJSPRegionContexts.JSP_SCRIPTLET_OPEN) { |
| for (int i = regions.length - 1; i >= 0; i--) { |
| IStructuredDocumentRegion region = regions[i]; |
| |
| // is there a better way to check whitespace? |
| if (region.getType() == DOMRegionContext.XML_CONTENT && region.getFullText().trim().equals("")) { //$NON-NLS-1$ |
| |
| preDelimiterWhitespace = region.getFullText(); |
| preDelimiterWhitespace = preDelimiterWhitespace.replaceAll("\r", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| preDelimiterWhitespace = preDelimiterWhitespace.replaceAll("\n", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| // need to determine indent for that first line... |
| String initialIndent = getInitialIndent(result); |
| |
| // fix the first line of java code |
| result = TextUtilities.getDefaultLineDelimiter(sDoc) + initialIndent + result; |
| |
| result = adjustIndent(result, preDelimiterWhitespace, TextUtilities.getDefaultLineDelimiter(sDoc)); |
| |
| // add whitespace before last delimiter to match |
| // it w/ the opening delimiter |
| result = result + TextUtilities.getDefaultLineDelimiter(sDoc) + preDelimiterWhitespace; |
| break; |
| } |
| } |
| } |
| else if (lastRegion != null && checkForELRegion(lastRegion)) {//Check for EL region, we don't want to replace EL region with corresponding java text,leave it as it is. |
| result = getJspText().substring(jspPos.offset, jspPos.offset + jspPos.length); |
| } |
| } |
| return result; |
| } |
| |
| private boolean checkForELRegion(IStructuredDocumentRegion container) { |
| Iterator regions = container.getRegions().iterator(); |
| ITextRegion region = null; |
| while (regions.hasNext()) { |
| region = (ITextRegion) regions.next(); |
| if (region instanceof ITextRegionCollection) { |
| ITextRegionCollection parentRegion = ((ITextRegionCollection) region); |
| Iterator childRegions = parentRegion.getRegions().iterator(); |
| while (childRegions.hasNext()) { |
| ITextRegion childRegion = (ITextRegion) childRegions.next(); |
| if (childRegion.getType() == DOMJSPRegionContexts.JSP_EL_OPEN) |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private String adjustIndent(String textBefore, String indent, String delim) { |
| |
| // first replace multiple indent with single indent |
| // the triple indent occurs because the scriptlet code |
| // actually occurs under: |
| // |
| // class |
| // method |
| // code |
| // |
| // in the translated java document |
| // BUG188636 - just get indent info from code formatter |
| String level1 = getCodeFormatter().createIndentationString(1); |
| String level3 = getCodeFormatter().createIndentationString(3); |
| String theOld = "\n" + level3; //$NON-NLS-1$ |
| String theNew = "\n" + level1; //$NON-NLS-1$ |
| textBefore = textBefore.replaceAll(theOld, theNew); |
| |
| // get indent after 2nd line break |
| StringBuffer textAfter = new StringBuffer(); |
| // will this work on mac? |
| textBefore = textBefore.replaceAll("\r", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| StringTokenizer st = new StringTokenizer(textBefore, "\n", true); //$NON-NLS-1$ |
| while (st.hasMoreTokens()) { |
| String tok = st.nextToken(); |
| if (tok.equals("\n")) { //$NON-NLS-1$ |
| textAfter.append(delim); |
| } |
| else { |
| // prepend each line w/ specified indent |
| textAfter.append(indent); |
| textAfter.append(tok); |
| } |
| } |
| return textAfter.toString(); |
| |
| } |
| |
| private String getInitialIndent(String result) { |
| // BUG188636 - just get initial indent from code formatter |
| String indent = getCodeFormatter().createIndentationString(1); |
| // // get indent after 2nd line break |
| // String indent = ""; //$NON-NLS-1$ |
| // StringTokenizer st = new StringTokenizer(result, "\r\n", false); |
| // //$NON-NLS-1$ |
| // if (st.countTokens() > 1) { |
| // String tok = st.nextToken(); |
| // tok = st.nextToken(); |
| // int index = 0; |
| // if (tok != null) { |
| // while (tok.charAt(index) == ' ' || tok.charAt(index) == '\t') { |
| // indent += tok.charAt(index); |
| // index++; |
| // } |
| // } |
| // } |
| return indent; |
| } |
| |
| private CodeFormatter getCodeFormatter() { |
| if (fCodeFormatter == null) |
| fCodeFormatter = ToolFactory.createCodeFormatter(null); |
| return fCodeFormatter; |
| } |
| |
| |
| /** |
| * Combines an array of edits into one MultiTextEdit (with the appropriate |
| * coverage region) |
| * |
| * @param edits |
| * @return |
| */ |
| private TextEdit createMultiTextEdit(TextEdit[] edits) { |
| |
| if (edits.length == 0) |
| return new MultiTextEdit(); |
| |
| /* should not specify a limited region because other edits outside |
| * these original edits might be added later. |
| */ |
| MultiTextEdit multiEdit = new MultiTextEdit(); |
| for (int i = 0; i < edits.length; i++) { |
| addToMultiEdit(edits[i], multiEdit); |
| } |
| return multiEdit; |
| } |
| |
| |
| private void addToMultiEdit(TextEdit edit, MultiTextEdit multiEdit) { |
| |
| // check for overlap here |
| // discard overlapping edits.. |
| // possible exponential performance hit... need a better way... |
| TextEdit[] children = multiEdit.getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i].covers(edit)) |
| // don't add |
| return; |
| } |
| multiEdit.addChild(edit); |
| } |
| |
| |
| /** |
| * @param translation |
| */ |
| private void addPositionsToDocuments() { |
| |
| // can be null if it's a NullJSPTranslation |
| if (getJavaDocument() != null && getJspDocument() != null) { |
| |
| HashMap java2jsp = getJava2JspMap(); |
| Iterator it = java2jsp.keySet().iterator(); |
| Position javaPos = null; |
| while (it.hasNext()) { |
| javaPos = (Position) it.next(); |
| try { |
| |
| fJavaDocument.addPosition(javaPos); |
| |
| } |
| catch (BadLocationException e) { |
| if (DEBUG) { |
| System.out.println("tyring to add Java Position:[" + javaPos.offset + ":" + javaPos.length + "] to " + getJavaPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| // System.out.println("substring :[" + |
| // fJavaDocument.get().substring(javaPos.offset) + |
| // "]"); //$NON-NLS-1$ //$NON-NLS-2$ |
| Logger.logException(e); |
| } |
| } |
| |
| try { |
| |
| fJspDocument.addPosition((Position) java2jsp.get(javaPos)); |
| |
| } |
| catch (BadLocationException e) { |
| if (DEBUG) { |
| System.out.println("tyring to add JSP Position:[" + ((Position) java2jsp.get(javaPos)).offset + ":" + ((Position) java2jsp.get(javaPos)).length + "] to " + getJavaPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| Logger.logException(e); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Recursively gets all child edits |
| * |
| * @param javaEdit |
| * @return all child edits |
| */ |
| private TextEdit[] getAllEdits(TextEdit javaEdit) { |
| |
| List result = new ArrayList(); |
| if (javaEdit instanceof MultiTextEdit) { |
| TextEdit[] children = javaEdit.getChildren(); |
| for (int i = 0; i < children.length; i++) |
| result.addAll(Arrays.asList(getAllEdits(children[i]))); |
| } |
| else |
| result.add(javaEdit); |
| return (TextEdit[]) result.toArray(new TextEdit[result.size()]); |
| } |
| |
| /** |
| * @param deltas |
| * @param jspPos |
| * @param replaceText |
| * @param jspText |
| * @param i |
| */ |
| private void debugReplace(PositionDelta[] deltas, Position jspPos, String replaceText, int i) { |
| String jspChunk; |
| jspChunk = getJspDocument().get().substring(jspPos.offset, jspPos.offset + jspPos.length); |
| if (!deltas[i].isDeleted) { |
| System.out.println("replacing:"); //$NON-NLS-1$ |
| System.out.println("jsp:[" + jspChunk + "]"); //$NON-NLS-1$ //$NON-NLS-2$ |
| System.out.println("w/ :[" + replaceText + "]"); //$NON-NLS-1$ //$NON-NLS-2$ |
| System.out.println("--------------------------------"); //$NON-NLS-1$ |
| } |
| } |
| } |