//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// J2ME-Lib source file
// Copyright (c) 2006 Elmar Sonnenschein / esoco GmbH
// Last Change: 10.11.2006 by eso
//
// J2ME-Lib is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// J2ME-Lib is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with J2ME-Lib; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or use the contact
// information from the GNU website http://www.gnu.org
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package de.esoco.j2me.model;
import de.esoco.j2me.J2meLib;
import de.esoco.j2me.storage.HierarchicalStorage;
import de.esoco.j2me.storage.StorageException;
import de.esoco.j2me.storage.StorageFullException;
import de.esoco.j2me.util.Arrays;
import de.esoco.j2me.util.IntArray;
import de.esoco.j2me.util.Log;
import java.io.IOException;
import java.util.Vector;
/********************************************************************
* Basic implementation of the MutableHierarchyNode interface with data fields,
* child object management, and corresponding methods. This implementation does
* not support child nodes which are not also BasicHierarchyNodes.
*
* <p>The implementation can be made fail-safe persistent by backing it with a
* HierarchicalStorage implementation with random access (normally provided by
* the root node). An instance will then store itself immediately after each
* change to it's data and/or structure.</p>
*
* @author eso
*/
public class BasicHierarchyNode implements MutableHierarchyNode
{
//~ Static fields/initializers ---------------------------------------------
/** Node type character constants */
private static final char NTCHAR_GROUP = 'G';
private static final char NTCHAR_TEXT = 'T';
/** The storage handle value for nodes that haven't been stored or loaded */
private static final int STORAGE_UNASSIGNED = -1;
/** The "previous node handle" value for the first child node */
private static final int FIRST_CHILD_NODE = -1;
//~ Instance fields --------------------------------------------------------
/** Node data: data of the node */
protected byte[] aData = null;
/** Node data: title of the node */
protected String sTitle;
/** A Vector containing the child nodes in their logical order */
private Vector aChildren = new Vector();
/** Handle for the storage record the node is stored in */
private int nStorageHandle = STORAGE_UNASSIGNED;
/** The parent node or NULL for the root node */
private BasicHierarchyNode rParent = null;
/** The previous node or NULL for the first node in a child list */
private BasicHierarchyNode rPrevious = null;
//~ Constructors -----------------------------------------------------------
/***************************************
* Constructs a node from a title and byte array data.
*
* @param rParent The parent node
* @param sTitle The title string
* @param rData The node data as a byte array
*/
public BasicHierarchyNode(BasicHierarchyNode rParent,
String sTitle,
byte[] rData)
{
this.rParent = rParent;
this.sTitle = sTitle;
if (rData != null)
{
setDataField(rData);
}
}
//~ Methods ----------------------------------------------------------------
/***************************************
* @see MutableHierarchyNode#createChild(String, String)
*/
public MutableHierarchyNode createChild(String sCreateOption,
String sTitle)
{
BasicHierarchyNode rNode = newChildNode(sTitle);
if (J2meLib.DEBUG)
{
Log.debug("New Node(" + sTitle + "): " +
rNode.getClass().getName());
}
if (sCreateOption == NT_TEXT)
{
rNode.aData = new byte[0];
}
return rNode;
}
/***************************************
* Recursively creates a copy of this node and all it's children. The copies
* will include all fields of the original nodes except for the storage
* handle. To make the copy persistent it must be written to a storage with
* the writeTo() method.
*
* <p>To copy the data fields of the node the method copyFields() will be
* called. If necessary, subclasses can overload it to copy their additional
* data fields after invoking super.copyFields().</p>
*
* @see de.esoco.j2me.model.HierarchyNode#deepCopy()
*/
public HierarchyNode deepCopy()
{
BasicHierarchyNode aCopy = newChildNode(sTitle);
copyFields(aCopy);
for (int i = 0, n = aChildren.size(); i < n; i++)
{
BasicHierarchyNode rChild = (BasicHierarchyNode) getChild(i);
aCopy.insertChildNode((BasicHierarchyNode) rChild.deepCopy(), i);
}
return aCopy;
}
/***************************************
* @see HierarchyNode#getChild(int)
*/
public HierarchyNode getChild(int nIndex)
{
return (HierarchyNode) aChildren.elementAt(nIndex);
}
/***************************************
* @see HierarchyNode#getChildCount()
*/
public int getChildCount()
{
return aChildren.size();
}
/***************************************
* @see MutableHierarchyNode#getChildCreateOptions()
*/
public String[] getChildCreateOptions()
{
return new String[] { NT_GROUP, NT_TEXT };
}
/***************************************
* @see HierarchyNode#getChildIndex(HierarchyNode)
*/
public int getChildIndex(HierarchyNode rChild)
{
return aChildren.indexOf(rChild);
}
/***************************************
* Returns a copy of the internal data array.
*
* @see de.esoco.j2me.model.HierarchyNode#getData()
*/
public byte[] getData()
{
return ((aData != null) ? Arrays.copy(aData) : null);
}
/***************************************
* @see HierarchyNode#getChild(int)
*/
public int getMaximumSize()
{
if (isGroup())
{
return UNLIMITED_SIZE;
}
else
{
return Short.MAX_VALUE;
}
}
/***************************************
* Returns the node type which will be either NT_GROUP or NT_TEXT.
*
* @return The string constant describing the node type
*
* @see HierarchyNode#getNodeType()
*/
public String getNodeType()
{
return ((aData == null) ? NT_GROUP : NT_TEXT);
}
/***************************************
* @see HierarchyNode#getParent()
*/
public HierarchyNode getParent()
{
return rParent;
}
/***************************************
* Returns the root node of the hierarchy (i.e. the node where the parent
* NULL).
*
* @return The root node of the heriarchy
*/
public HierarchyNode getRoot()
{
if (rParent != null)
{
return rParent.getRoot();
}
else
{
return this;
}
}
/***************************************
* @see HierarchyNode#getTitle()
*/
public String getTitle()
{
return sTitle;
}
/***************************************
* Returns the total number of children in the hierarchy of this node (not
* counting the starting node itself).
*
* @return The total number of children of the node
*/
public int getTotalChildCount()
{
int nCount = aChildren.size();
for (int i = 0, n = nCount; i < n; i++)
{
nCount += ((BasicHierarchyNode) getChild(i)).getTotalChildCount();
}
return nCount;
}
/***************************************
* Inserts a child object at a specific position into the child list. The
* node previously at that position and all nodes behind it will have an
* index that is one higher afterwards. The given child must be an instance
* that has been created by the createChild() method of this node, otherwise
* a ClassCastException will be thrown.
*
* <p><b>Attention</b>: The given child node ist directly inserted into the
* child list of the node. Therefore it must be a separate instance and no
* reference to it should be held and modified by the application
* afterwards. Otherwise side effects will occur. This is important for
* example, if the application manages a kind of clipbord to insert nodes
* from. If it cannot made sure that the node is unique and independent the
* result of a call to the method <code>deepCopy()</code> on the node should
* be inserted instead.</p>
*
* <p>This method also creates and updates all affected storage records if
* the node hierarchy is backed by a storage.</p>
*
* @param rNewChild The child node to add
* @param nIndex The position at which to insert (use getChildCount() to
* append the child to the end of the child list)
*
* @throws ClassCastException If the child node hasn't a class of a node
* created by createChild()
*
* @see de.esoco.j2me.model.MutableHierarchyNode#insertChild(HierarchyNode,
* int)
*/
public void insertChild(HierarchyNode rNewChild, int nIndex)
throws NodeAccessException, StorageException
{
insertChildNode((BasicHierarchyNode) rNewChild, nIndex);
HierarchicalStorage rStorage = getStorage();
if ((rStorage != null) && (nStorageHandle != STORAGE_UNASSIGNED))
{
try
{
// immediately write the child hierarchy to the storage
// open parent node for reading only so that it is not necessary
// to write out it's data again
rStorage.openNode(nStorageHandle, false);
writeChildNode(rStorage, nIndex);
rStorage.closeNode();
}
catch (IOException eIO)
{
throw new StorageException("Writing child node(s) to storage failed",
eIO);
}
// insertChildNode() modifies rPrevious of the next node, so it
// must also be stored
if (++nIndex < aChildren.size())
{
((BasicHierarchyNode) aChildren.elementAt(nIndex))
.updateStorage(rStorage);
}
}
}
/***************************************
* Returns TRUE if data string is null. Therefore leaf nodes must always
* contain at least an empty String.
*
* @return TRUE if Data is null, FALSE otherwise
*
* @see HierarchyNode#isGroup()
*/
public boolean isGroup()
{
return (aData == null);
}
/***************************************
* @see MutableHierarchyNode#removeChild(int)
*/
public void removeChild(int nIndex) throws StorageException
{
BasicHierarchyNode rChild = (BasicHierarchyNode) getChild(nIndex);
HierarchicalStorage rStorage = getStorage();
aChildren.removeElementAt(nIndex);
if (nIndex < aChildren.size())
{
// if the child wasn't the last node, update the reference to the
// previous node of it's successor which is now at the same
// position as the removed node
BasicHierarchyNode rNext = (BasicHierarchyNode) getChild(nIndex);
rNext.rPrevious = rChild.rPrevious;
rNext.updateStorage(rStorage);
}
rChild.deleteFromStorage(rStorage);
}
/***************************************
* Sets the data of the node. The new array must never be NULL. It's
* contents will be copied into the node.
*
* @see MutableHierarchyNode#setData(byte[])
*/
public void setData(byte[] rNewData) throws StorageException
{
setDataField(rNewData);
updateStorage(getStorage());
}
/***************************************
* @see MutableHierarchyNode#setTitle(String)
*/
public void setTitle(String sNewTitle) throws StorageException
{
setTitleField(sNewTitle);
updateStorage(getStorage());
}
/***************************************
* Invokes the internal toString(sIndent) method with an empty indentation
* string.
*
* @return A formatted, multi-line String for this node and all children
*/
public String toString()
{
return toString("");
}
/***************************************
* Copies the data fields of the node into another node. This is used by the
* method deepCopy() and should be overloaded by subclasses to copy their
* additional data fields. It will only copy the data elements of the node,
* not the fields that are used to manage the node hierarchy (like the
* parent reference or the child nodes).
*
* @param rTarget The target node to copy the data into
*/
protected void copyFields(BasicHierarchyNode rTarget)
{
rTarget.sTitle = sTitle;
rTarget.aData = ((aData != null) ? Arrays.copy(aData) : null);
}
/***************************************
* Deletes the node and all it's children from the underlying storage (if
* such exists). This will also reset the internal storage handle to
* NOT_IN_STORAGE so that further storage access is not possible. To make
* the node persistent again it would be necessary to write it to the
* storage with the method writeTo().
*
* @param rStorage The storage to delete the node(s) from
*
* @throws StorageException If deleting fails
*/
protected void deleteFromStorage(HierarchicalStorage rStorage)
throws StorageException
{
if ((rStorage != null) && (nStorageHandle != STORAGE_UNASSIGNED))
{
for (int i = 0, n = aChildren.size(); i < n; i++)
{
((BasicHierarchyNode) getChild(i)).deleteFromStorage(rStorage);
}
rStorage.deleteNode(nStorageHandle);
nStorageHandle = STORAGE_UNASSIGNED;
}
}
/***************************************
* Returns a storage implementation in which the node data shall be stored
* on each change. If this method return null, the data will not be stored
* automatically.
*
* <p>The default implementation simply return Parent.getStorage() or null
* if the node has no parent. Therefore it is necessary to implement a
* subclass as the root node to provide a storage for automatic saving at
* runtime.</p>
*
* @return HierarchicalStorage
*/
protected HierarchicalStorage getStorage()
{
if (rParent instanceof BasicHierarchyNode)
{
return ((BasicHierarchyNode) rParent).getStorage();
}
else
{
return null;
}
}
/***************************************
* Inserts a new child into the child list of this node. The new child will
* get the index of the insert position and the node currently at that
* position and all nodes behind will have an index that is one higher
* afterwards. This method will also set the parent of the new child node to
* this node and update the references to the previous child node in all
* affected nodes.
*
* <p>None of the changes made by this method will be made persistent,
* therefore this is the responsibility of the caller. When saving the
* changes it is necessary to consider that not only the inserted child but
* also the next node in the child list will be modified (it's reference to
* the previous node will be changed).</p>
*
* @param rChild The child node to insert
* @param nIndex The position where to insert the child node
*/
protected void insertChildNode(BasicHierarchyNode rChild, int nIndex)
{
int nPrev = nIndex - 1;
rChild.rParent = this;
if (nPrev >= 0)
{
rChild.rPrevious = (BasicHierarchyNode) aChildren.elementAt(nPrev);
}
else
{
rChild.rPrevious = null;
}
if (nIndex < aChildren.size())
{
BasicHierarchyNode rNext = (BasicHierarchyNode) aChildren.elementAt(nIndex);
rNext.rPrevious = rChild;
}
aChildren.insertElementAt(rChild, nIndex);
}
/***************************************
* Returns a new child node instance. Can be overloaded by subclasses that
* don't want to replace the complete createChild() method. This is only
* possible if the subclass doesn't change the handling of node types from
* BasicHierarchyNode (i.e. Data == NULL means it's a group node) but only
* wants to use it's own instances as children. The instance should be
* initialized to have no parent node (i.e. parent = NULL) because the
* parent will be set in the insertChild() method.
*
* @param sTitle The title of the new Node to be created
*
* @return An instance of an MutableHierarchyNode subclass
*/
protected BasicHierarchyNode newChildNode(String sTitle)
{
return new BasicHierarchyNode(null, sTitle, null);
}
/***************************************
* Reads the data fields of the node from a HierarchicalStorage
* implementation. This method is invoked internally by the readFrom()
* method. It must be overloaded by derived classes that need to read
* additional fields before or after (or even instead of) reading the base
* class fields.
*
* @param rStorage The storage to read from
*
* @return The storage handle of the previous node in the child list
*
* @throws StorageException If access to the storage fails
* @throws IOException If an I/O error occurs during reading
*/
protected int readFields(HierarchicalStorage rStorage)
throws StorageException, IOException
{
int nPreviousNodeStorageHandle = rStorage.readInt();
char cNodeType = (char) rStorage.readByte();
sTitle = rStorage.readString();
if (cNodeType != NTCHAR_GROUP)
{
aData = rStorage.readData();
}
if (J2meLib.DEBUG)
{
Log.debug("Node read (T'T'D): " + cNodeType + "'" + sTitle + "'" +
((aData != null) ? new String(aData) : ""));
}
return nPreviousNodeStorageHandle;
}
/***************************************
* Reads this Node from a HierarchicalStorage implementation. It reads it's
* children recursively from the storage, therefore it should normally only
* be invoked on the root node. This is enforced by this method being
* protected so that a specific root node subclass (the application's data
* model) should be implemented that provides corresponding top-level
* methods (e.g. load() and save()).
*
* <p>The method expects that the node to read from has already been
* initialized by invoking the method nextNode() on the storage instance. It
* is also required for the caller to close the node after the method
* returns.The method will invoke nextNode() for each child node it stores
* and close the node after invoking the child object's readFrom() method.
* </p>
*
* <p>Although subclassing of this method is possible because of the above,
* it is not recommended to do so except for special cases. Instead, the
* method <code>readFields()</code> (and it's counterpart <code>
* writeFields()</code>) should be overloaded in normal cases.</p>
*
* @param rStorage The storage to read from
*
* @return The integer value that defines the logical child order as defined
* by the <code>writeTo()</code> method
*
* @throws StorageException If access to the storage fails
* @throws IOException If an I/O error occurs during reading
*/
protected int readFrom(HierarchicalStorage rStorage)
throws StorageException, IOException
{
if (nStorageHandle == STORAGE_UNASSIGNED)
{
nStorageHandle = rStorage.getCurrentNode();
}
int nPreviousNodeStorageHandle = readFields(rStorage);
IntArray aPrevNodeHandles = new IntArray();
while (rStorage.nextNode())
{
BasicHierarchyNode rNode = newChildNode(null);
rNode.rParent = this;
aPrevNodeHandles.add(rNode.readFrom(rStorage));
rStorage.closeNode();
aChildren.addElement(rNode);
}
rebuildChildOrder(aPrevNodeHandles);
return nPreviousNodeStorageHandle;
}
/***************************************
* Sets the data attribute of the node without updating the underlying
* storage. This method can be overloaded by subclasses to intercept the
* setting of the value.
*
* @param rNewData The new data of the node
*/
protected void setDataField(byte[] rNewData)
{
aData = Arrays.copy(rNewData);
}
/***************************************
* Sets the title attribute of the node without updating the underlying
* storage. This method can be overloaded by subclasses to intercept the
* setting of the value.
*
* @param sNewTitle The new title of the node
*/
protected void setTitleField(String sNewTitle)
{
sTitle = sNewTitle;
}
/***************************************
* Returns a string with formatted output of the node and it's child
* hierarchy.
*
* @param sIndent The indentation prefix for the output
*
* @return A formatted, multi-line String for this node and all children
*/
protected String toString(String sIndent)
{
String s = "[" + sTitle + "]:";
for (int i = 0, n = aChildren.size(); i < n; i++)
{
s += ("\n" + sIndent + "+-");
HierarchyNode rChild = getChild(i);
s += ((BasicHierarchyNode) rChild).toString(sIndent + " ");
}
return s;
}
/***************************************
* Updates the data fields in the underlying storage. This will only be
* performed if a storage is provided by the root node and if the storage
* handle has been provided on reading the node for the first time (which is
* only available in storages that implement random access).
*
* @param rStorage The storage to update the child data in
*
* @throws StorageException If updating the data in the storage fails
*/
protected void updateStorage(HierarchicalStorage rStorage)
throws StorageException
{
if ((rStorage != null) && (nStorageHandle != STORAGE_UNASSIGNED))
{
try
{
rStorage.openNode(nStorageHandle, true);
writeFields(rStorage);
rStorage.closeNode();
}
catch (IOException eIO)
{
throw new StorageException("Update of fields in storage failed",
eIO);
}
}
}
/***************************************
* Writes a certain child node from the child list to a storage. Will also
* store the child node's own child hierarchy to the storage recursively.
*
* @param rStorage The storage to write to
* @param nIndex The index position of the child to write in the child
* list
*
* @throws StorageException If access to the storage fails
* @throws StorageFullException If the storage has no more capacity
* @throws IOException If an I/O error occurs during writing
* @throws NullPointerException If the storage is NULL
*/
protected void writeChildNode(HierarchicalStorage rStorage, int nIndex)
throws StorageException, StorageFullException, IOException
{
rStorage.createNode();
((BasicHierarchyNode) getChild(nIndex)).writeTo(rStorage);
rStorage.closeNode();
}
/***************************************
* Writes the data fields of the node to a HierarchicalStorage
* implementation. This method is invoked internally by the writeTo()
* method. It must be overloaded by derived classes that need to write
* additional fields before or after writing the base class fields. If it is
* overloaded it is absolutely necessary for the derived class to invoke
* <code>super.writeFields()</code>, unless it completely rebuilds the
* read/write mechanism of this class.
*
* @param rStorage The storage to write to
*
* @throws StorageException If access to the storage fails
* @throws IOException If an I/O error occurs during writing
*/
protected void writeFields(HierarchicalStorage rStorage)
throws StorageException, IOException
{
char cNodeType = ((aData == null) ? NTCHAR_GROUP : NTCHAR_TEXT);
// write the storage handle of the previous child node as a hint for
// reconstructing the child order when reading the nodes back in
rStorage.writeInt((rPrevious != null) ? rPrevious.nStorageHandle
: FIRST_CHILD_NODE);
rStorage.writeByte((byte) cNodeType);
rStorage.writeString(sTitle);
if (aData != null)
{
rStorage.writeData(aData);
}
if (J2meLib.DEBUG)
{
Log.debug("writing Node (T'T'D): " + cNodeType + "'" + sTitle +
"'" + ((aData != null) ? new String(aData) : ""));
}
}
/***************************************
* <p>Writes this Node to a HierarchicalStorage implementation. It call this
* method recursively on it's children, therefore it should normally only be
* invoked on the root node. This is enforced by this method being protected
* so that a specific root node subclass (the application's data model)
* should be implemented that provides corresponding top-level methods (e.g.
* load() and save()).</p>
*
* <p>The corresponding node must have been created already in the storage,
* this will not be done by this method. Otherwise it would be difficult to
* overload this method in subclasses. After the method returns, the node
* that has been written to needs to be closed by the caller. The method
* will create a new storage node for each child object it stores and close
* it after invoking the child object's writeTo() method.</p>
*
* <p>Although subclassing of this method is possible because of the above,
* it is not recommended to do so except for special cases. Instead, the
* method <code>writeFields()</code> (and it's counterpart <code>
* readFields()</code>) should be overloaded in normal cases.</p>
*
* @param rStorage The storage to write to
*
* @throws StorageException If access to the storage fails
* @throws StorageFullException If the storage has no more capacity
* @throws IOException If an I/O error occurs during writing
* @throws NullPointerException If the storage is NULL
*/
protected void writeTo(HierarchicalStorage rStorage)
throws StorageException, IOException
{
// in case of a new node, keep the handle of the associated record
if (nStorageHandle == STORAGE_UNASSIGNED)
{
nStorageHandle = rStorage.getCurrentNode();
}
writeFields(rStorage);
for (int i = 0, n = aChildren.size(); i < n; i++)
{
writeChildNode(rStorage, i);
}
}
/***************************************
* Rebuilds the order of the child nodes after they have been read from the
* storage.
*
* @param rPrevNodeHandles An IntArray containing the storage handles of
* the previous nodes of the child nodes at the
* same positions in this node's child vector
*
* @throws StorageException If the child hierarchy cannot be rebuilt due to
* inconsistencies
*/
private void rebuildChildOrder(IntArray rPrevNodeHandles)
throws StorageException
{
BasicHierarchyNode rPrev = null;
BasicHierarchyNode rChild = null;
int nSearchHandle = FIRST_CHILD_NODE;
int nCount = aChildren.size();
for (int i = 0; i < nCount; i++)
{
int pos = rPrevNodeHandles.find(nSearchHandle);
if (pos >= 0)
{
rChild = (BasicHierarchyNode) aChildren.elementAt(pos);
rChild.rPrevious = rPrev;
if (pos != i)
{
// Swap child AND it's PrevNodeHandle to correct array pos
HierarchyNode rTmp = (HierarchyNode) aChildren.elementAt(i);
int nTmp = rPrevNodeHandles.get(i);
aChildren.setElementAt(rChild, i);
aChildren.setElementAt(rTmp, pos);
rPrevNodeHandles.set(nSearchHandle, i);
rPrevNodeHandles.set(nTmp, pos);
}
nSearchHandle = rChild.nStorageHandle;
rPrev = rChild;
}
else
{
throw new StorageException("Child hierarchy inconsistent");
}
}
}
}