XML DOM API - Bind attributes

Answered

I'm trying to implement XML DOM API and bind xml to UI developed with Swing. I generated interfaces from dtd using IDEA's tool, implemented my own file editor provider that extends PerspectiveFileEditorProvider and editor itself that extends BasicDomElementComponent, built swing form and bound fields using bindProperties();
The problem is when a type set in an interface is GenericDomValue then everything works fine but for fields that bound to GenericAttributeValue the UI filed becomes disabled

If I create TextControl for such fields manually then everything works fine except for default value: I have to call the control's reset() method explicitly to fill the UI field with a value taken from xml, otherwise it's empty but if I start typing then changes reflect in xml so it means that binding was successful.
The main problem is it's not elegant to create text controls manually because I will have to create too many of them and, what much more important, undoing (CTRL+Z) for these fields causes "java.lang.RuntimeException: java.lang.reflect.InvocationTargetException" or
"java.lang.IllegalStateException: Attempt to modify PSI for non-committed Document!" depending on a way I create the controls, with DomUIFactory or TextControl constructor respectively.

interface field:

@NotNull
@Required
GenericAttributeValue<String> getPackagePath();

Creation with DomUIFactory:

DomUIControl<GenericDomValue> packagePathControl = DomUIFactory.createControl(domElement.getPackagePath(), true);
packagePathControl.bind(myPackagePath);
packagePathControl.reset();

Creation with TextControl constructor:

TextControl packagePathControl = new TextControl(new DomWrapper<String>() {
@NotNull
@Override
public DomElement getExistingDomElement() {
return domElement.getPackagePath();
}

@Nullable
@Override
public DomElement getWrappedElement() {
return null;
}

@Override
public void setValue(String value) throws IllegalAccessException, InvocationTargetException {
domElement.getPackagePath().setValue(value);
}

@Override
public String getValue() throws IllegalAccessException, InvocationTargetException {
return domElement.getPackagePath().getValue();
}
}, true);
packagePathControl.bind(myPackagePath);
packagePathControl.reset();

myPackagePath is a var of type TextPanel.

Overriden commit() method to prevent

java.lang.Throwable: Write-unsafe context! Model changes are allowed from write-safe contexts only. Please ensure you're using invokeLater/invokeAndWait with a correct modality state (not "any").
@Override
public void commit() {
ApplicationManager.getApplication().invokeLater(super::commit);
}




I will be grateful for the help.

11 comments
Comment actions Permalink

Please link full sources of everything around this DOM editing. Why does your editor implement BasicDomElementComponent instead of DomFileEditor?

0
Comment actions Permalink

Oh, my bad. I think I explained the editor implementing wrong. Here is a couple of code snippets related to the editor. Maybe I misunderstood the XML DOM API DevGuide...

public class ServicesFileEditorProvider extends PerspectiveFileEditorProvider {

@NotNull
@Override
public PerspectiveFileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) {
DomManager domManager = DomManager.getDomManager(project);
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
ServiceBuilder serviceBuilder = domManager.getFileElement((XmlFile) psiFile, ServiceBuilder.class).getRootElement();
return new DomFileEditor<>(project, file, "GUI", new DomEditorWindow(serviceBuilder));
}

@Override
public boolean accept(@NotNull Project project, @NotNull VirtualFile file) {
return Constants.isCorrectFile;
}

}

 

public class DomEditorWindow extends BasicDomElementComponent<ServiceBuilder> implements TreeSelectionListener {

private JPanel editorWindowContent;
private JTree serviceTree;
private JPanel content;

private TextPanel myPackagePath;
private TextPanel author;
private TextPanel namespace;
private JCheckBox autoNamespaceTables;

private JPanel serviceBuilderPanel;
private JLabel packagePathLabel;
private JLabel autoNamespaceTablesLabel;
private JLabel authorLabel;
private JLabel namespaceLabel;

private JPanel entitiesPanel;
private JPanel exceptionsPanel;
private JTable entitiesTable;
private JScrollPane entitiesTableScrollPane;

private JComponent active = serviceBuilderPanel;

public DomEditorWindow(ServiceBuilder domElement) {
super(domElement);

initStyle();
bindProperties();
initTree();
serviceTree.addTreeSelectionListener(this);
//some other things
}

@Override
public JComponent getComponent() {
return editorWindowContent;
}

@Override
public void commit() {
ApplicationManager.getApplication().invokeLater(super::commit);
}
}

ServiceBuilder represents a root tag so all other tags are included into it.

