Architecture of a plugin for XML attribute value validation

Answered

Hi guys,

Recently I started writing a plugin for IntelliJ IDEA that should provide completion for XML attributes. The value for attributes comes from Java classes, methods and fields. The XML file in case is a BPMN file that is generated by Activiti. 

For example: 

<activiti:formProperty id="companyDetails" type="bean" default="com.company.CompanyDetails" readable="false"></activiti:formProperty>

My plugin should check if in the current project context there is a bean "com.company.CompanyDetails". If there isn't such a thing then highlight it as an error. Other future functionality should check for fields and methods in expressions.

So far I followed the tutorial on how to access XML tags and attributes and how the extract values from them. The functionality described above exists in the IDEA, for example when I edit plugin.xml file.

My question here is: what is the right approach to achieve my goal? What functionality does IDEA already provides and what else do I need to write to make it work? What concepts should be used in such a project?

This is my first experience in writing a plugin for IDEA but the platform looks very promising.

Thanks for your help.

 

 

 

 

43 comments
Comment actions Permalink
Official comment

Hi,

I'd strongly suggest you to use DOM API http://www.jetbrains.org/intellij/sdk/docs/reference_guide/frameworks_and_external_apis/xml_dom_api.html.

It is much easier to provide "semantic" features than dealing with low level XML PSI.

For integration with Spring please see http://www.jetbrains.org/intellij/sdk/docs/reference_guide/frameworks_and_external_apis/spring_api.html

Comment actions Permalink

Hi Yann,

Thank you for your help.

Currently I am using the DOM API and I created all the interfaces that inherit from DomElement to get to the tag `activity:formProperty` from above. I added an attribute to get access to my fqdn in that XML tag.

@Attribute("default")
GenericAttributeValue<PsiClass> getAttributeDefault();

Next I implemented a simple version of 

BasicDomElementsInspection<DomElement>

Where I override only checkDomElement.

And I registered an extension for localInspection

<localInspection language="XML" implementationClass="DomFileInspection" enabledByDefault="true"
shortName="ActivitiXMLInspection" level="ERROR" displayName="Activiti XML inspection"/>

Now IDEA will do check if my fqdn are valid in the project's context and highlight any errors.

My next step is take attribute 

id="companyDetails"

And somehow to use it for later code inspection. For example this attribute value is used in other XML tags like this:

<activiti:formProperty id="companyName" expression="#{companyDetails.companyName}"
required="true"></activiti:formProperty>

How should I go next and do code inspection for 

companyDetails.companyName

 

0
Comment actions Permalink

Please see DOM tutorial linked above, Section "Resolving", 2nd paragraph (@NameValue + GenericAttributeValue<YourDomClass>)

There's also some OSS plugins using DOM API linked on the bottom of the page for further references.

HTH,

 Yann

0
Comment actions Permalink

Thanks @Yann, I'm working with the XML DOM API in front of me. Trying to figure out how to get the type of an attribute based on another attribute from the same tag.

All the best,

George

 

0
Comment actions Permalink

Hi Yann,

Unfortunately I didn't manage to do much progress using XML DOM API so I went and extended 

XmlRecursiveElementVisitor

In my extension I override 

visitXmlAttribute

and I do all the checks by hand. For example for an attribute like `expression` 

<activiti:formProperty id="companyName" expression="#{companyDetails.companyName}" required="true"></activiti:formProperty>

I split the String by hand and try to find PsiClass, PsiMethod and PsiVariable by hand, using JavaPsiFacade or PsiShortNamesCache. If I cannot find any of the above then I add a ProblemDescriptor for that XmlAttribute. Is this the right approach for my case, or is there a more elegant solution provided by the IntelliJ's APIs?

Thanks,

George

 

0
Comment actions Permalink

I assume you're referring to

#{companyDetails.companyName}

?

You can provide "normal" PsiReference by implementing additionally com.intellij.util.xml.CustomReferenceConverter for this DOM attribute's converter and return corresponding PsiReference(s) for elements (split on ".").

0
Comment actions Permalink

Yes, I'm referring to 

#{companyDetails.companyName}

I will give it a try using your suggestion. 

Thanks

0
Comment actions Permalink

Hi Yann,

I've created a CustomReferenceConverter with signature

