Parcourir la source

Text.PersistenceXml. Initial Import.

Brucey il y a 2 ans
Parent
commit
0e048bc97e

+ 19 - 0
persistencexml.mod/doc/intro.bbdoc

@@ -0,0 +1,19 @@
+<p>Persistence is a means of storing data beyond the execution of a program, in such a way that the data can be reproduced, in memory, at a later time.</p>
+<p>The Text.Persistence module attempts to do this through the use of reflection and xml.</p>
+<h3>Usage</h3>
+<p>
+The most simple usage is to pass an object into TPersist.Serialize(), which returns the serialized representation of it as a String.
+</p>
+<p>
+To reverse the process, the serialized String can be passed into TPersist.DeSerialize(), returning a new Object instance, which is essentially a "deep"
+copy of the original object.
+</p>
+<p>
+There are also methods, SerializeToFile(), SerializeToDoc(), and SerializeToStream() which output to different formats apporpriately.
+</p>
+<p>
+The DeSerialize() function itself can take a TxmlDoc, TStream or String data as a parameter, as well as there being the specialised methods of
+DeSerializeFromDoc(), DeSerializeFromFile(), and DeSerializeFromStream().
+</p>
+<p>
+</p>

+ 155 - 0
persistencexml.mod/examples/example_01.bmx

@@ -0,0 +1,155 @@
+SuperStrict
+
+Framework Text.PersistenceXml
+Import BRL.StandardIO
+
+Type TRect
+	Field x:Int
+	Field y:Int
+	Field w:Int
+	Field h:Int
+	Field ignoreMe:String = "Hello" {nopersist}
+End Type
+
+Type TObj
+	Field text:String
+	Field numbersi:Int[]
+	Field numbersf:Float[]
+	Field numbersd:Double[]
+	Field numbersl:Long[]
+	Field multi:Int[,,]
+	Field circularRef:TTest
+	Field refNull:TTest
+	Field emptyList:TList = New TList
+	Field list:TList = New TList
+	Field rect:TRect = New TRect
+	
+	Field map:TMap = New TMap
+	Field map2:TMap = New TMap
+		
+	Function Set:TObj()
+		Local this:TObj = New TObj
+		
+		this.text = "woot"
+		this.numbersi = [ 1, 2, 3, 4, 5, 6 ]
+		this.numbersf = [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 ]
+		this.numbersd = [ 1.0:Double, 2.0:Double, 3.0:Double, 4.0:Double, 5.0:Double, 6.0:Double ]
+		this.numbersl = [ 1:Long, 2:Long, 3:Long, 4:Long, 5:Long, 6:Long ]
+		this.multi    = New Int[3,4,5]
+		this.multi[0,0,0] = 22
+		this.multi[1,2,2] = 33
+		this.multi[2,3,4] = 44
+		this.list.AddLast("Item 1")
+		this.rect.x = 100
+		this.rect.y = 200
+		this.rect.w = 300
+		this.rect.h = 400
+		
+		this.map.Insert("Key 1", "Value 1")
+		
+		Return this
+	End Function
+	
+End Type
+
+Type TTest
+	Field one:String
+	Field two:Int
+	Field three:Float
+	Field four:Double
+	Field five:Long
+	
+	Field obj:TObj
+	
+	Field rects:TRect[]
+	
+	Function Set:TTest()
+		Local this:TTest = New TTest
+		
+		this.one = "Hello World"
+		this.two = 155
+		this.three = 3.33
+		this.four = 2.95
+		this.five = 222
+		
+		this.obj = TObj.Set()
+		this.obj.circularRef = this
+		
+		this.rects = New TRect[2]
+		'rects[0] = New TRect ' <- null!
+		this.rects[1] = New TRect
+		this.rects[1].y = 125
+		
+		Return this
+	End Function
+	
+End Type
+
+' register as a default serializer
+TXMLPersistenceBuilder.RegisterDefault(New TRectXmlSerializer)
+
+Local test:TTest = TTest.Set()
+Local persist:TPersist = New TXMLPersistenceBuilder.Build()
+
+' ++  Serialize to a String
+Local s:String = persist.SerializeToString(test)
+Print s
+
+persist.Free()
+
+' ++  De-serialize the String
+Local obj:Object = persist.DeSerializeObject(s)
+persist.Free()
+
+' ++ Create a Stream and Serialize the current object to it.
+Local stream:TStream = WriteStream("example.bmo")
+persist.SerializeToStream(obj, stream)
+stream.Close()
+
+persist.Free()
+
+' ++ De-serialize from a Stream.
+stream = ReadStream("example.bmo")
+obj = persist.DeSerializeFromStream(stream)
+persist.Free()
+
+' ++ Serialize and output the latest object... all should be well :-)
+TPersist.format = True
+Print persist.SerializeToString(obj)
+
+
+Type TRectXMLSerializer Extends TXMLSerializer
+
+	Global nil:TNode = New TMap._root
+
+	Method TypeName:String()
+		Return "TRect"
+	End Method
+	
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local rect:TRect = TRect(obj)
+		Local sb:TStringBuilder = New TStringBuilder
+		sb.AppendInt(rect.x)
+		sb.Append(",").AppendInt(rect.y)
+		sb.Append(",").AppendInt(rect.w)
+		sb.Append(",").AppendInt(rect.h)
+		node.SetContent(sb.ToString())
+	End Method
+	
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode)
+		Local rect:TRect = TRect(CreateObjectInstance(objType, node))
+		Local parts:String[] = node.GetContent().Split(",")
+		If parts.length = 4 Then
+			rect.x = Int(parts[0])
+			rect.y = Int(parts[1])
+			rect.w = Int(parts[2])
+			rect.h = Int(parts[3])
+		End If
+		Return rect	
+	End Method
+
+	Method Clone:TXMLSerializer()
+		Return New TRectXMLSerializer
+	End Method
+
+End Type

+ 108 - 0
persistencexml.mod/examples/example_02.bmx

@@ -0,0 +1,108 @@
+SuperStrict
+
+Framework Text.PersistenceXml
+Import BRL.StandardIO
+
+Type TRect
+	Field x:Int
+	Field y:Int
+	Field w:Int
+	Field h:Int
+	Field ignoreMe:String = "Hello" {nopersist}
+End Type
+
+Type TObj
+	Field text:String
+	Field numbersi:Int[]
+	Field numbersf:Float[]
+	Field numbersd:Double[]
+	Field numbersl:Long[]
+	Field multi:String[,,]
+	Field circularRef:TTest
+	Field refNull:TTest
+	Field emptyList:TList = New TList
+	Field list:TList = New TList
+	Field rect:TRect = New TRect
+		
+	Function Set:TObj()
+		Local this:TObj = New TObj
+		
+		this.text = "woot"
+		this.numbersi = [ 1, 2, 3, 4, 5, 6 ]
+		this.numbersf = [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 ]
+		this.numbersd = [ 1.0:Double, 2.0:Double, 3.0:Double, 4.0:Double, 5.0:Double, 6.0:Double ]
+		this.numbersl = [ 1:Long, 2:Long, 3:Long, 4:Long, 5:Long, 6:Long ]
+		this.multi    = New String[3,4,5]
+		this.multi[0,0,0] = 22
+		this.multi[1,2,2] = "<sd>"
+		this.multi[2,3,4] = 44
+		this.list.AddLast("Item 1")
+		this.rect.x = 100
+		this.rect.y = 200
+		this.rect.w = 300
+		this.rect.h = 400
+		
+		Return this
+	End Function
+	
+End Type
+
+Type TTest
+	Field one:String
+	Field two:Int
+	Field three:Float
+	Field four:Double
+	Field five:Long
+	
+	Field obj:TObj
+	
+	Field rects:TRect[]
+	
+	Field list:TList
+	
+	Function Set:TTest()
+		Local this:TTest = New TTest
+		
+		this.one = "Hello World"
+		this.two = 155
+		this.three = 3.33
+		this.four = 2.95
+		this.five = 222
+		
+		this.obj = TObj.Set()
+		this.obj.circularRef = this
+		
+		this.rects = New TRect[2]
+		'rects[0] = New TRect ' <- null!
+		this.rects[1] = New TRect
+		this.rects[1].y = 125
+		
+		this.list = New TList
+		' make lots of objects
+		For Local i:Int = 0 Until 1500
+			this.list.AddLast(TObj.Set())
+		Next
+		
+		Return this
+	End Function
+	
+End Type
+
+Local test:TTest = TTest.Set()
+Local obj:Object 
+
+Local persist:TPersist = New TXMLPersistenceBuilder.Build()
+
+' compress the data
+TPersist.compressed = True
+
+' ++ Compression only works with "files"
+persist.SerializeToFile(test, "example2.bmo")
+persist.Free()
+
+Print "Saved..."
+
+' ++ De-serialize from a file.
+obj = persist.DeSerializeFromFile("example2.bmo")
+
+

