From ce6d86d344de78bf4b631c1f6d4b4cc3222ba2f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= <tseeker@legacyworlds.com>
Date: Wed, 18 Jan 2012 09:28:35 +0100
Subject: [PATCH] Planet resources and resource providers in XML dumps

* Dump version bumped up to 2

* Added SQL view that shows resource delta and provider information for
all empire-owned planets

* Added new storage classes for resource providers and resource deltas

* Added row mapper which extracts all planet resources information
(providers and deltas)

* Modified dump generator to include planet resources / resource
providers
---
 .../lw/beans/bt/es/EmpireSummaryBean.java     | 219 +++++++++++++++---
 .../lw/beans/bt/es/PlanetResourceRow.java     |  96 ++++++++
 .../lw/beans/bt/es/ResourceRowMapper.java     |  58 +++++
 .../lw/beans/bt/es/data/DebugInformation.java |   2 +-
 .../es/data/InvalidDumpContentsException.java |  60 +++++
 .../beans/bt/es/data/PlanetInformation.java   | 130 +++++++++--
 .../bt/es/data/ResourceDeltaInformation.java  | 109 +++++++++
 .../es/data/ResourceProviderInformation.java  | 152 ++++++++++++
 .../parts/040-functions/200-bugs.sql          |  48 ++++
 .../010-dump-planet-resources-view.sql        |  73 ++++++
 .../010-dump-planet-resources-view.sql        |  11 +
 .../bt/es/TestBuildingsInformationMapper.java |   2 +
 .../lw/beans/bt/es/TestPlanetResourceRow.java | 102 ++++++++
 .../lw/beans/bt/es/TestResourceRowMapper.java | 129 +++++++++++
 .../TestInvalidDumpContentsException.java     |  85 +++++++
 .../es/data/TestResourceDeltaInformation.java | 174 ++++++++++++++
 .../data/TestResourceProviderInformation.java | 207 +++++++++++++++++
 17 files changed, 1607 insertions(+), 50 deletions(-)
 create mode 100644 legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/PlanetResourceRow.java
 create mode 100644 legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/ResourceRowMapper.java
 create mode 100644 legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/InvalidDumpContentsException.java
 create mode 100644 legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceDeltaInformation.java
 create mode 100644 legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceProviderInformation.java
 create mode 100644 legacyworlds-server-data/db-structure/tests/admin/040-functions/200-bugs/010-dump-planet-resources-view.sql
 create mode 100644 legacyworlds-server-data/db-structure/tests/user/040-functions/200-bugs/010-dump-planet-resources-view.sql
 create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestPlanetResourceRow.java
 create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestResourceRowMapper.java
 create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestInvalidDumpContentsException.java
 create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceDeltaInformation.java
 create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceProviderInformation.java

diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/EmpireSummaryBean.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/EmpireSummaryBean.java
index d61469b..bc2fa7d 100644
--- a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/EmpireSummaryBean.java
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/EmpireSummaryBean.java
@@ -20,6 +20,8 @@ import com.deepclone.lw.beans.bt.es.data.PlanetInformation;
 import com.deepclone.lw.beans.bt.es.data.QueueInformation;
 import com.deepclone.lw.beans.bt.es.data.QueueItemInformation;
 import com.deepclone.lw.beans.bt.es.data.ResearchInformation;
+import com.deepclone.lw.beans.bt.es.data.ResourceDeltaInformation;
+import com.deepclone.lw.beans.bt.es.data.ResourceProviderInformation;
 import com.deepclone.lw.beans.bt.es.data.ShipsInformation;
 import com.deepclone.lw.beans.bt.es.data.SystemInformation;
 import com.deepclone.lw.interfaces.bt.EmpireSummary;
@@ -40,6 +42,36 @@ import com.thoughtworks.xstream.XStream;
 public class EmpireSummaryBean
 		implements EmpireSummary
 {
+	/** Beginning of all dump SQL queries */
+	private static final String SQL_START = "SELECT * FROM bugs.dump_";
+
+	/** Ending of (almost) all dump SQL queries */
+	private static final String SQL_END = "_view WHERE empire_id = ?";
+
+	/** SQL query that accesses the main empire dump view */
+	private static final String Q_EMPIRE = SQL_START + "main" + SQL_END;
+
+	/** SQL query that accesses the research dump view */
+	private static final String Q_RESEARCH = SQL_START + "research" + SQL_END;
+
+	/** SQL query that accesses the empire's planets dump view */
+	private static final String Q_PLANETS = SQL_START + "planets" + SQL_END;
+
+	/** SQL query that accesses the planetary resources dump view */
+	private static final String Q_PLANET_RESOURCES = SQL_START + "planet_resources" + SQL_END;
+
+	/** SQL query that accesses the build queues dump view */
+	private static final String Q_BUILD_QUEUES = SQL_START + "queues" + SQL_END + " ORDER BY queue_order";
+
+	/** SQL query that accesses the buildings dump view */
+	private static final String Q_BUILDINGS = SQL_START + "buildings" + SQL_END;
+
+	/** SQL query that accesses the fleets dump view */
+	private static final String Q_FLEETS = SQL_START + "fleets" + SQL_END;
+
+	/** SQL query that accesses the ships dump view */
+	private static final String Q_SHIPS = SQL_START + "ships" + SQL_END;
+
 	/** JDBC access interface */
 	private JdbcTemplate dTemplate;
 
@@ -55,6 +87,9 @@ public class EmpireSummaryBean
 	/** Empire-owned planet row mapper */
 	private final PlanetInformationMapper mPlanet;
 
+	/** Planet resources row mapper */
+	private final ResourceRowMapper mPlanetResources;
+
 	/** Planet construction queue item row mapper */
 	private final QueueItemInformationMapper mQueueItem;
 
@@ -79,12 +114,14 @@ public class EmpireSummaryBean
 				AccountInformation.class , AllianceInformation.class , BuildingsInformation.class ,
 				DebugInformation.class , EmpireInformation.class , FleetInformation.class , MovementInformation.class ,
 				PlanetInformation.class , QueueInformation.class , QueueItemInformation.class ,
-				ResearchInformation.class , ShipsInformation.class , SystemInformation.class
+				ResearchInformation.class , ResourceDeltaInformation.class , ResourceProviderInformation.class ,
+				ShipsInformation.class , SystemInformation.class
 		} );
 
 		this.mMainInfo = new DebugInformationMapper( );
 		this.mResearch = new ResearchInformationMapper( );
 		this.mPlanet = new PlanetInformationMapper( );
+		this.mPlanetResources = new ResourceRowMapper( );
 		this.mQueueItem = new QueueItemInformationMapper( );
 		this.mBuildings = new BuildingsInformationMapper( );
 		this.mFleet = new FleetInformationMapper( );
