xml_dom_api.md: cleanups

This commit is contained in:
Karol Lewandowski 2022-03-18 13:24:41 +01:00
parent 66a6a2c37e
commit 5f6869d16a

View File

@ -7,14 +7,15 @@
## Abstract
This article is intended for plugin writers who create custom web server integrations, or some UI for easy XML editing.
It describes the *Document Object Model* (DOM) in IntelliJ Platform --- an easy way to work with DTD or Schema-based XML models.
It describes the *Document Object Model* (DOM) in IntelliJ Platform - an easy way to work with DTD or Schema-based XML models.
The following topics will be covered: working with DOM itself (reading/writing tags content, attributes, and subtags) and easy XML editing in the UI by connecting UI to DOM.
It's assumed that the reader is familiar with Java, Swing, IntelliJ Platform XML PSI (classes [`XmlTag`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlTag.java), [`XmlFile`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlFile.java), [`XmlTagValue`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlTagValue.java), etc.), IntelliJ Platform plugin development basics (application and project components, file editors).
## Introduction
So, how to operate with XML from an IntelliJ Platform plugin? Usually, one has to take `XmlFile`, get its root tag, and then find a required sub-tag by path.
So, how to operate with XML from an IntelliJ Platform plugin?
Usually, one has to take `XmlFile`, get its root tag, and then find a required sub-tag by path.
The path consists of tag names, each of them a string.
Typing these everywhere is tedious and error-prone.
Let's assume you have the following XML:
@ -33,8 +34,12 @@ Let's say you want to read the contents of the second bar element, namely, "239"
It's _not_ correct to create chained calls like
```java
file.getDocument().getRootTag().findFirstSubTag("foo").
findSubTags("bar")[1].getValue().getTrimmedText()
file.getDocument()
.getRootTag()
.findFirstSubTag("foo")
.findSubTags("bar")[1]
.getValue()
.getTrimmedText();
```
because each call here may return `null`.
@ -59,7 +64,8 @@ if (document != null) {
}
```
Looks awful, doesn't it? But there's a better way to do the same thing.
Looks awful, doesn't it?
But there's a better way to do the same thing.
You just need to extend a special interface - [`DomElement`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/DomElement.java).
For example, let's create several interfaces:
@ -98,22 +104,23 @@ if (bars.size() > 1) {
}
```
I suppose this looks a little bit nicer.
I suppose this looks a little nicer.
You often work with your model in more than one place.
Re-creating the model is too inefficient, so we cache it for you, and any subsequent calls to `DomManager.getFileElement()` will return the same instance.
So, it is useful to invoke this method just once, and then keep everywhere only the "root" object you've obtained.
In this case you won't need to repeat that scary first line, and the code will look even nicer.
In this case, you won't need to repeat that scary first line, and the code will look even nicer.
It is also important to note that with this scenario we avoid potential `NullPointerException`: our DOM guarantees that every method accessing a tags child will return a not-null element, even if the correspondingly-named sub-tag doesn't exist.
That may seem strange at a first glance, but it appears to be rather convenient.
How does it work? Simple.
Given those interfaces, DOM generates all code for accessing correct sub-tags and creating model elements at runtime.
It is also important to note that with this scenario we avoid potential `NullPointerException`: our DOM guarantees that every method accessing a tag child will return a not-null element, even if the correspondingly-named sub-tag doesn't exist.
That may seem strange at first glance, but it appears to be rather convenient.
How does it work?
Simple.
Given those interfaces, DOM generates all the code for accessing correct subtags and creating model elements at runtime.
The sub-tag names and element types are taken from method names, return types and method annotations, if any.
In most cases annotations can be omitted, as in our example, but this is discussed further in this article anyway.
Now let us explore more thoroughly what the DOM can do, and look at possible ways of representing various XML concepts such as tag content, attributes or sub-tags.
Later we will discuss basic methods for working with the model, as well as cover more advanced functionality.
Finally, we'll see how to easily create an UI editor for DOM model elements.
Later, we will discuss basic methods for working with the model, as well as cover more advanced functionality.
Finally, we'll see how to easily create a UI editor for DOM model elements.
## Building the Model
@ -154,9 +161,9 @@ Please also note that your implementation should have a no-argument constructor.
Let us consider an interesting case when `T` represents an enum value.
Usually, the converter just searches for enum elements with the names specified in XML.
But sometimes, for their names, you may need or want to use values that are not valid Java identifiers.
For example, CMP version in EJB may be "1.x" or "2.x", but you can't create Java enums with such names.
For example, the CMP version in EJB may be "1.x" or "2.x", but you can't create Java enums with such names.
For such cases, let your enum implement `NamedEnum` interface, and then name your enum elements as you wish.
Now, just provide the `getValue()` implementation that will return the right value to match with XML contents, and voilà\!
Now, just provide the `getValue()` implementation that will return the right value to match with XML contents, and voilà!
In our example, the code will look as follows:
```java
@ -177,7 +184,7 @@ enum CmpVersion implements NamedEnum {
```
As we have already mentioned, an XML tag may have lots of artifacts besides its value: there can be attributes, children, but rather often (e.g., according to DTD or Schema) it should have only the value.
Of course such tags also need a DOM element to associate with.
Of course, such tags also need a DOM element to associate with.
And we provide such an element:
```java
@ -196,15 +203,16 @@ interface GenericDomValue<T> {
So, you can just specify a particular `T` when using this interface - and everything will work.
Methods that work with `String` are provided for many reasons.
For example, your `T` is [`PsiClass`](upsource:///java/java-psi-api/src/com/intellij/psi/PsiClass.java).
It would be useful to highlight invalid values in UI.
To get the value to highlight (the string from the XML file) we have the `getStringValue()` method.
It would be useful to highlight invalid values in the UI.
To get the value to highlight (the string from the XML file), we have the `getStringValue()` method.
The error message will be taken from the converter via `getErrorMessage()`.
### Attributes
Attributes are also rather simple to deal with.
You can read their values, set them, and operate with different types.
So it's natural to create something like `GenericDomValue<T>` and then work as usual. "Something like" will be an inheritor, as shown below:
So it's natural to create something like `GenericDomValue<T>` and then work as usual.
"Something like" will be an inheritor, as shown below:
```java
interface GenericAttributeValue<T> extends GenericDomValue<T> {
@ -219,7 +227,8 @@ Consider that you want to work with an attribute named _some-class_ having a val
GenericAttributeValue<PsiClass> getMyAttributeValue();
```
That's all\! Now you can get/set values, resolve this `PsiClass`, get its `String` representation, etc.
That's all!
Now you can get/set values, resolve this `PsiClass`, get its `String` representation, etc.
The name of the attribute will be taken from the method name (see next paragraph).
If you name your method in a special way, you can even omit the annotation.
For example:
@ -258,7 +267,7 @@ There's also an annotation to designate such children explicitly: `@SubTag`.
Its "value" attribute contains a tag name.
If it is not specified, the name is implied from the method name using the current name strategy.
Sometimes it is the sub-tag's presence that means something, rather than its content --- `<unchecked>` in EJB method permissions, for example.
Sometimes it is the sub-tag's presence that means something, rather than its content - `<unchecked>` in EJB method permissions, for example.
If it exists, then permissions are unchecked, otherwise checked.
For such things one should create a special `GenericDomValue<Boolean>` child.
Usually its `getValue()` returns `true` if there's "true" in a tag value, `false` if there's "false" in a tag value, and `null` otherwise.
@ -267,9 +276,10 @@ In this case, `getValue()` will return `true` if the tag exists and `false` othe
Let's consider another interesting example inspired by EJB, where there is a relation that has two roles, each designating one relation end: first role and second role.
Both are represented by tags with the same values.
So, we could create a collection of role elements, and every time we access some role we would check if this collection has sufficient number of elements.
So, we could create a collection of role elements, and every time we access some role we would check if this collection has a sufficient number of elements.
But one of the main purposes of the DOM is to eliminate unnecessary checks.
So why cant we have a fixed (more than one) number of children with the same tag name? Let's have them\!
So why can't we have a fixed (more than one) number of children with the same tag name?
Let's have them!
```java
@SubTag(value = "ejb-relationship-role", index = 0)
@ -279,9 +289,9 @@ EjbRelationshipRole getEjbRelationshipRole1();
EjbRelationshipRole getEjbRelationshipRole2();
```
The first method will return the DOM element for the first subtag named `<ejb-relationship-role>`, and the second --- for the second one.
Hence the term "fixed-number" for such children.
According to DTD or Schema, there should be fixed number of subtags with the given name.
The first method will return the DOM element for the first subtag named `<ejb-relationship-role>`, and the second - for the second one.
Hence, the term "fixed number" for such children.
According to DTD or Schema, there should be a fixed number of subtags with the given name.
Most often this fixed number is 1; in our case with the relations it is 2.
Just like attributes, fixed-number children exist regardless of underlying tag existence.
If you need to delete tags, it can be done with the help of the same `undefine()` method.
@ -292,7 +302,7 @@ For children of [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij
One more common case in DTD and Schemas is when children have the same tag name and a non-fixed upper limit in count.
Their accessors differ from those of the fixed-number children in the following: the return result is `Collection` or `List` of a special type that extends `DomElement`, and if you want to use name strategies, the method name must be in pluralized form.
For example, in EJB we would have the following method:
For example, in the EJB we would have the following method:
```java
List<Entity> getEntities();
@ -301,15 +311,15 @@ List<Entity> getEntities();
There's also an annotation `@SubTagList` where you can explicitly specify the tag name.
Returned collections cannot be modified directly.
To delete an element from collection, just call `undefine()` on this element.
The tag will then be removed, and element will become invalid (`DomElement.isValid() == false`).
To delete an element from a collection, just call `undefine()` on this element.
The tag will then be removed, and an element will become invalid (`DomElement.isValid() == false`).
Note that this behavior differs from that of fixed-number children and attributes: they are always valid, even after `undefine()`.
Again, unlike those children types, collection children always have valid underlying XML tags.
Adding elements is a little bit harder.
Since all DOM elements are created internally, you can't just pass some of your DOM elements to some method, to add the element to the collection.
Adding elements is a bit harder.
Since all DOM elements are created internally, you can't just pass some of your DOM elements to some method to add the element to the collection.
In fact, you have to ask a parent element to add a child to the collection.
In our example it's done in the following way:
In our example, it's done in the following way:
```java
Entity addEntity(int index);
@ -338,7 +348,7 @@ List<Foo> getFoos();
List<Bar> getBars();
// all <foo> and <bar> elements
@SubTagsList({"foo", "bar"})
@SubTagsList({ "foo", "bar" })
List<FooBar> getMergedListOfFoosAndBars();
```
@ -347,8 +357,11 @@ The annotation here is mandatory - we cannot guess several tag names from one me
To add elements to such mixed collections, you should create "add" methods for each possible tag name:
```java
@SubTagsList(value={"foo","bar"}, tagName="foo") Fubar addFoo();
@SubTagsList(value={"foo","bar"}, tagName="bar") Fubar addBar(int index);
@SubTagsList(value = { "foo", "bar" }, tagName = "foo")
FooBar addFoo();
@SubTagsList(value = { "foo", "bar" }, tagName = "bar")
FooBar addBar(int index);
```
The index parameter in the last example means the index in the merged collection, not in the collection of tags named "bar".
@ -356,7 +369,8 @@ The index parameter in the last example means the index in the merged collection
### Dynamic Definition
You can extend existing DOM model at runtime by implementing `com.intellij.util.xml.reflect.DomExtender<T>`.
Register it in "extenderClass" attribute of EP `com.intellij.dom.extender`, where "domClass" specifies DOM class `<T>` to be extended. [`DomExtensionsRegistrar`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/reflect/DomExtensionsRegistrar.java) provides various methods to register dynamic attributes and children.
Register it in "extenderClass" attribute of EP `com.intellij.dom.extender`, where "domClass" specifies DOM class `<T>` to be extended.
[`DomExtensionsRegistrar`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/reflect/DomExtensionsRegistrar.java) provides various methods to register dynamic attributes and children.
If the contributed elements depend on anything other than plain XML file content (used framework version, libraries in classpath, ...), make sure to return `false` from `DomExtender.supportsStubs()`.
@ -371,9 +385,9 @@ Output correctness/completeness will largely depend on the input scheme and may
Follow these steps:
* Run IntelliJ IDEA with _Plugin DevKit_ enabled in [internal mode](enabling_internal.md)
* Run IntelliJ IDEA with _Plugin DevKit_ enabled in the [internal mode](enabling_internal.md)
* Select <menupath>Tools | Internal Actions | DevKit | Generate DOM Model</menupath>
* Select Scheme file and set options, then click "Generate" to generate sources
* Select <control>Scheme File</control> and set options, then click <control>Generate</control> to generate sources
* Modify generated sources according to your needs
### IDE Support
@ -390,10 +404,12 @@ _Plugin DevKit_ supports the following features for working with DOM related cod
It often happens that a collection contains same-named tags that may have different structure or even be represented by different types in the DTD or Schema.
As an example, JSF Managed Beans may be of three types.
If a `<managed-bean>` tag contains a `<map-entries>` sub-tag, then the Managed Bean type is `MapEntriesBean`.
If it contains a `<list-entries>` sub-tag — can you guess? Right — `ListEntriesBean`! Otherwise it's a `PropertyBean` (all three interfaces extend `ManagedBean`).
If it contains a `<list-entries>` sub-tag - can you guess?
Right - `ListEntriesBean`!
Otherwise, it's a `PropertyBean` (all three interfaces extend `ManagedBean`).
And when we write `List<ManagedBean> getManagedBeans()`, we expect to get not only a list where all elements are instances of the `ManagedBean` interface, but a list where each element is of a certain type, i.e. `MapEntriesBean`, `ListEntriesBean`, or `PropertyBean`.
In such cases one should decide which interface the DOM element should actually implement (according to the given tag).
In such cases, one should decide which interface the DOM element should actually implement (according to the given tag).
This is achieved by extending the [`TypeChooser`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/TypeChooser.java) abstract class:
```java
@ -420,14 +436,15 @@ We remember that in `GenericAttributeValue` there's also the `getXmlAttribute()`
In general case there is `getXmlElement()` method.
You can also get a DOM element by its underlying XML PSI element using the `DomManager.getDomElement()` method.
If DOM element has no underlying XML element, it can be created by calling `ensureTagExists()`.
If a DOM element has no underlying XML element, it can be created by calling `ensureTagExists()`.
To delete a tag, use the already known `undefine()` method.
This method will always delete the underlying XML element (tag or attribute).
If the element was a collection child, then neither it nor its entire sub-tree will be valid anymore.
If the element was a collection's child, then neither it nor its entire subtree will be valid anymore.
#### Tree Structure
In every normal tree there's always a possibility to walk up. `DomElement` is no exception.
In every normal tree, there's always a possibility to walk up.
`DomElement` is no exception.
Method `getParent()` just returns element's parent in tree.
The method `<T extends DomElement> T getParentOfType(Class<T> requiredClass, boolean strict)` returns the tree ancestor of the given class.
@ -439,7 +456,7 @@ Finally, `getRoot()` will return the `DomFileElement`, which is the root of ever
An element becomes invalid if it has been deleted explicitly or due to external PSI changes.
Fixed-number children and attributes are meant to stay valid as long as possible, no matter what happens with XML.
They can become invalid only if they have collection tree ancestor that has been deleted.
They can become invalid only if they have a collection tree ancestor that has been deleted.
Newly created DOM elements are always correct and valid, so their `isValid()` methods will return `true`.
@ -457,7 +474,7 @@ There's also `DomElement.getXmlElementName()` method that returns the name of a
<!-- TODO: using @Presentation -->
`DomElement.getPresentation()` returns an instance of [`ElementPresentation`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentation.java), an interface that knows presentable element type, name, and sometimes even its icon.
Presentations are actually obtained from presentation factory objects that, like ClassChoosers's, should be registered in [`ElementPresentationManager`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentationManager.java) as early as possible.
Presentations are actually obtained from presentation factory objects that, like `ClassChooser`s, should be registered in [`ElementPresentationManager`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentationManager.java) as early as possible.
You can specify type name and icon for all elements of some class, ways of getting type name, icon and presentable name for particular objects.
When not specified, presentable name is taken from the object itself, if it contains a method annotated with `@NameValue` annotation, that returns `String` or `GenericValue`.
If there's no such method, it will return `null`.
@ -472,9 +489,9 @@ DOM supports the following events: tag value changed, element defined/undefined/
#### Highlighting Annotations
The DOM supports error checking and highlighting.
It's based on annotations which you add to the DOM element in a special place (don't confuse these annotations with the ones of Java 5 they are very different).
It's based on annotations which you add to the DOM element in a special place (don't confuse these annotations with the ones of Java 5 - they are very different).
You need to implement the [`DomElementAnnotator`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/highlighting/DomElementsAnnotator.java) interface, and override `DomFileDescription.createAnnotator()` method, and create this annotator there.
In `DomElementsAnnotator.annotate(DomElement element, DomElementsProblemsHolder annotator)` you should report about all errors and warnings in the element's sub-tree to the annotator (`DomElementsProblemsHolder.createProblem()`).
In `DomElementsAnnotator.annotate(DomElement element, DomElementsProblemsHolder annotator)` you should report about all errors and warnings in the element's subtree to the annotator (`DomElementsProblemsHolder.createProblem()`).
You should return this annotator in the corresponding virtual method of the `DomFileDescription`.
#### Automatic Highlighting (BasicDomElementsInspection)
@ -494,13 +511,15 @@ To suppress spellchecking annotate your DomElement with `@com.intellij.spellchec
There is a common case in error highlighting, when one needs to say, that some required sub-tag or attribute is missing.
DOM will do this for you automatically, if you annotate the getter for that child with the `@Required` annotation.
For collection children getters, this annotation will mean, that the collection should be not empty (corresponding to '+' sign in DTD).
For collection children getters, this annotation will mean that the collection should be not empty (corresponding to '+' sign in DTD).
Also, when you create a new element that has required fixed-number or attribute children, their tags or attributes will also be created in XML.
### Resolving
Remember the interface `GenericDomValue<T>` and its sub-interface `GenericAttributeValue<T>`? Remember, that ANY class may be passed as `T` — for example, let's interpret `GenericDomValue<PsiClass>` as a reference to a class.
Then we can always consider it as a reference to an object of class `T`! With Strings or enums, it is not a very useful idea, but we'll use it in another way.
Remember the interface `GenericDomValue<T>` and its sub-interface `GenericAttributeValue<T>`?
Remember, that ANY class may be passed as `T` - for example, let's interpret `GenericDomValue<PsiClass>` as a reference to a class.
Then we can always consider it as a reference to an object of class `T`!
With strings or enums, it is not a very useful idea, but we'll use it in another way.
Very often XML has such a structure that an object is declared at some place, and is referenced at some other place (more precisely, in a tag or attribute value).
So, if you want to create a method like `GenericValue<MyDomElement> getMyDomElementReference()`, then you just have to specify a proper converter that will find an instance in your model of `MyDomElement` with the name specified in the `GenericDomValue`.
@ -509,7 +528,7 @@ Since creating such converters is quite boring, we've done it for you.
You don't have to annotate reference getters at all, as the name resolution will be made automatically.
Elements will be searched by name, and the name will be taken from the method annotated with `@NameValue`.
The converter used is [`DomResolveConverter`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/DomResolveConverter.java).
Its constructor takes a parameter, so it can't be referenced in `@Convert` annotation, but its subclasses (if you create them) can.
Its constructor takes a parameter, so it can't be referenced in `@Convert` annotation, but its subclasses (if you create them) - can.
If you still want to specify explicitly that your reference to `DomElement` should be resolved "model-wide", use the `@Resolve` annotation parameterized with the desired class.
The resolution scope will be taken from the `DomFileDescription.getResolveScope()`.
@ -519,39 +538,45 @@ If you want to create a custom converter and want to have this code insight with
It has one more method `getVariants()`, where you'll have to provide the collection consisting of all targets your reference may resolve to.
Those familiar with [`PsiReference`](upsource:///platform/core-api/src/com/intellij/psi/PsiReference.java) will recognize the similarities here.
If you need to choose a `Converter` depending on other values (e.g. in sibling/parent element) or any runtime condition (e.g. presence or version of library), you can use `WrappingConverter`. See also `GenericDomValueConvertersRegistry` for managing an extensible registry of available Converters to choose from.
If you need to choose a `Converter` depending on other values (e.g. in a sibling/parent element) or any runtime condition (e.g. presence or version of a library), you can use `WrappingConverter`.
See also `GenericDomValueConvertersRegistry` for managing an extensible registry of available Converters to choose from.
### Mock and Stable Elements
Your DOM elements do not have to be tied to a physical file. `DomManager.createMockElement()` will help you to create a virtual element of a given class with the given module.
An element may be physical or not. 'Physical' here means that DOM will create a mock document for it, so you can enjoy Undo functionality if you pass this document to the right place in file editor.
Your DOM elements do not have to be tied to a physical file.
`DomManager.createMockElement()` will help you to create a virtual element of a given class with the given module.
An element may be physical or not.
'Physical' here means that DOM will create a mock document for it, so you can enjoy <control>Undo</control> functionality if you pass this document to the right place in the file editor.
`DomElement.copyFrom()` allows you to copy information from one `DomElement` to another.
In fact, it just replaces XML tags, and all the old data is lost.
Nevertheless, the element's fixed-number children don't become invalid, they only contain new tag values, attribute values, etc.
Nevertheless, the element's fixed-number children don't become invalid.
They only contain new tag values, attribute values, etc.
The tree is actually rather conservative.
The combination of `createMockElement()` and `copyFrom()` is useful for editing element contents in dialogs.
You create a mock copy of an element, work with it in the dialog and then, if the user doesn't cancel, copy the element back to the main model.
Since it's a common case, a special shortcut method has been created in `DomElement`, called `createMockCopy()`.
IntelliJ Platform's XML parser is incremental: changes in text do not cause the whole file to be reparsed.
IntelliJ Platform's XML parser is incremental: changes in a text do not cause the whole file to be reparsed.
But you should keep in mind that this rule may sometimes not work correctly.
For example, your DOM elements can unexpectedly become broken as a result of manual editing of the XML file (even if it didn't happen inside those elements).
If a file editor depends on such a broken element, this can lead to closing the tab, which isn't very nice from the user's point of view.
For example, suppose you have an entity bean named "SomeEntity".
You open an editor for it, then you go into the XML, change the tag name from entity to session, and then back to entity.
Of course, no DOM element can survive after such blasphemy.
But notwithstanding, you still want your editor to stay open! Well, there is a solution, and it's called `DomManager.createStableValue(Factory factory)`.
But notwithstanding, you still want your editor to stay open!
Well, there is a solution, and it's called `DomManager.createStableValue(Factory factory)`.
This method creates a DOM element that delegates all its functionality to some real element (returned from the factory parameter).
As soon as that real element becomes invalid, the factory is called once more, and if it returns something valid, it becomes the new delegate.
And so on... In the example with EJB, the factory would once again look for an Entity Bean named "SomeEntity".
And so on...
In the example with EJB, the factory would once again look for an Entity Bean named "SomeEntity".
Stable DOM elements also implement the `StableElement` interface, which has the following methods:
* `DomElement getWrappedElement()` just returns the current element to which all method calls are delegated;
* `void invalidate()` makes the wrapped element invalid. Any following method call will cause the factory to create a new delegate;
* `void revalidate()` calls the factory, and if it returns something new (i.e. not the same as the current wrapped element) invalidates the old value and adopts the new one.
* `DomElement getWrappedElement()` - just returns the current element to which all method calls are delegated;
* `void invalidate()` - makes the wrapped element invalid. Any following method call will cause the factory to create a new delegate;
* `void revalidate()` - calls the factory, and if it returns something new (i.e. not the same as the current wrapped element) invalidates the old value and adopts the new one.
### Visitor
@ -559,9 +584,12 @@ Visitor is a very common design pattern.
DOM model also has a visitor, and it's called `DomElementVisitor`.
The `DomElement` interface has methods `accept()` and `acceptChildren()` that take this visitor as a parameter.
If you look at the interface `DomElementVisitor` itself, you may be surprised, since it has only one method: `visitDomElement(DomElement)`.
Where is the Visitor pattern? Where are all those methods with names like `visitT(T)` that are usually found in it? There are no such methods, because the actual interfaces (T's) aren't known to anyone except you.
But when you instantiate the `DomElementVisitor` interface, you may add there these `visitT()` methods, and they will be called! You may even name them just `visit()`, specify the type of the parameter, and everything will be fine.
For example, if you have two DOM element classes — `Foo` and `Bar` — your visitor may look like this:
Where is the visitor pattern?
Where are all those methods with names like `visitT(T)` that are usually found in it?
There are no such methods, because the actual interfaces (T's) aren't known to anyone except you.
But when you instantiate the `DomElementVisitor` interface, you may add there these `visitT()` methods, and they will be called!
You may even name them just `visit()`, specify the type of the parameter, and everything will be fine.
For example, if you have two DOM element classes - `Foo` and `Bar` - your visitor may look like this:
```java
class MyVisitor implements DomElementVisitor {
@ -605,7 +633,7 @@ Simply register it using extension point `com.intellij.dom.implementation` and D
### Models Across Multiple Files
Many frameworks require a set of XML configuration files ("fileset") to work as one model, so resolving/navigation works across all related DOM files.
Depending on implementation/plugin, providing filesets implicitly (using existing framework's setup in project) or via user configuration (usually via dedicated `Facet`) can be achieved.
Depending on implementation/plugin, providing filesets implicitly (using existing framework's setup in a project) or via user configuration (usually via dedicated `Facet`) can be achieved.
Extend [`DomModelFactory`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/model/impl/DomModelFactory.java) (or [`BaseDomModelFactory`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/model/impl/BaseDomModelFactory.java) for non-`Module` scope) and provide implementation of your `DomModel`.
Usually you will want to add searcher/utility methods to work with your `DomModel` implementation.
@ -631,7 +659,7 @@ Set and increase `stubVersion` of `com.intellij.dom.fileMetaData` extension when
All forms that deal with DOM are organized in a special way.
They support two main things: getting data from XML into the UI, and saving UI data to XML.
The former is called resetting, the latter committing.
The former is called resetting, the latter - committing.
There's [`Committable`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/Committable.java) interface that has corresponding methods: `commit()` and `reset()`.
There's also a way of structuring your forms into smaller parts, namely the Composite pattern: [`CompositeCommittable`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/CompositeCommittable.java).
Methods `commit()` and `reset()` are invoked automatically on editor tab switch or undo.
@ -639,13 +667,14 @@ So you only need to ensure that all your Swing structure is organized in a tree
DOM controls are special descendants of `Committable`.
All of them implement `DomUIControl`.
Note that they are not Swing components they are only a way of connecting DOM model and Swing components.
One end of the connection — the DOM element — is usually specified in the controls constructor.
The other end — Swing component — can be obtained in 2 ways.
Note that they are not Swing components - they are only a way of connecting DOM model and Swing components.
One end of the connection - the DOM element - is usually specified in the control's constructor.
The other end - Swing component - can be obtained in 2 ways.
The first is to ask DOM control to create it.
But that is rather inconvenient if you want to create the forms in, say, IntelliJ IDEA's GUI Designer.
In that case, you'll need the second way: ask the control to `bind()` to an existing Swing component of a correct type (that depends on the type of value that you're editing).
After that, your Swing components will be synchronized with DOM, they'll even highlight errors reported by `DomElementsAnnotator`.
After that, your Swing components will be synchronized with DOM.
They'll even highlight errors reported by `DomElementsAnnotator`.
Sometimes you may need to do some work (enable or disable some components, change their values) after a particular DOM control is committed.
Then you should define the `addCommitListener()` method of that DOM control and override the `CommitListener.afterCommit()` method.
@ -655,10 +684,16 @@ This method will be invoked inside the same write action as the main `commit()`,
With simple controls, you can edit `GenericDomValue`: simple text, class names, enums and boolean values.
These controls take a special object as a constructor parameter.
This object should implement the `DomWrapper` interface that knows how to set/get values to/from DOM model.
This object should implement the `DomWrapper` interface that knows how to set/get values to/from a DOM model.
We have three major DomWrapper's: [`DomFixedWrapper<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomFixedWrapper.java) redirecting calls to [`GenericDomValue<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java), [`DomStringWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomStringWrapper.java) redirecting calls to string accessors of [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java), and [`DomCollectionWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomCollectionWrapper.java) that gets/sets values of the first element of the given [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java) collection.
Some controls (those having a text field as part of itself) take additional boolean constructor parameter — _commitOnEveryChange_, whose meaning is evident from the name.
We have three major DomWrapper's: [`DomFixedWrapper<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomFixedWrapper.java) redirecting calls to
[`GenericDomValue<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java),
[`DomStringWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomStringWrapper.java)
redirecting calls to string accessors of
[`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java),
and [`DomCollectionWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomCollectionWrapper.java)
that gets/sets values of the first element of the given [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java) collection.
Some controls (those having a text field as part of itself) take an additional boolean constructor parameter - _commitOnEveryChange_, whose meaning is evident from the name.
We don't recommend using it anywhere except small dialogs, because committing on every change slows down the system significantly.
Most often these controls are created by `DomUIFactory.createControl(GenericDomValue)`.
@ -687,11 +722,11 @@ Or you can just delegate to that renderer in your own way.
##### BooleanEnumControl
Sometimes, when there are only 2 alternatives, it's convenient to use a check box instead of combo box.
Sometimes, when there are only 2 alternatives, it's convenient to use a checkbox instead of a combobox.
This control is designed specially for such cases.
While being (and being bound to) a check box, the control edits not just "true" or "false", but any two String values, or two enum elements.
While being (and being bound to) a checkbox, the control edits not just "true" or "false", but any two String values, or two enum elements.
In the last case, it has a boolean _invertedOrder_ parameter, to specify which element corresponds to the checked state.
By default _invertedOrder_ is set to `false`, so the first element corresponds to the unchecked state, and the second to the checked one.
By default, _invertedOrder_ is set to `false`, so the first element corresponds to the unchecked state, and the second - to the checked one.
If you set the parameter to `true`, the states will swap.
### Editor-Based Controls
@ -703,7 +738,7 @@ Since there's currently no way to instantiate Editor directly through the Open A
This control allows you to edit simple string values.
The control is bound to a `TextPanel` component.
There's also an inheritor of that panel `MultiLineTextPanel`.
There's also an inheritor of that panel - `MultiLineTextPanel`.
If you bind a `StringControl` to it, a big editor will appear on the screen.
In case you don't have space for a big editor, bind it to a `BigTextPanel`.
Then it will be filled with a text editor, and the browse button will be added to open a dialog with the big editor where you can type a longer string.
@ -726,9 +761,10 @@ It is bound to `PsiTypePanel`.
There is a special table component where each row represents one collection child.
It's called `DomCollectionControl<T>`, where `T` is your collection element type.
To function properly, it needs `DomElement` (parent of the collection), some description of the collection (sub-tag name or a `DomCollectionChildDescription` from DOM reflection), and a `ColumnInfo` array.
This can be passed to the constructor, or can be created in a `DomCollectionControl` inheritor, in an overriden method `createColumnInfos()`.
This can be passed to the constructor, or can be created in a `DomCollectionControl` inheritor, in an overridden method `createColumnInfos()`.
What is a column info? It's just a somewhat more comfortable way to work with the table model.
What is a column info?
It's just a somewhat more comfortable way to work with the table model.
It uses Java 5 generics and is more object-oriented.
So, it's named `ColumnInfo<Item,Aspect>`, where `Item` is a type variable corresponding to the type of elements in the collection, and `Aspect` is a type variable corresponding to this particular column information type: `String`, `PsiClass`, `Boolean`, etc.
The basic things that a column knows are: column name, column class, reading value (Aspect `valueOf(Item)`), writing value (`setValue(Item item, Aspect aspect)`), cell renderer (`getRenderer(Item)`), cell "editability" (`isCellEditable(Item)`), cell editor (`getEditor(Item)`), etc.
@ -742,13 +778,14 @@ But such collections are encountered very rarely.
A more common case is when a collection element is more complex and has several `GenericDomValue` children.
Then one may create a column for each of those children.
The appropriate column info is `ChildGenericValueColumnInfo<T>`.
It will ask you for a `DomFixedChildDescription` (one more thing from DOM reflection), a renderer and an editor nothing else.
It will ask you for a `DomFixedChildDescription` (one more thing from DOM reflection), a renderer and an editor - nothing else.
So, the main things left to customize are the renderer and the editor.
As for the renderer, there are two main choices: `DefaultTableCellRenderer`, and IntelliJ Platform's `BooleanTableCellRenderer`.
Editors are more complicated, but they closely resemble simple DOM controls.
`BooleanTableCellEditor`, `DefaultCellEditor(JTextField)`, `ComboTableCellEditor`, etc. `DomUIFactory.createCellEditor()` will create any of them automatically (including the editor for `PsiClass`), so that you won't need to think about which one to select every time.
`BooleanTableCellEditor`, `DefaultCellEditor(JTextField)`, `ComboTableCellEditor`, etc.
`DomUIFactory.createCellEditor()` will create any of them automatically (including the editor for `PsiClass`), so that you won't need to think about which one to select every time.
Collection control is a complex control, so it's bound to a complex Swing component.
It's called [`DomTableView`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomTableView.java).
@ -777,9 +814,9 @@ The easiest way to create a DOM-based UI form is to extend the
[`BasicDomElementComponent`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/BasicDomElementComponent.java) class.
This will require you to pass some DOM element to the constructor.
Then you bind an IntelliJ IDEA GUI Designer form to your subclass and design a beautiful form there.
You will surely want to bind some controls to DOM UI, in which case you should of course ensure that they have right types.
You will surely want to bind some controls to DOM UI, in which case you should of course ensure that they have the right types.
Finally, you should create some DOM controls in class' constructor and bind them.
But you can create controls and bind them to the `DomElement`'s children `GenericDomValue`'s automatically.
But you can create controls and bind them to the `DomElement`'s children - `GenericDomValue`'s automatically.
Just name your components properly and call the `bindProperties()` method in the constructor.
The field names should correspond to the getter names for the element's children.
@ -808,9 +845,9 @@ public class ConverterComponent extends BasicDomElementComponent<Converter> {
}
```
All the fields here are now bound to controls in a GUI form.
All the fields here are now bound to the controls in the GUI form.
Very often you'll have to create your own file editor.
Very often, you'll have to create your own file editor.
Then, to use all the binding and undo functionality, it's suggested to inherit your
[`FileEditorProvider`](upsource:///platform/analysis-api/src/com/intellij/openapi/fileEditor/FileEditorProvider.java)
from