/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.click.eclipse.ui.editor; | |
import java.io.InputStream; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
import org.apache.click.eclipse.ClickPlugin; | |
import org.apache.click.eclipse.ClickUtils; | |
import org.apache.click.eclipse.preferences.ClickProjectPropertyPage.VariableModel; | |
import org.apache.click.eclipse.ui.editor.TemplateObject.TemplateObjectElement; | |
import org.apache.click.eclipse.ui.editor.TemplateObject.TemplateObjectMethod; | |
import org.apache.click.eclipse.ui.editor.TemplateObject.TemplateObjectProperty; | |
import org.eclipse.core.resources.IFile; | |
import org.eclipse.core.resources.IFolder; | |
import org.eclipse.core.resources.IProject; | |
import org.eclipse.core.resources.ProjectScope; | |
import org.eclipse.jdt.core.Flags; | |
import org.eclipse.jdt.core.IField; | |
import org.eclipse.jdt.core.IJavaProject; | |
import org.eclipse.jdt.core.IType; | |
import org.eclipse.jdt.core.JavaCore; | |
import org.eclipse.jdt.core.Signature; | |
import org.eclipse.jface.text.ITextViewer; | |
import org.eclipse.jface.text.contentassist.CompletionProposal; | |
import org.eclipse.jface.text.contentassist.ICompletionProposal; | |
import org.eclipse.jface.text.contentassist.IContentAssistProcessor; | |
import org.eclipse.swt.graphics.Image; | |
import org.eclipse.ui.preferences.ScopedPreferenceStore; | |
import org.eclipse.wst.xml.ui.internal.contentassist.XMLContentAssistProcessor; | |
/** | |
* {@link IContentAssistProcessor} implementation for the Velocity Template Editor. | |
* | |
* @author Naoki Takezoe | |
*/ | |
public class TemplateContentAssistProcessor extends XMLContentAssistProcessor { | |
private IFile file; | |
private final Image IMAGE_DIRECTIVE = ClickPlugin.getImageDescriptor("/icons/directive.gif").createImage(); | |
private final Image IMAGE_METHOD = ClickPlugin.getImageDescriptor("/icons/method.gif").createImage(); | |
private final Image IMAGE_FIELD = ClickPlugin.getImageDescriptor("/icons/field.gif").createImage(); | |
private final Image IMAGE_VAR = ClickPlugin.getImageDescriptor("/icons/localvar.gif").createImage(); | |
private final Pattern PATTERN_SET = Pattern.compile("#set\\s*\\(\\s*\\$(.+?)\\s*="); | |
private final Pattern PATTERN_MACRO = Pattern.compile("#macro\\s*\\(\\s*(.+?)[\\s\\)]"); | |
private static final Map<String, String> defaultObjects = new HashMap<String, String>(); | |
static { | |
defaultObjects.put("imports", "org.apache.click.util.PageImports"); | |
defaultObjects.put("context", "java.lang.String"); | |
defaultObjects.put("messages", "java.util.Map"); | |
defaultObjects.put("path", "java.lang.String"); | |
defaultObjects.put("request", "javax.servlet.http.HttpServletRequest"); | |
defaultObjects.put("response", "javax.servlet.http.HttpServletResponse"); | |
defaultObjects.put("session", "org.apache.click.util.SessionMap"); | |
}; | |
/** | |
* Returns the word under the caret position. | |
*/ | |
private static String getLastWord(ITextViewer textViewer, int documentPosition){ | |
String source = textViewer.getDocument().get(); | |
StringBuffer sb = new StringBuffer(); | |
for(int i=0;i<documentPosition;i++){ | |
char c = source.charAt(i); | |
if(Character.isWhitespace(c)){ | |
sb.setLength(0); | |
} else if(c=='#' || c=='$'){ | |
sb.setLength(0); | |
sb.append(c); | |
} else { | |
sb.append(c); | |
} | |
} | |
return sb.toString(); | |
} | |
/** | |
* Appends the completion proposal to the <code>result</code>. | |
*/ | |
private static void registerProposal(List<ICompletionProposal> result, int offset, | |
String matchString, String replaceString, String displayString, Image image){ | |
int position = replaceString.length(); | |
if(replaceString.endsWith(")") && displayString.indexOf("()") < 0){ | |
position--; | |
} | |
if(replaceString.endsWith("}") && displayString.indexOf("{}") < 0){ | |
position--; | |
} | |
if(replaceString.startsWith(matchString)){ | |
result.add(new CompletionProposal( | |
replaceString, offset - matchString.length(), | |
matchString.length(), position, image, displayString, null, null)); | |
} | |
if(matchString.startsWith("${") && replaceString.startsWith("$") && | |
!replaceString.startsWith("${")){ | |
registerProposal(result, offset, matchString, | |
"${" + replaceString.substring(1), displayString, image); | |
} | |
} | |
/** | |
* Returns completion proposals. | |
*/ | |
public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int offset) { | |
String matchString = getLastWord(textViewer, offset); | |
List<ICompletionProposal> result = new ArrayList<ICompletionProposal>(); | |
if(!matchString.startsWith("#") && !matchString.startsWith("$")){ | |
ICompletionProposal[] proposals = super.computeCompletionProposals(textViewer, offset); | |
if(proposals!=null){ | |
for(int i=0;i<proposals.length;i++){ | |
result.add(proposals[i]); | |
} | |
} | |
} | |
IType format = null; | |
List<VariableModel> preferenceObjects = null; | |
if(this.file != null){ | |
// for the format object | |
format = ClickUtils.getFormat(file.getProject()); | |
if(matchString.startsWith("$format.") || matchString.startsWith("${format.")){ | |
if(format != null){ | |
return processType(format, result, matchString, offset); | |
} | |
} | |
// other default objects | |
for(Iterator<Map.Entry<String, String>> ite = defaultObjects.entrySet().iterator(); ite.hasNext(); ){ | |
Map.Entry<String, String> entry = ite.next(); | |
if(matchString.startsWith("$" + entry.getKey() + ".") || matchString.startsWith("${" + entry.getKey() + ".")){ | |
IType type = findType((String)entry.getValue()); | |
if(type != null){ | |
return processType(type, result, matchString, offset); | |
} | |
} | |
} | |
// prefeberce objects | |
ScopedPreferenceStore store = new ScopedPreferenceStore( | |
new ProjectScope(file.getProject()), ClickPlugin.PLUGIN_ID); | |
String vars = store.getString(ClickPlugin.PREF_VELOCITY_VARS); | |
if(vars != null && vars.length() > 0){ | |
preferenceObjects = VariableModel.deserialize(vars); | |
for(int i=0;i<preferenceObjects.size();i++){ | |
VariableModel model = preferenceObjects.get(i); | |
if(matchString.startsWith("$" + model.name + ".") || matchString.startsWith("${" + model.name + ".")){ | |
IType type = findType((String)model.type); | |
if(type != null){ | |
return processType(type, result, matchString, offset); | |
} | |
} | |
} | |
} | |
} | |
Map<String, TemplateObject> fields = extractPageFields(); | |
for(Iterator<Map.Entry<String, TemplateObject>> ite = fields.entrySet().iterator(); ite.hasNext();){ | |
Map.Entry<String, TemplateObject> entry = ite.next(); | |
String name = entry.getKey(); | |
if(matchString.startsWith("$" + name + ".") || matchString.startsWith("${" + name + ".")){ | |
TemplateObject obj = (TemplateObject)entry.getValue(); | |
if(obj.getType()!=null){ | |
return processType(obj.getType(), result, matchString, offset); | |
} | |
} | |
} | |
if(format==null){ | |
registerProposal(result, offset, matchString, "$format", "$format", IMAGE_VAR); | |
} else { | |
registerProposal(result, offset, matchString, | |
"$format", "$format - " + format.getFullyQualifiedName(), IMAGE_VAR); | |
} | |
// for page class fields | |
for(Iterator<Map.Entry<String, TemplateObject>> ite = fields.entrySet().iterator(); ite.hasNext();){ | |
Map.Entry<String, TemplateObject> entry = ite.next(); | |
String name = entry.getKey(); | |
TemplateObject obj = (TemplateObject)entry.getValue(); | |
registerProposal(result, offset, matchString, | |
"$" + name, "$" + name + " - " + obj.getTypeName(), IMAGE_FIELD); | |
} | |
// #set($xxxx) | |
String source = textViewer.getDocument().get().substring(0, offset); | |
Matcher matcher = PATTERN_SET.matcher(source); | |
while(matcher.find()){ | |
String name = matcher.group(1); | |
registerProposal(result, offset, matchString, "$" + name, "$" + name, IMAGE_VAR); | |
} | |
// #macro(xxxx) | |
matcher = PATTERN_MACRO.matcher(source); | |
while(matcher.find()){ | |
String name = matcher.group(1); | |
registerProposal(result, offset, matchString, "#" + name + "()", name, IMAGE_DIRECTIVE); | |
} | |
readMacroVM(result, offset, matchString); | |
registerProposal(result, offset, matchString, "$imports", "$imports - PageImports", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$context", "$context - String", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$messages", "$messages - Map", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$path", "$path - String", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$request", "$request - HttpServletRequest", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$response", "$response - HttpServletResponse", IMAGE_VAR); | |
registerProposal(result, offset, matchString, "$session", "$session - SessionMap", IMAGE_VAR); | |
if(preferenceObjects != null){ | |
for(int i=0;i<preferenceObjects.size();i++){ | |
VariableModel model = (VariableModel) preferenceObjects.get(i); | |
registerProposal(result, offset, matchString, | |
"$" + model.name, "$" + model.name + " - " + model.type, IMAGE_VAR); | |
} | |
} | |
registerProposal(result, offset, matchString, "#if()", "if", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#set()", "set", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#foreach()", "foreach", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#else", "else", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#elseif()", "elsif", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#end", "end", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#include()", "include", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#parse()", "parse", IMAGE_DIRECTIVE); | |
registerProposal(result, offset, matchString, "#macro()", "macro", IMAGE_DIRECTIVE); | |
return (ICompletionProposal[])result.toArray(new ICompletionProposal[result.size()]); | |
} | |
private IType findType(String className){ | |
try { | |
IJavaProject project = JavaCore.create(this.file.getProject()); | |
if(project != null){ | |
IType type = project.findType(className); | |
return type; | |
} | |
} catch(Exception ex){ | |
} | |
return null; | |
} | |
/** | |
* Read macro.vm and add macros to completion proposals. | |
*/ | |
private void readMacroVM(List<ICompletionProposal> result, int offset, String matchString){ | |
IProject project = this.file.getProject(); | |
String folderName = ClickUtils.getWebAppRootFolder(project); | |
IFolder folder = project.getFolder(folderName); | |
IFile macroFile = folder.getFile("macro.vm"); | |
if(macroFile!=null && macroFile.exists()){ | |
try { | |
InputStream in = macroFile.getContents(); | |
byte[] buf = new byte[in.available()]; | |
in.read(buf); | |
in.close(); | |
String source = new String(buf, macroFile.getCharset()); | |
Matcher matcher = PATTERN_MACRO.matcher(source); | |
while(matcher.find()){ | |
String name = matcher.group(1); | |
registerProposal(result, offset, matchString, "#" + name + "()", | |
name + " - macro.vm", IMAGE_DIRECTIVE); | |
} | |
} catch(Exception ex){ | |
ClickPlugin.log(ex); | |
} | |
} | |
} | |
/** | |
* Returns completion proposals for the java object. | |
*/ | |
private ICompletionProposal[] processType(IType type, List<ICompletionProposal> result, String matchString, int offset){ | |
String prefix = matchString; | |
int index = matchString.lastIndexOf('.'); | |
if(index >= 0){ | |
prefix = prefix.substring(0, index); | |
} | |
TemplateObject obj = new TemplateObject(type); | |
obj = evaluate(obj, matchString); | |
if(obj != null){ | |
TemplateObjectElement[] children = obj.getChildren(); | |
for(int i=0;i<children.length;i++){ | |
if(children[i] instanceof TemplateObjectMethod){ | |
registerProposal(result, offset, matchString, | |
prefix + "." + children[i].getName()+"()", children[i].getDisplayName(), IMAGE_METHOD); | |
} else { | |
registerProposal(result, offset, matchString, | |
prefix + "." + children[i].getName(), children[i].getDisplayName(), IMAGE_FIELD); | |
} | |
} | |
} | |
return result.toArray(new ICompletionProposal[result.size()]); | |
} | |
/** | |
* Evaluates the given expression and returns the return type. | |
* | |
* @param obj the <code>TemplateObject</code> of the top level object | |
* @param expression the Velocity expression | |
* @return the return type of the given expression or <code>null</code> | |
*/ | |
private TemplateObject evaluate(TemplateObject obj, String expression){ | |
if(expression.endsWith(".")){ | |
expression += "_"; | |
} | |
String[] dim = expression.split("\\."); | |
for(int i=0; i < dim.length && obj != null; i++){ | |
if(i == 0 || i == dim.length-1){ | |
continue; | |
} | |
if(dim[i].endsWith(")")){ | |
// method | |
String[] methodInfo = dim[i].split("\\("); | |
if(methodInfo.length > 0){ | |
TemplateObjectMethod method = obj.getMethod(methodInfo[0]); | |
if(method != null){ | |
obj = method.toTemplateObject(); | |
continue; | |
} | |
} | |
} else { | |
// property | |
TemplateObjectProperty property = obj.getProperty(dim[i]); | |
if(property != null){ | |
obj = property.toTemplateObject(); | |
continue; | |
} | |
} | |
obj = null; | |
} | |
return obj; | |
} | |
/** | |
* Extracts public fields from the page class. | |
*/ | |
private Map<String, TemplateObject> extractPageFields(){ | |
HashMap<String, TemplateObject> map = new HashMap<String, TemplateObject>(); | |
if(this.file != null){ | |
try { | |
IType type = ClickUtils.getPageClassFromTemplate(this.file); | |
IJavaProject javaProject = type.getJavaProject(); | |
IField[] fields = type.getFields(); | |
for(int i=0;i<fields.length;i++){ | |
if(!Flags.isPublic(fields[i].getFlags())){ | |
continue; | |
} | |
String className = ClickUtils.removeTypeParameter(Signature.toString(fields[i].getTypeSignature())); | |
// primitive types | |
if(ClickUtils.isPrimitive(className)){ | |
TemplateObject obj = new TemplateObject(className); | |
map.put(fields[i].getElementName(), obj); | |
continue; | |
} | |
// object types | |
className = ClickUtils.resolveClassName(type, className); | |
IType fieldType = javaProject.findType(className); | |
if(fieldType != null){ | |
TemplateObject obj = new TemplateObject(fieldType); | |
map.put(fields[i].getElementName(), obj); | |
} | |
} | |
} catch(Exception ex){ | |
//ClickPlugin.log(ex); | |
} | |
} | |
return map; | |
} | |
/** | |
* Sets the editing filr in the editor. | |
* | |
* @param file the editing file | |
*/ | |
public void setFile(IFile file){ | |
this.file = file; | |
} | |
/** | |
* Releases internal resources such as icons. | |
*/ | |
public void release() { | |
IMAGE_DIRECTIVE.dispose(); | |
IMAGE_METHOD.dispose(); | |
IMAGE_FIELD.dispose(); | |
IMAGE_VAR.dispose(); | |
super.release(); | |
} | |
} |