Skip to content

XMLExporter has several bugs/inconsistencies with BinaryExporter #2310

@JosiahGoeman

Description

@JosiahGoeman

I've been struggling a bit with the XMLExporter recently in my game and this prompted me to write some unit tests for the JmeExporter/JmeImporter implementations. You can see the tests on my fork here:
https://github.com/JosiahGoeman/jmonkeyengine/blob/master/jme3-plugins/src/test/java/com/jme3/export/InputOutputCapsuleTest.java.
All write* and read* methods in OutputCapsule and InputCapsule respectively are tested here. I put in all the edge cases I could think of. BinaryExporter/BinaryImporter pass all tests no problemo, but XMLExporter/XMLImporter do not.

Here's the problems I've discovered so far:

  1. Write an empty string -> Reads defVal as if attribute was not present
  2. Write a string containing an apostrophe/single quote -> throws IOException when reading
  3. Write a string containing tab, newline, or carriage return -> Reads string with these characters replaced with spaces
  4. Write any BitSet object -> Reads BitSet containing a single zero
  5. Write String[] containing a null string -> Reads array with null string having been replaced with an empty string
  6. Write a 2d array of any type except int[][] containing a null element -> Throws NullPointerException when writing
  7. Write an ArrayList containing a null element -> Throws IOException when reading
  8. Write an ArrayList<ByteBuffer> -> Reads ArrayList with same number of entries, but all are null
XMLExporterMREs.java
package com.mygame;

import com.jme3.asset.AssetInfo;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.export.binary.BinaryImporter;
import com.jme3.export.xml.XMLExporter;
import com.jme3.export.xml.XMLImporter;
import com.jme3.math.Vector3f;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Objects;
import org.lwjgl.BufferUtils;