@@ -116,46 +153,174 @@ public class EmpireSummaryBean
 	@Override
 	public String getSummary( int empireId )
 	{
-		String sql = "SELECT * FROM bugs.dump_main_view WHERE empire_id = ?";
-		DebugInformation di = this.dTemplate.queryForObject( sql , this.mMainInfo , empireId );
+		DebugInformation empireDump = this.dTemplate.queryForObject( Q_EMPIRE , this.mMainInfo , empireId );
+		this.getResearch( empireId , empireDump );
 
-		sql = "SELECT * FROM bugs.dump_research_view WHERE empire_id = ?";
-		for ( ResearchInformation ri : this.dTemplate.query( sql , this.mResearch , empireId ) ) {
-			di.getResearch( ).add( ri );
+		this.getPlanets( empireId , empireDump );
+		this.getFleets( empireId , empireDump );
+
+		return this.xStream.toXML( empireDump );
+	}
+
+
+	/**
+	 * Get research information
+	 * 
+	 * <p>
+	 * Read all research-related information from the appropriate dump view, and add corresponding
+	 * entries to the top-level instance.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param empireDump
+	 *            the top-level instance
+	 */
+	private void getResearch( int empireId , DebugInformation empireDump )
+	{
+		for ( ResearchInformation ri : this.dTemplate.query( Q_RESEARCH , this.mResearch , empireId ) ) {
+			empireDump.getResearch( ).add( ri );
+		}
+	}
+
+
+	/**
+	 * Get empire planets and related information
+	 * 
+	 * <p>
+	 * Read the planet list and basic planet information from the appropriate dump view, then read
+	 * all extra information (resources, build queues, buildings).
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param empireDump
+	 *            the top-level instance
+	 */
+	private void getPlanets( int empireId , DebugInformation empireDump )
+	{
+		HashMap< Integer , PlanetInformation > planets = new HashMap< Integer , PlanetInformation >( );
+		for ( PlanetInformation planet : this.dTemplate.query( Q_PLANETS , this.mPlanet , empireId ) ) {
+			empireDump.getPlanets( ).add( planet );
+			planets.put( planet.getId( ) , planet );
 		}
 
-		sql = "SELECT * FROM bugs.dump_planets_view WHERE empire_id = ?";
-		Map< Integer , PlanetInformation > planets = new HashMap< Integer , PlanetInformation >( );
-		for ( PlanetInformation pi : this.dTemplate.query( sql , this.mPlanet , empireId ) ) {
-			di.getPlanets( ).add( pi );
-			planets.put( pi.getId( ) , pi );
-		}
+		this.getPlanetResources( empireId , planets );
+		this.getBuildQueues( empireId , planets );
+		this.getBuildings( empireId , planets );
+	}
 
-		sql = "SELECT * FROM bugs.dump_queues_view WHERE empire_id = ? ORDER BY queue_order";
-		for ( QueueItemInformation qii : this.dTemplate.query( sql , this.mQueueItem , empireId ) ) {
-			PlanetInformation pi = planets.get( qii.getPlanetId( ) );
-			QueueInformation qi = ( qii.isMilitary( ) ? pi.getMilitaryQueue( ) : pi.getCivilianQueue( ) );
-			qi.getItems( ).add( qii );
-		}
 
-		sql = "SELECT * FROM bugs.dump_buildings_view WHERE empire_id = ?";
-		for ( BuildingsInformation bi : this.dTemplate.query( sql , this.mBuildings , empireId ) ) {
+	/**
+	 * Add resources information to planets
+	 * 
+	 * <p>
+	 * Access resources information for the empire's planets and add the records to each planet.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param planets
+	 *            a map associating planet records with their identifier
+	 */
+	private void getPlanetResources( int empireId , Map< Integer , PlanetInformation > planets )
+	{
+		for ( PlanetResourceRow resRow : this.dTemplate.query( Q_PLANET_RESOURCES , this.mPlanetResources , empireId ) ) {
+			PlanetInformation planet = planets.get( resRow.getPlanetId( ) );
+			planet.getResourceDeltas( ).add( resRow.getDelta( ) );
+
+			ResourceProviderInformation resProv = resRow.getProvider( );
+			if ( resProv != null ) {
+				planet.getResourceProviders( ).add( resProv );
+			}
+		}
+	}
+
+
+	/**
+	 * Add build queue information to planet records
+	 * 
+	 * <p>
+	 * Access the contents of build queues and add the records to the corresponding planets.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param planets
+	 *            the map of planets by identifier
+	 */
+	private void getBuildQueues( int empireId , Map< Integer , PlanetInformation > planets )
+	{
+		for ( QueueItemInformation queueItem : this.dTemplate.query( Q_BUILD_QUEUES , this.mQueueItem , empireId ) ) {
+			PlanetInformation planet = planets.get( queueItem.getPlanetId( ) );
+
+			QueueInformation queue;
+			if ( queueItem.isMilitary( ) ) {
+				queue = planet.getMilitaryQueue( );
+			} else {
+				queue = planet.getCivilianQueue( );
+			}
+
+			queue.getItems( ).add( queueItem );
+		}
+	}
+
+
+	/**
+	 * Add buildings information to planet records
+	 * 
+	 * <p>
+	 * Access the contents of the buildings dump view and add the building records to the
+	 * corresponding planets.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param planets
+	 *            the map of planets by identifier
+	 */
+	private void getBuildings( int empireId , Map< Integer , PlanetInformation > planets )
+	{
+		for ( BuildingsInformation bi : this.dTemplate.query( Q_BUILDINGS , this.mBuildings , empireId ) ) {
 			planets.get( bi.getPlanetId( ) ).getBuildings( ).add( bi );
 		}
+	}
 
-		sql = "SELECT * FROM bugs.dump_fleets_view WHERE empire_id = ?";
+
+	/**
+	 * Retrieve information about the empire's fleets
+	 * 
+	 * <p>
+	 * Access the list of fleets, add the records to the top-level instance, then retrieve all
+	 * necessary information.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param empireDump
+	 *            the top-level empire dump
+	 */
+	private void getFleets( int empireId , DebugInformation empireDump )
+	{
 		Map< Long , FleetInformation > fleets = new HashMap< Long , FleetInformation >( );
-		for ( FleetInformation fi : this.dTemplate.query( sql , this.mFleet , empireId ) ) {
-			di.getFleets( ).add( fi );
+		for ( FleetInformation fi : this.dTemplate.query( Q_FLEETS , this.mFleet , empireId ) ) {
+			empireDump.getFleets( ).add( fi );
 			fleets.put( fi.getId( ) , fi );
 		}
+		this.getShips( empireId , fleets );
+	}
 
-		sql = "SELECT * FROM bugs.dump_ships_view WHERE empire_id = ?";
-		for ( ShipsInformation si : this.dTemplate.query( sql , this.mShips , empireId ) ) {
+
+	/**
+	 * Add ships information to fleet records
+	 * 
+	 * <p>
+	 * Retrieve information about ships and add the records to the corresponding fleets.
+	 * 
+	 * @param empireId
+	 *            the empire's identifier
+	 * @param fleets
+	 *            the map of fleets by identifier
+	 */
+	private void getShips( int empireId , Map< Long , FleetInformation > fleets )
+	{
+		for ( ShipsInformation si : this.dTemplate.query( Q_SHIPS , this.mShips , empireId ) ) {
 			fleets.get( si.getFleetId( ) ).getShips( ).add( si );
 		}
-
-		return this.xStream.toXML( di );
 	}
 
 }
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/PlanetResourceRow.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/PlanetResourceRow.java
new file mode 100644
index 0000000..3d6a7e2
--- /dev/null
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/PlanetResourceRow.java
@@ -0,0 +1,96 @@
+package com.deepclone.lw.beans.bt.es;
+
+
+import com.deepclone.lw.beans.bt.es.data.ResourceDeltaInformation;
+import com.deepclone.lw.beans.bt.es.data.ResourceProviderInformation;
+
+
+
+/**
+ * Planet resource row information
+ * 
+ * <p>
+ * Planet resources and resource providers are both extracted from the same
+ * <code>bugs.planet_resources_view</code> rows. Because of that, this class is used to transmit the
+ * information (as a {@link ResourceDeltaInformation} instance and an optional
+ * {@link ResourceProviderInformation} instance) from the row mapper to the summary creation
+ * component.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+class PlanetResourceRow
+{
+
+	/** Identifier of the planet */
+	private final int planetId;
+
+	/** Resource delta information */
+	private final ResourceDeltaInformation delta = new ResourceDeltaInformation( );
+
+	/** Resource provider information */
+	private ResourceProviderInformation provider;
+
+
+	/**
+	 * Initialise the instance by setting the planet identifier
+	 * 
+	 * @param planetId
+	 *            the planet's identifier
+	 */
+	public PlanetResourceRow( int planetId )
+	{
+		this.planetId = planetId;
+	}
+
+
+	/** @return the planet's identifier */
+	public int getPlanetId( )
+	{
+		return this.planetId;
+	}
+
+
+	/** @return the resource delta information record */
+	public ResourceDeltaInformation getDelta( )
+	{
+		return this.delta;
+	}
+
+
+	/** @return the resource provider information record */
+	public ResourceProviderInformation getProvider( )
+	{
+		return this.provider;
+	}
+
+
+	/**
+	 * Set the resource provider information record
+	 * 
+	 * @param provider
+	 *            the resource provider information record
+	 */
+	public void setProvider( ResourceProviderInformation provider )
+	{
+		this.provider = provider;
+	}
+
+
+	/**
+	 * Set the name of the resource
+	 * 
+	 * <p>
+	 * This method should be called once the rest of the record(s) has been initialised. It will set
+	 * the resource name on both the delta and the provider (if the latter exists).
+	 * 
+	 * @param resource
+	 *            the name of the resource
+	 */
+	public void setResource( String resource )
+	{
+		this.delta.setResource( resource );
+		if ( this.provider != null ) {
+			this.provider.setResource( resource );
+		}
+	}
+}
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/ResourceRowMapper.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/ResourceRowMapper.java
new file mode 100644
index 0000000..5925cfd
--- /dev/null
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/ResourceRowMapper.java
@@ -0,0 +1,58 @@
+package com.deepclone.lw.beans.bt.es;
+
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.springframework.jdbc.core.RowMapper;
+
+import com.deepclone.lw.beans.bt.es.data.ResourceProviderInformation;
+
+
+
+/**
+ * Mapper which extract planet resources and resource providers information
+ * 
+ * <p>
+ * This class maps rows from <code>bugs.dump_planet_resources_view</code> into
+ * {@link PlanetResourceRow} instances.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
+class ResourceRowMapper
+		implements RowMapper< PlanetResourceRow >
+{
+
+	/**
+	 * Map a row from <code>bugs.dump_planet_resources_view</code>
+	 * 
+	 * <p>
+	 * Generate the {@link PlanetResourceRow} instance with the correct planet identifier, resource
+	 * name, income and upkeep. If there is also a resource provider, attach a
+	 * {@link ResourceProviderInformation} instance to the result.
+	 */
+	@Override
+	public PlanetResourceRow mapRow( ResultSet rs , int rowNum )
+			throws SQLException
+	{
+		PlanetResourceRow row = new PlanetResourceRow( rs.getInt( "planet_id" ) );
+
+		row.getDelta( ).setIncome( rs.getDouble( "pres_income" ) );
+		row.getDelta( ).setUpkeep( rs.getDouble( "pres_upkeep" ) );
+
+		double pCapacity = rs.getDouble( "resprov_quantity_max" );
+		if ( !rs.wasNull( ) ) {
+			ResourceProviderInformation rpi = new ResourceProviderInformation( );
+			rpi.setMaximalQuantity( pCapacity );
+			rpi.setCurrentQuantity( rs.getDouble( "resprov_quantity" ) );
+			rpi.setDifficulty( rs.getDouble( "resprov_difficulty" ) );
+			rpi.setRecovery( rs.getDouble( "resprov_recovery" ) );
+			row.setProvider( rpi );
+		}
+
+		row.setResource( rs.getString( "resource_name" ) );
+		return row;
+	}
+
+}
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/DebugInformation.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/DebugInformation.java
index 76074b8..2959182 100644
--- a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/DebugInformation.java
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/DebugInformation.java
@@ -19,7 +19,7 @@ public class DebugInformation
 
 	@XStreamAsAttribute
 	@XStreamAlias( "dump-version" )
-	private int version = 1;
+	private int version = 2;
 
 	private SystemInformation system = new SystemInformation( );
 
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/InvalidDumpContentsException.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/InvalidDumpContentsException.java
new file mode 100644
index 0000000..2044984
--- /dev/null
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/InvalidDumpContentsException.java
@@ -0,0 +1,60 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import java.io.Serializable;
+
+
+
+/**
+ * Invalid dump contents exception
+ * 
+ * <p>
+ * This runtime exception is thrown by the debugging data record classes when one of the fields is
+ * set to an incorrect value.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
+public final class InvalidDumpContentsException
+		extends RuntimeException
+{
+	/** Serialisation identifier */
+	private static final long serialVersionUID = 1L;
+
+	/** Class in which the error was caused */
+	private final Class< ? extends Serializable > recordType;
+
+	/** Name of the field to which invalid contents were assigned */
+	private final String field;
+
+
+	/**
+	 * Initialise the exception by setting the record and field
+	 * 
+	 * @param type
+	 *            the type of the XML dump record
+	 * @param field
+	 *            the field to which invalid contents were assigned
+	 */
+	InvalidDumpContentsException( Class< ? extends Serializable > type , String field )
+	{
+		super( "Invalid contents for field " + field + " of record " + type.getSimpleName( ) );
+		this.recordType = type;
+		this.field = field;
+	}
+
+
+	/** @return the type of the XML dump record */
+	public Class< ? extends Serializable > getRecordType( )
+	{
+		return this.recordType;
+	}
+
+
+	/** @return the field to which invalid contents were assigned */
+	public String getField( )
+	{
+		return this.field;
+	}
+
+}
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/PlanetInformation.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/PlanetInformation.java
index de9c6b6..bd60545 100644
--- a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/PlanetInformation.java
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/PlanetInformation.java
@@ -7,118 +7,204 @@ import java.util.List;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
 
 
 
