blob: 7bb663337135b4ad26753d2821992841c2ef87db [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2006 BEA Systems 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:
* BEA Systems - initial implementation
*
* Bug 154474 EL: 'and', 'or', ... operator
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=154474
* Bernhard Huemer <bernhard.huemer@gmail.com>
*
*******************************************************************************/
package org.eclipse.jst.jsp.core.internal.java.jspel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.text.Position;
import org.eclipse.jst.jsp.core.internal.JSPCoreMessages;
import org.eclipse.jst.jsp.core.internal.contentmodel.TaglibController;
import org.eclipse.jst.jsp.core.internal.contentmodel.tld.CMDocumentImpl;
import org.eclipse.jst.jsp.core.internal.contentmodel.tld.TLDCMDocumentManager;
import org.eclipse.jst.jsp.core.internal.contentmodel.tld.TaglibTracker;
import org.eclipse.jst.jsp.core.internal.contentmodel.tld.provisional.TLDFunction;
import org.eclipse.jst.jsp.core.jspel.ELProblem;
import org.eclipse.osgi.util.NLS;
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.ITextRegionCollection;
public class ELGeneratorVisitor implements JSPELParserVisitor {
private static final String ENDL = "\n"; //$NON-NLS-1$
private static final String fExpressionHeader1 = "public String _elExpression"; //$NON-NLS-1$
private static final String fExpressionHeader2 = "()" + ENDL + //$NON-NLS-1$
"\t\tthrows java.io.IOException, javax.servlet.ServletException, javax.servlet.jsp.JspException {" + ENDL + //$NON-NLS-1$
"javax.servlet.jsp.PageContext pageContext = null;" + ENDL + //$NON-NLS-1$
"java.util.Map param = null;" + ENDL + //$NON-NLS-1$
"java.util.Map paramValues = null;" + ENDL + //$NON-NLS-1$
"java.util.Map header = null;" + ENDL + //$NON-NLS-1$
"java.util.Map headerValues = null;" + ENDL + //$NON-NLS-1$
"java.util.Map cookie = null;" + ENDL + //$NON-NLS-1$
"java.util.Map initParam = null;" + ENDL + //$NON-NLS-1$
"java.util.Map pageScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map requestScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map sessionScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map applicationScope = null;" + ENDL + //$NON-NLS-1$
"return \"\"+"; //$NON-NLS-1$
private static final String fExpressionHeader2_param = "()" + ENDL + //$NON-NLS-1$
"\t\tthrows java.io.IOException, javax.servlet.ServletException, javax.servlet.jsp.JspException {" + ENDL + //$NON-NLS-1$
"javax.servlet.jsp.PageContext pageContext = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, String> param = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, String[]> paramValues = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, String> header = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, String[]> headerValues = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, javax.servlet.http.Cookie> cookie = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, String> initParam = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, Object> pageScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, Object> requestScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, Object> sessionScope = null;" + ENDL + //$NON-NLS-1$
"java.util.Map<String, Object> applicationScope = null;" + ENDL + //$NON-NLS-1$
"return \"\"+"; //$NON-NLS-1$
private static final String fJspImplicitObjects[] = { "pageContext" }; //$NON-NLS-1$
private static final String fJspImplicitMaps[] = { "param", "paramValues", "header", "headerValues", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
"cookie", "initParam", "pageScope", "requestScope", "sessionScope", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
"applicationScope" }; //$NON-NLS-1$
private static final HashMap fJSPImplicitObjectMap = new HashMap(fJspImplicitObjects.length);
static {
for(int i = 0; i < fJspImplicitObjects.length; i++) {
fJSPImplicitObjectMap.put(fJspImplicitObjects[i], new Boolean(true));
}
for(int i = 0; i < fJspImplicitMaps.length; i++) {
fJSPImplicitObjectMap.put(fJspImplicitMaps[i], new Boolean(false));
}
}
private static final String fFooter = " ;" + ENDL + "}" + ENDL; //$NON-NLS-1$ //$NON-NLS-2$
private StringBuffer fResult;
private Map fCodeMap;
private int fOffsetInUserCode;
private static int methodCounter = 0;
private IStructuredDocument fDocument = null;
private int fContentStart;
private static Map fOperatorMap;
// start of the generated function definition, if any:
private int fGeneratedFunctionStart;
// this flag lets us know if we were unable to generate for some reason. One possible reason is that the expression
// contains a reference to a variable for which information is only available at runtime.
private boolean fCanGenerate = true;
private IStructuredDocumentRegion fCurrentNode;
private boolean fUseParameterizedTypes;
private List fELProblems;
/**
* Tranlsation of XML-style operators to java
*/
static {
fOperatorMap = new HashMap();
fOperatorMap.put("gt", ">"); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("lt", "<"); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("ge", ">="); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("le", "<="); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("mod", "%"); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("eq", "=="); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("and", "&&"); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("or", "||"); //$NON-NLS-1$ //$NON-NLS-2$
fOperatorMap.put("not", "!"); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* The constructor squirrels away a few things we'll need later
*
* @param result
* @param codeMap
* @param translator
* @param jspReferenceRegion
* @param contentStart
*/
public ELGeneratorVisitor(StringBuffer result, IStructuredDocumentRegion currentNode, Map codeMap, IStructuredDocument document, ITextRegionCollection jspReferenceRegion, int contentStart)
{
fResult = result;
fCodeMap = codeMap;
fOffsetInUserCode = result.length();
fContentStart = contentStart;
fDocument = document;
fCurrentNode = currentNode;
fGeneratedFunctionStart = -1; //set when generating function definition
fUseParameterizedTypes = compilerSupportsParameterizedTypes();
fELProblems = new ArrayList();
}
/**
* Append a token to the output stream. Automatically calculating mapping.
*
* @param token
*/
private void append(Token token)
{
append(token.image, token.beginColumn - 1, token.endColumn);
}
/**
* Append a translation for the corresponding input token.
*
* @param translated
* @param token
*/
private void append(String translated, Token token)
{
append(translated, token.beginColumn - 1, token.endColumn);
}
/**
* Append a string explicitly giving the input mapping.
*
* @param newText
* @param jspPositionStart
* @param jspPositionEnd
*/
private void append(String newText, int jspPositionStart, int jspPositionEnd)
{
fResult.append(newText);
Position javaRange = new Position(fOffsetInUserCode, newText.length());
Position jspRange = new Position(fContentStart + jspPositionStart, jspPositionEnd - jspPositionStart);
fCodeMap.put(javaRange, jspRange);
fOffsetInUserCode += newText.length();
}
/**
* Append text that will be unmapped and therefore will not be available for completion.
*
* @param newText
*/
private void append(String newText)
{
fResult.append(newText);
fOffsetInUserCode += newText.length();
}
/**
* Generate a function invocation.
*
* @param fullFunctionName
* @return
*/
protected String genFunction(String fullFunctionName) {
TLDCMDocumentManager docMgr = TaglibController.getTLDCMDocumentManager(fDocument);
int colonIndex = fullFunctionName.indexOf(':');
String prefix = fullFunctionName.substring(0, colonIndex);
String functionName = fullFunctionName.substring(colonIndex + 1);
if (docMgr == null)
return null;
Iterator taglibs = docMgr.getCMDocumentTrackers(fCurrentNode.getStartOffset()).iterator();
while (taglibs.hasNext()) {
TaglibTracker tracker = (TaglibTracker)taglibs.next();
if(tracker.getPrefix().equals(prefix)) {
CMDocumentImpl doc = (CMDocumentImpl)tracker.getDocument();
List functions = doc.getFunctions();
for(Iterator it = functions.iterator(); it.hasNext(); ) {
TLDFunction function = (TLDFunction)it.next();
if(function.getName().equals(functionName)) {
String javaFuncName = getFunctionNameFromSignature(function.getSignature());
if (javaFuncName == null)
javaFuncName = functionName;
return function.getClassName() + "." + javaFuncName; //$NON-NLS-1$
}
}
}
}
return null;
}
/**
* Handle a simple node -- fallback
*/
public Object visit(SimpleNode node, Object data) {
return(node.childrenAccept(this, data));
}
static synchronized int getMethodCounter() {
return methodCounter++;
}
/**
* Handle top-level expression
*/
public Object visit(ASTExpression node, Object data) {
return node.childrenAccept(this, data);
}
public void startFunctionDefinition(int start) {
fGeneratedFunctionStart = fResult.length();
append(fExpressionHeader1, start, start);
append(Integer.toString(getMethodCounter()), start, start);
if (fUseParameterizedTypes)
append(fExpressionHeader2_param, start, start);
else
append(fExpressionHeader2, start, start);
}
public void endFunctionDefinition(int end) {
if (fGeneratedFunctionStart < 0) {
throw new IllegalStateException("Cannot end function definition because none has been started."); //$NON-NLS-1$
}
append(fFooter, end, end);
// something is preventing good code generation so empty out the result
// and the map.
if (!fCanGenerate) {
fResult.delete(fGeneratedFunctionStart, fResult.length());
fOffsetInUserCode = fResult.length();
// remove all fCodeMap entries for the removed code:
for (Iterator it = fCodeMap.entrySet().iterator(); it.hasNext();) {
Map.Entry entry = (Entry) it.next();
if (entry.getKey() instanceof Position) {
Position pos = (Position) entry.getKey();
if (pos.getOffset() >= fGeneratedFunctionStart) {
it.remove();
}
}
}
}
fGeneratedFunctionStart = -1;
}
private boolean compilerSupportsParameterizedTypes() {
if (fDocument != null) {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IPath location = TaglibController.getLocation(fDocument);
if (location != null && location.segmentCount() > 0) {
IJavaProject project = JavaCore.create(root.getProject(location.segment(0)));
String compliance = project.getOption(JavaCore.COMPILER_SOURCE, true);
try {
return Float.parseFloat(compliance) >= 1.5;
}
catch (NumberFormatException e) {
return false;
}
}
}
return false;
}
/**
* Generically generate an operator node.
*
* @param node
* @param data
*/
private void generateOperatorNode(ASTOperatorExpression node, Object data) {
for(int i = 0; i < node.children.length; i++) {
node.children[i].jjtAccept(this, data);
if( node.children.length - i > 1) {
appendOperator((Token)node.getOperatorTokens().get(i));
}
}
}
/**
* Append an operator to the output stream after translation (if any)
*
* @param token
* @return
*/
private String appendOperator(Token token) {
String tokenImage = token.image.trim();
String translated = (String)fOperatorMap.get(tokenImage);
if(null != translated) {
append(translated, token);
} else {
append(token);
}
return(translated);
}
/**
* Handle or Expression
*/
public Object visit(ASTOrExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Handle and expression
*/
public Object visit(ASTAndExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Handle equality
*/
public Object visit(ASTEqualityExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Handle Relational
*/
public Object visit(ASTRelationalExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Handle addition
*/
public Object visit(ASTAddExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Handle multiply
*/
public Object visit(ASTMultiplyExpression node, Object data) {
generateOperatorNode(node, data);
return(null);
}
/**
* Choice Expression (ternary operator)
*/
public Object visit(ASTChoiceExpression node, Object data) {
node.children[0].jjtAccept(this, data);
append("?"); //$NON-NLS-1$
node.children[1].jjtAccept(this, data);
append(":"); //$NON-NLS-1$
node.children[2].jjtAccept(this,data);
return null;
}
/**
* Handle unary
*/
public Object visit(ASTUnaryExpression node, Object data) {
if(JSPELParserConstants.EMPTY == node.firstToken.kind) {
append("((null == "); //$NON-NLS-1$
node.childrenAccept(this, data);
append(") || ("); //$NON-NLS-1$
node.childrenAccept(this, data);
append(").isEmpty())"); //$NON-NLS-1$
} else if(JSPELParserConstants.NOT1 == node.firstToken.kind || JSPELParserConstants.NOT2 == node.firstToken.kind) {
append("(!"); //$NON-NLS-1$
node.childrenAccept(this, data);
append(")"); //$NON-NLS-1$
} else if(JSPELParserConstants.MINUS == node.firstToken.kind) {
append("(-"); //$NON-NLS-1$
node.childrenAccept(this, data);
append(")"); //$NON-NLS-1$
} else {
node.childrenAccept(this, data);
}
return null;
}
/**
* Value node
*/
public Object visit(ASTValue node, Object data) {
if(node.jjtGetNumChildren() >= 2) {
if(node.jjtGetChild(0) instanceof ASTValuePrefix && node.jjtGetChild(1) instanceof ASTValueSuffix) {
ASTValuePrefix prefix = (ASTValuePrefix) node.jjtGetChild(0);
ASTValueSuffix suffix = (ASTValueSuffix) node.jjtGetChild(1);
//content assist can cause a null pointer here without the extra null check
if(prefix.firstToken.image.equals("pageContext") && suffix.getPropertyNameToken() != null && suffix.getPropertyNameToken().image.equals("request")) {
append("((HttpServletRequest)");
}
}
}
return node.childrenAccept(this, data);
}
/**
* Value Prefix
*/
public Object visit(ASTValuePrefix node, Object data) {
// this is a raw identifier. May sure it's an implicit object.
// This is the primary place where modification is needed to
// support JSF backing beans.
if(null == node.children) {
if(isCompletingObject(node.firstToken.image)) {
append(node.firstToken);
} else {
fCanGenerate = false;
}
return(null);
}
return node.childrenAccept(this, data);
}
/**
* Function for testing implicit objects.
*
* @param image
* @return
*/
private boolean isCompletingObject(String image) {
Boolean value = (Boolean)fJSPImplicitObjectMap.get(image);
return null == value ? false : value.booleanValue();
}
/**
* Value suffix
*/
public Object visit(ASTValueSuffix node, Object data) {
if(JSPELParserConstants.LBRACKET == node.firstToken.kind) {
fCanGenerate = false;
} else if(null != node.getPropertyNameToken()) {
Token suffix = node.getPropertyNameToken();
String ucaseName = suffix.image.substring(0, 1).toUpperCase() + suffix.image.substring(1, suffix.image.length());
// This is a special case. Note that the type system, no matter how much type information
// we would have wouldn't give us the correct result. We're looking for "pageContext.request"
// here and will add a downcast to (HTTPServletRequest)
append(node.firstToken);
append("get" + ucaseName + "()", suffix); //$NON-NLS-1$ //$NON-NLS-2$
SimpleNode parent = (SimpleNode) node.jjtGetParent();
if(suffix.image.equals("request") && parent instanceof ASTValue && //$NON-NLS-1$
parent.jjtGetParent() instanceof ASTUnaryExpression && parent.firstToken.image.equals("pageContext")) { //$NON-NLS-1$
append(")");
}
} else if(node.getLastToken().image.equals(".") && node.getLastToken().next.image.equals("")) { //$NON-NLS-1$ //$NON-NLS-2$
//this allows for content assist in the case of something along the lines of "pageContext." and then ctl-space
append(node.firstToken);
append("get()", node.getLastToken().beginColumn, node.getLastToken().beginColumn); //$NON-NLS-1$
} else {
append(node.firstToken);
}
return null;
}
/**
* Function invocation
*/
public Object visit(ASTFunctionInvocation node, Object data) {
String functionTranslation = genFunction(node.getFullFunctionName());
if(null != functionTranslation) {
//find the token representing the function name
Token jspFuncNameToken = getJSPFuncNameToken(node);
/* if there is a dot in the function name then separate out the class path
* from the function name and append.
* else just append
* in both cases use the jsp function name token as the mapped token
*/
int indexOfDot = functionTranslation.lastIndexOf('.');
if(indexOfDot != -1) {
String funcClass = functionTranslation.substring(0,indexOfDot+1);
String funcName = functionTranslation.substring(indexOfDot+1);
append(funcClass, jspFuncNameToken);
append(funcName, jspFuncNameToken);
} else {
append(functionTranslation, jspFuncNameToken);
}
//append any parameters
append("(");
if(node.children != null) {
for(int i = 0; i < node.children.length; i++) {
node.children[i].jjtAccept(this, data);
if( node.children.length - i > 1){
append(","); //$NON-NLS-1$
}
}
}
append(")"); //$NON-NLS-1$
}
else {
//column offsets are 1 based not 0 based, thus subtract one
final int problemOffset = fContentStart + node.getFirstToken().beginColumn - 1;
final int problemLength = node.getLastToken().endColumn - 1;
//could not find function translation so report error
fELProblems.add(new ELProblem(new Position(problemOffset, problemLength), NLS.bind(JSPCoreMessages.JSPELTranslator_0, node.getFullFunctionName())));
//error message to be injected into translation purely for debugging purposes
String errorMsg = "\"Could not find function translation for: " + node.getFullFunctionName() + "\""; //$NON-NLS-1$ //$NON-NLS-2$
append(errorMsg);
}
return null;
}
/**
* @return the {@link ELProblem}s found by this visitor
*/
public List getELProblems() {
return fELProblems;
}
/**
* Literal
*/
public Object visit(ASTLiteral node, Object data) {
if (isSingleQuotedStringLiteral(node)) {
//replace the single quotes with double quotes quotes
//so java compiler will be happy
//(see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=104943)
String image = node.firstToken.image;
image = "\"" + image.substring(1, image.length()-1) + "\""; //$NON-NLS-1$ //$NON-NLS-2$
node.firstToken.image = image;
}
append(node.firstToken);
return null;
}
/**
* Indicates whether the given ASTLiteral is a single quoted string literal,
* As opposed to a double quoted ASTLiteral
*
* @param node the ASTLiteral to check to see if it is single quoted
*
* @return true, if the given token is a single quoted string literal,
* false otherwise
*/
private static boolean isSingleQuotedStringLiteral(ASTLiteral node) {
String content = node.firstToken.image;
return content.length() > 1 && content.startsWith("'") && content.endsWith("'"); //$NON-NLS-1$ // $NON-NLS-2$
}
/**
* <p>Given a method signature parse out the method name and return it.
* The method name in the signature is found by finding a word with
* whitespace before it and a '<code>(</code>' after it.</p>
*
* @param methodSignature the signature of the method to get the method name out of.
* @return the method name from the given signature, or <code>null</code> if it
* can not be found.
*/
private static String getFunctionNameFromSignature (String methodSignature) {
int length = methodSignature.length();
char c = 0;
int identifierStart = -1;
int whitespaceStart = -1;
// keep track of the index of the last identifier before the (
for (int i = 0; i < length; i++) {
c = methodSignature.charAt(i);
if (Character.isJavaIdentifierPart(c) && whitespaceStart >= identifierStart)
identifierStart = i;
else if (Character.isWhitespace(c))
whitespaceStart = i;
else if (c == '(') {
if (identifierStart >= 0) {
return methodSignature.substring(identifierStart, i).trim();
}
}
}
return null;
}
/**
* Returns the {@link Token} the represents the function name in
* the {@link ASTFunctionInvocation}. This is designated as the
* first token after the {@link Token} whose image is ":".
* If such a token can not be found then the first token of the
* {@link ASTFunctionInvocation} is returned.
*
* @param funcInvo the {@link ASTFunctionInvocation} to find the function name {@link Token} in
* @return the {@link Token} in the given {@link ASTFunctionInvocation} that represents the
* function name, or if that can't be found the first {@link Token} in the {@link ASTFunctionInvocation}.
*/
private Token getJSPFuncNameToken(ASTFunctionInvocation funcInvo) {
Token funcNameToken = funcInvo.getFirstToken();
Token temp = funcInvo.getFirstToken();
do {
if(temp.image.equals(":")) {
funcNameToken = temp.next;
}
} while(temp.next != null && funcNameToken == null);
return funcNameToken;
}
}