public class ExpressionReferenceConverter extends Converter<PsiReference> implements CustomReferenceConverter<PsiReference> {
@NotNull
@Override
public PsiReference[] createReferences(GenericDomValue genericDomValue, PsiElement psiElement, ConvertContext convertContext) {

And inside this method I try to get references for a Java class. Using PsiShortNamesCache I search for class "CompanyDetails" and I get a not null PsiClass object called myPsiClass. That means that my class exists in my project. In order to enable syntax highlight and code completion I need to create a PsiReference that will make my string "companyDetails" to be a PsiReference to the "myPsiClass"?

Thanks,

George

 

 

0
Comment actions Permalink

Hello George,

 

please check out com.intellij.util.xml.converters.ClassValueConverterImpl it should serve as good inspiration :)

0
Comment actions Permalink

Thanks @Yann, I didn't update my question recently, so now I am able to create references. I extend the

com.intellij.util.xml.PsiClassConverter

class and I override the createReferences method to do the String manipulation that I need. So far so good, but the string now is highlighted with red and it complains that "Cannot resolve class xxx".

Further on I extended 

com.intellij.util.xml.ResolvingConverter<PsiClass>

and here I override the method fromString and if I have a valid PsiClass I will return it, otherwise null. With this converter active, my string is not red anymore and Ctrl + click works, so I have navigation to declaration. This method doesn't provide code completion though.

How does IntelliJ provide code completion and syntax highlight for an XML attribute of this type?

<tag default="com.example.MyAwesomeClass"></tag>

Because in my interface that extends DomElement I declared this attribute as 

@Attribute("default")
GenericAttributeValue<PsiClass> getDefaultAttr();

and IntelliJ does everything for me: package navigation, syntax highlight, code completion. That is the type of functionality I'm looking to achieve with my other xml attributes.

Many thanks,

George

0
Comment actions Permalink

Completion is provided by the PsiReferences you return. Please investigate that you return correct TextRange(s) for all references.

0
Comment actions Permalink

Do you mean by the

public Collection<PsiReference> getVariants(ConvertContext context) 

method from ResolvingConverter?

Thanks @Yann

0
Comment actions Permalink

no, the variants returned via com.intellij.psi.PsiReference#getVariants. you wouldn't need ResolvingConverter when you use CustomReferenceConverter

0
Comment actions Permalink

Hi @Yann,

So far I tried 2 options in order to return an instance of PsiReference:

1. I create a new PsiJavaCodeReferenceElement for a string between two dots. Eg myClass.myMethod(): I create the reference element for myClass. This is what I use in method createReferences() when I override it from CustomReferenceConverter. With this method I get the text highlighted with red if the class doesn't exist.

PsiJavaCodeReferenceElement classReferenceElement =
JavaPsiFacade.getElementFactory(project)
.createReferenceElementByFQClassName(
beanClass.getQualifiedName(),
GlobalSearchScope.projectScope(project));

2. I try to use MyCustomReference to override getVariants() to return variants. An object of this class I use instead of the above in the createReferences() of my CustomReferenceConverter. So far I don't have much progress on this variant.

class MyCustomReference extends PsiReferenceBase<PsiElement>

Which of the two approaches is more suitable in my case?

Many thanks,

George

 

 

0
Comment actions Permalink

I would strongly suggest to re-use existing class reference provider, e.g. via com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet

 

0
Comment actions Permalink

Or maybe extend 

package com.intellij.util.xml.PsiClassConverter

because this has most of what I am after?

Thanks @Yann

 

0
Comment actions Permalink

That's another possibility of course.

0
Comment actions Permalink

@Yann,

As a side note, very impressive the response time and the details. Sorry for being such a noob in this area.

Many thanks :)

0
Comment actions Permalink

You're welcome, we try to help as much as possible :)

0
Comment actions Permalink

Hi @Yann,

I went with 