+/**
+ * Planet XML record
+ * 
+ * <p>
+ * This class is used in XML dumps to represent the information about a planet. It includes a few
+ * basic values, as well as information about buildings, queues and resources.
+ * 
+ * <p>
+ * This record exists since version 1 (B6M1) of XML dumps. However, the following changes were made
+ * later.
+ * <ul>
+ * <li>Version 2 (B6M2):
+ * <ul>
+ * <li>resource providers added,
+ * </ul>
+ * </ul>
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
 @XStreamAlias( "planet" )
+@SuppressWarnings( "serial" )
 public class PlanetInformation
 		implements Serializable
 {
 
-	private static final long serialVersionUID = 1L;
-
+	/** Planet identifier */
 	@XStreamAsAttribute
-	@XStreamAlias( "id" )
-	private int id;
+	private Integer id;
 
+	/** Planet name */
 	@XStreamAsAttribute
-	@XStreamAlias( "id" )
 	private String name;
 
+	/** Current population of the planet */
 	@XStreamAlias( "population" )
-	private double population;
+	private Double population;
 
+	/** Current happiness of the planet's population */
 	@XStreamAlias( "current-happiness" )
-	private double currentHappiness;
+	private Double currentHappiness;
 
+	/** Target happiness of the planet's population */
 	@XStreamAlias( "target-happiness" )
 	private double targetHappiness;
 
+	/** List of buildings */
 	@XStreamAlias( "buildings" )
-	private List< BuildingsInformation > buildings = new LinkedList< BuildingsInformation >( );
+	private final List< BuildingsInformation > buildings = new LinkedList< BuildingsInformation >( );
 
+	/** Civilian construction queue */
 	@XStreamAlias( "civ-queue" )
-	private QueueInformation civilianQueue = new QueueInformation( );
+	private final QueueInformation civilianQueue = new QueueInformation( );
 
+	/** Military construction queue */
 	@XStreamAlias( "mil-queue" )