public interface ServiceBuilder extends DomElement {

@NotNull
GenericAttributeValue<Boolean> getAutoNamespaceTables();

@NotNull
GenericAttributeValue<String> getAutoImportDefaultReferences();

@NotNull
GenericAttributeValue<String> getDependencyInjector();


@NotNull
GenericAttributeValue<String> getMvccEnabled();

@NotNull
@Required
GenericAttributeValue<String> getPackagePath();


@NotNull
GenericDomValue<String> getAuthor();

@NotNull
@Required
GenericDomValue<String> getNamespace();

@NotNull
List<Entity> getEntities();

Entity addEntity();

@NotNull
Exceptions getExceptions();

@NotNull
List<ServiceBuilderImport> getServiceBuilderImports();

ServiceBuilderImport addServiceBuilderImport();

}
0
Comment actions Permalink

Here is a link to github repo. This is a kind of working repo so there are some unused classes and parts of code that are written just for testing purposes.
https://github.com/IvanRiazantsev/service-builder-editor

0
Comment actions Permalink

Thanks for sharing the code.

Indeed attributes are not bound automatically in com.intellij.util.xml.ui.BasicDomElementComponent#bindProperties(com.intellij.util.xml.DomElement) due to check ("description instanceof DomFixedChildDescription)". You could easily override bindProperties() in your class and add similar automatic handling for attributes.

 

PS: How is this existing plugin related to yours? https://plugins.jetbrains.com/plugin/10739-liferay-intellij-plugin

0
Comment actions Permalink

Thanks, I'll try to override bindProperties(). One more question if you don't mind: DomCollectionControl and DomTableView work fine but if I change xml file explicitly then the changes don't reflect in the table. I solve the problem by adding a dom event listener but the problem is with Disposable parentDisposable.

public abstract void addDomEventListener(DomEventListener listener, Disposable parentDisposable);

Whatever I pass, If I change the xml then changes reflect in UI but also the method throws an exception. Here is a stack trace and a code snippet of my last attempt where I try to pass "this" as parentDisposable (this at current context relates to DomEditorWindow class).

DomManager.getDomManager(getProject()).addDomEventListener(event -> {
if (event.toString().equals("Changed File XmlFile:service.xml")) {
exceptionsControl.reset();
}

}, this);



