/*******************************************************************************
* Copyright (c) 2007-2011 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is 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
*
* Contributor:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.seam.internal.core.el;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.jboss.tools.common.el.core.ca.AbstractELCompletionEngine;
import org.jboss.tools.common.el.core.ca.MessagesELTextProposal;
import org.jboss.tools.common.el.core.model.ELArgumentInvocation;
import org.jboss.tools.common.el.core.model.ELExpression;
import org.jboss.tools.common.el.core.model.ELInstance;
import org.jboss.tools.common.el.core.model.ELInvocationExpression;
import org.jboss.tools.common.el.core.model.ELModel;
import org.jboss.tools.common.el.core.model.ELObjectType;
import org.jboss.tools.common.el.core.model.ELPropertyInvocation;
import org.jboss.tools.common.el.core.parser.ELParser;
import org.jboss.tools.common.el.core.parser.ELParserFactory;
import org.jboss.tools.common.el.core.parser.ELParserUtil;
import org.jboss.tools.common.el.core.resolver.ELContext;
import org.jboss.tools.common.el.core.resolver.ELResolution;
import org.jboss.tools.common.el.core.resolver.ELResolutionImpl;
import org.jboss.tools.common.el.core.resolver.ELSegment;
import org.jboss.tools.common.el.core.resolver.ElVarSearcher;
import org.jboss.tools.common.el.core.resolver.JavaMemberELSegment;
import org.jboss.tools.common.el.core.resolver.TypeInfoCollector;
import org.jboss.tools.common.el.core.resolver.TypeInfoCollector.MemberInfo;
import org.jboss.tools.common.el.core.resolver.Var;
import org.jboss.tools.common.model.XModelObject;
import org.jboss.tools.common.model.XModelObjectConstants;
import org.jboss.tools.common.model.filesystems.FileSystemsHelper;
import org.jboss.tools.common.text.TextProposal;
import org.jboss.tools.common.util.StringUtil;
import org.jboss.tools.jst.web.kb.el.MessagePropertyELSegmentImpl;
import org.jboss.tools.seam.core.IBijectedAttribute;
import org.jboss.tools.seam.core.ISeamComponent;
import org.jboss.tools.seam.core.ISeamContextShortVariable;
import org.jboss.tools.seam.core.ISeamContextVariable;
import org.jboss.tools.seam.core.ISeamElement;
import org.jboss.tools.seam.core.ISeamMessages;
import org.jboss.tools.seam.core.ISeamProject;
import org.jboss.tools.seam.core.ISeamXmlFactory;
import org.jboss.tools.seam.core.ScopeType;
import org.jboss.tools.seam.core.SeamCorePlugin;
import org.jboss.tools.seam.internal.core.el.SeamExpressionResolver.MessagesInfo;
/**
* Utility class used to collect info for EL
*
* @author Jeremy
*/
public final class SeamELCompletionEngine extends AbstractELCompletionEngine<ISeamContextVariable> {
private static final ImageDescriptor SEAM_EL_PROPOSAL_IMAGE =
SeamCorePlugin.getDefault().getImageDescriptorFromRegistry(SeamCorePlugin.CA_SEAM_EL_IMAGE_PATH);
private static final ImageDescriptor SEAM_MESSAGES_PROPOSAL_IMAGE =
SeamCorePlugin.getDefault().getImageDescriptorFromRegistry(SeamCorePlugin.CA_SEAM_MESSAGES_IMAGE_PATH);
private static ELParserFactory factory = ELParserUtil.getJbossFactory();
/**
* Constructs SeamELCompletionEngine object
*/
public SeamELCompletionEngine() {
}
/*
* (non-Javadoc)
* @see org.jboss.tools.common.el.core.ca.AbstractELCompletionEngine#getELProposalImageForMember(org.jboss.tools.common.el.core.resolver.TypeInfoCollector.MemberInfo)
*/
@Override
public ImageDescriptor getELProposalImageForMember(MemberInfo memberInfo) {
return SEAM_EL_PROPOSAL_IMAGE;
}
/*
* (non-Javadoc)
* @see org.jboss.tools.common.el.core.resolver.ELCompletionEngine#getParserFactory()
*/
@Override
public ELParserFactory getParserFactory() {
return factory;
}
@Override
protected void log(Exception e) {
SeamCorePlugin.getPluginLog().logError(e);
}
/**
* Returns a list of Seam Context Variables that is represented by EL. Null if El is not resolved.
* @param project
* @param file
* @param el
* @return
* @throws BadLocationException
* @throws StringIndexOutOfBoundsException
*/
public List<ISeamContextVariable> resolveSeamVariableFromEL(ISeamProject project, IFile file, String el) throws BadLocationException, StringIndexOutOfBoundsException {
List<ISeamContextVariable> resolvedVariables = EMPTY_VARIABLES_LIST;
if(!el.startsWith("#{")) {
el = "#{" + el + "}";
}
ELParser parser = factory.createParser();
ELModel model = parser.parse(el);
List<ELInstance> is = model.getInstances();
if(is.size() < 1) return resolvedVariables;
ELExpression ex = is.get(0).getExpression();
if(!(ex instanceof ELInvocationExpression)) return resolvedVariables;
ELInvocationExpression expr = (ELInvocationExpression)ex;
boolean isIncomplete = expr.getType() == ELObjectType.EL_PROPERTY_INVOCATION
&& ((ELPropertyInvocation) expr).getName() == null;
ELInvocationExpression left = expr;
// ScopeType scope = getScope(project, file);
if (expr.getLeft() == null && isIncomplete) {
resolvedVariables = resolveVariables(project, file, expr, true, true);
} else {
while (left != null) {
List<ISeamContextVariable> resolvedVars = resolveVariables(project, file, left,
left == expr, true);
if (resolvedVars != null && !resolvedVars.isEmpty()) {
resolvedVariables = resolvedVars;
break;
}
left = (ELInvocationExpression) left.getLeft();
}
}
if (left != expr && !resolvedVariables.isEmpty()) {
resolvedVariables.clear();
}
return resolvedVariables;
}
@Override
public List<ISeamContextVariable> resolveVariables(IFile file, ELContext context, ELInvocationExpression expr, boolean isFinal, boolean onlyEqualNames, int offset) {
ISeamProject project = SeamCorePlugin.getSeamProject(file.getProject(), true);
// ScopeType scope = getScope(project, file);
return resolveVariables(project, file, expr, isFinal, onlyEqualNames);
}
@Override
protected TypeInfoCollector.MemberInfo getMemberInfoByVariable(ISeamContextVariable var, ELContext context, boolean onlyEqualNames, int offset) {
return SeamExpressionResolver.getMemberInfoByVariable(var, true, this, offset);
}
@Override
protected void setImage(TextProposal proposal, ISeamContextVariable var) {
if (isSeamMessagesComponentVariable((ISeamContextVariable)var)) {
proposal.setImageDescriptor(SEAM_MESSAGES_PROPOSAL_IMAGE);
} else {
proposal.setImageDescriptor(getELProposalImageForMember(null));
}
}
@Override
protected boolean isSingularAttribute(ISeamContextVariable var) {
return var instanceof IBijectedAttribute;
}
@Override
protected void setImage(TextProposal kbProposal, TypeInfoCollector.MemberPresentation proposal) {
if (proposal.getMember() instanceof MessagesInfo) {
kbProposal.setImageDescriptor(SEAM_MESSAGES_PROPOSAL_IMAGE);
} else {
super.setImage(kbProposal, proposal);
}
}
@Override
protected boolean isSingularMember(TypeInfoCollector.MemberInfo mbr) {
return (mbr instanceof MessagesInfo);
}
@Override
protected void resolveLastSegment(ELInvocationExpression expr,
List<TypeInfoCollector.MemberInfo> members,
ELResolutionImpl resolution,
boolean returnEqualedVariablesOnly, boolean varIsUsed) {
if(resolveLastSegmentInMessages(expr, members, resolution, returnEqualedVariablesOnly, varIsUsed)) {
return;
} else {
super.resolveLastSegment(expr, members, resolution, returnEqualedVariablesOnly, varIsUsed);
}
}
private boolean resolveLastSegmentInMessages(ELInvocationExpression expr,
List<TypeInfoCollector.MemberInfo> members,
ELResolutionImpl resolution,
boolean returnEqualedVariablesOnly, boolean varIsUsed) {
if(members.isEmpty() || !(members.get(0) instanceof MessagesInfo)) {
return false;
}
MessagesInfo messagesInfo = ((MessagesInfo)members.get(0));
MessagePropertyELSegmentImpl segment = null;
if(expr instanceof ELPropertyInvocation) {
segment = new MessagePropertyELSegmentImpl(((ELPropertyInvocation)expr).getName());
} else if (expr instanceof ELArgumentInvocation) {
segment = new MessagePropertyELSegmentImpl(((ELArgumentInvocation)expr).getArgument().getOpenArgumentToken().getNextToken());
}
Set<TextProposal> kbProposals = new TreeSet<TextProposal>(TextProposal.KB_PROPOSAL_ORDER);
if (segment.getToken() != null) {
String propertyName = segment.getToken().getText();
Map<String, List<XModelObject>> properties = messagesInfo.getPropertiesMap();
List<XModelObject> os = properties.get(StringUtil.trimQuotes(propertyName));
if(os != null) {
for(XModelObject o: os) {
segment.addObject(o);
}
// Using 'base name' in seam is not a good idea.
// if(!os.isEmpty()) {
// segment.setBaseName(getBundle(os.get(0)));
// } else {
// segment.setBaseName("messages");
// }
}
}
if(segment.getToken()!=null) {
resolution.addSegment(segment);
}
resolution.setProposals(kbProposals);
if (expr.getType() == ELObjectType.EL_PROPERTY_INVOCATION && ((ELPropertyInvocation)expr).getName() == null) {
// return all the methods + properties
for (TypeInfoCollector.MemberInfo mbr : members) {
processSingularMember(mbr, kbProposals);
}
} else
if(expr.getType() != ELObjectType.EL_ARGUMENT_INVOCATION) {
String filter = (expr.getMemberName() == null ? "" : expr.getMemberName());
for (TypeInfoCollector.MemberInfo mbr : members) {
Collection<String> keys = ((MessagesInfo)mbr).getKeys();
for (String key : keys) {
if(returnEqualedVariablesOnly) {
// This is used for validation.
if (key.equals(filter)) {
MessagesELTextProposal kbProposal = createProposal(messagesInfo, key);
kbProposals.add(kbProposal);
break;
}
} else if (key.startsWith(filter)) {
// This is used for CA.
MessagesELTextProposal kbProposal = createProposal(messagesInfo, key);
if (key.indexOf('.') == -1) kbProposal.setReplacementString(key.substring(filter.length()));
else kbProposal.setReplacementString('[' + kbProposal.getReplacementString());
kbProposals.add(kbProposal);
}
}
}
} else if(expr.getType() == ELObjectType.EL_ARGUMENT_INVOCATION) {
String filter = expr.getMemberName() == null ? "" : expr.getMemberName();
boolean b = filter.startsWith("'") || filter.startsWith("\""); //$NON-NLS-1$ //$NON-NLS-2$
boolean e = filter.length() > 1 && filter.endsWith("'") || filter.endsWith("\"");//$NON-NLS-1$ //$NON-NLS-2$
filter = StringUtil.trimQuotes(filter);
for (TypeInfoCollector.MemberInfo mbr : members) {
if ((!b && filter.length() > 0) || (b && e && filter.length() == 0)) {
//Value is set as expression itself, we cannot compute it
resolution.setMapOrCollectionOrBundleAmoungTheTokens(true);
return true;
}
Collection<String> keys = ((MessagesInfo)mbr).getKeys();
for (String key : keys) {
if(returnEqualedVariablesOnly) {
// This is used for validation.
if (key.equals(filter)) {
MessagesELTextProposal kbProposal = createProposal(messagesInfo, key);
kbProposals.add(kbProposal);
break;
}
} else if (key.startsWith(filter)) {
// This is used for CA.
MessagesELTextProposal kbProposal = createProposal(messagesInfo, key);
String existingString = expr.getMemberName() == null ? "" : expr.getMemberName();
// Because we're in argument invocation we should fix the proposal by surrounding it with quotes as needed
String replacement = kbProposal.getReplacementString();
String label = kbProposal.getLabel();
if (!replacement.startsWith("'")) {
replacement = '\'' + key + '\'';
label = "['" + key + "']";
}
replacement = replacement.startsWith(existingString) ?
replacement.substring(existingString.length()) :
replacement;
kbProposal.setReplacementString(replacement);
kbProposal.setLabel(label);
kbProposals.add(kbProposal);
}
}
}
}
segment.setResolved(!kbProposals.isEmpty());
if (resolution.isResolved()){
resolution.setLastResolvedToken(expr);
}
return true;
}
protected void processSingularMember(TypeInfoCollector.MemberInfo mbr, Set<TextProposal> kbProposals) {
if (mbr instanceof MessagesInfo) {
// Surround the "long" keys containing the dots with [' ']
Map<String, List<XModelObject>> properties = ((MessagesInfo)mbr).getPropertiesMap();
TreeSet<String> keys = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
keys.addAll(properties.keySet());
Iterator<String> sortedKeys = keys.iterator();
while(sortedKeys.hasNext()) {
String key = sortedKeys.next();
if (key == null || key.length() == 0)
continue;
MessagesELTextProposal proposal = createProposal((MessagesInfo)mbr, key);
if (key.indexOf('.') != -1) {
proposal.setReplacementString("['" + key + "']"); //$NON-NLS-1$ //$NON-NLS-2$
proposal.setLabel("['" + key + "']");
} else {
proposal.setReplacementString(key);
proposal.setLabel(key);
}
kbProposals.add(proposal);
}
}
}
private MessagesELTextProposal createProposal(MessagesInfo mbr, String proposal) {
MessagesELTextProposal kbProposal = new MessagesELTextProposal();
if (proposal.indexOf('.') != -1) {
kbProposal.setReplacementString('\'' + proposal + '\'');
kbProposal.setLabel("['" + proposal + "']");
} else {
kbProposal.setReplacementString(proposal);
kbProposal.setLabel(proposal);
}
kbProposal.setAlternateMatch(proposal);
kbProposal.setImageDescriptor(SEAM_MESSAGES_PROPOSAL_IMAGE);
Map<String, List<XModelObject>> properties = mbr.getPropertiesMap();
List<XModelObject> ps = properties.get(proposal);
// String bundle = ps.isEmpty() ? "messages" : getBundle(ps.get(0)); // Using 'base name' in seam is not a good idea.
// kbProposal.setBaseName(bundle); // Using 'base name' in seam is not a good idea.
kbProposal.setPropertyName(proposal);
kbProposal.setObjects(ps);
return kbProposal;
}
private String getBundle(XModelObject o) {
StringBuilder sb = new StringBuilder();
XModelObject f = FileSystemsHelper.getFile(o);
if(f != null) {
sb.append(f.getAttributeValue(XModelObjectConstants.ATTR_NAME));
f = f.getParent();
while(f != null && f.getFileType() == XModelObject.FOLDER) {
sb.insert(0, ".").insert(0, f.getAttributeValue(XModelObjectConstants.ATTR_NAME));
f = f.getParent();
}
}
return sb.toString();
}
protected void filterSingularMember(TypeInfoCollector.MemberInfo mbr, Set<TypeInfoCollector.MemberPresentation> proposalsToFilter) {
Collection<String> keys = ((MessagesInfo)mbr).getKeys();
for (String key : keys) {
proposalsToFilter.add(new TypeInfoCollector.MemberPresentation(key, key, mbr));
}
}
/**
* Returns scope for the resource
* @param project
* @param resource
* @return
*/
public static ScopeType getScope(ISeamProject project, IResource resource) {
if (project == null || resource == null)
return null;
if (!"java".equalsIgnoreCase(resource.getFileExtension())) //$NON-NLS-1$
return null;
Set<ISeamComponent> components = project.getComponentsByPath(resource.getFullPath());
if (components.size() > 1) // Don't use scope in case of more than one component
return null;
for (ISeamComponent component : components) {
return component.getScope();
}
return null;
}
public List<ISeamContextVariable> resolveVariables(ISeamProject project, IFile file, ELInvocationExpression expr, boolean isFinal, boolean onlyEqualNames) {
List<ISeamContextVariable> resolvedVars = EMPTY_VARIABLES_LIST;
if (project == null)
return EMPTY_VARIABLES_LIST;
String varName = expr.toString();
if (varName != null) {
resolvedVars = SeamExpressionResolver.resolveVariables(project, file, varName, onlyEqualNames);
}
if (resolvedVars != null && !resolvedVars.isEmpty()) {
if(isFinal) {
return resolvedVars;
}
List<ISeamContextVariable> newResolvedVars = new ArrayList<ISeamContextVariable>();
for (ISeamContextVariable var : resolvedVars) {
// Do filter by equals (name)
// In case of the last pass - do not filter by startsWith(name) instead of equals
if (varName.equals(var.getName())) {
newResolvedVars.add(var);
}
}
return newResolvedVars;
}
return EMPTY_VARIABLES_LIST;
}
/**
* @param documentContent
* @param offset
* @param region
* @return
* @throws StringIndexOutOfBoundsException
*/
public String getJavaElementExpression(IDocument document, int offset, IRegion region, int start, int end) throws StringIndexOutOfBoundsException {
if (document == null || document.get() == null || offset > document.get().length())
return null;
ELInvocationExpression expr = findExpressionAtOffset(
document,
region.getOffset() + region.getLength(),
start, end);
if (expr == null) return null;
ELInvocationExpression left = expr;
while(left != null && left.getLeft() != null) left = left.getLeft();
String prefixPart = document.get().substring(expr.getStartPosition(), offset);
while(left != null) {
String varText = left.getText();
if (varText != null && varText.startsWith(prefixPart)) {
return varText;
}
if(left == expr) break;
left = (ELInvocationExpression)left.getParent();
}
return null;
}
List<IJavaElement> EMPTY_JAVA_ELEMENTS = Collections.unmodifiableList(new ArrayList<IJavaElement>());
/**
* Create the array of suggestions from expression.
* @param project Seam project
* @param file File
* @param offset TODO
* @param document
* @param prefix the prefix to search for
* @param position Offset of the prefix
*/
public List<IJavaElement> getJavaElementsForExpression(ISeamProject project, IFile file, String expression, int offset) throws BadLocationException, StringIndexOutOfBoundsException {
ELExpression expr = parseOperand(expression);
if(!(expr instanceof ELInvocationExpression)) {
return EMPTY_JAVA_ELEMENTS;
}
return getJavaElementsForELOperandTokens(project, file, (ELInvocationExpression)expr);
}
/**
* Create the array of suggestions.
* @param project Seam project
* @param file File
* @param document
* @param prefix the prefix to search for
* @param position Offset of the prefix
*/
public List<IJavaElement> getJavaElementsForELOperandTokens(
ISeamProject project, IFile file,
ELInvocationExpression expr) throws BadLocationException, StringIndexOutOfBoundsException {
List<IJavaElement> res = EMPTY_JAVA_ELEMENTS;
ElVarSearcher varSearcher = new ElVarSearcher(file, this);
List<Var> vars = varSearcher.findAllVars(file, expr.getStartPosition());
ELResolution resolution = resolveELOperand(file, null, expr, true, vars, varSearcher, 0);
if (resolution!=null && resolution.isResolved()) {
ELSegment segment = resolution.getLastSegment();
if(segment instanceof JavaMemberELSegment) {
IJavaElement el = ((JavaMemberELSegment)segment).getJavaElement();
if (el != null) {
if(res == EMPTY_JAVA_ELEMENTS) {
res = new ArrayList<IJavaElement>();
}
res.add(el);
return res;
}
}
}
return res;
}
public static ISeamMessages getSeamMessagesComponentVariable(ISeamContextVariable variable) {
if (variable instanceof ISeamMessages) {
return (ISeamMessages)variable;
} else if (variable instanceof ISeamXmlFactory) {
ISeamXmlFactory factory = (ISeamXmlFactory)variable;
String value = factory.getValue();
if (value != null && value.length() > 0) {
if (value.startsWith("#{") || value.startsWith("${")) //$NON-NLS-1$ //$NON-NLS-2$
value = value.substring(2);
if (value.endsWith("}")) //$NON-NLS-1$
value = value.substring(0, value.length() - 1);
}
if (value != null && value.length() > 0) {
ISeamProject p = ((ISeamElement)factory).getSeamProject();
if (p != null) {
List<ISeamContextVariable> resolvedValues = SeamExpressionResolver.resolveVariables(p, null, value, true);
for (ISeamContextVariable var : resolvedValues) {
if (var.getName().equals(value)) {
if (var instanceof ISeamMessages) {
return (ISeamMessages)var;
}
}
}
}
}
} else if(variable instanceof ISeamContextShortVariable) {
ISeamContextShortVariable sv = (ISeamContextShortVariable)variable;
return getSeamMessagesComponentVariable(sv.getOriginal());
}
return null;
}
public static boolean isSeamMessagesComponentVariable(ISeamContextVariable variable) {
return (null != getSeamMessagesComponentVariable(variable));
}
@Override
protected boolean isStaticMethodsCollectingEnabled() {
return true; // Static methods are always enabled for Seam
}
}