/*
* Copyright 2013 The WicketForge-Team
*
* Licensed 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 wicketforge.psi.hierarchy;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wicketforge.util.WicketPsiUtil;
import java.util.*;
class ClassWicketIdReferences {
private final Map<PsiElement, List<PsiCallExpression>> addMap;
private final Map<PsiCallExpression, ClassWicketIdNewComponentItem> newComponentItemMap;
private ClassWicketIdReferences(@NotNull Map<PsiElement, List<PsiCallExpression>> addMap, @NotNull Map<PsiCallExpression, ClassWicketIdNewComponentItem> newComponentItemMap) {
this.addMap = addMap;
this.newComponentItemMap = newComponentItemMap;
}
/**
* @param psiElement PsiClass or PsiCallExpression reference from a WicketMarkup component
* @return List with all added wicket components (PsiCallExpression)
*/
@Nullable
public List<PsiCallExpression> getAdded(@Nullable PsiElement psiElement) {
return addMap.get(psiElement);
}
/**
* @param callExpression
* @return
*/
@Nullable
public ClassWicketIdNewComponentItem getNewComponentItem(@Nullable PsiCallExpression callExpression) {
return newComponentItemMap.get(callExpression);
}
/**
* @return
*/
public boolean containsClass(@NotNull PsiClass psiClass) {
return addMap.containsKey(psiClass);
}
public static ClassWicketIdReferences build(@NotNull final PsiClass psiClass) {
return build(psiClass, true);
}
public static ClassWicketIdReferences build(@NotNull final PsiClass psiClass, final boolean onlyThisMarkupContainer) {
final Map<PsiElement, List<PsiCallExpression>> componentAddMap = new HashMap<PsiElement, List<PsiCallExpression>>(); // Key: PsiClass or PsiCallExpression reference from a WicketMarkup component
final Map<PsiElement, List<PsiCallExpression>> componentReplaceMap = new HashMap<PsiElement, List<PsiCallExpression>>();
psiClass.accept(new JavaRecursiveElementVisitor() {
private MarkupReferences markupReferences = new MarkupReferences();
@Override
public void visitClass(PsiClass aClass) {
if (onlyThisMarkupContainer && !aClass.equals(psiClass) && WicketPsiUtil.isWicketComponentWithAssociatedMarkup(aClass)) {
return; // we do not visit inner classes that have own markup
}
if (!(aClass instanceof PsiAnonymousClass) && WicketPsiUtil.isMarkupContainer(aClass)) {
markupReferences.pushCurrent(new SmartList<PsiElement>(aClass));
super.visitClass(aClass);
markupReferences.popCurrent();
} else {
super.visitClass(aClass);
}
}
@Override
public void visitNewExpression(PsiNewExpression expression) {
PsiClass aClass = expression.getAnonymousClass();
if (aClass != null && WicketPsiUtil.isMarkupContainer(aClass)) {
markupReferences.pushCurrent(new SmartList<PsiElement>(expression));
super.visitNewExpression(expression);
markupReferences.popCurrent();
} else {
super.visitNewExpression(expression);
}
}
@Override
public void visitMethod(PsiMethod method) {
// if we have 'populateItem' method () -> we add parameter variable to our var stack, so item.add(...)
// could be resolved to ListView/Loop hierarchy.
// We dont need to check if its populateItem from a specific class (ex ListView) because first visitCallExpression
// checks if add is from a MarkupContainer -> then he tries to resolve variable. If we have a variable in our stack
// that is not from wicket populateItem, this does not matter.
// (Normally we would check this but AbstractRepeater has only a onPopulate method. Only Loop and ListView have populateItem
// these are not inherited. So we dont make this (security) check for now...)
if ("populateItem".equals(method.getName())) {
PsiParameter[] parameters = method.getParameterList().getParameters();
if (parameters.length > 0) {
markupReferences.put(parameters[0], markupReferences.getCurrent());
markupReferences.pushCurrent(null); // enhancement 80 -> we have no currentMarkupReference inside populateItem -> should use item.add that can be resolved...
super.visitMethod(method);
markupReferences.popCurrent();
return; // in this case super already done...
}
}
super.visitMethod(method);
}
@Override
public void visitCallExpression(PsiCallExpression callExpression) {
// first super, so assignement adds could be resolved, ex: add(link = new Link(...))
super.visitCallExpression(callExpression);
if (!(callExpression instanceof PsiMethodCallExpression)) {
return;
}
PsiMethod method = callExpression.resolveMethod();
if (method == null) {
return;
}
PsiClass methodCallClass = method.getContainingClass();
if (methodCallClass == null) {
return;
}
String methodName = method.getName();
Map<PsiElement, List<PsiCallExpression>> currentComponentMap;
if (ArrayUtil.contains(methodName, "add", "addOrReplace", "autoAdd", "replace", "addToBorder", "replaceInBorder") && WicketPsiUtil.isMarkupContainer(methodCallClass)) {
currentComponentMap = componentAddMap;
} else if (ArrayUtil.contains(methodName, "replaceWith") && WicketPsiUtil.isWicketComponent(methodCallClass)) {
currentComponentMap = componentReplaceMap;
} else {
return;
}
// the markupReference class for the given add(...) etc...
List<? extends PsiElement> markupReferenceList = null;
// if call expression has a reference we got to search for it. ex: link.add(...) or MyPage.this.add(...)
PsiReferenceExpression callMethodReference = PsiTreeUtil.getRequiredChildOfType(callExpression, PsiReferenceExpression.class);
PsiElement element = PsiTreeUtil.getChildOfAnyType(callMethodReference, PsiReferenceExpression.class, PsiThisExpression.class);
if (element instanceof PsiReferenceExpression) {
// ex: link.add(...)
element = ((PsiReferenceExpression) element).resolve();
if (element instanceof PsiVariable) {
markupReferenceList = new SmartList<PsiElement>(markupReferences.get((PsiVariable) element));
if (currentComponentMap != componentReplaceMap) {
for (Iterator<? extends PsiElement> iterator = markupReferenceList.iterator(); iterator.hasNext(); ) {
PsiElement markupReference = iterator.next();
if (markupReference instanceof PsiCallExpression) { // check instanceOf, markupReference can also be PsiClass (issue 67)
// this one will be our markupReference
PsiClass classToBeCreated = WicketPsiUtil.getClassToBeCreated((PsiCallExpression) markupReference);
// just to be sure our markupReference is not one with own markup (ex: someone could add components to an instance of an inner panel, bad practice but possible)
// except borders, because they are WicketComponentWithAssociatedMarkup but add goes to BodyContainer of Border
if (classToBeCreated != null && !classToBeCreated.equals(psiClass) && WicketPsiUtil.isWicketComponentWithAssociatedMarkup(classToBeCreated) && !WicketPsiUtil.isWicketBorder(classToBeCreated)) {
iterator.remove();
}
}
}
}
}
} else if (element instanceof PsiThisExpression) {
// ex: MyPage.this.add(...)
element = ((PsiThisExpression) element).getQualifier();
if (element == null) {
// this.add(...) -> current
markupReferenceList = markupReferences.getCurrent();
} else {
// MyPage.this.add(...) -> resolve PsiClass references by this...
element = ((PsiJavaCodeReferenceElement) element).resolve();
if (element instanceof PsiClass) {
markupReferenceList = new SmartList<PsiElement>(element);
}
}
} else {
// no reference -> so add to our current
markupReferenceList = markupReferences.getCurrent();
}
// no markupReference to add -> return
if (markupReferenceList == null || markupReferenceList.isEmpty()) {
return;
}
// go thru all call argument expressions
PsiExpressionList callExpressionList = callExpression.getArgumentList();
if (callExpressionList != null) {
for (PsiElement markupReference : markupReferenceList) {
List<PsiCallExpression> addList = currentComponentMap.get(markupReference);
for (PsiExpression callParameterExpression : callExpressionList.getExpressions()) {
// resolve expressions for new wicket component
List<PsiCallExpression> callExpressions = resolveExpressionNewWicketComponent(callParameterExpression);
if (callExpressions.size() > 0) {
// and add to its
if (addList == null) {
addList = callExpressions;
currentComponentMap.put(markupReference, addList);
} else {
addList.addAll(callExpressions);
}
}
}
}
}
}
@Override
public void visitField(PsiField field) {
super.visitField(field);
// if field has an initializer...
PsiExpression initializer = field.getInitializer();
if (initializer != null) {
// put this into our var container
markupReferences.put(field, resolveExpressionNewWicketComponent(initializer));
}
}
@Override
public void visitDeclarationStatement(PsiDeclarationStatement statement) {
super.visitDeclarationStatement(statement);
for (PsiElement element : statement.getDeclaredElements()) {
// if is variable and has an initializer...
if (element instanceof PsiVariable) {
PsiExpression initializer = ((PsiVariable) element).getInitializer();
if (initializer != null) {
// put this into our var container
markupReferences.put((PsiVariable) element, resolveExpressionNewWicketComponent(initializer));
}
}
}
}
@Override
public void visitAssignmentExpression(PsiAssignmentExpression expression) {
super.visitAssignmentExpression(expression);
PsiExpression leftExpression = expression.getLExpression();
if (leftExpression instanceof PsiReference) {
PsiElement resolvedElement = ((PsiReference) leftExpression).resolve();
if (resolvedElement instanceof PsiVariable) {
PsiExpression initializer = expression.getRExpression();
if (initializer != null) {
// put assigned expression to our var container
markupReferences.put((PsiVariable) resolvedElement, resolveExpressionNewWicketComponent(initializer));
}
}
}
}
/**
*
* @param expression
* @return referenced PsiCallExpression's (if they are Wicket Components)
*/
@NotNull
private List<PsiCallExpression> resolveExpressionNewWicketComponent(@Nullable PsiExpression expression) {
List<PsiCallExpression> list = new SmartList<PsiCallExpression>();
if (expression instanceof PsiConditionalExpression) {
List<PsiCallExpression> callExpressions = resolveExpressionNewWicketComponentInternal(((PsiConditionalExpression) expression).getThenExpression());
if (callExpressions != null) {
list.addAll(callExpressions);
}
callExpressions = resolveExpressionNewWicketComponentInternal(((PsiConditionalExpression) expression).getElseExpression());
if (callExpressions != null) {
list.addAll(callExpressions);
}
} else {
List<PsiCallExpression> callExpressions = resolveExpressionNewWicketComponentInternal(expression);
if (callExpressions != null) {
list.addAll(callExpressions);
}
}
return list;
}
/**
*
*/
@Nullable
private List<PsiCallExpression> resolveExpressionNewWicketComponentInternal(@Nullable PsiExpression expression) {
// get new Expression/Variable from method chaining ex: new Label(...).setOutputMarkupId(true).setEnabled(true);
if (expression instanceof PsiMethodCallExpression) {
PsiMethodCallExpression methodCallExpression = null;
while (expression instanceof PsiMethodCallExpression) {
methodCallExpression = (PsiMethodCallExpression) expression;
// check for ComponentFactory (like DateTextField.forDatePattern(...))
PsiClass classToBeCreated = WicketPsiUtil.getClassToBeCreated(methodCallExpression);
if (classToBeCreated != null && WicketPsiUtil.isWicketComponent(classToBeCreated)) {
return new SmartList<PsiCallExpression>(methodCallExpression);
}
PsiElement element = expression.getFirstChild();
if (element instanceof PsiReferenceExpression) {
element = element.getFirstChild();
if (element instanceof PsiExpression) {
expression = (PsiExpression) element;
} else {
break;
}
} else {
break;
}
}
// check if chaining-method returns wicketcomponent (issue 69)
if (expression instanceof PsiReferenceExpression) {
PsiMethod method = methodCallExpression.resolveMethod();
if (method == null) {
return null;
}
PsiClass returnClass = PsiUtil.resolveClassInClassTypeOnly(method.getReturnType());
if (returnClass == null || !WicketPsiUtil.isWicketComponent(returnClass) || "get".equals(method.getName())) {
return null;
}
}
}
// get Variable from assignement ex: add(link = new Link(...))
if (expression instanceof PsiAssignmentExpression) {
expression = ((PsiAssignmentExpression) expression).getLExpression();
}
// resolve
if (expression instanceof PsiReference) {
// if it's a reference -> find possibly callExpression from our var stack
PsiElement resolvedElement = ((PsiReference) expression).resolve();
if (resolvedElement instanceof PsiVariable) {
List<PsiCallExpression> result = new SmartList<PsiCallExpression>();
for (PsiElement element : markupReferences.get((PsiVariable) resolvedElement)) {
if (element instanceof PsiCallExpression) {
result.add((PsiCallExpression) element);
}
}
return result;
}
} else if (expression instanceof PsiCallExpression) {
// check if its a new wicket component
PsiClass classToBeCreated = WicketPsiUtil.getClassToBeCreated((PsiCallExpression) expression);
if (classToBeCreated != null && WicketPsiUtil.isWicketComponent(classToBeCreated)) {
return new SmartList<PsiCallExpression>((PsiCallExpression) expression);
}
}
return null;
}
});
// merge all componentReplaceMap into componentAddMap
for (Map.Entry<PsiElement, List<PsiCallExpression>> entry : componentReplaceMap.entrySet()) {
// we need callExpression
PsiElement key = entry.getKey();
if (key instanceof PsiCallExpression) {
PsiCallExpression callExpression = (PsiCallExpression) key;
for (List<PsiCallExpression> list : componentAddMap.values()) {
if (list.contains(callExpression)) {
list.addAll(entry.getValue());
}
}
}
}
// put all new wicket component expressions to a list as ClassWicketIdNewComponentItem
Map<PsiCallExpression, ClassWicketIdNewComponentItem> newComponentItemMap = new HashMap<PsiCallExpression, ClassWicketIdNewComponentItem>();
for (List<PsiCallExpression> list : componentAddMap.values()) {
for (PsiCallExpression callExpression : list) {
if (!newComponentItemMap.containsKey(callExpression)) {
ClassWicketIdNewComponentItem newComponentItem = ClassWicketIdNewComponentItem.create(callExpression);
if (newComponentItem != null) {
newComponentItemMap.put(callExpression, newComponentItem);
}
}
}
}
return new ClassWicketIdReferences(componentAddMap, newComponentItemMap);
}
private static final class MarkupReferences {
private Stack<List<? extends PsiElement>> currentStack = new Stack<List<? extends PsiElement>>();
private Map<PsiVariable, List<? extends PsiElement>> variableMap = new HashMap<PsiVariable, List<? extends PsiElement>>();
/**
* Get PsiClass or PsiCallExpression as MarkupContainer of WicketComponent from variable.
*
* @param variable PsiVariable
* @return PsiClass, PsiCallExpression of WicketComponent or null.
*/
@NotNull
private List<? extends PsiElement> get(@NotNull PsiVariable variable) {
List<? extends PsiElement> result = variableMap.get(variable);
return result == null ? Collections.<PsiElement>emptyList() : result;
}
/**
* Put assigned PsiClass or PsiCallExpression of a WicketComponent to variable into our variableMap.
*
* @param variable PsiVariable
* @param elements PsiClass or PsiCallExpression
*/
private void put(@NotNull PsiVariable variable, @Nullable List<? extends PsiElement> elements) {
if (elements == null || elements.isEmpty()) {
// remove variable
variableMap.remove(variable);
} else {
// ...add to our stack
variableMap.put(variable, elements);
}
}
/**
* current is -> the current reference for add(...) -> PsiClass or PsiCallExpression (for anonymous classes)
* @param current
*/
private void pushCurrent(@Nullable List<? extends PsiElement> current) {
currentStack.push(current);
}
@Nullable
private List<? extends PsiElement> popCurrent() {
return currentStack.pop();
}
@Nullable
private List<? extends PsiElement> getCurrent() {
return currentStack.isEmpty() ? null : currentStack.peek();
}
}
}