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.

 

 

 

 

0
43 comments

Hi @Yann,

Do you know if there's a built in converter from String to Java Object types? I put Integer and Boolean on my XmlAttribute and it does the conversion automatically but when I put Long it fails to do the conversion. I wrote a converter by hand, but I wonder if there is one that does the magic.

Thanks

0

Yes, indeed - that's an interesting find. com.intellij.util.xml.impl.ConverterManagerImpl#ConverterManagerImpl does not register default/fallback converter for java.lang.Long. Will discuss whether we should change this. You can take/reuse com.intellij.util.xml.converters.values.NumberValueConverter for now.

0

Thanks a lot @Yann, I will reuse Number converter.

0

We will provide more "out-of-the-box" converters, please watch https://youtrack.jetbrains.com/issue/IDEA-166505

2

Thanks @Yann,

That will be very helpful.

Now I have another question:

I have this XmlTag 

<activiti:formProperty id="addressId" type="long" expression="${address.id}" writable="false"></activiti:formProperty>

In this case, the attribute "type" vary from tag to tag and attribute "expression" holds different values. It can be an expression (like above), a value (like 'abc', 1234 etc). I am using the Dom API and I declared a base interface for xml tag formProperty and a few children of this base that will vary the type of expression, based on type. I extended TypeChooser for this and I return each children type based on the attribute "type". This works fine, I added missing converters for Java Object types and now it does the error highlight for the types that match.

My problem is that I have many children interfaces from the base. I have something like 5 interfaces now and this will increase once I start changing types for other xml tags.

I tried to extend GenericAttributeValue<T> and to return my T using a converter on this attribute, but due to type erasure is not possible. Is there another option to having n inheritors for each variation of attribute "type"? 

Thanks in advance

0

If the only difference is "dynamic" converter type for "expression" attribute, you can try approach with WrappingConverter to return "matching" converter according to "type" attribute value.

0

Hi @Yann, me again :)

First of all, thanks for the WrappingConverter response, it works like a charm.

Now I have a few questions:

1. I tried to convert my plugin to a Maven project. I added the pom.xml file, Maven compile works, it generates the jar and the plugin works if installed. So far so good. My problem here is that when I try to debug the plugin I get an error from IntelliJ: 

java.lang.Throwable: Extension class com.pay4.activiti.plugin.core.ActivitiDomFileDescription does not implement class com.intellij.util.xml.DomFileDescription. It came from ExtensionComponentAdapter[com.pay4.activiti.plugin.core.ActivitiDomFileDescription]: plugin=PluginDescriptor[name='Activiti BPMN', classpath='C:\Users\george.racu\.IntelliJIdea2016.3\system\plugins-sandbox\plugins\activiti-plugin']

The same code works fine if I remove the pom.xml file. Would you need more info here, please let me know.

2. PsiAdapter#getTypeClassName returns a trailing square bracket if the PsiType is like List<AnyTypeHere>. The return will be "AnyTypeHere>". I used this method for the return type of a method like this:

PsiMethod myMethod = getPsiMethod();
PsiType psiType = myMethod.getReturnType();
String typeClassName = PsiAdapter.getTypeClassName(psiType);

Maybe this is a bug or I use this method where I shouldn't.

3. I would like to start writing some tests. For example I have this piece of XML:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.example.com/bpmn">
<process id="NewProcess" name="New Process" isExecutable="true" activiti:candidateStarterUsers="anonymousUser" activiti:candidateStarterGroups="WithPermission(default)">
<startEvent id="startevent1" name="Start" activiti:initiator="initiator" activiti:formKey="newProcessForm">
<extensionElements>
<activiti:formProperty id="companyDetails" type="bean" default="com.example.CompanyDetails"></activiti:formProperty>
</extensionElements>
<startEvent>
</process?
</definitions>

And I would like to test that I have the correct reference for attribute "default". So far I followed the tutorial on how to structure the project for tests, I have the above xml in testData folder and I wrote a small CodeInsightTest class that extends LightCodeInsightFixtureTestCase but I am stuck at how to retrieve the references for attribute "default".

Many thanks,

George

 

0

Hi,

1. is most certainly a classpath issue, you might be compiling/running against different versions of IJ SDK? also I'd suggest to take a look at building your plugin using provided gradle plugin, http://www.jetbrains.org/intellij/sdk/docs/tutorials/build_system.html which handles all the little details for you

2. use com.intellij.psi.util.PsiTypesUtil#getPsiClass and then its com.intellij.psi.PsiClass#getQualifiedName instead

3. put <caret> at the position and use com.intellij.testFramework.fixtures.CodeInsightTestFixture#getReferenceAtCaretPosition

 

HTH, Yann

0

Thank a lot @Yann for all the help. I finished most of the basic functionality for my plugin now. I am left with testing, but that's a story for another time.

When you come to London give me a shout and I'll buy you a drink, or more :).

Many thanks,

George

0

Glad to hear :) Looking forward to see your plugin going live

0

I don't know if it will go open source or public on JetBrains repository as is very customized to our own extension of Activiti but who knows, maybe one day. We started using it internally and people seem to be happy with, so let's assume success :). Next step is to write many tests before adding more functionality.

Many thanks

0

Thanks @Yann, I saw the converters in the new release. Good work :)

0

Thanks for the converters.  Useful stuff.

0

Please sign in to leave a comment.