+ 26 - 0
persistencexml.mod/examples/example_03.bmx

@@ -0,0 +1,26 @@
+SuperStrict
+
+Framework Text.PersistenceXml
+Import BRL.StandardIO
+
+Type abc
+	Field map:TMap = New TMap
+End Type
+
+Local persist:TPersist = New TXMLPersistenceBuilder.Build()
+
+Local map:TMap = New TMap
+map.Insert("fish", "chips")
+map.insert("abc", New abc)
+
+TPersist.format = True
+Local s:String = persist.SerializeToString(map)
+Print s + "~n~n~n"
+
+persist.Free()
+
+Local obj:Object = persist.DeSerializeObject(s)
+persist.Free()
+
+Print persist.SerializeToString(obj)
+

+ 39 - 0
persistencexml.mod/examples/example_04.bmx

@@ -0,0 +1,39 @@
+SuperStrict
+
+Framework Text.PersistenceXml
+Import BRL.StandardIO
+Import BRL.Random
+
+Type abc
+	Const bits:String = "<>`~~~q~r~n!@£$%^&*()|}{:?abcdefghijklmnopqrstuvwxyz¡€#¢∞§¶•ªº–≠"
+
+	Field text:String
+	Method Create:abc()
+		For Local i:Int = 0 Until 128
+			Local index:Int = Rand(0, bits.length-1)
+			text:+ bits[index..index+1]
+		Next
+		
+		Return Self
+	End Method
+End Type
+
+SeedRnd(MilliSecs())
+
+
+Local obj:abc = New abc.Create()
+Print obj.text
+
+Local persist:TPersist = New TXMLPersistenceBuilder.Build()
+
+TPersist.format = True
+Local s:String = persist.SerializeToString(obj)
+Print s + "~n~n~n"
+
+persist.Free()
+
+obj = abc(persist.DeSerialize(s))
+persist.Free()
+
+Print persist.SerializeToString(obj)
+Print obj.text

+ 23 - 0
persistencexml.mod/examples/example_tlist.bmx

@@ -0,0 +1,23 @@
+SuperStrict
+
+Framework BRL.Standardio
+Import Text.PersistenceXml
+
+Local list:TList = New TList
+list.AddLast("Hello")
+list.AddLast("World")
+
+
+Local persist:TPersist = New TXMLPersistenceBuilder.Build()
+
+Local s:String = persist.SerializeToString(list)
+
+Print s
+
+persist.Free()
+
+list = TList(persist.DeserializeObject(s))
+
+For Local s:String = EachIn list
+	Print s
+Next

+ 38 - 0
persistencexml.mod/glue.c