public class ExpressionReferenceConverter extends Converter<PsiReference> implements CustomReferenceConverter<PsiReference> {

and in this class when createReferences() is called I return an instance of 

protected class MyLocalReference extends PsiReferenceBase<PsiElement>{}

MyLocalReference matches a PsiClass from first class name in attribute value. Now IDEA highlights values that contain an invalid PsiClass and also provides Go to declaration. Go to declaration happens on the whole value of the attribute, not only on the part where the class is. Eg "companyDetails" bellow.

This is an XML tag with attribute in question

<activiti:formProperty id="companyName" expression="#{companyDetails.companyName}" required="true"></activiti:formProperty>

And here is declared in an interface for tag <activiti:formProperty/>

@Nullable
@Attribute("expression")
@Convert(ExpressionReferenceConverter.class)
GenericAttributeValue<PsiReference> getAttributeExpression();

Next I try to provide autocompletion. I tried to return an Object[] from getVariants() in MyLocalReference but the editor doesn't show anything. It says "No suggestions" even that getVariants() returns a non empty array.

What is your advice in this matter?

Thanks again,

George

0
Comment actions Permalink

Hi @Yann,

I hope you had a good holiday break. While my plugin makes progress, I hit a blockage. In my interface that describes an XML Tag, I have an attribute called 

activiti:expression

The problem with this attribute is that when I declare it in an annotation like this

@Attribute("activiti:expression")
@Convert(ExpressionReferenceConverter.class)
GenericAttributeValue<PsiClass> getAttributeExpression();

it doesn't get picked up by IntelliJ. I believe this is because of the : in the attribute name.

Is this a limitation from IntelliJ or there is a way to use attributes with colon in name?

Thanks

 

0
Comment actions Permalink

It says "No suggestions" even that getVariants() returns a non empty array.

 

Most probably TextRange(s) not matching?

1
Comment actions Permalink


Please remove "activiti:" prefix from the value.

@Attribute("activiti:expression")
0
Comment actions Permalink

Hi @Yann,

I removed the "activiti:" prefix but still no luck. If I change the value in the @Attribute annotation and the name of the XML tag in the XML document then my 

ExpressionReferenceConverter

gets hit, otherwise when it has the ":" in the value it doesn't. I put breakpoints on getReferences() and fromString() on my converter but no luck, I don't get any hit.

Thanks.

 

 

0
Comment actions Permalink

The "activiti:" prefix is definitely wrong. Please double-check you don't have another "expression" attribute somewhere (in hierarchy?) in your DOM.

0
Comment actions Permalink

There is another attribute "expression" but on a different XML tag. For example:

<definitions> <process> <startEvent> <extensionElements> <activiti:formProperty expression="xyz"> </activiti:formProperty></...>

<definitions> <process> <serviceTask activity:expression="abc"> </serviceTask> </...>

In this case the first "expression" attribute gets picked up automatically but "activiti:expression" doesn't.

Both attributes are marked using @Attribute annotation in interfaces FormProperty and ServiceTask respectively and have the value declared in annotations. 

Thanks

0
Comment actions Permalink

Is the <formProperty> tag registered with activiti-namespace? Do you have ANY Dom support for it in the parent file? Is the <definitions> also DOM?

0
Comment actions Permalink

Here are my interfaces for Dom elements:

public interface ActivitiDomElement extends com.intellij.util.xml.DomElement {
String getValue();
void setValue(String s);
}
public interface TDefinitionsImpl extends ActivitiDomElement {
@SubTag("process")
ProcessTag getTagProcess();
}
public interface ProcessTag extends ActivitiDomElement {
@SubTag("startEvent")
StartEvent getStartEvent();

@SubTagList("serviceTask")
List<ServiceTask> getServiceTasks();
}
public interface StartEvent extends ActivitiDomElement {
@SubTag("extensionElements")
ExtensionElements getExtensionElements();
}
public interface ExtensionElements extends ActivitiDomElement {
@SubTagList("activiti:formProperty")
List<FormProperty> getFormProperties();

@SubTagList("activiti:taskListener")
List<TaskListener> getTaskListeners();
}
public interface FormProperty extends ActivitiDomElement {

@NameValue
@Attribute("id")
GenericAttributeValue<String> getAttributeId();

@Nullable
@Attribute("type")
GenericAttributeValue<FormPropertyTypeEnum> getAttributeType();

@Attribute("default")
GenericAttributeValue<PsiClass> getAttributeDefault();

@Nullable
@Attribute("expression")
@Convert(ExpressionReferenceConverter.class)
GenericAttributeValue<PsiReference> getAttributeExpression();

@SubTagList("activiti:value")
List<ValueTag> getActivitiValueTags();
}
public interface ServiceTask extends ActivitiDomElement {

@NameValue
@Attribute("id")
GenericAttributeValue<String> getAttributeId();

@Attribute("expression")
@Convert(ExpressionReferenceConverter.class)
GenericAttributeValue<PsiReference> getAttributeExpression();

@Attribute("resultVariableName")
GenericAttributeValue<PsiReference> getAttributeResultVariableName();
}
<dom.fileDescription implementation="co.uk.pay4.activiti.plugin.core.ActivitiDomFileDescription"/>
public class ActivitiDomFileDescription extends com.intellij.util.xml.DomFileDescription<TDefinitionsImpl> {
public ActivitiDomFileDescription() {
super(TDefinitionsImpl.class, "definitions");
}
}

There is no namespace prefix on any interface. I presume you refer to @Namespace on interface?

Thanks

P.S. please view my updated version with plugin.xml declaration as well.

 

 

 

0
Comment actions Permalink

I cannot judge from seeing some excerpt from your DOM :) Yes, you will need to register tags with their corresponding @Namespace and/or make sure the DomFileDescription namespace registration(s) does the right thing. To verify, add a dummy boolean attribute in your DOM and see if you get proper validation/completion.

1
Comment actions Permalink

Hi @Yann,

You were right, there was a namespace error.

Now I registered "activiti" namespace in DomFileDescription, I made an extension of GenericAttributeValue that I annotated with @Namespace("activiti") and I use this extension as definition for an attribute annotated with @Attribute("expression") and not @Attribute("activiti:expression"). My converter works now.

Stay tuned, I will come back with other questions.

Many thanks,

George

 

0

Please sign in to leave a comment.