package org.mevenide.idea.psi.util;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiTreeChangeAdapter;
import com.intellij.psi.PsiTreeChangeEvent;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import com.intellij.util.IncorrectOperationException;
import java.util.HashSet;
import java.util.Set;
import javax.swing.event.EventListenerList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mevenide.idea.util.IDEUtils;
import org.mevenide.idea.util.event.*;
/**
* Manages events for table-like tag model, where a single tag path represents the "container" tag,
* under which there are multiple "row" tags.
*
* <p>PSI events are mapped for the specified row they changed and cause an {@link
* java.beans.IndexedPropertyChangeEvent} to be triggered.</p>
*
* @author Arik
*/
public class PsiIndexedPropertyChangeListener extends PsiTreeChangeAdapter
implements BeanRowsObservable {
/**
* Logging.
*/
private static final Log LOG = LogFactory.getLog(PsiIndexedPropertyChangeListener.class);
/**
* Synchronization lock.
*/
private final Object LOCK = new Object();
/**
* Maps properties to XML tag paths.
*/
private final XmlTagPropertyMapper mapper = new XmlTagPropertyMapper();
/**
* Property change listeners container.
*/
private final EventListenerList listenerList = new EventListenerList();
/**
* The path to the tag containing all row tags.
*/
private final XmlTagPath containerPath;
/**
* The name of the tags that represent individual rows.
*/
private final String rowTagName;
/**
* The path to the tag containing all row tags.
*/
private final XmlTagPath rowPath;
/**
* Is the current set of PSI events generated by the XML tag path, or by an outside source?
*/
private boolean ignoreEvents = false;
/**
* @param pContainerPath
* @param pRowTagName
*/
public PsiIndexedPropertyChangeListener(final XmlTagPath pContainerPath,
final String pRowTagName) {
containerPath = pContainerPath;
rowTagName = pRowTagName;
rowPath = new XmlTagPath(containerPath, rowTagName);
}
/**
* Returns the path to the container tag.
*
* @return tag path
*/
public final XmlTagPath getContainerPath() {
return containerPath;
}
/**
* Returns the name of the row tags.
*
* @return string
*/
public final String getRowTagName() {
return rowTagName;
}
/**
* Registers the given listener for events.
*
* @param pListener the listener to register
*/
public void addBeanRowsListener(BeanRowsListener pListener) {
synchronized (LOCK) {
listenerList.add(BeanRowsListener.class, pListener);
}
}
/**
* Unregisters the given listener.
*
* @param pListener the listener to remove
*/
public void removeBeanRowsListener(BeanRowsListener pListener) {
synchronized (LOCK) {
listenerList.remove(BeanRowsListener.class, pListener);
}
}
/**
* Registers the given property name with the specified tag path.
*
* @param pPropertyName the property name
* @param pTagPath the corresponding tag path
*/
public final void registerTag(final String pPropertyName,
final XmlFile pFile,
final String pTagPath) {
synchronized (LOCK) {
final XmlTagPath tagPath = new XmlTagPath(rowPath, pTagPath);
mapper.putTagPath(pPropertyName, tagPath);
}
}
public final String[] getValues() {
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null)
return new String[0];
final XmlTag[] rowTags = containerTag.findSubTags(rowTagName);
final String[] values = new String[rowTags.length];
for (int i = 0; i < rowTags.length; i++)
values[i] = rowTags[i].getValue().getTrimmedText();
return values;
}
public final String getValue(final int pRow) {
return getValue(pRow, null);
}
public final String getValue(final int pRow, final String pPropertyName) {
synchronized (LOCK) {
rowPath.setRow(pRow);
final XmlTagPath path;
if (pPropertyName == null || pPropertyName.trim().length() == 0)
path = rowPath;
else
path = mapper.getTagPath(pPropertyName);
return path.getStringValue();
}
}
public final void setValue(final int pRow,
final Object pValue) {
setValue(pRow, null, pValue);
}
public final void setValue(final int pRow,
final String pPropertyName,
final Object pValue) {
final String value = pValue == null ? null : pValue.toString();
synchronized (LOCK) {
ignoreEvents = true;
try {
rowPath.setRow(pRow);
final XmlTagPath path;
if (pPropertyName == null || pPropertyName.trim().length() == 0)
path = rowPath;
else
path = mapper.getTagPath(pPropertyName);
path.setValueProtected(value);
}
finally {
ignoreEvents = false;
}
}
}
public final int getRowCount() {
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null)
return 0;
final XmlTag[] rowTags = containerTag.findSubTags(rowTagName);
return rowTags.length;
}
public final int appendRow() {
final RowAppenderRunnable addTagCmd = new RowAppenderRunnable();
IDEUtils.runCommand(containerPath.getProject(), addTagCmd);
return addTagCmd.getResult();
}
public void deleteRows(final int... pRowIndices) {
final Runnable deleteTagsCmd = new Runnable() {
public void run() {
try {
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null)
return;
final XmlTag[] childTags = containerTag.findSubTags(rowTagName);
final Set<XmlTag> tags = new HashSet<XmlTag>(pRowIndices.length);
for (int i = 0; i < pRowIndices.length; i++)
tags.add(childTags[pRowIndices[i]]);
for (XmlTag xmlTag : tags)
xmlTag.delete();
}
catch (IncorrectOperationException e) {
LOG.error(e.getMessage(), e);
}
}
};
IDEUtils.runCommand(containerPath.getProject(), deleteTagsCmd);
}
/**
* Returns the row index that this element belongs to.
*
* <p>If the given PSI element is not a child of any row, this method will return {@code
* null}.</p>
*
* @param pElement the PSI element to test
*
* @return the row tag
*/
private Integer findRowForElement(final PsiElement pElement) {
synchronized (LOCK) {
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null)
return null;
final XmlTag[] rowTags = containerTag.findSubTags(rowTagName);
for (int i = 0; i < rowTags.length; i++) {
rowPath.setRow(i);
final XmlTag rowTag = rowPath.getTag();
if (PsiTreeUtil.isAncestor(rowTag, pElement, false))
return i;
}
return null;
}
}
private void handleChildAddition(final PsiElement pChild) {
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null)
return;
final XmlTag[] rowTags = containerTag.findSubTags(rowTagName);
for (int i = 0; i < rowTags.length; i++) {
rowPath.setRow(i);
final XmlTag rowTag = rowPath.getTag();
if (PsiTreeUtil.isAncestor(rowTag, pChild, false)) {
if (pChild.equals(rowTag))
fireRowAddedEvent(i);
else {
final String property = mapper.findPropertyForElement(pChild);
if (property != null)
fireRowChangedEvent(i, property);
}
break;
}
}
}
private void handleChildRemoval(final PsiElement pParent, final PsiElement pChild) {
final XmlTag parent = PsiTreeUtil.getParentOfType(pParent,
XmlTag.class,
false);
if (parent == null)
return;
//
//if the removed tag was the container tag, fire global change event
//
final XmlTag containerTag = containerPath.getTag();
if (containerTag == null) {
if (pChild instanceof XmlTag) {
final XmlTag tag = (XmlTag) pChild;
final String[] path = PsiUtils.getPathAndConcat(parent, tag.getName());
if (path.equals(containerPath.getPathTokens()))
fireRowsChangedEvent();
}
return;
}
//
//if the removed child was a text node, simply find which property it belonged to
//
if (pChild instanceof XmlText) {
final XmlTag[] rowTags = containerTag.findSubTags(rowTagName);
for (int i = 0; i < rowTags.length; i++) {
rowPath.setRow(i);
if (PsiTreeUtil.isAncestor(rowPath.getTag(), pChild, false)) {
final String property = mapper.findPropertyForElement(pChild);
if (property != null)
fireRowChangedEvent(i, property);
}
}
}
//
//if the removed child is a tag, check if it is a row tag or a property tag
//
else if (pChild instanceof XmlTag) {
rowPath.setRow(null);
final XmlTag tag = (XmlTag) pChild;
final String[] path = PsiUtils.getPathAndConcat(parent, tag.getName());
//if the removed tag is a registered property tag
final String propertyName = mapper.findPropertyByPath(path);
if (propertyName != null) {
final int row = findRowForElement(parent);
fireRowChangedEvent(row, propertyName);
}
//if the removed tag is a row tag
else if (rowTagName.equals(tag.getName())) {
//TODO: inspect event's offset to determine row number
//fireRowRemovedEvent(rowByOffset);
fireRowsChangedEvent();
}
}
}
@Override
public final void childAdded(final PsiTreeChangeEvent pEvent) {
synchronized (LOCK) {
if (ignoreEvents)
return;
handleChildAddition(pEvent.getChild());
}
}
@Override
public final void childRemoved(final PsiTreeChangeEvent pEvent) {
synchronized (LOCK) {
if (ignoreEvents)
return;
handleChildRemoval(pEvent.getParent(), pEvent.getChild());
}
}
@Override
public final void childReplaced(final PsiTreeChangeEvent pEvent) {
synchronized (LOCK) {
if (ignoreEvents)
return;
handleChildRemoval(pEvent.getParent(), pEvent.getOldChild());
handleChildAddition(pEvent.getNewChild());
}
}
private BeanRowsListener[] getListeners() {
return listenerList.getListeners(BeanRowsListener.class);
}
private void fireRowAddedEvent(final int pRow) {
BeanRowEvent event = null;
for (BeanRowsListener listener : getListeners()) {
if (event == null)
event = new BeanRowAddedEvent(this, pRow);
listener.rowAdded(event);
}
}
private void fireRowRemovedEvent(final int pRow) {
BeanRowEvent event = null;
for (BeanRowsListener listener : getListeners()) {
if (event == null)
event = new BeanRowRemovedEvent(this, pRow);
listener.rowRemoved(event);
}
}
private void fireRowChangedEvent(final int pRow,
final String pProperty) {
final XmlTagPath path = mapper.getTagPath(pProperty);
final String value = path.getStringValue();
BeanRowEvent event = null;
for (BeanRowsListener listener : getListeners()) {
if (event == null)
event = new BeanRowChangedEvent(this, pRow, pProperty, value);
listener.rowChanged(event);
}
}
private void fireRowsChangedEvent() {
BeanRowEvent event = null;
for (BeanRowsListener listener : getListeners()) {
if (event == null)
event = new BeanRowsChangedEvent(this);
listener.rowsChanged(event);
}
}
private class RowAppenderRunnable implements Runnable {
private int result = -1;
public void run() {
result = -1;
try {
final XmlTag containerTag = containerPath.ensureTag();
final String namespace = containerTag.getNamespace();
final XmlTag rowTag = (XmlTag) containerTag.add(
containerTag.createChildTag(rowTagName,
namespace,
null,
false));
final XmlTag[] childTags = containerTag.findSubTags(rowTagName);
for (int i = 0; i < childTags.length; i++) {
XmlTag tag = childTags[i];
if (tag.equals(rowTag)) {
result = i;
break;
}
}
}
catch (IncorrectOperationException e) {
LOG.error(e.getMessage(), e);
}
}
public final int getResult() {
return result;
}
}
}