com.intellij.psi.PsiInvalidElementAccessException: Element: class com.intellij.psi.impl.source.xml.XmlTagImpl #XML because: containing file is null
invalidated at: see attachment
at com.intellij.psi.impl.source.tree.CompositePsiElement.getContainingFile(CompositePsiElement.java:107)
at com.intellij.util.xml.impl.DomImplUtil.getContainingFile(DomImplUtil.java:303)
at com.intellij.util.xml.impl.DomImplUtil.getFile(DomImplUtil.java:289)
at com.intellij.util.xml.impl.PhysicalDomParentStrategy.getContainingFile(PhysicalDomParentStrategy.java:84)
at com.intellij.util.xml.impl.DomInvocationHandler.getFile(DomInvocationHandler.java:547)
at com.intellij.util.xml.impl.CollectionChildDescriptionImpl.getCollectionSubTags(CollectionChildDescriptionImpl.java:53)
at com.intellij.util.xml.impl.DomInvocationHandler.getCollectionSubTags(DomInvocationHandler.java:882)
at com.intellij.util.xml.impl.DomInvocationHandler.getCollectionChildren(DomInvocationHandler.java:858)
at com.intellij.util.xml.impl.CollectionChildDescriptionImpl.getValues(CollectionChildDescriptionImpl.java:100)
at com.intellij.util.xml.ui.DomCollectionControl.getCollectionElements(DomCollectionControl.java:283)
at com.intellij.util.xml.ui.DomCollectionControl.reset(DomCollectionControl.java:277)
at com.ivanryazantsev.liferay.servicebuildereditor.toolwindow.DomEditorWindow.lambda$new$0(DomEditorWindow.java:102)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.util.EventDispatcher.dispatchVoidMethod(EventDispatcher.java:132)
at com.intellij.util.EventDispatcher.access$000(EventDispatcher.java:26)
at com.intellij.util.EventDispatcher$1.invoke(EventDispatcher.java:90)
at com.sun.proxy.$Proxy125.eventOccured(Unknown Source)
at com.intellij.util.xml.impl.DomManagerImpl.fireEvent(DomManagerImpl.java:213)
at com.intellij.util.xml.impl.DomManagerImpl$1.modelChanged(DomManagerImpl.java:95)
at com.intellij.pom.core.impl.PomModelImpl.lambda$null$1(PomModelImpl.java:185)
at com.intellij.psi.impl.DebugUtil.performPsiModification(DebugUtil.java:553)
at com.intellij.pom.core.impl.PomModelImpl.lambda$runTransaction$2(PomModelImpl.java:144)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeNonCancelableSection(CoreProgressManager.java:187)
at com.intellij.pom.core.impl.PomModelImpl.runTransaction(PomModelImpl.java:135)
at com.intellij.psi.impl.DiffLog.lambda$doActualPsiChange$0(DiffLog.java:267)
at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.lambda$performActionWithFormatterDisabled$6(CodeStyleManagerImpl.java:692)
at com.intellij.psi.impl.source.PostprocessReformattingAspect.disablePostprocessFormattingInside(PostprocessReformattingAspect.java:104)
at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.lambda$performActionWithFormatterDisabled$8(CodeStyleManagerImpl.java:721)
at com.intellij.formatting.FormatterImpl.runWithFormattingDisabled(FormatterImpl.java:691)
at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.performActionWithFormatterDisabled(CodeStyleManagerImpl.java:719)
at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.performActionWithFormatterDisabled(CodeStyleManagerImpl.java:691)
at com.intellij.psi.impl.DiffLog.doActualPsiChange(DiffLog.java:255)
at com.intellij.psi.impl.DocumentCommitThread.lambda$doCommit$8(DocumentCommitThread.java:701)
at com.intellij.psi.impl.PsiDocumentManagerBase.commitToExistingPsi(PsiDocumentManagerBase.java:397)
at com.intellij.psi.impl.PsiDocumentManagerBase.lambda$finishCommitInWriteAction$1(PsiDocumentManagerBase.java:369)
at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:586)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:532)
at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:86)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeNonCancelableSection(CoreProgressManager.java:192)
at com.intellij.psi.impl.PsiDocumentManagerBase.finishCommitInWriteAction(PsiDocumentManagerBase.java:364)
at com.intellij.psi.impl.PsiDocumentManagerImpl.finishCommitInWriteAction(PsiDocumentManagerImpl.java:145)
at com.intellij.psi.impl.PsiDocumentManagerBase$1.run(PsiDocumentManagerBase.java:325)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:1054)
at com.intellij.psi.impl.PsiDocumentManagerBase.finishCommit(PsiDocumentManagerBase.java:332)
at com.intellij.psi.impl.DocumentCommitThread.lambda$createFinishCommitInEDTRunnable$5(DocumentCommitThread.java:488)
at com.intellij.openapi.application.TransactionGuardImpl.runSyncTransaction(TransactionGuardImpl.java:88)
at com.intellij.openapi.application.TransactionGuardImpl.lambda$submitTransaction$1(TransactionGuardImpl.java:111)
at com.intellij.openapi.application.impl.LaterInvocator$FlushQueue.doRun(LaterInvocator.java:435)
at com.intellij.openapi.application.impl.LaterInvocator$FlushQueue.runNextEvent(LaterInvocator.java:419)
at com.intellij.openapi.application.impl.LaterInvocator$FlushQueue.run(LaterInvocator.java:403)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:764)
at java.awt.EventQueue.access$500(EventQueue.java:98)
at java.awt.EventQueue$3.run(EventQueue.java:715)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:734)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:729)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:678)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:373)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:205)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

0
Comment actions Permalink

Answering to your PS, you attached a link to an official Liferay plugin but it doesn't provide a GUI editor for service builder XML. There's Liferay IDE which is an official plugin for Eclipse and it provides much more tools, including GUI service builder editor which I'm trying to create as an IDEA plugin.

0
Comment actions Permalink

Oh, I forgot to mention! The exception gets thrown if I try to remove, for example, service-builder tag's attributes explicitly

0
Comment actions Permalink

Ok, I've found proper solution to your problem

add

addComponent(exceptionsControl);

after creation and remove DomEventListener completely.

Reason: your "main" panel doesn't know about "sub-panels" like exception table, so its reset() is not called upon activation of GUI tab.

0
Comment actions Permalink

Thank you for the help! Good luck!

0
Comment actions Permalink

Could you please help me a bit more? Is it possible to define a default value for boolean attribute? I tried to achieve this by implementing custom DomWrapper and it worked but if I explicitly remove attribute from XML then it doesn't appear again when I check the corresponding checkBox.

0
Comment actions Permalink

There is no such builtin feature for handling booleans AFAIK.

0

Please sign in to leave a comment.