-	private QueueInformation militaryQueue = new QueueInformation( );
+	private final QueueInformation militaryQueue = new QueueInformation( );
+
+	/** Planet resource deltas */
+	@XStreamImplicit
+	private final LinkedList< ResourceDeltaInformation > resourceDeltas = new LinkedList< ResourceDeltaInformation >( );
+
+	/** List of resource providers */
+	@XStreamImplicit( itemFieldName = "resource-provider" )
+	private final LinkedList< ResourceProviderInformation > resourceProviders = new LinkedList< ResourceProviderInformation >( );
 
 
-	public int getId( )
+	/** @return the planet's identifier */
+	public Integer getId( )
 	{
-		return id;
+		return this.id;
 	}
 
 
+	/**
+	 * Set the planet's identifier
+	 * 
+	 * @param id
+	 *            the planet's identifier
+	 */
 	public void setId( int id )
 	{
 		this.id = id;
 	}
 
 
+	/** @return the planet's name */
 	public String getName( )
 	{
-		return name;
+		return this.name;
 	}
 
 
+	/**
+	 * Set the planet's name
+	 * 
+	 * @param name
+	 *            the planet's name
+	 */
 	public void setName( String name )
 	{
 		this.name = name;
 	}
 
 
-	public double getPopulation( )
+	/** @return the planet's current population */
+	public Double getPopulation( )
 	{
-		return population;
+		return this.population;
 	}
 
 
+	/**
+	 * Set the planet's current population
+	 * 
+	 * @param population
+	 *            the planet's current population
+	 */
 	public void setPopulation( double population )
 	{
 		this.population = population;
 	}
 
 
-	public double getCurrentHappiness( )
+	/** @return the current happiness of the planet's population */
+	public Double getCurrentHappiness( )
 	{
-		return currentHappiness;
+		return this.currentHappiness;
 	}
 
 
+	/**
+	 * Set the current happiness of the planet's population
+	 * 
+	 * @param currentHappiness
+	 *            the current happiness of the planet's population
+	 */
 	public void setCurrentHappiness( double currentHappiness )
 	{
 		this.currentHappiness = currentHappiness;
 	}
 
 
-	public double getTargetHappiness( )
+	/** @return the target happiness of the planet's population */
+	public Double getTargetHappiness( )
 	{
-		return targetHappiness;
+		return this.targetHappiness;
 	}
 
 
+	/**
+	 * Set the target happiness of the planet's population
+	 * 
+	 * @param targetHappiness
+	 *            the target happiness of the planet's population
+	 */
 	public void setTargetHappiness( double targetHappiness )
 	{
 		this.targetHappiness = targetHappiness;
 	}
 
 
+	/** @return the list of buildings constructed on the planet */
 	public List< BuildingsInformation > getBuildings( )
 	{
-		return buildings;
+		return this.buildings;
 	}
 
 
+	/** @return the civilian construction queue */
 	public QueueInformation getCivilianQueue( )
 	{
-		return civilianQueue;
+		return this.civilianQueue;
 	}
 
 
+	/** @return the military construction queue */
 	public QueueInformation getMilitaryQueue( )
 	{
-		return militaryQueue;
+		return this.militaryQueue;
+	}
+
+
+	/** @return the list of resource delta records */
+	public LinkedList< ResourceDeltaInformation > getResourceDeltas( )
+	{
+		return this.resourceDeltas;
+	}
+
+
+	/** @return the list of resource provider records */
+	public LinkedList< ResourceProviderInformation > getResourceProviders( )
+	{
+		return this.resourceProviders;
 	}
 
 }
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceDeltaInformation.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceDeltaInformation.java
new file mode 100644
index 0000000..679b077
--- /dev/null
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceDeltaInformation.java
@@ -0,0 +1,109 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import java.io.Serializable;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+
+
+/**
+ * Resource delta information
+ * 
+ * <p>
+ * This class represents records which contain a resource's delta - that is, the income and upkeep
+ * for some type of resource.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+@XStreamAlias( "resource-delta" )
+public class ResourceDeltaInformation
+		implements Serializable
+{
+
+	/** Identifier of the resource */
+	@XStreamAlias( "id" )
+	@XStreamAsAttribute
+	private String resource;
+
+	/** Income for that type of resource */
+	@XStreamAsAttribute
+	private Double income;
+
+	/** Upkeep for that type of resource */
+	@XStreamAsAttribute
+	private Double upkeep;
+
+
+	/** @return the resource's identifier */
+	public String getResource( )
+	{
+		return this.resource;
+	}
+
+
+	/**
+	 * Set the resource's identifier
+	 * 
+	 * @param resource
+	 *            the resource's identifier
+	 * 
+	 * @throws InvalidDumpContentsException
+	 *             if the specified resource type is <code>null</code>
+	 */
+	public void setResource( String resource )
+			throws InvalidDumpContentsException
+	{
+		if ( resource == null ) {
+			throw new InvalidDumpContentsException( this.getClass( ) , "resource" );
+		}
+		this.resource = resource;
+	}
+
+
+	/**
+	 * @return the income for that type of resource, or <code>null</code> if no income information
+	 *         is available
+	 */
+	public Double getIncome( )
+	{
+		return this.income;
+	}
+
+
+	/**
+	 * Set the income for that type of resource
+	 * 
+	 * @param income
+	 *            the income for that type of resource
+	 */
+	public void setIncome( double income )
+	{
+		this.income = income;
+	}
+
+
+	/**
+	 * @return the upkeep for that type of resource, or <code>null</code> if no upkeep information
+	 *         is available
+	 */
+	public Double getUpkeep( )
+	{
+		return this.upkeep;
+	}
+
+
+	/**
+	 * Set the upkeep for that type of resource
+	 * 
+	 * @param upkeep
+	 *            the upkeep for that type of resource
+	 */
+	public void setUpkeep( double upkeep )
+	{
+		this.upkeep = upkeep;
+	}
+
+}
diff --git a/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceProviderInformation.java b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceProviderInformation.java
new file mode 100644
index 0000000..c29054c
--- /dev/null
+++ b/legacyworlds-server-beans-bt/src/main/java/com/deepclone/lw/beans/bt/es/data/ResourceProviderInformation.java
@@ -0,0 +1,152 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import java.io.Serializable;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+
+
+/**
+ * Resource provider XML record
+ * 
+ * <p>
+ * This class represents the information about a resource provider that will be serialised to the
+ * debugging XML dump when a player posts a bug report.
+ * 
+ * <p>
+ * This record exists since version 2 (B6M2) of XML dumps.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+public class ResourceProviderInformation
+		implements Serializable
+{
+
+	/** Identifier of the resource */
+	@XStreamAsAttribute
+	private String resource;
+
+	/** Maximal quantity in the provider */
+	@XStreamAsAttribute
+	@XStreamAlias( "max" )
+	private Double maximalQuantity;
+
+	/** Current quantity in the provider */
+	@XStreamAsAttribute
+	@XStreamAlias( "current" )
+	private Double currentQuantity;
+
+	/** Extraction difficulty */
+	@XStreamAsAttribute
+	private Double difficulty;
+
+	/** Recovery rate */
+	@XStreamAsAttribute
+	private Double recovery;
+
+
+	/** @return the resource's identifier */
+	public String getResource( )
+	{
+		return this.resource;
+	}
+
+
+	/**
+	 * Set the resource's identifier
+	 * 
+	 * @param resource
+	 *            the resource's identifier
+	 * 
+	 * @throws InvalidDumpContentsException
+	 *             if the resource's identifier is <code>null</code>
+	 */
+	public void setResource( String resource )
+			throws InvalidDumpContentsException
+	{
+		if ( resource == null ) {
+			throw new InvalidDumpContentsException( this.getClass( ) , "resource" );
+		}
+		this.resource = resource;
+	}
+
+
+	/** @return the provider's total capacity */
+	public Double getMaximalQuantity( )
+	{
+		return this.maximalQuantity;
+	}
+
+
+	/**
+	 * Set the maximal quantity of resources in the provider
+	 * 
+	 * @param maximalQuantity
+	 *            the provider's total capacity
+	 */
+	public void setMaximalQuantity( double maximalQuantity )
+	{
+		this.maximalQuantity = maximalQuantity;
+	}
+
+
+	/** @return the current quantity of resources in the provider */
+	public Double getCurrentQuantity( )
+	{
+		return this.currentQuantity;
+	}
+
+
+	/**
+	 * Set the current quantity of resources in the provider
+	 * 
+	 * @param currentQuantity
+	 *            the current quantity of resources in the provider
+	 */
+	public void setCurrentQuantity( double currentQuantity )
+	{
+		this.currentQuantity = currentQuantity;
+	}
+
+
+	/** @return the provider's extraction difficulty */
+	public Double getDifficulty( )
+	{
+		return this.difficulty;
+	}
+
+
+	/**
+	 * Set the provider's extraction difficulty
+	 * 
+	 * @param difficulty
+	 *            the provider's extraction difficulty
+	 */
+	public void setDifficulty( double difficulty )
+	{
+		this.difficulty = difficulty;
+	}
+
+
+	/** @return the provider's recovery rate */
+	public Double getRecovery( )
+	{
+		return this.recovery;
+	}
+
+
+	/**
+	 * Set the provider's recovery rate
+	 * 
+	 * @param recovery
+	 *            the provider's recovery rate
+	 */
+	public void setRecovery( double recovery )
+	{
+		this.recovery = recovery;
+	}
+
+}
diff --git a/legacyworlds-server-data/db-structure/parts/040-functions/200-bugs.sql b/legacyworlds-server-data/db-structure/parts/040-functions/200-bugs.sql
index f8d7282..1d9a23d 100644
--- a/legacyworlds-server-data/db-structure/parts/040-functions/200-bugs.sql
+++ b/legacyworlds-server-data/db-structure/parts/040-functions/200-bugs.sql
@@ -1220,6 +1220,54 @@ CREATE VIEW bugs.dump_planets_view
 GRANT SELECT ON bugs.dump_planets_view TO :dbuser;
 
 
