package com.anjlab.eclipse.tapestry5;
import static com.anjlab.eclipse.tapestry5.EclipseUtils.evalExpression;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.internal.ui.text.javadoc.JavadocContentAccess2;
@SuppressWarnings("restriction")
public class TapestryComponentSpecification
{
private TapestryContext tapestryContext;
private List<Property> properties = new ArrayList<Property>();
private List<Parameter> parameters = new ArrayList<Parameter>();
private List<Component> components = new ArrayList<Component>();
public static TapestryComponentSpecification EMPTY = new TapestryComponentSpecification(null, null);
public TapestryComponentSpecification(IType type, TapestryContext tapestryContext)
{
if (type == null)
{
// Empty specification
return;
}
this.tapestryContext = tapestryContext;
try
{
findAllProperties(type);
findAllParameters(type);
findAllEmbeddedComponents(type);
}
catch (JavaModelException e)
{
Activator.getDefault().logError("Error reading component fields", e);
}
}
public TapestryContext getTapestryContext()
{
return tapestryContext;
}
private void findAllEmbeddedComponents(IType type) throws JavaModelException
{
findEmbeddedComponents(type);
IType superclass = findSuperclass(type);
if (superclass != null)
{
findEmbeddedComponents(superclass);
}
}
private void findAllProperties(IType type) throws JavaModelException
{
findProperties(type);
IType superclass = findSuperclass(type);
if (superclass != null)
{
findProperties(superclass);
}
}
private void findAllParameters(IType type) throws JavaModelException
{
findParameters(type);
IType superclass = findSuperclass(type);
if (superclass != null)
{
findParameters(superclass);
}
}
protected IType findSuperclass(IType type) throws JavaModelException
{
String superclassName = type.getSuperclassName();
if (superclassName == null)
{
// Return "java.lang.Object"?
return null;
}
String className = EclipseUtils.toClassNameFromImports(
type.getJavaProject().getProject(),
type,
superclassName);
IType superclass = EclipseUtils.findTypeDeclaration(
type.getJavaProject().getProject(),
IJavaSearchConstants.CLASS,
className);
return superclass;
}
private interface AnnotatedFieldHandler
{
void handle(IType type, IField field, IAnnotation annotation) throws JavaModelException;
}
private void findEmbeddedComponents(IType type) throws JavaModelException
{
handleAnnotatedFields(type, "org.apache.tapestry5.annotations.Component", new AnnotatedFieldHandler()
{
@Override
public void handle(IType type, IField field, IAnnotation annotation) throws JavaModelException
{
components.add(createComponent(type, field, annotation));
}
});
handleAnnotatedFields(type, "org.apache.tapestry5.annotations.InjectComponent", new AnnotatedFieldHandler()
{
@Override
public void handle(IType type, IField field, IAnnotation annotation) throws JavaModelException
{
Component newComponent = createComponentFromInjection(type, field, annotation);
// Find if there's any components already added with this ID, maybe via @Component annotation
// TODO Lookup component by name more efficiently
for (Component component : components)
{
if (StringUtils.equalsIgnoreCase(component.getId(), newComponent.getId()))
{
return;
}
}
components.add(newComponent);
}
});
}
private Component createComponentFromInjection(IType type, final IField field, IAnnotation annotation) throws JavaModelException
{
Component component = new Component();
component.setSpecification(this);
component.setName(field.getElementName());
component.setNameRange(field.getNameRange());
component.setJavadocValue(new LazyValue<String>()
{
@Override
protected String eval() throws CoreException
{
return JavadocContentAccess2.getHTMLContent(field, true);
}
});
for (IMemberValuePair pair : annotation.getMemberValuePairs())
{
component.setId(String.valueOf(pair.getValue()));
}
setComponentDefaults(type, field, component);
return component;
}
protected Component createComponent(IType type, final IField field, IAnnotation annotation) throws JavaModelException
{
Component component = new Component();
component.setSpecification(this);
component.setName(field.getElementName());
component.setNameRange(field.getNameRange());
component.setJavadocValue(new LazyValue<String>()
{
@Override
protected String eval() throws CoreException
{
return JavadocContentAccess2.getHTMLContent(field, true);
}
});
for (IMemberValuePair pair : annotation.getMemberValuePairs())
{
if ("id".equals(pair.getMemberName()))
{
component.setId(String.valueOf(pair.getValue()));
}
else if ("type".equals(pair.getMemberName()))
{
component.setType(String.valueOf(pair.getValue()));
}
else if ("inheritInformalParameters".equals(pair.getMemberName()))
{
component.setInheritInformalParameters("true".equals(String.valueOf(pair.getValue())));
}
else if ("publishParameters".equals(pair.getMemberName()))
{
component.setPublishParameters(String.valueOf(pair.getValue()));
}
else if ("parameters".equals(pair.getMemberName()))
{
if (pair.getValue() instanceof Object[])
{
Object[] values = (Object[]) pair.getValue();
String[] parameters = new String[values.length];
for (int i = 0; i < values.length; i++)
{
parameters[i] = String.valueOf(values[i]);
}
component.setParameters(parameters);
}
else if (pair.getValue() instanceof String)
{
component.setParameters(new String[] { (String) pair.getValue() });
}
}
}
setComponentDefaults(type, field, component);
// TODO See if there's any @Mixins or @MixinClasses attached to this field
return component;
}
private void setComponentDefaults(IType type, IField field, Component component) throws JavaModelException
{
if (StringUtils.isEmpty(component.getId()))
{
component.setId(component.getName());
}
if (StringUtils.isEmpty(component.getType()))
{
String typeName = EclipseUtils.resolveTypeNameForMember(type, field, field.getTypeSignature());
component.setType(typeName);
component.setJavaType(true);
}
}
private void findParameters(IType type) throws JavaModelException
{
handleAnnotatedFields(type, "org.apache.tapestry5.annotations.Parameter", new AnnotatedFieldHandler()
{
@Override
public void handle(IType type, IField field, IAnnotation annotation) throws JavaModelException
{
parameters.add(createParameter(type, field, annotation));
}
});
}
protected void handleAnnotatedFields(IType type, String annotationFQName, AnnotatedFieldHandler handler) throws JavaModelException
{
String annotationPackage = annotationFQName.substring(0, annotationFQName.lastIndexOf('.'));
String annotationSimpleName = annotationFQName.substring(annotationFQName.lastIndexOf('.') + 1);
boolean annotationInImports = false;
ICompilationUnit compilationUnit = type.getCompilationUnit();
if (compilationUnit != null)
{
for (IImportDeclaration importDecl : compilationUnit.getImports())
{
annotationInImports =
importDecl.getElementName().equals(annotationFQName)
|| importDecl.getElementName().equals(annotationPackage + ".*");
if (annotationInImports)
{
break;
}
}
}
for (IField field : type.getFields())
{
for (IAnnotation annotation : field.getAnnotations())
{
if (annotation.getElementName().equals(annotationFQName)
|| (annotation.getElementName().equals(annotationSimpleName) && annotationInImports))
{
handler.handle(type, field, annotation);
}
}
}
}
protected Parameter createParameter(IType type, final IField field, IAnnotation annotation) throws JavaModelException
{
Parameter parameter = new Parameter();
parameter.setSpecification(this);
parameter.setName(field.getElementName());
parameter.setNameRange(field.getNameRange());
parameter.setJavadocValue(new LazyValue<String>()
{
@Override
protected String eval() throws CoreException
{
return JavadocContentAccess2.getHTMLContent(field, true);
}
});
for (IMemberValuePair pair : annotation.getMemberValuePairs())
{
if ("value".equals(pair.getMemberName()))
{
parameter.setValue(String.valueOf(pair.getValue()));
}
else if ("required".equals(pair.getMemberName()))
{
parameter.setRequired("true".equals(String.valueOf(pair.getValue())));
}
else if ("principal".equals(pair.getMemberName()))
{
parameter.setPrincipal("true".equals(String.valueOf(pair.getValue())));
}
else if ("name".equals(pair.getMemberName()))
{
parameter.setName(String.valueOf(pair.getValue()));
}
else if ("defaultPrefix".equals(pair.getMemberName()))
{
parameter.setDefaultPrefix(
evalExpression(type.getJavaProject().getProject(), pair.getValue()));
}
else if ("cache".equals(pair.getMemberName()))
{
parameter.setCache("true".equals(String.valueOf(pair.getValue())));
}
else if ("autoconnect".equals(pair.getMemberName()))
{
parameter.setAutoconnect("true".equals(String.valueOf(pair.getValue())));
}
else if ("allowNull".equals(pair.getMemberName()))
{
parameter.setAllowNull("true".equals(String.valueOf(pair.getValue())));
}
}
return parameter;
}
private void findProperties(IType type) throws JavaModelException
{
handleAnnotatedFields(type, "org.apache.tapestry5.annotations.Property", new AnnotatedFieldHandler()
{
@Override
public void handle(IType type, IField field, IAnnotation annotation) throws JavaModelException
{
properties.add(createProperty(type, field, annotation));
}
});
for (IMethod method : type.getMethods())
{
Property property;
property = findProperty(method, "get");
if (property != null)
{
property.setRead(true);
continue;
}
property = findProperty(method, "is");
if (property != null)
{
property.setRead(true);
continue;
}
property = findProperty(method, "set");
if (property != null)
{
property.setWrite(true);
continue;
}
}
}
private Property findProperty(IMethod method, String prefixName) throws JavaModelException
{
Property property = null;
if (method.getElementName().startsWith(prefixName))
{
if (method.getElementName().length() <= prefixName.length())
{
return null;
}
String propertyName = method.getElementName().substring(prefixName.length());
propertyName = Character.toLowerCase(propertyName.charAt(0))
+ (propertyName.length() == 1 ? "" : propertyName.substring(1));
property = getProperty(propertyName);
if (property == null)
{
property = createProperty(method, propertyName);
properties.add(property);
}
}
return property;
}
private Property getProperty(String propertyName)
{
for (Property property : properties)
{
if (propertyName.equals(property.getName()))
{
return property;
}
}
return null;
}
protected Property createProperty(IType type, IField field, IAnnotation annotation) throws JavaModelException
{
Property property = createProperty(field, field.getElementName());
for (IMemberValuePair pair : annotation.getMemberValuePairs())
{
if ("read".equals(pair.getMemberName()))
{
property.setRead("true".equals(String.valueOf(pair.getValue())));
}
else if ("write".equals(pair.getMemberName()))
{
property.setWrite("true".equals(String.valueOf(pair.getValue())));
}
}
return property;
}
private Property createProperty(final IMember member, String name) throws JavaModelException
{
Property property = new Property();
property.setSpecification(this);
property.setName(name);
property.setNameRange(member.getNameRange());
property.setJavadocValue(new LazyValue<String>()
{
@Override
protected String eval() throws CoreException
{
return JavadocContentAccess2.getHTMLContent(member, true);
}
});
return property;
}
public List<Component> getComponents()
{
return Collections.unmodifiableList(components);
}
public List<Parameter> getParameters()
{
return Collections.unmodifiableList(parameters);
}
/**
* TODO Optimize this heavy method
*
* TODO Support a list of applied t:mixins,
* probably better implement t:mixins support on a higher level than component specification
*
* @param tapestryProject
* @return parameters of this component including published parameters of its embedded components
*/
public List<Parameter> getParameters(TapestryProject tapestryProject)
{
// TODO Cache results somehow?
List<Parameter> allParameters = new ArrayList<Parameter>();
allParameters.addAll(parameters);
for (Component component : components)
{
if (StringUtils.isEmpty(component.getPublishParameters()))
{
continue;
}
String componentName = TapestryUtils.getComponentName(tapestryProject, component);
if (componentName != null)
{
try
{
TapestryContext componentContext = tapestryProject.findComponentContext(componentName);
TapestryComponentSpecification componentSpecification = componentContext.getSpecification();
List<Parameter> componentParameters = componentSpecification.getParameters();
String[] publishedParameters = component.getPublishParameters().split(",");
for (String publishedParameter : publishedParameters)
{
for (Parameter parameter : componentParameters)
{
if (StringUtils.equals(publishedParameter.trim(), parameter.getName()))
{
allParameters.add(parameter);
break;
}
}
}
}
catch (JavaModelException e)
{
// Ignore
}
}
}
return allParameters;
}
public List<Property> getProperties()
{
return Collections.unmodifiableList(properties);
}
public Parameter getParameter(TapestryProject tapestryProject, String name)
{
for (Parameter parameter : getParameters(tapestryProject))
{
if (StringUtils.equalsIgnoreCase(name, parameter.getName()))
{
return parameter;
}
}
return null;
}
}