@@ -0,0 +1,38 @@
+/*
+ Copyright (c) 2008-2022 Bruce A Henderson
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/ 
+#include "brl.mod/blitz.mod/blitz.h"
+
+void * bbEmptyStringPtr() {
+	return &bbEmptyString;
+}
+
+void * bbNullObjectPtr() {
+	return &bbNullObject;
+}
+
+void * bbEmptyArrayPtr() {
+	return &bbEmptyArray;
+}
+
+void * bbObjectRef(BBObject * obj) {
+	return obj;
+}

+ 1255 - 0
persistencexml.mod/persistencexml.bmx

@@ -0,0 +1,1255 @@
+' Copyright (c) 2008-2022 Bruce A Henderson
+' 
+' Permission is hereby granted, free of charge, to any person obtaining a copy
+' of this software and associated documentation files (the "Software"), to deal
+' in the Software without restriction, including without limitation the rights
+' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+' copies of the Software, and to permit persons to whom the Software is
+' furnished to do so, subject to the following conditions:
+' 
+' The above copyright notice and this permission notice shall be included in
+' all copies or substantial portions of the Software.
+' 
+' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+' OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+' THE SOFTWARE.
+' 
+SuperStrict
+
+Rem
+bbdoc: Persistence XML
+about: An XML-based object-persistence framework. 
+End Rem
+Module Text.PersistenceXml
+
+ModuleInfo "Version: 1.06"
+ModuleInfo "Author: Bruce A Henderson"
+ModuleInfo "License: MIT"
+ModuleInfo "Copyright: 2008-2022 Bruce A Henderson"
+
+ModuleInfo "History: 1.06"
+ModuleInfo "History: Updated to use Text.XML."
+ModuleInfo "History: 1.05"
+ModuleInfo "History: Improved persistence."
+ModuleInfo "History: 1.04"
+ModuleInfo "History: Improved persistence."
+ModuleInfo "History: Added unit tests."
+ModuleInfo "History: 1.03"
+ModuleInfo "History: Added custom serializers."
+ModuleInfo "History: 1.02"
+ModuleInfo "History: Added XML parsing options arg for deserialization."
+ModuleInfo "History: Fixed 64-bit address ref issue."
+ModuleInfo "History: 1.01"
+ModuleInfo "History: Added encoding for String and String Array fields. (Ronny Otto)"
+ModuleInfo "History: 1.00"
+ModuleInfo "History: Initial Release"
+
+Import Text.XML
+Import BRL.Reflection
+Import BRL.Map
+Import BRL.Stream
+
+Import "glue.c"
+
+Rem
+bbdoc: Object Persistence.
+End Rem
+Type TPersist
+
+	Rem
+	bbdoc: File format version
+	End Rem
+	Const BMO_VERSION:Int = 8
+
+	Field doc:TxmlDoc
+	Field objectMap:TMap = New TMap
+	
+	Field lastNode:TxmlNode
+	
+	Rem
+	bbdoc: Serialized formatting.
+	about: Set to True to have the data formatted nicely. Default is False - off.
+	End Rem
+	Global format:Int = False
+	
+	Rem
+	bbdoc: Compressed serialization.
+	about: Set to True to compress the serialized data. Default is False - no compression.
+	End Rem
+	Global compressed:Int = False
+	
+?ptr64
+	Global bbEmptyString:String = Base36(Long(bbEmptyStringPtr()))
+	Global bbNullObject:String = Base36(Long(bbNullObjectPtr()))
+	Global bbEmptyArray:String = Base36(Long(bbEmptyArrayPtr()))
+?Not ptr64
+	Global bbEmptyString:String = Base36(Int(bbEmptyStringPtr()))
+	Global bbNullObject:String = Base36(Int(bbNullObjectPtr()))
+	Global bbEmptyArray:String = Base36(Int(bbEmptyArrayPtr()))
+?
+	Field fileVersion:Int
+	
+	Field serializers:TMap = New TMap
+	Field _inited:Int
+
+	Rem
+	bbdoc: Serializes the specified Object into a String.
+	End Rem
+	Method Serialize:String(obj:Object)
+		Return SerializeToString(obj)
+	End Method
+	
+	Method Free()
+		If doc Then
+			doc.Free()
+			doc = Null
+		End If
+		If lastNode Then
+			lastNode = Null
+		End If
+		objectMap.Clear()
+	End Method
+	
+	Rem
+	bbdoc: Serializes an Object to a String.
+	End Rem
+	Method SerializeToString:String(obj:Object)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+		Free()
+		SerializeObject(obj)
+		
+		Return ToString()
+	End Method
+	
+	Rem
+	bbdoc: Serializes an Object to the file @filename.
+	End Rem
+	Method SerializeToFile(obj:Object, filename:String)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+		Free()
+		SerializeObject(obj)
+		
+		If doc Then
+			doc.saveFile(filename, format)
+		End If
+		Free()
+	End Method
+	
+	Rem
+	bbdoc: Serializes an Object to a TxmlDoc structure.
+	about: It is up to the user to free the returned TxmlDoc object.
+	End Rem
+	Method SerializeToDoc:TxmlDoc(obj:Object)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+		Free()
+		SerializeObject(obj)
+		
+		Local exportDoc:TxmlDoc = doc
+		doc = Null
+		Free()
+		Return exportDoc
+	End Method
+
+	Rem
+	bbdoc: Serializes an Object to a Stream.
+	about: It is up to the user to close the stream.
+	End Rem
+	Method SerializeToStream(obj:Object, stream:TStream)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+		Free()
+		SerializeObject(obj)
+		
+		If doc Then
+			doc.saveFile(stream, format)
+		End If
+		Free()
+	End Method
+
+	Rem
+	bbdoc: Returns the serialized object as a string.
+	End Rem
+	Method ToString:String()
+		If doc Then
+			Return doc.ToStringFormat(format)
+		End If
+	End Method
+	
+	Method ProcessArray(arrayObject:Object, size:Int, node:TxmlNode, typeId:TTypeId)
+	
+		Local elementType:TTypeId = typeId.ElementType()
+		
+		Select elementType
+			Case ByteTypeId, ShortTypeId, IntTypeId, LongTypeId, FloatTypeId, DoubleTypeId
+
+				Local sb:TStringBuilder = new TStringBuilder()
+				
+				For Local i:Int = 0 Until size
+				
+					Local aObj:Object = typeId.GetArrayElement(arrayObject, i)
+
+					If i Then
+						sb.Append(" ")
+					End If
+					sb.Append(String(aObj))
+				Next
+				
+				node.SetContent(sb.ToString())
+			Default
+
+				For Local i:Int = 0 Until size
+				
+					Local elementNode:TxmlNode = node.addChild("val")
+					
+					Local aObj:Object = typeId.GetArrayElement(arrayObject, i)
+				
+					Select elementType
+						Case StringTypeId
+							' only if not empty
+							If String(aObj) Then
+								elementNode.setContent(String(aObj))
+							End If
+						Default
+							Local objRef:String = GetObjRef(aObj)
+							
+							' file version 5 ... array cells can contain references
+							If Not Contains(objRef, aObj) Then
+								SerializeObject(aObj, elementNode)
+							Else
+								elementNode.setAttribute("ref", objRef)
+							End If
+					End Select
+				Next
+				
+		End Select
+		
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method SerializeFields(tid:TTypeId, obj:Object, node:TxmlNode)
+		For Local f:TField = EachIn tid.EnumFields()
+			SerializeField(f, obj, node)
+		Next
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method CreateSerializedFieldNode:TxmlNode(f:TField, node:TxmlNode)
+		Local fieldNode:TxmlNode = node.addChild("field")
+		fieldNode.setAttribute("name", f.Name())
+		Return fieldNode
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method SerializeField(f:TField, obj:Object, node:TxmlNode)
+		If f.MetaData("nopersist") Then
+			Return
+		End If
+	
+		Local fieldType:TTypeId = f.TypeId()
+		Local fieldNode:TxmlNode = CreateSerializedFieldNode(f, node)
+		
+		Local t:String
+		Select fieldType
+			Case ByteTypeId
+				t = "byte"
+				fieldNode.setContent(f.GetInt(obj))
+			Case ShortTypeId
+				t = "short"
+				fieldNode.setContent(f.GetInt(obj))
+			Case IntTypeId
+				t = "int"
+				fieldNode.setContent(f.GetInt(obj))
+			Case LongTypeId
+				t = "long"
+				fieldNode.setContent(f.GetLong(obj))
+			Case FloatTypeId
+				t = "float"
+				fieldNode.setContent(f.GetFloat(obj))
+			Case DoubleTypeId
+				t = "double"
+				fieldNode.setContent(f.GetDouble(obj))
+			Case UIntTypeId
+				t = "uint"
+				fieldNode.setContent(f.GetUInt(obj))
+			Case ULongTypeId
+				t = "ulong"
+				fieldNode.setContent(f.GetULong(obj))
+			Default
+				t = fieldType.Name()
+
+				If fieldType.ExtendsType( ArrayTypeId ) Then
+				
+					' prefix and strip brackets
+					Local dims:Int = t.split("[").length
+					If dims = 1 Then
+						t = "array:" + t.Replace("[]", "")
+					Else
+						t = "array:" + t
+					End If
+					
+					dims = fieldType.ArrayDimensions(f.Get(obj))
+					If dims > 1 Then
+						Local scales:String
+						For Local i:Int = 0 Until dims - 1
+							scales :+ (fieldType.ArrayLength(f.Get(obj), i) / fieldType.ArrayLength(f.Get(obj), i + 1))
+							scales :+ ","
+						Next
+						
+						scales:+ fieldType.ArrayLength(f.Get(obj), dims - 1)
+						
+						fieldNode.setAttribute("scales", scales)
+					End If
+
+					ProcessArray(f.Get(obj), fieldType.ArrayLength(f.Get(obj)), fieldNode, fieldType)
+
+				Else
+					Local fieldObject:Object = f.Get(obj)
+					Local fieldRef:String = GetObjRef(fieldObject)
+					
+					If fieldRef <> bbEmptyString And fieldRef <> bbNullObject And fieldRef <> bbEmptyArray Then
+						If fieldObject Then
+							If Not Contains(fieldRef, fieldObject) Then
+								SerializeObject(fieldObject, fieldNode)
+							Else
+								fieldNode.setAttribute("ref", fieldRef)
+							End If
+						End If
+					End If
+				End If
+		End Select
+		
+		fieldNode.setAttribute("type", t)
+	End Method
+
+	Method SerializeByType(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local serializer:TXMLSerializer = TXMLSerializer(serializers.ValueForKey(tid.Name()))
+		If serializer Then
+			serializer.Serialize(tid, obj, node)
+		Else
+			SerializeFields(tid, obj, node)
+		End If
+	End Method
+		
+	Rem
+	bbdoc: 
+	End Rem
+	Method SerializeObject:TxmlNode(obj:Object, parent:TxmlNode = Null)
+
+		Local node:TxmlNode
+		
+		If Not doc Then
+			doc = TxmlDoc.newDoc("1.0")
+			parent = TxmlNode.newNode("bmo") ' BlitzMax Object
+			parent.SetAttribute("ver", BMO_VERSION) ' set the format version
+			doc.setRootElement(parent)
+		Else
+			If Not parent Then
+				parent = doc.GetRootElement()
+			End If
+		End If
+		
+		If obj Then
+			Local objRef:String = GetObjRef(obj)
+			
+			If objRef = bbEmptyString Or objRef = bbNullObject Or objRef = bbEmptyArray Then
+				Return Null
+			End If
+			
+			Local objectIsArray:Int = False
+		
+			Local tid:TTypeId = TTypeId.ForObject(obj)
+			Local tidName:String = tid.Name()
+
+			' Is this an array "Object" ?
+			If tidName.EndsWith("[]") Then
+				tidName = "_array_"
+				objectIsArray = True
+			End If
+			
+			node = parent.addChild(tidName)
+			
+			node.setAttribute("ref", objRef)
+			
+			AddObjectRef(obj, node)
+
+			' We need to handle array objects differently..
+			If objectIsArray Then
+			
+				tidName = tid.Name()[..tid.Name().length - 2]
+				
+				Local size:Int
+				
+				' it's possible that the array is zero-length, in which case the object type
+				' is undefined. Therefore we default it to type "Object".
+				' This doesn't matter, since it's essentially a Null Object which has no
+				' inherent value. We only store the instance so that the de-serialized object will
+				' look similar.
+				Try
+					size = tid.ArrayLength(obj)
+				Catch e$
+					tidName = "Object"
+					size = 0
+				End Try
+
+				node.setAttribute("type", tidName)
+				node.setAttribute("size", size)
+
+				If size > 0 Then
+					ProcessArray(obj, size, node, tid)
+				End If
+			
+			Else
+
+				' special case for String object
+				If tid = StringTypeId Then
+					If String(obj)
+						'Local s:String = doc.encodeEntities(String(obj))
+						'node.setContent(s)
+						node.setContent(String(obj))
+					Else
+						node.setContent("")
+					End If
+				Else
+					SerializeByType(tid, obj, node)
+				End If
+				
+			End If
+	
+		End If
+		
+		Return node
+		
+	End Method
+	
+	Method Contains:Int(ref:String, obj:Object)
+		Local cobj:Object = objectMap.ValueForKey(ref)
+		If Not cobj Then
+			Return False
+		End If
+		
+		' same object already exists!
+		If cobj = obj Then
+			Return True
+		End If
+		
+		' same ref but different object????
+		Throw TPersistCollisionException.CreateException(ref, obj, cobj)
+	End Method
+
+	Method Delete()
+		Free()
+	End Method
+	
+	Rem
+	bbdoc: De-serializes @text into an Object structure.
+	about: Accepts a TxmlDoc, TStream or a String (of data).
+	End Rem
+	Method DeSerialize:Object(data:Object)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+		
+		If TxmlDoc(data) Then
+			Return DeSerializeFromDoc(TxmlDoc(data))
+		Else If TStream(data) Then
+			Return DeSerializeFromStream(TStream(data))
+		Else If String(data) Then
+			Return DeSerializeObject(String(data), Null)
+		End If
+	End Method
+	
+	Rem
+	bbdoc: De-serializes @doc into an Object structure.
+	about: It is up to the user to free the supplied TxmlDoc.
+	End Rem
+	Method DeSerializeFromDoc:Object(xmlDoc:TxmlDoc)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+
+		doc = xmlDoc
+
+		Local root:TxmlNode = doc.GetRootElement()
+		fileVersion = root.GetAttribute("ver").ToInt() ' get the format version
+		Local obj:Object = DeSerializeObject("", root)
+		doc = Null
+		Free()
+		Return obj
+	End Method
+
+	Rem
+	bbdoc: De-serializes the file @filename into an Object structure.
+	End Rem
+	Method DeSerializeFromFile:Object(filename:Object)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+	
+		doc = TxmlDoc.parseFile(filename)
+
+		If doc Then
+			Local root:TxmlNode = doc.GetRootElement()
+			fileVersion = root.GetAttribute("ver").ToInt() ' get the format version
+			Local obj:Object = DeSerializeObject("", root)
+			Free()
+			Return obj
+		End If
+	End Method
+
+	Rem
+	bbdoc: De-serializes @stream into an Object structure.
+	End Rem
+	Method DeSerializeFromStream:Object(stream:TStream)
+		If Not _inited Throw "Use TXMLPersistenceBuilder to create TPersist instance."
+
+		Return DeSerializeFromFile(stream)
+	End Method
+	
+	Method DeserializeByType:Object(objType:TTypeId, node:TxmlNode)
+		Local serializer:TXMLSerializer = TXMLSerializer(serializers.ValueForKey(objType.Name()))
+		If serializer Then
+			Return serializer.Deserialize(objType, node)
+		Else
+			Local obj:Object = CreateObjectInstance(objType, node)
+			DeserializeFields(objType, obj, node)
+			Return obj
+		End If
+	End Method
+	
+	Method AddObjectRef(obj:Object, node:TxmlNode)
+		objectMap.Insert(node.getAttribute("ref"), obj)
+	End Method
+	
+	Method CreateObjectInstance:Object(objType:TTypeId, node:TxmlNode)
+		' create the object
+		Local obj:Object = objType.NewObject()
+		AddObjectRef(obj, node)
+		Return obj
+	End Method
+	
+	Method DeserializeFields(objType:TTypeId, obj:Object, node:TxmlNode)
+		' does the node contain child nodes?
+		If node.getChildren() <> Null Then
+			For Local fieldNode:TxmlNode = EachIn node.getChildren()
+			
+				' this should be a field
+				If fieldNode.GetName() = "field" Then
+				
+					Local fieldObj:TField = objType.FindField(fieldNode.getAttribute("name"))
+					
+					Local fieldType:String = fieldNode.getAttribute("type")
+					Select fieldType
+						Case "byte", "short", "int"
+							fieldObj.SetInt(obj, fieldNode.GetContent().toInt())
+						Case "long"
+							fieldObj.SetLong(obj, fieldNode.GetContent().toLong())
+						Case "float"
+							fieldObj.SetFloat(obj, fieldNode.GetContent().toFloat())
+						Case "double"
+							fieldObj.SetDouble(obj, fieldNode.GetContent().toDouble())
+						Default
+							If fieldType.StartsWith("array:") Then
+
+								Local arrayType:TTypeId = fieldObj.TypeId()
+								Local arrayElementType:TTypeId = arrayType.ElementType()
+
+								If fileVersion Then
+									
+									' for file version 3+
+									Local scalesi:Int[]
+									Local scales:String[] = fieldNode.getAttribute("scales").split(",")
+									If scales.length > 1 Then
+										scalesi = New Int[scales.length]
+										For Local i:Int = 0 Until scales.length
+											scalesi[i] = Int(scales[i])
+										Next
+									End If
+									
+									' for file Version 1+
+									Select arrayElementType
+										Case ByteTypeId, ShortTypeId, IntTypeId, LongTypeId, FloatTypeId, DoubleTypeId
+										
+											Local arrayList:String[]
+											Local content:String = fieldNode.GetContent().Trim()
+
+											If content Then
+												arrayList = content.Split(" ")
+											Else
+												arrayList = New String[0]
+											End If
+											
+											Local arrayObj:Object = arrayType.NewArray(arrayList.length, scalesi)
+											fieldObj.Set(obj, arrayObj)
+											
+											For Local i:Int = 0 Until arrayList.length
+												arrayType.SetArrayElement(arrayObj, i, arrayList[i])
+											Next
+											
+										Default
+											Local arrayList:TList = fieldNode.getChildren()
+											
+											If arrayList ' Birdie
+												Local arrayObj:Object = arrayType.NewArray(arrayList.Count(), scalesi)
+												fieldObj.Set(obj, arrayObj)
+												
+												Local i:Int
+												For Local arrayNode:TxmlNode = EachIn arrayList
+				
+													Select arrayElementType
+														Case StringTypeId
+															arrayType.SetArrayElement(arrayObj, i, arrayNode.GetContent())
+														Default
+															' file version 5 ... array cells can contain references
+															' is this a reference?
+															Local ref:String = arrayNode.getAttribute("ref")
+															If ref Then
+																Local objRef:Object = objectMap.ValueForKey(ref)
+																If objRef Then
+																	arrayType.SetArrayElement(arrayObj, i, objRef)
+																Else
+																	Throw "Reference not mapped yet : " + ref
+																End If
+															Else
+																arrayType.SetArrayElement(arrayObj, i, DeSerializeObject("", arrayNode))
+															End If	
+													End Select
+				
+													i:+ 1
+												Next
+											EndIf
+									End Select
+								Else
+									' For file version 0 (zero)
+									
+									Local arrayList:TList = fieldNode.getChildren()
+									If arrayList 'Birdie
+										Local arrayObj:Object = arrayType.NewArray(arrayList.Count())
+										fieldObj.Set(obj, arrayObj)
+									
+										Local i:Int
+										For Local arrayNode:TxmlNode = EachIn arrayList
+		
+											Select arrayElementType
+												Case ByteTypeId, ShortTypeId, IntTypeId, LongTypeId, FloatTypeId, DoubleTypeId, StringTypeId
+													arrayType.SetArrayElement(arrayObj, i, arrayNode.GetContent())
+												Default
+													arrayType.SetArrayElement(arrayObj, i, DeSerializeObject("", arrayNode))
+											End Select
+		
+											i:+ 1
+										Next
+									EndIf
+								End If
+							Else
+								If fieldType = "string" And fileVersion < 8 Then
+									fieldObj.SetString(obj, fieldNode.GetContent())
+								Else
+									' is this a reference?
+									Local ref:String = fieldNode.getAttribute("ref")
+									If ref Then
+										Local objRef:Object = objectMap.ValueForKey(ref)
+										If objRef Then
+											fieldObj.Set(obj, objRef)
+										Else
+											Throw "Reference not mapped yet : " + ref
+										End If
+									Else
+										fieldObj.Set(obj, DeSerializeObject("", fieldNode))
+									End If
+								End If
+							End If
+					End Select
+					
+				End If
+			Next
+		End If
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method DeSerializeObject:Object(Text:String, parent:TxmlNode = Null, parentIsNode:Int = False)
+
+		Local node:TxmlNode
+		
+		If Not doc Then
+			
+			doc = TxmlDoc.readDoc(Text)
+			parent = doc.GetRootElement()
+			fileVersion = parent.GetAttribute("ver").ToInt() ' get the format version
+			node = TxmlNode(parent.GetFirstChild())
+			lastNode = node
+		Else
+			If Not parent Then
+				' find the next element node, if there is one. (content are also "nodes")
+				node = TxmlNode(lastNode.NextSibling())
+				'While node And (node.getType() <> XML_ELEMENT_NODE)
+				'	node = TxmlNode(node.NextSibling())
+				'Wend
+				If Not node Then
+					Return Null
+				End If
+				lastNode = node
+			Else
+				If parentIsNode Then
+					node = parent
+				Else
+					node = TxmlNode(parent.GetFirstChild())
+				End If
+				lastNode = node
+			End If
+		End If
+		
+		Local obj:Object 
+		
+		
+		If node Then
+		
+			Local nodeName:String = node.GetName()
+			
+			' Is this an array "Object" ?
+			If nodeName = "_array_" Then
+			
+				Local objType:TTypeId = TTypeId.ForName(node.getAttribute("type") + "[]")
+				
+				Local size:Int = node.getAttribute("size").toInt()
+				obj = objType.NewArray(size)
+				AddObjectRef(obj, node)
+
+				If size > 0 Then
+					Local arrayElementType:TTypeId = objType.ElementType()
+	
+					Select arrayElementType
+						Case ByteTypeId, ShortTypeId, IntTypeId, LongTypeId, FloatTypeId, DoubleTypeId
+						
+							Local arrayList:String[] = node.GetContent().Split(" ")
+							
+							For Local i:Int = 0 Until arrayList.length
+								objType.SetArrayElement(obj, i, arrayList[i])
+							Next
+							
+						Default
+							Local arrayList:TList = node.getChildren()
+							
+							If arrayList
+								
+								Local i:Int
+								For Local arrayNode:TxmlNode = EachIn arrayList
+		
+									Select arrayElementType
+										Case StringTypeId
+											objType.SetArrayElement(obj, i, arrayNode.GetContent())
+										Default
+											' file version 5 ... array cells can contain references
+											' is this a reference?
+											Local ref:String = arrayNode.getAttribute("ref")
+											If ref Then
+												Local objRef:Object = objectMap.ValueForKey(ref)
+												If objRef Then
+													objType.SetArrayElement(obj, i, objRef)
+												Else
+													Throw "Reference not mapped yet : " + ref
+												End If
+											Else
+												objType.SetArrayElement(obj, i, DeSerializeObject("", arrayNode))
+											End If	
+
+									End Select
+		
+									i:+ 1
+								Next
+							EndIf
+					End Select
+				End If
+			
+			Else
+			
+				Local objType:TTypeId = TTypeId.ForName(nodeName)
+	
+				' special case for String object
+				If objType = StringTypeId Then
+					obj = node.GetContent()
+					AddObjectRef(obj, node)
+					Return obj
+				End If
+
+				obj = DeserializeByType(objType, node)
+			End If
+		End If
+	
+		Return obj
+
+	End Method
+
+
+	Function GetObjRef:String(obj:Object)
+?ptr64
+		Return Base36(Long(bbObjectRef(obj)))
+?Not ptr64
+		Return Base36(Int(bbObjectRef(obj)))
+?
+	End Function
+
+?ptr64
+	Function Base36:String( val:Long )
+		Const size:Int = 13
+?Not ptr64
+	Function Base36:String( val:Int )
+		Const size:Int = 6
+?
+		Local vLong:Long = $FFFFFFFFFFFFFFFF:Long & Long(Byte Ptr(val))
+		Local buf:Short[size]
+		For Local k:Int=(size-1) To 0 Step -1
+			Local n:Int=(vLong Mod 36) + 48
+			If n > 57 n:+ 7
+			buf[k]=n
+			vLong = vLong / 36
+		Next
+		
+		' strip leading zeros
+		Local offset:Int = 0
+		While offset < size
+			If buf[offset] - Asc("0") Exit
+			offset:+ 1
+		Wend
+
+		Return String.FromShorts( Short Ptr(buf) + offset,size-offset )
+	End Function
+
+	Method AddSerializer(serializer:TXMLSerializer)
+		serializers.Insert(serializer.TypeName(), serializer)
+		serializer.persist = Self
+	End Method
+
+	Method DeserializeReferencedObject:Object(node:TxmlNode, direct:Int = False)
+		Local obj:Object
+		Local ref:String = node.getAttribute("ref")
+		If ref Then
+			Local objRef:Object = objectMap.ValueForKey(ref)
+			If objRef Then
+				obj = objRef
+			Else
+				Throw "Reference not mapped yet : " + ref
+			End If
+		Else
+			obj = DeserializeObject("", node, direct)
+		End If
+		Return obj
+	End Method
+
+	Method SerializeReferencedObject:TxmlNode(obj:Object, node:TxmlNode)
+		Local ref:String = GetObjRef(obj)
+		If Contains(ref, obj)
+			node.setAttribute("ref", ref)
+		Else
+			Return SerializeObject(obj, node)
+		End If
+	End Method
+	
+End Type
+
+Type TPersistCollisionException Extends TPersistException
+
+	Field ref:String
+	Field obj1:Object
+	Field obj2:Object
+	
+	Function CreateException:TPersistCollisionException(ref:String, obj1:Object, obj2:Object)
+		Local e:TPersistCollisionException = New TPersistCollisionException
+		e.ref = ref
+		e.obj1 = obj1
+		e.obj2 = obj2
+		Return e
+	End Function
+	
+	Method ToString:String()
+		Return "Persist Collision. Matching ref '" + ref + "' for different objects"
+	End Method
+
+End Type
+
+Type TPersistException Extends TRuntimeException
+End Type
+
+Rem
+bbdoc: 
+End Rem
+Type TXMLPersistenceBuilder
+
+	Global defaultSerializers:TMap = New TMap
+	Field serializers:TMap = New TMap
+	
+	Method New()
+		For Local s:TXMLSerializer = EachIn defaultSerializers.Values()
+			Register(s.Clone())
+		Next
+	End Method
+
+	Rem
+	bbdoc: 
+	End Rem
+	Method Build:TPersist()
+		Local persist:TPersist = New TPersist
+		persist._inited = True
+		
+		For Local s:TXMLSerializer = EachIn serializers.Values()
+			persist.AddSerializer(s)
+		Next
+		
+		Return persist
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method Register:TXMLPersistenceBuilder(serializer:TXMLSerializer)
+		serializers.Insert(serializer.TypeName(), serializer)
+		Return Self
+	End Method
+
+	Rem
+	bbdoc: 
+	End Rem
+	Function RegisterDefault(serializer:TXMLSerializer)
+		defaultSerializers.Insert(serializer.TypeName(), serializer)
+	End Function
+
+End Type
+
+Rem
+bbdoc: 
+End Rem
+Type TXMLSerializer
+	Field persist:TPersist
+
+	Rem
+	bbdoc: Returns the typeid name that the serializer handles - For example, "TMap"
+	End Rem
+	Method TypeName:String() Abstract
+	
+	Rem
+	bbdoc: Serializes the object.
+	End Rem
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode) Abstract
+
+	Rem
+	bbdoc: Deserializes the object.
+	End Rem
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode) Abstract
+
+	Rem
+	bbdoc: Returns a new instance.
+	End Rem	
+	Method Clone:TXMLSerializer() Abstract
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method SerializeObject:TxmlNode(obj:Object, node:TxmlNode)
+		Return persist.SerializeObject(obj, node)
+	End Method
+	
+	Rem
+	bbdoc: Iterates over all of the object fields, serializing them.
+	End Rem
+	Method SerializeFields(tid:TTypeId, obj:Object, node:TxmlNode)
+		persist.SerializeFields(tid, obj, node)
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method GetFileVersion:Int()
+		Return persist.fileVersion
+	End Method
+	
+	Method DeserializeObject:Object(node:TxmlNode, direct:Int = False)
+		Return persist.DeserializeObject("", node, direct)
+	End Method
+	
+	Rem
+	bbdoc: Returns True if the reference has already been processed.
+	End Rem
+	Method Contains:Int(ref:String, obj:Object)
+		Return persist.Contains(ref, obj)
+	End Method
+	
+	Rem
+	bbdoc: Adds the object reference to the object map, in order to track what object instances have been processed.
+	End Rem
+	Method AddObjectRef(ref:String, obj:Object)
+		persist.objectMap.Insert(ref, obj)
+	End Method
+	
+	Rem
+	bbdoc: Convenience method for checking and adding an object reference.
+	returns: True if the object has already been processed.
+	End Rem
+	Method AddObjectRefAsRequired:Int(ref:String, obj:Object)
+		If Contains(ref, obj) Then
+			Return True
+		End If
+		AddObjectRef(ref, obj)
+	End Method
+
+	Rem
+	bbdoc: Adds the xml reference to the object map, in order to track what object instances have been processed.
+	End Rem
+	Method AddObjectRefNode(node:TxmlNode, obj:Object)
+		persist.AddObjectRef(obj, node)
+	End Method
+	
+	Rem
+	bbdoc: Returns a String representation of an object reference, suitable for serializing.
+	End Rem
+	Method GetObjRef:String(obj:Object)
+		Return TPersist.GetObjRef(obj)
+	End Method
+	
+	Method GetReferencedObj:Object(ref:String)
+		Return persist.objectMap.ValueForKey(ref)
+	End Method
+
+	Rem
+	bbdoc: Serializes a single field.
+	End Rem
+	Method SerializeField(f:TField, obj:Object, node:TxmlNode)
+		persist.SerializeField(f, obj, node)
+	End Method
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Method CreateObjectInstance:Object(objType:TTypeId, node:TxmlNode)
+		Return persist.CreateObjectInstance(objType, node)
+	End Method
+
+	Method DeserializeFields(objType:TTypeId, obj:Object, node:TxmlNode)
+		persist.DeserializeFields(objType, obj, node)
+	End Method
+	
+	Method DeserializeReferencedObject:Object(node:TxmlNode, direct:Int = False)
+		Return persist.DeserializeReferencedObject(node, direct)
+	End Method
+	
+	Method SerializeReferencedObject:TxmlNode(obj:Object, node:TxmlNode)
+		Return persist.SerializeReferencedObject(obj, node)
+	End Method
+	
+End Type
+
+Type TMapXMLSerializer Extends TXMLSerializer
+
+	Global nil:TNode = New TMap._root
+
+	Method TypeName:String()
+		Return "TMap"
+	End Method
+	
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local map:TMap = TMap(obj)
+		
+		If map Then
+			For Local mapNode:TNode = EachIn map
+				Local n:TxmlNode = node.addChild("n")
+				
+				SerializeReferencedObject(mapNode.Key(), n.addChild("k"))
+				SerializeReferencedObject(mapNode.Value(), n.addChild("v"))
+			Next
+		End If
+	End Method
+	
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode)
+		Local map:TMap = TMap(CreateObjectInstance(objType, node))
+	
+		Local ver:Int = GetFileVersion()
+	
+		If ver <= 5
+			Local attr:String = node.getAttribute("nil")
+			If attr Then
+				AddObjectRef(attr, nil)
+			End If
+		
+			DeserializeFields(objType, map, node)
+		Else
+			If node.getChildren() Then
+				For Local mapNode:TxmlNode = EachIn node.getChildren()
+					If ver < 8 Then
+						Local key:Object = DeserializeObject(TxmlNode(mapNode.getFirstChild()))
+						Local value:Object = DeserializeObject(TxmlNode(mapNode.getLastChild()))
+
+						map.Insert(key, value)
+					Else
+
+						Local key:Object = DeserializeReferencedObject(TxmlNode(mapNode.getFirstChild()))
+						Local value:Object = DeserializeReferencedObject(TxmlNode(mapNode.getLastChild()))
+				
+						map.Insert(key, value)
+					End If
+				Next
+			End If
+		End If
+		
+		Return map
+	End Method
+
+	Method Clone:TXMLSerializer()
+		Return New TMapXMLSerializer
+	End Method
+
+End Type
+
+Type TListXMLSerializer Extends TXMLSerializer
+
+	Method TypeName:String()
+		Return "TList"
+	End Method
+	
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local list:TList = TList(obj)
+		
+		If list Then
+			For Local item:Object = EachIn list
+				SerializeReferencedObject(item, node.addChild("e"))
+			Next
+		End If
+	End Method
+	
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode)
+		Local list:TList = TList(CreateObjectInstance(objType, node))
+		
+		Local ver:Int = GetFileVersion()
+
+		If ver <= 5
+			DeserializeFields(objType, list, node)
+		Else
+			If node.getChildren() Then
+				For Local listNode:TxmlNode = EachIn node.getChildren()
+					If ver < 8 Then
+						list.AddLast(DeserializeObject(listNode, True))
+					Else
+						list.AddLast(DeserializeReferencedObject(listNode))
+					End If
+				Next
+			End If
+		End If
+		
+		Return list
+	End Method
+
+	Method Clone:TXMLSerializer()
+		Return New TListXMLSerializer
+	End Method
+
+End Type
+
+Type TIntMapXMLSerializer Extends TXMLSerializer
+
+	Method TypeName:String()
+		Return "TIntMap"
+	End Method
+	
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local map:TIntMap = TIntMap(obj)
+		
+		If map Then
+			For Local mapNode:TIntNode = EachIn map
+				Local v:TxmlNode = node.addChild("e")
+				If mapNode.Value() Then
+					SerializeReferencedObject(mapNode.Value(), v)
+				End If
+				v.setAttribute("index", mapNode.Key())
+			Next
+		End If
+	End Method
+	
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode)
+		Local map:TIntMap = TIntMap(CreateObjectInstance(objType, node))
+		If node.getChildren() Then
+			Local ver:Int = GetFileVersion()
+			
+			For Local mapNode:TxmlNode = EachIn node.getChildren()
+				Local index:Int = Int(mapNode.getAttribute("index"))
+				Local obj:Object
+				If ver < 8 Then
+					obj = DeserializeObject(mapNode, True)
+				Else
+					obj = DeserializeReferencedObject(mapNode)
+				End If
+				map.Insert(index, obj)
+			Next
+		End If
+		Return map
+	End Method
+
+	Method Clone:TXMLSerializer()
+		Return New TIntMapXMLSerializer
+	End Method
+
+End Type
+
+Type TStringMapXMLSerializer Extends TXMLSerializer
+
+	Method TypeName:String()
+		Return "TStringMap"
+	End Method
+	
+	Method Serialize(tid:TTypeId, obj:Object, node:TxmlNode)
+		Local map:TStringMap = TStringMap(obj)
+		
+		If map Then
+			For Local mapNode:TStringNode = EachIn map
+				Local n:TxmlNode = node.addChild("n")
+				SerializeReferencedObject(mapNode.Key(), n.addChild("k"))
+				SerializeReferencedObject(mapNode.Value(), n.addChild("v"))
+			Next
+		End If
+	End Method
+	
+	Method Deserialize:Object(objType:TTypeId, node:TxmlNode)
+		Local map:TStringMap = TStringMap(CreateObjectInstance(objType, node))
+
+		If node.getChildren() Then
+			Local ver:Int = GetFileVersion()
+
+			For Local mapNode:TxmlNode = EachIn node.getChildren()
+				Local keyNode:TxmlNode = TxmlNode(mapNode.getFirstChild())
+				Local valueNode:TxmlNode = TxmlNode(mapNode.getLastChild())
+				
+				Local k:String
+				Local v:Object
+				
+				If ver < 8 Then
+					k = String(DeserializeObject(keyNode))
+					v = DeserializeObject(valueNode)
+				Else
+					k = String(DeserializeReferencedObject(keyNode))
+					v = DeserializeReferencedObject(valueNode)
+				End If
+				map.Insert(k, v)
+			Next
+		End If
+
+		Return map
+	End Method
+
+	Method Clone:TXMLSerializer()
+		Return New TStringMapXMLSerializer
+	End Method
+
+End Type
+
+TXMLPersistenceBuilder.RegisterDefault(New TMapXMLSerializer)
+TXMLPersistenceBuilder.RegisterDefault(New TListXMLSerializer)
+TXMLPersistenceBuilder.RegisterDefault(New TIntMapXMLSerializer)
+TXMLPersistenceBuilder.RegisterDefault(New TStringMapXMLSerializer)
+
+Extern
+	Function bbEmptyStringPtr:Byte Ptr()
+	Function bbNullObjectPtr:Byte Ptr()
+	Function bbEmptyArrayPtr:Byte Ptr()
+	Function bbObjectRef:Byte Ptr(obj:Object)
+End Extern

+ 172 - 0
persistencexml.mod/test/fields.bmx

@@ -0,0 +1,172 @@
+SuperStrict
+
+Import brl.maxunit
+Import text.persistencexml
+
+Import "types.bmx"
+
+Type FieldsTest Extends TTest
+
+	Const NUM_INT:Int = 12345
+	Const NUM_LONG:Long = 84343238901:Long
+	Const NUM_FLOAT:Float = 10.688:Float
+	Const NUM_DOUBLE:Double = 9420.0394:Double
+	Const NUM_BYTE:Byte = 129
+	Const NUM_SHORT:Short = 40000
+	
+	Const STR_ONE:String = "ABCDEFG"
+	Const STR_TWO:String = "HIJKLMNOP"
+	Const STR_THREE:String = "QRSTUVWXYZ"
+	
+	Const OBJ_ONE:Int = 6644
+	Const OBJ_TWO:Int = 7755
+	Const OBJ_THREE:Int = 8833
+	
+	Field persist:TPersist
+	
+	Field obj1:TObject
+	Field obj2:TObject
+	Field obj3:TObject
+
+	Method Setup() { before }
+		persist = New TXMLPersistenceBuilder.Build()
+		
+		obj1 = New TObject.Create(OBJ_ONE)
+		obj2 = New TObject.Create(OBJ_TWO)
+		obj3 = New TObject.Create(OBJ_THREE)
+	End Method
+
+	Method TearDown() { after }
+		persist.Free()
+	End Method
+
+	Method testNumbers() { test }
+
+		Local numbers:TNumbers = New TNumbers.Create(NUM_INT, NUM_LONG, NUM_FLOAT, NUM_DOUBLE, NUM_BYTE, NUM_SHORT)
+		
+		Local s:String = persist.SerializeToString(numbers)
+
+		persist.Free()
+		
+		Local result:TNumbers = TNumbers(persist.DeserializeObject(s))
+	
+		assertEquals(NUM_INT, result.a)
+		assertEquals(NUM_LONG, result.b)
+		assertEquals(NUM_FLOAT, result.c)
+		assertEquals(NUM_DOUBLE, result.d)
+		assertEquals(NUM_BYTE, result.e)
+		assertEquals(NUM_SHORT, result.f)
+	
+	End Method
+	
+	Method testStrings() { test }
+	
+		Local strings:TStrings = New TStrings.Create(STR_ONE, STR_TWO, STR_THREE)
+		
+		Local s:String = persist.SerializeToString(strings)
+		
+		validateStringRefs(s, STR_ONE, 1)
+		validateStringRefs(s, STR_TWO, 1)
+		validateStringRefs(s, STR_THREE, 1)
+		
+		persist.Free()
+		
+		Local result:TStrings = TStrings(persist.DeserializeObject(s))
+	
+		assertEquals(STR_ONE, result.a)
+		assertEquals(STR_TWO, result.b)
+		assertEquals(STR_THREE, result.c)
+	
+	End Method
+
+	' should only store a single copy of the string
+	Method testStringRefs() { test }
+	
+		Local strings:TStrings = New TStrings.Create(STR_ONE, STR_ONE, STR_ONE)
+		
+		Local s:String = persist.SerializeToString(strings)
+		
+		validateStringRefs(s, STR_ONE, 1)
+				
+		persist.Free()
+		
+		Local result:TStrings = TStrings(persist.DeserializeObject(s))
+	
+		assertEquals(STR_ONE, result.a)
+		assertEquals(STR_ONE, result.b)
+		assertEquals(STR_ONE, result.c)
+	
+	End Method
+	
+	Method testObjects() { test }
+		
+		Local container:TObjectContainer = New TObjectContainer.Create(obj1, obj2, obj3)
+
+		Local s:String = persist.SerializeToString(container)
+
+		validateStringRefs(s, OBJ_ONE, 1)
+		validateStringRefs(s, OBJ_TWO, 1)
+		validateStringRefs(s, OBJ_THREE, 1)
+
+		persist.Free()
+
+		Local result:TObjectContainer = TObjectContainer(persist.DeserializeObject(s))
+
+		assertEquals(OBJ_ONE, result.object1.code)
+		assertEquals(OBJ_TWO, result.object2.code)
+		assertEquals(OBJ_THREE, result.object3.code)
+	End Method
+
+	Method testObjectRefs() { test }
+		
+		Local container:TObjectContainer = New TObjectContainer.Create(obj1, obj2, obj1)
+
+		Local s:String = persist.SerializeToString(container)
+
+		validateStringRefs(s, OBJ_ONE, 1)
+		validateStringRefs(s, OBJ_TWO, 1)
+
+		persist.Free()
+
+		Local result:TObjectContainer = TObjectContainer(persist.DeserializeObject(s))
+
+		assertEquals(OBJ_ONE, result.object1.code)
+		assertEquals(OBJ_TWO, result.object2.code)
+		assertEquals(OBJ_ONE, result.object3.code)
+	End Method
+
+	Method testEmptyStrings() { test }
+	
+		Local strings:TStrings = New TStrings.Create(Null, STR_ONE, Null)
+		
+		assertNull(strings.a)
+		assertEquals(STR_ONE, strings.b)
+		assertNull(strings.c)
+
+		Local s:String = persist.SerializeToString(strings)
+
+		validateStringRefs(s, STR_ONE, 1)
+
+		persist.Free()
+
+		Local result:TStrings = TStrings(persist.DeserializeObject(s))
+	
+		assertNull(result.a)
+		assertEquals(STR_ONE, result.b)
+		assertNull(result.c)
+		
+	End Method
+	
+	Method validateStringRefs(ser:String, txt:String, expected:Int)
+		Local count:Int
+		Local i:Int = ser.Find(txt, 0)
+
+		While i <> -1
+			count :+ 1
+			i = ser.Find(txt, i + txt.length)
+		Wend
+		
+		assertEquals(expected, count, "Refs mismatch")
+	End Method
+
+End Type

+ 143 - 0
persistencexml.mod/test/list.bmx

@@ -0,0 +1,143 @@
+SuperStrict
+
+Import brl.maxunit
+Import text.persistencexml
+
+Import "types.bmx"
+
+Type ListTest Extends TTest
+
+	Const STR_ONE:String = "ABCDEFG"
+	Const STR_TWO:String = "HIJKLMNOP"
+	Const STR_THREE:String = "QRSTUVWXYZ"
+	Const STR_FOUR:String = "1234567890"
+
+	Const OBJ_ONE:Int = 6644
+	Const OBJ_TWO:Int = 7755
+	Const OBJ_THREE:Int = 8833
+
+	Field persist:TPersist
+
+	Field obj1:TObject
+	Field obj2:TObject
+	Field obj3:TObject
+
+	Method Setup() { before }
+		persist = New TXMLPersistenceBuilder.Build()
+
+		obj1 = New TObject.Create(OBJ_ONE)
+		obj2 = New TObject.Create(OBJ_TWO)
+		obj3 = New TObject.Create(OBJ_THREE)
+	End Method
+
+	Method TearDown() { after }
+		persist.Free()
+	End Method
+
+	Method testBasic() { test }
+	
+		Local list:TList = New TList
+		
+		list.AddLast(STR_ONE)
+		list.AddLast(STR_TWO)
+		list.AddLast(STR_THREE)
+		list.AddLast(STR_FOUR)
+
+		assertEquals(4, list.Count())
+
+		Local s:String = persist.SerializeToString(list)
+
+		validateRefs(s, STR_ONE, 1)
+		validateRefs(s, STR_TWO, 1)
+		validateRefs(s, STR_THREE, 1)
+		validateRefs(s, STR_FOUR, 1)
+		
+		persist.Free()
+		
+		Local result:TList = TList(persist.DeserializeObject(s))
+
+		assertEquals(4, result.Count())		
+		assertTrue(result.Contains(STR_ONE))
+		assertTrue(result.Contains(STR_TWO))
+		assertTrue(result.Contains(STR_THREE))
+		assertTrue(result.Contains(STR_FOUR))
+	End Method
+
+	Method testStringRefs() { test }
+	
+		Local list:TList = New TList
+		
+		list.AddLast(STR_ONE)
+		list.AddLast(STR_TWO)
+		list.AddLast(STR_THREE)
+		list.AddLast(STR_FOUR)
+		list.AddLast(STR_ONE)
+		list.AddLast(STR_TWO)
+		list.AddLast(STR_THREE)
+		list.AddLast(STR_FOUR)
+
+		assertEquals(8, list.Count())
+
+		Local s:String = persist.SerializeToString(list)
+
+		validateRefs(s, STR_ONE, 1)
+		validateRefs(s, STR_TWO, 1)
+		validateRefs(s, STR_THREE, 1)
+		validateRefs(s, STR_FOUR, 1)
+		
+		persist.Free()
+
+		Local result:TList = TList(persist.DeserializeObject(s))
+
+		assertEquals(8, result.Count())		
+
+	End Method
+	
+	Method testObjectRef() { test }
+	
+		Local ids:Int[] = [30, 60, 30, 74, 100]
+	
+		Local room1:TRoom = New TRoom.Create(30)
+		Local room2:TRoom = New TRoom.Create(74)
+		Local room3:TRoom = New TRoom.Create(60)
+		Local room4:TRoom = New TRoom.Create(100)
+		
+		Local list:TList = New TList
+		
+		list.AddLast(room1)
+		list.AddLast(room3)
+		list.AddLast(room1)
+		list.AddLast(room2)
+		list.AddLast(room4)
+		
+		assertEquals(5, list.Count())
+
+		Local s:String = persist.SerializeToString(list)
+
+		persist.Free()
+
+		Local result:TList = TList(persist.DeserializeObject(s))
+
+		assertEquals(5, result.Count())		
+		
+		Local i:Int
+		For Local room:TRoom = EachIn result
+			assertEquals(ids[i], room.id)
+			i :+ 1
+		Next
+	
+	End Method
+	
+	Method validateRefs(ser:String, txt:String, expected:Int)
+		Local count:Int
+		Local i:Int = ser.Find(txt, 0)
+
+		While i <> -1
+			count :+ 1
+			i = ser.Find(txt, i + txt.length)
+		Wend
+		
+		assertEquals(expected, count, "Refs mismatch")
+	End Method
+
+End Type

+ 129 - 0
persistencexml.mod/test/map.bmx

@@ -0,0 +1,129 @@
+SuperStrict
+
+Import brl.maxunit
+Import text.persistencexml
+
+Import "types.bmx"
+
+Type MapsTest Extends TTest
+
+	Const STR_ONE:String = "ABCDEFG"
+	Const STR_TWO:String = "HIJKLMNOP"
+	Const STR_THREE:String = "QRSTUVWXYZ"
+	Const STR_FOUR:String = "1234567890"
+
+	Const OBJ_ONE:Int = 6644
+	Const OBJ_TWO:Int = 7755
+	Const OBJ_THREE:Int = 8833
+
+	Field persist:TPersist
+
+	Field obj1:TObject
+	Field obj2:TObject
+	Field obj3:TObject
+
+	Method Setup() { before }
+		persist = New TXMLPersistenceBuilder.Build()
+
+		obj1 = New TObject.Create(OBJ_ONE)
+		obj2 = New TObject.Create(OBJ_TWO)
+		obj3 = New TObject.Create(OBJ_THREE)
+	End Method
+
+	Method TearDown() { after }
+		persist.Free()
+	End Method
+
+	Method testBasic() { test }
+	
+		Local map:TMap = New TMap
+		
+		map.Insert(STR_ONE, STR_TWO)
+		map.Insert(STR_THREE, STR_FOUR)
+			
+		Local s:String = persist.SerializeToString(map)
+
+		validateRefs(s, STR_ONE, 1)
+		validateRefs(s, STR_TWO, 1)
+		validateRefs(s, STR_THREE, 1)
+		validateRefs(s, STR_FOUR, 1)
+		
+		persist.Free()
+		
+		Local result:TMap = TMap(persist.DeserializeObject(s))
+		
+		assertEquals(STR_TWO, String(result.ValueForKey(STR_ONE)))
+		assertEquals(STR_FOUR, String(result.ValueForKey(STR_THREE)))
+		
+	End Method
+	
+	' should only store a single copy of the string
+	Method testStringRefs() { test }
+	
+		Local map:TMap = New TMap
+		
+		map.Insert(STR_ONE, STR_ONE)
+		map.Insert(STR_TWO, STR_ONE)
+		map.Insert(STR_THREE, STR_TWO)
+		map.Insert(STR_FOUR, STR_TWO)
+
+		Local s:String = persist.SerializeToString(map)
+
+		validateRefs(s, STR_ONE, 1)
+		validateRefs(s, STR_TWO, 1)
+		validateRefs(s, STR_THREE, 1)
+		validateRefs(s, STR_FOUR, 1)
+		
+		persist.Free()
+		
+		Local result:TMap = TMap(persist.DeserializeObject(s))
+
+		assertEquals(STR_ONE, String(result.ValueForKey(STR_ONE)))
+		assertEquals(STR_ONE, String(result.ValueForKey(STR_TWO)))
+		assertEquals(STR_TWO, String(result.ValueForKey(STR_THREE)))
+	
+		assertEquals(STR_TWO, String(result.ValueForKey(STR_FOUR)))
+	End Method
+	
+	Method testObjects() { test }
+		Local map:TMap = New TMap
+		
+		map.Insert(STR_ONE, obj1)
+		map.Insert(STR_TWO, obj2)
+		map.Insert(STR_THREE, obj3)
+		map.Insert(obj1, obj1)
+		map.Insert(obj2, obj2)
+		map.Insert(obj3, obj3)
+			
+		Local s:String = persist.SerializeToString(map)
+	
+		validateRefs(s, OBJ_ONE, 1)
+		validateRefs(s, OBJ_TWO, 1)
+		validateRefs(s, OBJ_THREE, 1)
+
+		persist.Free()
+		
+		Local result:TMap = TMap(persist.DeserializeObject(s))
+	
+		Local obj:TObject = TObject(result.ValueForKey(STR_TWO))
+	
+		assertEquals(OBJ_ONE, TObject(result.ValueForKey(STR_ONE)).code)
+		assertEquals(OBJ_TWO, TObject(result.ValueForKey(STR_TWO)).code)
+		assertEquals(OBJ_THREE, TObject(result.ValueForKey(STR_THREE)).code)
+		assertEquals(OBJ_TWO, TObject(result.ValueForKey(obj)).code)
+
+	End Method
+	
+	Method validateRefs(ser:String, txt:String, expected:Int)
+		Local count:Int
+		Local i:Int = ser.Find(txt, 0)
+
+		While i <> -1
+			count :+ 1
+			i = ser.Find(txt, i + txt.length)
+		Wend
+		
+		assertEquals(expected, count, "Refs mismatch")
+	End Method
+
+End Type

+ 11 - 0
persistencexml.mod/test/tests.bmx

@@ -0,0 +1,11 @@
+SuperStrict
+
+Framework brl.maxunit
+
+Import "fields.bmx"
+Import "map.bmx"
+Import "list.bmx"
+
+' run the tests!
+New TTestSuite.run()
+

+ 73 - 0
persistencexml.mod/test/types.bmx

@@ -0,0 +1,73 @@
+SuperStrict
+
+
+Type TNumbers
+
+	Field a:Int
+	Field b:Long
+	Field c:Float
+	Field d:Double
+	Field e:Byte
+	Field f:Short
+	
+	Method Create:TNumbers(a:Int, b:Long, c:Float, d:Double, e:Byte, f:Short)
+		Self.a = a
+		Self.b = b
+		Self.c = c
+		Self.d = d
+		Self.e = e
+		Self.f = f
+		Return Self
+	End Method
+
+End Type
+
+Type TStrings
+
+	Field a:String
+	Field b:String
+	Field c:String
+
+	Method Create:TStrings(a:String, b:String, c:String)
+		Self.a = a
+		Self.b = b
+		Self.c = c
+		Return Self
+	End Method
+
+End Type
+
+Type TObject
+
+	Field code:Int
+
+	Method Create:TObject(code:Int)
+		Self.code = code
+		Return Self
+	End Method
+
+End Type
+
+Type TObjectContainer
+
+	Field object1:TObject
+	Field object2:TObject
+	Field object3:TObject
+
+	Method Create:TObjectContainer(object1:TObject, object2:TObject, object3:TObject)
+		Self.object1 = object1
+		Self.object2 = object2
+		Self.object3 = object3
+		Return Self
+	End Method
+
+End Type
+
+Type TRoom
+	Field id:Int
+	
+	Method Create:TRoom(id:Int)
+		Self.Id = id
+		Return Self
+	End Method
+End Type