+
+/*
+ * Planet resources view for XML dumps
+ * ------------------------------------
+ * 
+ * This view combines both planet resources and resource providers for all
+ * empire-owned planets. It is meant to be used in the XML dump generator.
+ * 
+ * 
+ * Columns:
+ *		empire_id				The empire's identifier
+ *		planet_id				The planet's identifier
+ *		resource_name			The string identifying the resource
+ *		pres_income				The planet's income for that resource type 
+ *		pres_upkeep				The planet's upkeep for that resource type
+ *		resprov_quantity_max	The resource provider's capacity, or NULL
+ *									if there is no resource provider of that
+ *									type on the planet
+ *		resprov_quantity		The resource provider's current quantity, or
+ *									NULL if there is no resource provider of
+ *									that type on the planet
+ *		resprov_difficulty		The resource provider's extraction difficulty,
+ *									or NULL if there is no resource provider
+ *									of that type on the planet
+ *		resprov_recovery		The resource provider's recovery rate, or NULL
+ *									if there is no resource provider of that
+ *									 type on the planet
+ */
+CREATE VIEW bugs.dump_planet_resources_view
+	AS SELECT empire_id , planet_id ,
+				name AS resource_name ,
+				pres_income , pres_upkeep ,
+				resprov_quantity_max , resprov_quantity ,
+				resprov_difficulty , resprov_recovery
+			FROM emp.planets
+				INNER JOIN verse.planet_resources
+					USING ( planet_id )
+				INNER JOIN defs.strings
+					ON resource_name_id = id
+				LEFT OUTER JOIN verse.resource_providers
+					USING ( planet_id , resource_name_id )
+			ORDER BY name;
+
+GRANT SELECT
+	ON bugs.dump_planet_resources_view
+	TO :dbuser;
+
+
 CREATE VIEW bugs.dump_queues_view
 	AS SELECT ep.empire_id , ep.planet_id , FALSE AS military , q.queue_order ,
 				q.building_id AS item_id , qin.name AS item_name ,
diff --git a/legacyworlds-server-data/db-structure/tests/admin/040-functions/200-bugs/010-dump-planet-resources-view.sql b/legacyworlds-server-data/db-structure/tests/admin/040-functions/200-bugs/010-dump-planet-resources-view.sql
new file mode 100644
index 0000000..23c8aa5
--- /dev/null
+++ b/legacyworlds-server-data/db-structure/tests/admin/040-functions/200-bugs/010-dump-planet-resources-view.sql
@@ -0,0 +1,73 @@
+/*
+ * Tests for bugs.dump_planet_resources_view
+ */
+BEGIN;
+	/*
+	 * We need a couple of resources (one natural, one basic), three planets
+	 * with valid planet resource records (two of the planets will have a
+	 * resource provider), two empires (owning a planet with and without
+	 * resource providers, respectively). 
+	 */
+	\i utils/strings.sql
+	\i utils/resources.sql
+	\i utils/accounts.sql
+	\i utils/naming.sql
+	\i utils/universe.sql
+	SELECT _create_natural_resources( 1 , 'natRes' );
+	SELECT _create_resources( 1 , 'basicRes' );
+	SELECT _create_raw_planets( 3 , 'planet' );
+	INSERT INTO verse.planet_resources(
+			planet_id , resource_name_id , pres_income , pres_upkeep
+		) VALUES (
+			_get_map_name( 'planet1' ) , _get_string( 'basicRes1' ) , 1 , 2
+		) , (
+			_get_map_name( 'planet1' ) , _get_string( 'natRes1' ) , 3 , 4
+		) , (
+			_get_map_name( 'planet2' ) , _get_string( 'basicRes1' ) , 5 , 6
+		) , (
+			_get_map_name( 'planet2' ) , _get_string( 'natRes1' ) , 7 , 8
+		) , (
+			_get_map_name( 'planet3' ) , _get_string( 'basicRes1' ) , 9 , 10
+		) , (
+			_get_map_name( 'planet3' ) , _get_string( 'natRes1' ) , 11 , 12
+		);
+	SELECT _create_resource_provider( 'planet1' , 'natRes1' );
+	SELECT _create_resource_provider( 'planet3' , 'natRes1' );
+
+	SELECT _create_emp_names( 2 , 'empire' );
+	SELECT emp.create_empire( _get_emp_name( 'empire1' ) ,
+				_get_map_name( 'planet1' ) ,
+				200.0 );
+	SELECT emp.create_empire( _get_emp_name( 'empire2' ) ,
+				_get_map_name( 'planet2' ) ,
+				200.0 );
+
+
+
+	/***** TESTS BEGIN HERE *****/
+	SELECT plan( 2 );
+
+	SELECT diag_test_name( 'bugs.dump_planet_resources_view - Records without resource providers' );
+	SELECT set_eq( $$
+		SELECT empire_id , planet_id , resource_name , pres_income , pres_upkeep
+			FROM bugs.dump_planet_resources_view
+			WHERE resprov_quantity IS NULL
+	$$ , $$ VALUES (
+		_get_emp_name( 'empire1' ) , _get_map_name( 'planet1' ) , 'basicRes1' , 1 , 2
+	) , (
+		_get_emp_name( 'empire2' ) , _get_map_name( 'planet2' ) , 'basicRes1' , 5 , 6
+	) , (
+		_get_emp_name( 'empire2' ) , _get_map_name( 'planet2' ) , 'natRes1' , 7 , 8
+	) $$ );
+
+	SELECT diag_test_name( 'bugs.dump_planet_resources_view - Records with resource providers' );
+	SELECT set_eq( $$
+		SELECT empire_id , planet_id , resource_name , pres_income , pres_upkeep
+			FROM bugs.dump_planet_resources_view
+			WHERE resprov_quantity IS NOT NULL
+	$$ , $$ VALUES (
+		_get_emp_name( 'empire1' ) , _get_map_name( 'planet1' ) , 'natRes1' , 3 , 4
+	) $$ );
+
+	SELECT * FROM finish( );
+ROLLBACK;
\ No newline at end of file
diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/200-bugs/010-dump-planet-resources-view.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/200-bugs/010-dump-planet-resources-view.sql
new file mode 100644
index 0000000..5100491
--- /dev/null
+++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/200-bugs/010-dump-planet-resources-view.sql
@@ -0,0 +1,11 @@
+/*
+ * Test privileges on bugs.dump_planet_resources_view
+ */
+BEGIN;
+	SELECT plan( 1 );
+
+	SELECT diag_test_name( 'bugs.dump_planet_resources_view - Privileges' );
+	SELECT lives_ok( 'SELECT * FROM bugs.dump_planet_resources_view' );
+	
+	SELECT * FROM finish( );
+ROLLBACK;
\ No newline at end of file
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestBuildingsInformationMapper.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestBuildingsInformationMapper.java
index c32bf40..cb5209a 100644
--- a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestBuildingsInformationMapper.java
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestBuildingsInformationMapper.java
@@ -16,6 +16,8 @@ import com.deepclone.lw.testing.MockResultSet;
 
 /**
  * Tests for the {@link BuildingsInformationMapper} class.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
  */
 public class TestBuildingsInformationMapper
 {
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestPlanetResourceRow.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestPlanetResourceRow.java
new file mode 100644
index 0000000..f02a3a9
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestPlanetResourceRow.java
@@ -0,0 +1,102 @@
+package com.deepclone.lw.beans.bt.es;
+
+
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.beans.bt.es.data.ResourceProviderInformation;
+
+
+
+/**
+ * Tests for the {@link PlanetResourceRow} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestPlanetResourceRow
+{
+
+	/** Identifier of the planet in the test object */
+	private static final int TEST_IDENTIFIER = 42;
+
+	/** Resource name used in the tests */
+	private static final String TEST_NAME = "Test";
+
+	/** The planet resource row instance used in tests */
+	private PlanetResourceRow prr;
+
+
+	/**
+	 * Create a planet resource row instance with the planet identifier specified by
+	 * {@link #TEST_IDENTIFIER}.
+	 */
+	@Before
+	public void setUp( )
+	{
+		this.prr = new PlanetResourceRow( TEST_IDENTIFIER );
+	}
+
+
+	/**
+	 * Planet identifier has been initialised correctly.
+	 */
+	@Test
+	public void testPlanetIdentifier( )
+	{
+		assertEquals( TEST_IDENTIFIER , this.prr.getPlanetId( ) );
+	}
+
+
+	/**
+	 * By default, the resource delta exists, the provider doesn't.
+	 */
+	@Test
+	public void testDefaultValues( )
+	{
+		assertNotNull( this.prr.getDelta( ) );
+		assertNull( this.prr.getProvider( ) );
+	}
+
+
+	/**
+	 * Setting and reading the resource provider record.
+	 */
+	@Test
+	public void testSetProvider( )
+	{
+		ResourceProviderInformation rpi = new ResourceProviderInformation( );
+		rpi.setResource( TEST_NAME );
+		this.prr.setProvider( rpi );
+		assertNotNull( this.prr.getProvider( ) );
+		assertEquals( TEST_NAME , this.prr.getProvider( ).getResource( ) );
+	}
+
+
+	/**
+	 * Setting the resource name when there is no resource provider record.
+	 */
+	@Test
+	public void testSetNameDeltaOnly( )
+	{
+		this.prr.setResource( TEST_NAME );
+		assertEquals( TEST_NAME , this.prr.getDelta( ).getResource( ) );
+		assertNull( this.prr.getProvider( ) );
+	}
+
+
+	/**
+	 * Setting the resource name when there are both a resource delta record and a resource provider
+	 * record.
+	 */
+	@Test
+	public void testSetNameFull( )
+	{
+		this.prr.setProvider( new ResourceProviderInformation( ) );
+		this.prr.setResource( TEST_NAME );
+		assertEquals( TEST_NAME , this.prr.getDelta( ).getResource( ) );
+		assertNotNull( this.prr.getProvider( ) );
+		assertEquals( TEST_NAME , this.prr.getProvider( ).getResource( ) );
+	}
+
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestResourceRowMapper.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestResourceRowMapper.java
new file mode 100644
index 0000000..2235fbd
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/TestResourceRowMapper.java
@@ -0,0 +1,129 @@
+package com.deepclone.lw.beans.bt.es;
+
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.testing.MockResultSet;
+
+
+
+/**
+ * Tests for the {@link ResourceRowMapper} class.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestResourceRowMapper
+{
+	/** Planet identifiers found in the "results" */
+	private static final int[] PLANET_IDS = new int[] {
+			1 , 2
+	};
+
+	/** Resource names found in the two "results", respectively */
+	private static final String[] RESOURCE_NAMES = new String[] {
+			"Test1" , "Test2"
+	};
+
+	/** Income values found in the two results */
+	private static final double[] INCOME_VALUES = new double[] {
+			3.0 , 4.0
+	};
+
+	/** Upkeep values found in the two results */
+	private static final double[] UPKEEP_VALUES = new double[] {
+			5.0 , 6.0
+	};
+
+	/** Resource provider quantity for the second row */
+	private static final double RP_QUANTITY = 7.0;
+
+	/** Resource provider capacity for the second row */
+	private static final double RP_CAPACITY = 8.0;
+
+	/** Resource provider extraction difficulty for the second row */
+	private static final double RP_DIFFICULTY = 9.0;
+
+	/** Resource provider recovery rate for the second row */
+	private static final double RP_RECOVERY = 10.0;
+
+	/** The fake result set used in the tests */
+	private ResultSet resultSet;
+
+	/** The mapper used in the tests */
+	private ResourceRowMapper mapper;
+
+
+	/** Create the mapper and the contents of the fake result set */
+	@Before
+	public void setUp( )
+	{
+		this.mapper = new ResourceRowMapper( );
+
+		@SuppressWarnings( "unchecked" )
+		HashMap< String , Object > rows[] = new HashMap[ 2 ];
+		for ( int i = 0 ; i < 2 ; i++ ) {
+			HashMap< String , Object > row = new HashMap< String , Object >( );
+			row.put( "planet_id" , PLANET_IDS[ i ] );
+			row.put( "resource_name" , RESOURCE_NAMES[ i ] );
+			row.put( "pres_income" , INCOME_VALUES[ i ] );
+			row.put( "pres_upkeep" , UPKEEP_VALUES[ i ] );
+			if ( i == 1 ) {
+				row.put( "resprov_quantity_max" , RP_CAPACITY );
+				row.put( "resprov_quantity" , RP_QUANTITY );
+				row.put( "resprov_difficulty" , RP_DIFFICULTY );
+				row.put( "resprov_recovery" , RP_RECOVERY );
+			}
+			rows[ i ] = row;
+		}
+
+		this.resultSet = MockResultSet.create( rows );
+	}
+
+
+	/**
+	 * Mapping a row with no provider information
+	 */
+	@Test
+	public void testMapNoProvider( )
+			throws SQLException
+	{
+		this.resultSet.absolute( 1 );
+
+		PlanetResourceRow row = this.mapper.mapRow( this.resultSet , 0 );
+		assertNotNull( row );
+		assertEquals( PLANET_IDS[ 0 ] , row.getPlanetId( ) );
+		assertEquals( RESOURCE_NAMES[ 0 ] , row.getDelta( ).getResource( ) );
+		assertEquals( INCOME_VALUES[ 0 ] , row.getDelta( ).getIncome( ) , 0 );
+		assertEquals( UPKEEP_VALUES[ 0 ] , row.getDelta( ).getUpkeep( ) , 0 );
+		assertNull( row.getProvider( ) );
+	}
+
+
+	/**
+	 * Mapping a row with a provider information record
+	 */
+	@Test
+	public void testMapWithProvider( )
+			throws SQLException
+	{
+		this.resultSet.absolute( 2 );
+
+		PlanetResourceRow row = this.mapper.mapRow( this.resultSet , 0 );
+		assertNotNull( row );
+		assertEquals( PLANET_IDS[ 1 ] , row.getPlanetId( ) );
+		assertEquals( RESOURCE_NAMES[ 1 ] , row.getDelta( ).getResource( ) );
+		assertEquals( INCOME_VALUES[ 1 ] , row.getDelta( ).getIncome( ) , 0 );
+		assertEquals( UPKEEP_VALUES[ 1 ] , row.getDelta( ).getUpkeep( ) , 0 );
+		assertNotNull( row.getProvider( ) );
+		assertEquals( RP_CAPACITY , row.getProvider( ).getMaximalQuantity( ) , 0 );
+		assertEquals( RP_QUANTITY , row.getProvider( ).getCurrentQuantity( ) , 0 );
+		assertEquals( RP_DIFFICULTY , row.getProvider( ).getDifficulty( ) , 0 );
+		assertEquals( RP_RECOVERY , row.getProvider( ).getRecovery( ) , 0 );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestInvalidDumpContentsException.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestInvalidDumpContentsException.java
new file mode 100644
index 0000000..a88992d
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestInvalidDumpContentsException.java
@@ -0,0 +1,85 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.Serializable;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.beans.bt.es.data.InvalidDumpContentsException;
+
+
+
+/**
+ * Tests for the {@link InvalidDumpContentsException} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestInvalidDumpContentsException
+{
+
+	/**
+	 * A dummy class used to test the {@link InvalidDumpContentsException} class.
+	 * 
+	 * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+	 */
+	@SuppressWarnings( "serial" )
+	private static final class TestRecord
+			implements Serializable
+	{
+		// EMPTY
+	}
+
+	/** Test string used as the "field" parameter */
+	private static final String TEST_STRING = "Test";
+
+	/** Exception instance used to run tests */
+	private InvalidDumpContentsException exception;
+
+
+	/**
+	 * Set up the test by creating an exception instance using {@link TestRecord} as the target
+	 * class and {@link #TEST_STRING} as the field name.
+	 */
+	@Before
+	public void setUp( )
+	{
+		this.exception = new InvalidDumpContentsException( TestRecord.class , TEST_STRING );
+	}
+
+
+	/**
+	 * The constructor must set the record's class and field name.
+	 */
+	@Test
+	public void testExceptionData( )
+	{
+		assertEquals( TestRecord.class , this.exception.getRecordType( ) );
+		assertEquals( TEST_STRING , this.exception.getField( ) );
+		assertNotNull( this.exception.getMessage( ) );
+	}
+
+
+	/**
+	 * The exception's message must end with the name of the class.
+	 */
+	@Test
+	public void testClassNameInMessage( )
+	{
+		assertTrue( this.exception.getMessage( ).endsWith( " " + TestRecord.class.getSimpleName( ) ) );
+	}
+
+
+	/**
+	 * The exception's message must contain the name of the field.
+	 */
+	@Test
+	public void testFieldNameInMessage( )
+	{
+		assertTrue( this.exception.getMessage( ).contains( " " + TEST_STRING + " " ) );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceDeltaInformation.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceDeltaInformation.java
new file mode 100644
index 0000000..2d899f6
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceDeltaInformation.java
@@ -0,0 +1,174 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.thoughtworks.xstream.XStream;
+
+
+
+/**
+ * Tests for the {@link ResourceDeltaInformation} XML dump storage class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestResourceDeltaInformation
+{
+
+	/** A string used in tests */
+	private static final String TEST_STRING = "This is a test";
+
+	/** A real number used in tests */
+	private static final Double TEST_DOUBLE = 4.2;
+
+	/** A record used in the tests */
+	private ResourceDeltaInformation rdi;
+
+
+	/**
+	 * Set up the test by creating a new resource delta information record.
+	 */
+	@Before
+	public void setUp( )
+	{
+		this.rdi = new ResourceDeltaInformation( );
+	}
+
+
+	/**
+	 * All fields are set to <code>null</code> by default
+	 */
+	@Test
+	public void testDefaultValues( )
+	{
+		assertNull( this.rdi.getResource( ) );
+		assertNull( this.rdi.getIncome( ) );
+		assertNull( this.rdi.getUpkeep( ) );
+	}
+
+
+	/**
+	 * Setting and reading the resource's name
+	 */
+	@Test
+	public void testResourceName( )
+	{
+		this.rdi.setResource( TEST_STRING );
+		assertEquals( TEST_STRING , this.rdi.getResource( ) );
+	}
+
+
+	/**
+	 * Setting the resource name to <code>null</code> throws {@link InvalidDumpContentsException}
+	 */
+	@Test
+	public void testNullResourceName( )
+	{
+		try {
+			this.rdi.setResource( null );
+			fail( "No InvalidDumpContentsException thrown" );
+		} catch ( InvalidDumpContentsException exception ) {
+			assertEquals( ResourceDeltaInformation.class , exception.getRecordType( ) );
+			assertEquals( "resource" , exception.getField( ) );
+		}
+	}
+
+
+	/**
+	 * Setting and reading the income
+	 */
+	@Test
+	public void testIncome( )
+	{
+		this.rdi.setIncome( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rdi.getIncome( ) );
+	}
+
+
+	/**
+	 * Setting and reading the upkeep
+	 */
+	@Test
+	public void testUpkeep( )
+	{
+		this.rdi.setUpkeep( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rdi.getUpkeep( ) );
+	}
+
+
+	/**
+	 * Serialising the instance to XML
+	 */
+	@Test
+	public void testXMLSerialisation( )
+	{
+		this.rdi.setResource( TEST_STRING );
+		this.rdi.setIncome( 0.1 );
+		this.rdi.setUpkeep( 0.2 );
+
+		XStream xstream = this.createXStreamInstance( );
+		String serialised = xstream.toXML( this.rdi );
+		assertNotNull( serialised );
+		assertTrue( serialised.startsWith( "<resource-delta " ) );
+		assertTrue( serialised.endsWith( "/>" ) );
+		assertTrue( serialised.contains( " id=\"" + TEST_STRING + "\"" ) );
+		assertTrue( serialised.contains( " income=\"0.1\"" ) );
+		assertTrue( serialised.contains( " upkeep=\"0.2\"" ) );
+	}
+
+
+	/**
+	 * Deserialising an instance that contains data
+	 */
+	@Test
+	public void testXMLDeserialisation( )
+	{
+		String xml = "<resource-delta id=\"Test\" income=\"0.1\" upkeep=\"0.2\" />";
+		XStream xstream = this.createXStreamInstance( );
+		Object deserialised = xstream.fromXML( xml );
+
+		assertNotNull( deserialised );
+		assertEquals( ResourceDeltaInformation.class , deserialised.getClass( ) );
+		this.rdi = (ResourceDeltaInformation) deserialised;
+
+		assertEquals( "Test" , this.rdi.getResource( ) );
+		assertEquals( (Double) 0.1 , this.rdi.getIncome( ) );
+		assertEquals( (Double) 0.2 , this.rdi.getUpkeep( ) );
+	}
+
+
+	/**
+	 * Deserialising an instance that contains no data
+	 */
+	@Test
+	public void testXMLDeserialisationNoData( )
+	{
+		String xml = "<resource-delta />";
+		XStream xstream = this.createXStreamInstance( );
+		Object deserialised = xstream.fromXML( xml );
+
+		assertNotNull( deserialised );
+		assertEquals( ResourceDeltaInformation.class , deserialised.getClass( ) );
+		this.rdi = (ResourceDeltaInformation) deserialised;
+
+		assertNull( this.rdi.getResource( ) );
+		assertNull( this.rdi.getIncome( ) );
+		assertNull( this.rdi.getUpkeep( ) );
+	}
+
+
+	/**
+	 * Create and set up the {@link XStream} instance used in the serialisation tests
+	 * 
+	 * @return the {@link XStream} instance to use
+	 */
+	private XStream createXStreamInstance( )
+	{
+		XStream xstream = new XStream( );
+		xstream.processAnnotations( ResourceDeltaInformation.class );
+		return xstream;
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceProviderInformation.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceProviderInformation.java
new file mode 100644
index 0000000..d545624
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/bt/es/data/TestResourceProviderInformation.java
@@ -0,0 +1,207 @@
+package com.deepclone.lw.beans.bt.es.data;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.thoughtworks.xstream.XStream;
+
+
+
+/**
+ * Tests for the {@link ResourceProviderInformation} XML dump storage class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestResourceProviderInformation
+{
+	/** A string used in tests */
+	private static final String TEST_STRING = "This is a test";
+
+	/** A real number used in tests */
+	private static final Double TEST_DOUBLE = 4.2;
+
+	/** A resource provider information record */
+	private ResourceProviderInformation rpi;
+
+
+	@Before
+	public void setUp( )
+	{
+		this.rpi = new ResourceProviderInformation( );
+	}
+
+
+	/**
+	 * A new record's fields are all set to <code>null</code>.
+	 */
+	@Test
+	public void testDefaultValues( )
+	{
+		assertNull( this.rpi.getResource( ) );
+		assertNull( this.rpi.getCurrentQuantity( ) );
+		assertNull( this.rpi.getMaximalQuantity( ) );
+		assertNull( this.rpi.getDifficulty( ) );
+		assertNull( this.rpi.getRecovery( ) );
+	}
+
+
+	/**
+	 * Setting and reading the resource's name
+	 */
+	@Test
+	public void testResourceName( )
+	{
+		this.rpi.setResource( TEST_STRING );
+		assertEquals( TEST_STRING , this.rpi.getResource( ) );
+	}
+
+
+	/**
+	 * Setting the resource name to <code>null</code> throws {@link InvalidDumpContentsException}
+	 */
+	@Test
+	public void testNullResourceName( )
+	{
+		try {
+			this.rpi.setResource( null );
+			fail( "No InvalidDumpContentsException thrown" );
+		} catch ( InvalidDumpContentsException exception ) {
+			assertEquals( ResourceProviderInformation.class , exception.getRecordType( ) );
+			assertEquals( "resource" , exception.getField( ) );
+		}
+	}
+
+
+	/**
+	 * Setting and reading the current quantity
+	 */
+	@Test
+	public void testCurrentQuantity( )
+	{
+		this.rpi.setCurrentQuantity( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rpi.getCurrentQuantity( ) );
+	}
+
+
+	/**
+	 * Setting and reading the maximal quantity
+	 */
+	@Test
+	public void testMaximalQuantity( )
+	{
+		this.rpi.setMaximalQuantity( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rpi.getMaximalQuantity( ) );
+	}
+
+
+	/**
+	 * Setting and reading the extraction difficulty
+	 */
+	@Test
+	public void testDifficulty( )
+	{
+		this.rpi.setDifficulty( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rpi.getDifficulty( ) );
+	}
+
+
+	/**
+	 * Setting and reading the recovery rate
+	 */
+	@Test
+	public void testRecovery( )
+	{
+		this.rpi.setRecovery( TEST_DOUBLE );
+		assertEquals( TEST_DOUBLE , this.rpi.getRecovery( ) );
+	}
+
+
+	/**
+	 * Serialising the instance to XML
+	 */
+	@Test
+	public void testXMLSerialisation( )
+	{
+		this.rpi.setResource( TEST_STRING );
+		this.rpi.setCurrentQuantity( 0.1 );
+		this.rpi.setMaximalQuantity( 0.2 );
+		this.rpi.setDifficulty( 0.3 );
+		this.rpi.setRecovery( 0.4 );
+
+		XStream xstream = this.createXStreamInstance( );
+		String serialised = xstream.toXML( this.rpi );
+		assertNotNull( serialised );
+		assertTrue( serialised.startsWith( "<resource-provider " ) );
+		assertTrue( serialised.endsWith( "/>" ) );
+		assertTrue( serialised.contains( " resource=\"" + TEST_STRING + "\"" ) );
+		assertTrue( serialised.contains( " current=\"0.1\"" ) );
+		assertTrue( serialised.contains( " max=\"0.2\"" ) );
+		assertTrue( serialised.contains( " difficulty=\"0.3\"" ) );
+		assertTrue( serialised.contains( " recovery=\"0.4\"" ) );
+	}
+
+
+	/**
+	 * Deserialising an instance that contains data
+	 */
+	@Test
+	public void testXMLDeserialisation( )
+	{
+		String xml = "<resource-provider resource=\"Test\" current=\"0.1\" max=\"0.2\" difficulty=\"0.3\" recovery=\"0.4\" />";
+		XStream xstream = this.createXStreamInstance( );
+		Object deserialised = xstream.fromXML( xml );
+
+		assertNotNull( deserialised );
+		assertEquals( ResourceProviderInformation.class , deserialised.getClass( ) );
+		this.rpi = (ResourceProviderInformation) deserialised;
+
+		assertEquals( "Test" , this.rpi.getResource( ) );
+		assertEquals( (Double) 0.1 , this.rpi.getCurrentQuantity( ) );
+		assertEquals( (Double) 0.2 , this.rpi.getMaximalQuantity( ) );
+		assertEquals( (Double) 0.3 , this.rpi.getDifficulty( ) );
+		assertEquals( (Double) 0.4 , this.rpi.getRecovery( ) );
+	}
+
+
+	/**
+	 * Deserialising an instance that contains no data
+	 */
+	@Test
+	public void testXMLDeserialisationNoData( )
+	{
+		String xml = "<resource-provider />";
+		XStream xstream = this.createXStreamInstance( );
+		Object deserialised = xstream.fromXML( xml );
+
+		assertNotNull( deserialised );
+		assertEquals( ResourceProviderInformation.class , deserialised.getClass( ) );
+		this.rpi = (ResourceProviderInformation) deserialised;
+
+		assertNull( this.rpi.getResource( ) );
+		assertNull( this.rpi.getCurrentQuantity( ) );
+		assertNull( this.rpi.getMaximalQuantity( ) );
+		assertNull( this.rpi.getDifficulty( ) );
+		assertNull( this.rpi.getRecovery( ) );
+	}
+
+
+	/**
+	 * Create and set up the {@link XStream} instance used in the serialisation tests
+	 * 
+	 * @return the {@link XStream} instance to use
+	 */
+	private XStream createXStreamInstance( )
+	{
+		XStream xstream = new XStream( );
+		xstream.processAnnotations( ResourceProviderInformation.class );
+		xstream.alias( "resource-provider" , ResourceProviderInformation.class );
+		return xstream;
+	}
+}