blob: 18c59071e2d9ffad1d346834edeef5d94415695f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.internal.core.text;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.pde.core.IModel;
import org.eclipse.pde.core.IModelChangeProvider;
import org.eclipse.pde.core.IModelChangedEvent;
import org.eclipse.pde.core.ModelChangedEvent;
/**
* DocumentObject
*
*/
public abstract class DocumentObject extends DocumentElementNode implements IDocumentObject {
// TODO: MP: TEO: LOW: Integrate with plugin model?
// TODO: MP: TEO: LOW: Investigate document node to see if any methods to pull down
private static final long serialVersionUID = 1L;
private transient IModel fModel;
private transient boolean fInTheModel;
/**
* @param model
* @param tagName
*/
public DocumentObject(IModel model, String tagName) {
super();
fModel = model;
fInTheModel = false;
setXMLTagName(tagName);
}
@Override
public void setSharedModel(IModel model) {
fModel = model;
}
@Override
public IModel getSharedModel() {
return fModel;
}
@Override
public void reset() {
// TODO: MP: TEO: LOW: Reset parent fields? or super.reset?
fModel = null;
fInTheModel = false;
}
@Override
public boolean isInTheModel() {
return fInTheModel;
}
@Override
public void setInTheModel(boolean inModel) {
fInTheModel = inModel;
}
@Override
public boolean isEditable() {
// Convenience method
return fModel.isEditable();
}
protected boolean shouldFireEvent() {
if (isInTheModel() && isEditable()) {
return true;
}
return false;
}
@Override
protected String getLineDelimiter() {
if (fModel instanceof IEditingModel) {
IDocument document = ((IEditingModel) fModel).getDocument();
return TextUtilities.getDefaultLineDelimiter(document);
}
return super.getLineDelimiter();
}
@Override
public void reconnect(IDocumentElementNode parent, IModel model) {
super.reconnect(parent, model);
// Transient field: In The Model
// Value set to true when added to the parent; however, serialized
// children's value remains unchanged. Since, reconnect and add calls
// are made so close together, set value to true for parent and all
// children
fInTheModel = true;
// Transient field: Model
fModel = model;
}
/**
* @param property
* @param oldValue
* @param newValue
*/
protected void firePropertyChanged(String property, Object oldValue, Object newValue) {
firePropertyChanged(this, property, oldValue, newValue);
}
/**
* @param object
* @param property
* @param oldValue
* @param newValue
*/
private void firePropertyChanged(Object object, String property, Object oldValue, Object newValue) {
if (fModel.isEditable() && (fModel instanceof IModelChangeProvider)) {
IModelChangeProvider provider = (IModelChangeProvider) fModel;
provider.fireModelObjectChanged(object, property, oldValue, newValue);
}
}
/**
* @param child
* @param changeType
*/
protected void fireStructureChanged(Object child, int changeType) {
fireStructureChanged(new Object[] {child}, changeType);
}
/**
* @param children
* @param changeType
*/
protected void fireStructureChanged(Object[] children, int changeType) {
if (fModel.isEditable() && (fModel instanceof IModelChangeProvider)) {
IModelChangeProvider provider = (IModelChangeProvider) fModel;
IModelChangedEvent event = new ModelChangedEvent(provider, changeType, children, null);
provider.fireModelChanged(event);
}
}
@Override
public void addChildNode(IDocumentElementNode child) {
if (child instanceof IDocumentObject) {
((IDocumentObject) child).setInTheModel(true);
}
super.addChildNode(child);
}
@Override
public void addChildNode(IDocumentElementNode child, int position) {
// Ensure the position is valid
// 0 <= position <= number of children
if ((position < 0) || (position > getChildCount())) {
return;
}
if (child instanceof IDocumentObject) {
((IDocumentObject) child).setInTheModel(true);
}
super.addChildNode(child, position);
}
/**
* @param child
* @param position
* @param fireEvent
*/
@Override
public void addChildNode(IDocumentElementNode child, int position, boolean fireEvent) {
addChildNode(child, position);
// Fire event
if (fireEvent && shouldFireEvent()) {
fireStructureChanged(child, IModelChangedEvent.INSERT);
}
}
/**
* @param child
* @param fireEvent
*/
@Override
public void addChildNode(IDocumentElementNode child, boolean fireEvent) {
addChildNode(child);
// Fire event
if (fireEvent && shouldFireEvent()) {
fireStructureChanged(child, IModelChangedEvent.INSERT);
}
}
@Override
public IDocumentElementNode removeChildNode(IDocumentElementNode child) {
IDocumentElementNode node = super.removeChildNode(child);
if ((node != null) && (node instanceof IDocumentObject)) {
((IDocumentObject) node).setInTheModel(false);
}
return node;
}
@Override
public IDocumentElementNode removeChildNode(int index) {
IDocumentElementNode node = super.removeChildNode(index);
if ((node != null) && (node instanceof IDocumentObject)) {
((IDocumentObject) node).setInTheModel(false);
}
return node;
}
@Override
public IDocumentElementNode removeChildNode(IDocumentElementNode child, boolean fireEvent) {
IDocumentElementNode node = removeChildNode(child);
// Fire event
if (fireEvent && shouldFireEvent()) {
fireStructureChanged(child, IModelChangedEvent.REMOVE);
}
return node;
}
@Override
public IDocumentElementNode removeChildNode(int index, Class<?> clazz, boolean fireEvent) {
IDocumentElementNode node = removeChildNode(index, clazz);
// Fire event
if (fireEvent && shouldFireEvent()) {
fireStructureChanged(node, IModelChangedEvent.REMOVE);
}
return node;
}
@Override
public IDocumentElementNode removeChildNode(int index, Class<?> clazz) {
// Validate index
if ((index < 0) || (index >= getChildCount()) || (clazz.isInstance(getChildAt(index)) == false)) {
// 0 <= index < child element count
// Cannot remove a node that is not the specified type
return null;
}
// Remove the node
IDocumentElementNode node = removeChildNode(index);
return node;
}
@Override
public void write(String indent, PrintWriter writer) {
// Used for text transfers for copy, cut, paste operations
writer.write(write(true));
}
/**
* @param newNode
* @param clazz
*/
@Override
public void setChildNode(IDocumentElementNode newNode, Class<?> clazz) {
// Determine whether to fire the event
boolean fireEvent = shouldFireEvent();
// Get the old node
IDocumentElementNode oldNode = getChildNode(clazz);
if ((newNode == null) && (oldNode == null)) {
// NEW = NULL, OLD = NULL
// If the new and old nodes are not defined, nothing to do
return;
} else if (newNode == null) {
// NEW = NULL, OLD = DEF
// Remove the old node
removeChildNode(oldNode, fireEvent);
} else if (oldNode == null) {
// NEW = DEF, OLD = NULL
// Add the new node to the end of the list
addChildNode(newNode, fireEvent);
} else {
// NEW = DEF, OLD = DEF
replaceChildNode(newNode, oldNode, fireEvent);
}
}
/**
* @param newNode
* @param oldNode
* @param fireEvent
*/
protected void replaceChildNode(IDocumentElementNode newNode, IDocumentElementNode oldNode, boolean fireEvent) {
// Get the index of the old node
int position = indexOf(oldNode);
// Validate position
if (position < 0) {
return;
}
// Add the new node to the same position occupied by the old node
addChildNode(newNode, position, fireEvent);
// Remove the old node
removeChildNode(oldNode, fireEvent);
}
@Override
public IDocumentElementNode getChildNode(Class<?> clazz) {
// Linear search O(n)
ArrayList<IDocumentElementNode> children = getChildNodesList();
Iterator<IDocumentElementNode> iterator = children.iterator();
while (iterator.hasNext()) {
IDocumentElementNode node = iterator.next();
if (clazz.isInstance(node)) {
return node;
}
}
return null;
}
@Override
public int getChildNodeCount(Class<?> clazz) {
// Linear search O(n)
int count = 0;
ArrayList<IDocumentElementNode> children = getChildNodesList();
Iterator<IDocumentElementNode> iterator = children.iterator();
while (iterator.hasNext()) {
IDocumentElementNode node = iterator.next();
if (clazz.isInstance(node)) {
count++;
}
}
return count;
}
@Override
public ArrayList<IDocumentElementNode> getChildNodesList(Class<?> clazz, boolean match) {
return getChildNodesList(new Class[] {clazz}, match);
}
@Override
public ArrayList<IDocumentElementNode> getChildNodesList(Class<?>[] classes, boolean match) {
ArrayList<IDocumentElementNode> filteredChildren = new ArrayList<>();
ArrayList<IDocumentElementNode> children = getChildNodesList();
Iterator<IDocumentElementNode> iterator = children.iterator();
while (iterator.hasNext()) {
IDocumentElementNode node = iterator.next();
for (Class<?> clazz : classes) {
if (clazz.isInstance(node) == match) {
filteredChildren.add(node);
break;
}
}
}
return filteredChildren;
}
@Override
public IDocumentElementNode getNextSibling(IDocumentElementNode node, Class<?> clazz) {
int position = indexOf(node);
int lastIndex = getChildCount() - 1;
if ((position < 0) || (position >= lastIndex)) {
// Either the node was not found or the node was found but it is
// at the last index
return null;
}
// Get the next node of the given type
for (int i = position + 1; i <= lastIndex; i++) {
IDocumentElementNode currentNode = getChildAt(i);
if (clazz.isInstance(currentNode)) {
return currentNode;
}
}
return null;
}
@Override
public IDocumentElementNode getPreviousSibling(IDocumentElementNode node, Class<?> clazz) {
int position = indexOf(node);
if ((position <= 0) || (position >= getChildCount())) {
// Either the item was not found or the item was found but it is
// at the first index
return null;
}
// Get the previous node of the given type
for (int i = position - 1; i >= 0; i--) {
IDocumentElementNode currentNode = getChildAt(i);
if (clazz.isInstance(currentNode)) {
return currentNode;
}
}
return null;
}
@Override
public boolean hasChildNodes(Class<?> clazz) {
ArrayList<IDocumentElementNode> children = getChildNodesList();
Iterator<IDocumentElementNode> iterator = children.iterator();
while (iterator.hasNext()) {
IDocumentElementNode node = iterator.next();
if (clazz.isInstance(node)) {
return true;
}
}
return false;
}
@Override
public boolean isFirstChildNode(IDocumentElementNode node, Class<?> clazz) {
int position = indexOf(node);
// Check to see if node is found
if ((position < 0) || (position >= getChildCount())) {
// Node not found
return false;
} else if (position == 0) {
// Node found in the first position
return true;
}
// Check to see if there is any node before the specified node of the
// same type
// Assertion: Position > 0
for (int i = 0; i < position; i++) {
if (clazz.isInstance(getChildAt(i))) {
// Another node of the same type found before the specified node
return false;
}
}
// All nodes before the specified node were of a different type
return true;
}
@Override
public boolean isLastChildNode(IDocumentElementNode node, Class<?> clazz) {
int position = indexOf(node);
int lastIndex = getChildCount() - 1;
// Check to see if node is found
if ((position < 0) || (position > lastIndex)) {
// Node not found
return false;
} else if (position == lastIndex) {
// Node found in the last position
return true;
}
// Check to see if there is any node after the specified node of the
// same type
// Assertion: Position < lastIndex
for (int i = position + 1; i <= lastIndex; i++) {
if (clazz.isInstance(getChildAt(i))) {
// Another node of the same type found after the specified node
return false;
}
}
// All nodes after the specified node were of a different type
return true;
}
@Override
public void swap(IDocumentElementNode child1, IDocumentElementNode child2, boolean fireEvent) {
super.swap(child1, child2);
// Fire event
if (fireEvent && shouldFireEvent()) {
firePropertyChanged(this, IDocumentElementNode.F_PROPERTY_CHANGE_TYPE_SWAP, child1, child2);
}
}
/**
* @param node
* @param newRelativeIndex
*/
@Override
public void moveChildNode(IDocumentElementNode node, int newRelativeIndex, boolean fireEvent) {
// TODO: MP: TEO: MED: Test Problem, if generic not viewable, may appear that node did not move
// TODO: MP: TEO: MED: Test relative index > 1 or < -1
// TODO: MP: TEO: MED: BUG: Add item to end, move existing item before it down, teo overwrites new item
if (newRelativeIndex == 0) {
// Nothing to do
return;
}
// Get the current index of the node
int currentIndex = indexOf(node);
// Ensure the node exists
if (currentIndex == -1) {
return;
}
// Calculate the new index
int newIndex = newRelativeIndex + currentIndex;
// Validate the new index
// 0 <= newIndex < child element count
if ((newIndex < 0) || (newIndex >= getChildCount())) {
return;
}
// If we are only moving a node up and down one position use a swap
// operation. Otherwise, delete the node, clone it and then re-insert
// the node
if ((newRelativeIndex == -1) || (newRelativeIndex == 1)) {
IDocumentElementNode sibling = getChildAt(newIndex);
// Ensure sibling exists
if (sibling == null) {
return;
}
swap(node, sibling, fireEvent);
} else {
// Remove the node
removeChildNode(node, fireEvent);
// Clone the node
// Needed to create a text edit operation that inserts a new element
// rather than replacing the old one
IDocumentElementNode clone = clone(node);
// Removing the node and moving it to a positive relative index alters
// the indexing for insertion; however, this pads the new relative
// index by 1, allowing it to be inserted one position after as
// desired
// Add the node back at the specified index
addChildNode(clone, newIndex, fireEvent);
}
}
@Override
public IDocumentElementNode clone(IDocumentElementNode node) {
IDocumentElementNode clone = null;
try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
// Serialize
try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(node);
out.flush();
}
byte[] bytes = bout.toByteArray();
// Deserialize
try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bin)) {
clone = (IDocumentElementNode) in.readObject();
}
// Reconnect
clone.reconnect(this, fModel);
} catch (IOException | ClassNotFoundException e) {
}
return clone;
}
@Override
public boolean getBooleanAttributeValue(String name, boolean defaultValue) {
String value = getXMLAttributeValue(name);
if (value == null) {
return defaultValue;
} else if (value.equalsIgnoreCase(ATTRIBUTE_VALUE_TRUE)) {
return true;
} else if (value.equalsIgnoreCase(ATTRIBUTE_VALUE_FALSE)) {
return false;
}
return defaultValue;
}
@Override
public boolean setBooleanAttributeValue(String name, boolean value) {
String newValue = Boolean.toString(value);
return setXMLAttribute(name, newValue);
}
@Override
public boolean setXMLAttribute(String name, String newValue) {
String oldValue = getXMLAttributeValue(name);
boolean changed = super.setXMLAttribute(name, newValue);
// Fire an event if in the model
if (changed && shouldFireEvent()) {
firePropertyChanged(name, oldValue, newValue);
}
return changed;
}
@Override
public boolean setXMLContent(String text) {
String oldText = null;
// Get old text node
IDocumentTextNode node = getTextNode();
if (node == null) {
// Text does not exist
oldText = ""; //$NON-NLS-1$
} else {
// Text exists
oldText = node.getText();
}
boolean changed = super.setXMLContent(text);
// Fire an event
if (changed && shouldFireEvent()) {
if (node != null) {
firePropertyChanged(node, IDocumentTextNode.F_PROPERTY_CHANGE_TYPE_PCDATA, oldText, text);
} else {
fireStructureChanged(this, IModelChangedEvent.INSERT);
}
}
return changed;
}
@Override
protected Charset getFileEncoding() {
if ((fModel != null) && (fModel instanceof IEditingModel)) {
return ((IEditingModel) fModel).getCharset();
}
return super.getFileEncoding();
}
}