public class XMLExporterMREs
{
	static class TestString implements Savable
	{
		static String testString;

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			capsule.write(testString, "test_string", null);
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			String readString = capsule.readString("test_string", null);
			if(!Objects.equals(testString, readString))
				throw new IOException("Expected " + makeWhitespaceExplicit(testString) + " but got " + makeWhitespaceExplicit(readString));
		}
	}

	static class TestBitSet implements Savable
	{
		private static final BitSet testBitSet = BitSet.valueOf("BitSet".getBytes());

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			capsule.write(testBitSet, "test_bit_set", null);
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			BitSet bs = capsule.readBitSet("test_bit_set", null);

			if(!testBitSet.equals(bs))
				throw new IOException("Expected " + testBitSet + " but got " + bs);
		}
	}

	static class TestStringArrayWithNull implements Savable
	{
		private static final String[] testStringArray = new String[]
		{
			"hello",
			null,
			"world"
		};

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			try
			{
				capsule.write(testStringArray, "test_string_array", null);
			} catch(Exception e)
			{
				System.out.println(e);
			}
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			String[] readArray = capsule.readStringArray("test_string_array", null);

			if(!Arrays.equals(testStringArray, readArray))
				throw new IOException("Expected " + Arrays.toString(testStringArray) + " but got " + Arrays.toString(readArray));
		}
	}

	static class Test2DArrayWithNull implements Savable
	{
		private static final byte[][] testByteArray2d = new byte[][]
		{
			new byte[]
			{
				0, 1, 2
			},
			null,
			new byte[]
			{
				0, 1, 2
			}
		};

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			capsule.write(testByteArray2d, "test_array_2d", null);
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			byte[][] readArray = capsule.readByteArray2D("test_array_2d", null);

			if(!Arrays.deepEquals(testByteArray2d, readArray))
				throw new IOException("Expected " + Arrays.toString(testByteArray2d) + " but got " + Arrays.toString(readArray));
		}
	}

	static class TestListWithNull implements Savable
	{
		static final ArrayList<Savable> testArrayList = new ArrayList();

		static
		{
			testArrayList.add(new Vector3f());
			testArrayList.add(null);
			testArrayList.add(new Vector3f());
		}

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			capsule.writeSavableArrayList(testArrayList, "test_array_list", null);
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			ArrayList<Savable> readArrayList = capsule.readSavableArrayList("test_array_list", null);

			if(!testArrayList.equals(readArrayList))
				throw new IOException("Expected " + testArrayList + " but got " + readArrayList);
		}
	}

	static class TestByteBufferList implements Savable
	{
		static final ArrayList<ByteBuffer> testArrayList = new ArrayList();

		static
		{
			testArrayList.add(BufferUtils.createByteBuffer(3).put(new byte[]
			{
				Byte.MIN_VALUE, Byte.MAX_VALUE
			}).rewind());
			testArrayList.add(BufferUtils.createByteBuffer(3).put(new byte[]
			{
				Byte.MIN_VALUE, Byte.MAX_VALUE
			}).rewind());
		}

		@Override
		public void write(JmeExporter je) throws IOException
		{
			OutputCapsule capsule = je.getCapsule(this);

			capsule.writeByteBufferArrayList(testArrayList, "test_array_list", null);
		}

		@Override
		public void read(JmeImporter ji) throws IOException
		{
			InputCapsule capsule = ji.getCapsule(this);

			ArrayList<ByteBuffer> readArrayList = capsule.readByteBufferArrayList("test_array_list", null);

			if(!testArrayList.equals(readArrayList))
				throw new IOException("Expected " + testArrayList + " but got " + readArrayList);
		}
	}

	public static void main(String[] args)
	{
		String[] problemStrings = new String[]
		{
			"",
			"\'",
			"\t",
			"\n",
			"\r"
		};

		for(String s : problemStrings)
		{
			TestString.testString = s;

			System.out.println("Testing string: " + makeWhitespaceExplicit(TestString.testString) + "");
			saveAndLoad(new BinaryExporter(), new BinaryImporter(), new TestString());
			saveAndLoad(new XMLExporter(), new XMLImporter(), new TestString());
			System.out.println();
		}

		System.out.println("Testing BitSet");
		saveAndLoad(new BinaryExporter(), new BinaryImporter(), new TestBitSet());
		saveAndLoad(new XMLExporter(), new XMLImporter(), new TestBitSet());
		System.out.println();

		System.out.println("Testing String[] with null element");
		saveAndLoad(new BinaryExporter(), new BinaryImporter(), new TestStringArrayWithNull());
		saveAndLoad(new XMLExporter(), new XMLImporter(), new TestStringArrayWithNull());
		System.out.println();

		//for the sake of brevity, I'm only showing this on write(byte[][]),
		//but it happens on all the other write() overloads that accept a 2d array, except int[][] curiously.
		System.out.println("Testing byte[][] with null element");
		saveAndLoad(new BinaryExporter(), new BinaryImporter(), new Test2DArrayWithNull());
		try
		{
			saveAndLoad(new XMLExporter(), new XMLImporter(), new Test2DArrayWithNull());
		} catch(NullPointerException e)
		{
			System.out.println("\n\t" + e);
		}
		System.out.println();

		System.out.println("Testing ArrayList<Savable> with null element");
		saveAndLoad(new BinaryExporter(), new BinaryImporter(), new TestListWithNull());
		try
		{
			saveAndLoad(new XMLExporter(), new XMLImporter(), new TestListWithNull());
		} catch(NullPointerException e)
		{
			System.out.println("\n\t" + e);
		}
		System.out.println();

		System.out.println("Testing ArrayList<ByteBuffer>");
		saveAndLoad(new BinaryExporter(), new BinaryImporter(), new TestByteBufferList());
		saveAndLoad(new XMLExporter(), new XMLImporter(), new TestByteBufferList());
		System.out.println();
	}

	private static String makeWhitespaceExplicit(String s)
	{
		if(s == null)
			return "null";

		return "\"" + s.replaceAll("\t", "\\\\t").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r").replaceAll("\s", "\\\\s") + "\"";
	}

	private static void saveAndLoad(JmeExporter exporter, JmeImporter importer, Savable savable)
	{
		System.out.print("Testing implementation: " + exporter.getClass().getSimpleName() + "...");

		// export
		byte[] exportedBytes = null;
		try(ByteArrayOutputStream outStream = new ByteArrayOutputStream())
		{
			exporter.save(savable, outStream);
			exportedBytes = outStream.toByteArray();
		} catch(IOException e)
		{
			System.out.println("\n\t" + e);
			return;
		}

		//if(exporter instanceof XMLExporter)
		//	System.out.println(new String(exportedBytes));
		// import
		try(ByteArrayInputStream inStream = new ByteArrayInputStream(exportedBytes))
		{
			AssetInfo info = new AssetInfo(null, null)
			{
				@Override
				public InputStream openStream()
				{
					return inStream;
				}
			};
			importer.load(info);    // this is where assertions will fail if loaded data does not match saved data.
		} catch(IOException e)
		{
			System.out.println("\n\t" + e);
			return;
		}

		System.out.println("ok.");
	}
}

I intend to continue investigating and hopefully resolve these problems.

Metadata

Metadata

Assignees

No one assigned

    Labels

    defectSomething that is supposed to work, but doesn't. Less severe than a "bug"

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions