diff --git a/legacyworlds-server-main/data/resources.xml b/legacyworlds-server-main/data/resources.xml new file mode 100644 index 0000000..77a3e62 --- /dev/null +++ b/legacyworlds-server-main/data/resources.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-main/data/resources.xsd b/legacyworlds-server-main/data/resources.xsd new file mode 100644 index 0000000..a0a46f2 --- /dev/null +++ b/legacyworlds-server-main/data/resources.xsd @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportResources.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportResources.java new file mode 100644 index 0000000..b66063b --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportResources.java @@ -0,0 +1,372 @@ +package com.deepclone.lw.cli; + + +import java.io.File; + +import javax.sql.DataSource; + +import org.apache.log4j.Logger; +import org.postgresql.util.PGobject; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.FileSystemXmlApplicationContext; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import com.deepclone.lw.cli.xmlimport.ResourceLoader; +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.resources.BasicResource; +import com.deepclone.lw.cli.xmlimport.data.resources.NaturalResource; +import com.deepclone.lw.cli.xmlimport.data.resources.Resources; +import com.deepclone.lw.cli.xmlimport.data.resources.UOCResourceErrorCode; +import com.deepclone.lw.utils.StoredProc; + + + +/** + * Resources import tool + * + *

+ * This class defines the body of the resource import tool. It loads the data file specified on the + * command line, validates it and then loads the resource definitions into the database. + * + * @author T. Rosser + */ +public class ImportResources + extends CLITool +{ + /** Logging system */ + private final Logger logger = Logger.getLogger( ImportResources.class ); + + /** File to read the definitions from */ + private File file; + + /** Spring transaction template */ + private TransactionTemplate tTemplate; + + /** Basic resource with category update or create stored procedure */ + private StoredProc uocBasicResourceWithCategory; + + /** Basic resource without category update or create stored procedure */ + private StoredProc uocBasicResourceWithoutCategory; + + /** Natural resource with category update or create stored procedure */ + private StoredProc uocNaturalResourceWithCategory; + + /** Natural resource without category update or create stored procedure */ + private StoredProc uocNaturalResourceWithoutCategory; + + + /** + * Create the Spring context + * + *

+ * Load the definition of the data source as a Spring context, then adds the transaction + * management component. + * + * @return the Spring application context + */ + private ClassPathXmlApplicationContext createContext( ) + { + FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( new String[] { + this.getDataSource( ) + } ); + ctx.refresh( ); + + return new ClassPathXmlApplicationContext( new String[] { + "configuration/transaction-bean.xml" + } , true , ctx ); + } + + + /** + * Create database access templates + * + *

+ * Initialise the transaction template and the four stored procedure definitions. + * + * @param ctx + * the Spring application context + */ + private void createTemplates( ApplicationContext ctx ) + { + DataSource dSource = ctx.getBean( DataSource.class ); + PlatformTransactionManager tManager = ctx.getBean( PlatformTransactionManager.class ); + + this.uocBasicResourceWithoutCategory = new StoredProc( dSource , "defs" , "uoc_resource" ) + .addParameter( "_name" , java.sql.Types.VARCHAR ) + .addParameter( "_description" , java.sql.Types.VARCHAR ) + .addParameter( "_weight" , java.sql.Types.INTEGER ).addOutput( "_return" , java.sql.Types.OTHER ); + + this.uocBasicResourceWithCategory = new StoredProc( dSource , "defs" , "uoc_resource" ) + .addParameter( "_name" , java.sql.Types.VARCHAR ) + .addParameter( "_description" , java.sql.Types.VARCHAR ) + .addParameter( "_category" , java.sql.Types.VARCHAR ).addParameter( "_weight" , java.sql.Types.INTEGER ) + .addOutput( "_return" , java.sql.Types.OTHER ); + + this.uocNaturalResourceWithoutCategory = new StoredProc( dSource , "defs" , "uoc_natural_resource" ) + .addParameter( "_name" , java.sql.Types.VARCHAR ) + .addParameter( "_description" , java.sql.Types.VARCHAR ) + .addParameter( "_weight" , java.sql.Types.INTEGER ).addParameter( "_presence" , java.sql.Types.DOUBLE ) + .addParameter( "_quantity_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_quantity_dev" , java.sql.Types.DOUBLE ) + .addParameter( "_difficulty_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_difficulty_dev" , java.sql.Types.DOUBLE ) + .addParameter( "_recovery_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_recovery_dev" , java.sql.Types.DOUBLE ).addOutput( "_return" , java.sql.Types.OTHER ); + + this.uocNaturalResourceWithCategory = new StoredProc( dSource , "defs" , "uoc_natural_resource" ) + .addParameter( "_name" , java.sql.Types.VARCHAR ) + .addParameter( "_description" , java.sql.Types.VARCHAR ) + .addParameter( "_category" , java.sql.Types.VARCHAR ).addParameter( "_weight" , java.sql.Types.INTEGER ) + .addParameter( "_presence" , java.sql.Types.DOUBLE ) + .addParameter( "_quantity_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_quantity_dev" , java.sql.Types.DOUBLE ) + .addParameter( "_difficulty_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_difficulty_dev" , java.sql.Types.DOUBLE ) + .addParameter( "_recovery_avg" , java.sql.Types.DOUBLE ) + .addParameter( "_recovery_dev" , java.sql.Types.DOUBLE ).addOutput( "_return" , java.sql.Types.OTHER ); + + this.tTemplate = new TransactionTemplate( tManager ); + } + + + /** + * Import all resource definitions + * + *

+ * Import all resource definitions from the top-level Resources data instance based on the type + * of resource, and whether or not it has a category. + * + * @param data + * the top level Resources data instance + * + * @throws DataImportException + * when some external resource definition fails to load + */ + private void importResources( Resources data ) + throws DataImportException + { + for ( BasicResource br : data ) { + if ( br instanceof NaturalResource ) { + if ( br.getCategory( ) == null ) { + this.importNaturalResourceWithoutCategory( (NaturalResource) br ); + } else { + this.importNaturalResourceWithCategory( (NaturalResource) br ); + } + } else { + if ( br.getCategory( ) == null ) { + this.importBasicResourceWithoutCategory( br ); + } else { + this.importBasicResourceWithCategory( br ); + } + } + } + } + + + /** + * Import a Natural Resource + * + *

+ * Import a natural resource that does not have a defined category. + * + * @param nr + * the resources definition data + * + * @throws DataImportException + * when some external resource definition fails to load + */ + private void importNaturalResourceWithoutCategory( NaturalResource nr ) + throws DataImportException + { + UOCResourceErrorCode err = UOCResourceErrorCode.valueOf( ( (PGobject) this.uocNaturalResourceWithoutCategory + .execute( nr.getName( ) , nr.getDescription( ) , nr.getWeight( ) , nr.getPresenceProbability( ) , + nr.getQuantity( ).getAverage( ) , nr.getQuantity( ).getDeviation( ) , + nr.getDifficulty( ).getAverage( ) , nr.getDifficulty( ).getDeviation( ) , + nr.getRecovery( ).getAverage( ) , nr.getRecovery( ).getDeviation( ) ).get( "_return" ) ) + .getValue( ) ); + + if ( !err.equals( UOCResourceErrorCode.CREATED ) && !err.equals( UOCResourceErrorCode.UPDATED ) ) { + throw new DataImportException( "uocNaturalResourceWithoutCategory returned error code " + err.toString( ) ); + } + } + + + /** + * Import a Natural Resource + * + *

+ * Import a natural resource that does have a defined category. + * + * @param nr + * the resources definition data + * + * @throws DataImportException + * when some external resource definition fails to load + */ + private void importNaturalResourceWithCategory( NaturalResource nr ) + throws DataImportException + { + UOCResourceErrorCode err = UOCResourceErrorCode.valueOf( ( (PGobject) this.uocNaturalResourceWithCategory + .execute( nr.getName( ) , nr.getDescription( ) , nr.getCategory( ) , nr.getWeight( ) , + nr.getPresenceProbability( ) , nr.getQuantity( ).getAverage( ) , + nr.getQuantity( ).getDeviation( ) , nr.getDifficulty( ).getAverage( ) , + nr.getDifficulty( ).getDeviation( ) , nr.getRecovery( ).getAverage( ) , + nr.getRecovery( ).getDeviation( ) ).get( "_return" ) ).getValue( ) ); + + if ( !err.equals( UOCResourceErrorCode.CREATED ) && !err.equals( UOCResourceErrorCode.UPDATED ) ) { + throw new DataImportException( "uocNaturalResourceWithCategory returned error code " + err.toString( ) ); + } + } + + + /** + * Import a Basic Resource + * + *

+ * Import a basic resource that does not have a defined category. + * + * @param br + * the resources definition data + * + * @throws DataImportException + * when some external resource definition fails to load + */ + private void importBasicResourceWithoutCategory( BasicResource br ) + throws DataImportException + { + UOCResourceErrorCode err = UOCResourceErrorCode.valueOf( ( (PGobject) this.uocBasicResourceWithoutCategory + .execute( br.getName( ) , br.getDescription( ) , br.getWeight( ) ).get( "_return" ) ).getValue( ) ); + + if ( !err.equals( UOCResourceErrorCode.CREATED ) && !err.equals( UOCResourceErrorCode.UPDATED ) ) { + throw new DataImportException( "uocBasicResourceWithoutCategory returned error code " + err.toString( ) ); + } + } + + + /** + * Import a Basic Resource + * + *

+ * Import a basic resource that does not have a defined category. + * + * @param br + * the resources definition data + * + * @throws DataImportException + * when some external resource definition fails to load + */ + private void importBasicResourceWithCategory( BasicResource br ) + throws DataImportException + { + UOCResourceErrorCode err = UOCResourceErrorCode + .valueOf( ( (PGobject) this.uocBasicResourceWithCategory.execute( br.getName( ) , br.getDescription( ) , + br.getCategory( ) , br.getWeight( ) ).get( "_return" ) ).getValue( ) ); + + if ( !err.equals( UOCResourceErrorCode.CREATED ) && !err.equals( UOCResourceErrorCode.UPDATED ) ) { + throw new DataImportException( "uocBasicResourceWithCategory returned error code " + err.toString( ) ); + } + } + + + /** + * Run the resource definitions import tool + * + *

+ * Loads the data file, the connects to the database and creates or updates all definitions. + */ + @Override + public void run( ) + { + Resources data; + try { + data = new ResourceLoader( this.file ).load( ); + } catch ( DataImportException e ) { + System.err.println( "Error while loading '" + this.file + "': " + e.getMessage( ) ); + this.logger.error( "Error while loading resources data" , e ); + return; + } + + AbstractApplicationContext ctx = this.createContext( ); + this.createTemplates( ctx ); + this.executeImportTransaction( data ); + ToolBase.destroyContext( ctx ); + } + + + /** + * Execute the resource definitions importation transaction + * + *

+ * Run a transaction and execute the importation code inside it. Roll back if anything goes + * wrong. + * + * @param data + * the Resources definitions instance + */ + private void executeImportTransaction( final Resources data ) + { + boolean rv = this.tTemplate.execute( new TransactionCallback< Boolean >( ) { + @Override + public Boolean doInTransaction( TransactionStatus status ) + { + boolean rv = ImportResources.this.doTransaction( data ); + if ( !rv ) { + status.setRollbackOnly( ); + } + return rv; + } + } ); + if ( rv ) { + this.logger.info( "Resource import successful" ); + } + } + + + /** + * Import transaction body + * + *

+ * Import all definitions and handle exceptions. + * + * @param data + * the Resources definitions instance + * @return + */ + private boolean doTransaction( Resources data ) + { + try { + this.importResources( data ); + return true; + } catch ( RuntimeException e ) { + this.logger.error( "Caught runtime exception" , e ); + } catch ( DataImportException e ) { + this.logger.error( "Error while importing resources" , e ); + } + return false; + } + + + /** + * Obtain the name of the definitions file + * + *

+ * Check the command line options, setting the definitions file accordingly. + */ + @Override + public boolean setOptions( String... options ) + { + if ( options.length != 1 ) { + return false; + } + this.file = new File( options[ 0 ] ); + if ( ! ( this.file.isFile( ) && this.file.canRead( ) ) ) { + return false; + } + return true; + } +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java index 8d6e856..339182d 100644 --- a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java @@ -217,12 +217,12 @@ public class ImportText private boolean doTransaction( I18NText data ) { try { - ImportText.this.importText( data ); + this.importText( data ); return true; } catch ( RuntimeException e ) { - ImportText.this.logger.error( "Caught runtime exception" , e ); + this.logger.error( "Caught runtime exception" , e ); } catch ( DataImportException e ) { - ImportText.this.logger.error( "Error while importing external strings" , e ); + this.logger.error( "Error while importing external strings" , e ); } return false; } diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/ResourceLoader.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/ResourceLoader.java new file mode 100644 index 0000000..47656d8 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/ResourceLoader.java @@ -0,0 +1,118 @@ +package com.deepclone.lw.cli.xmlimport; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.resources.NaturalResource; +import com.deepclone.lw.cli.xmlimport.data.resources.Resources; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.XStreamException; + + + +/** + * Resource Loader + * + *

+ * This class can be used to load all resource definitions. It extracts them from the XML file and + * verifies them. + * + * @author T. Rosser + */ +public class ResourceLoader +{ + /** The file to read the XML from */ + private final File file; + + + /** + * Initialise the loader + * + * @param file + * the XML file that contains the definitions + */ + public ResourceLoader( File file ) + { + this.file = file.getAbsoluteFile( ); + } + + + /** + * Initialise the necessary XStream instance + * + *

+ * Initialise the XStream instance by processing the annotations of all the resource importable + * data classes. + * + * @return the XStream instance to use when loading the data + */ + private XStream initXStream( ) + { + XStream xstream = new XStream( ); + xstream.processAnnotations( Resources.class ); + xstream.processAnnotations( NaturalResource.class ); + return xstream; + } + + + /** + * Load the resource definitions + * + *

+ * Load the XML file and process the definitions using XStream. + * + * @return the top-level importable data instance + * + * @throws DataImportException + * if reading from the file or parsing its contents fail + */ + private Resources loadXMLFile( ) + throws DataImportException + { + FileInputStream fis; + try { + fis = new FileInputStream( this.file ); + } catch ( FileNotFoundException e ) { + throw new DataImportException( "Unable to load resource definitions" , e ); + } + + try { + try { + XStream xstream = this.initXStream( ); + return (Resources) xstream.fromXML( fis ); + } finally { + fis.close( ); + } + } catch ( IOException e ) { + throw new DataImportException( "Input error while loading resource definitions" , e ); + } catch ( XStreamException e ) { + throw new DataImportException( "XML error while loading resource definitions" , e ); + } + } + + + /** + * Load and process resource definitions + * + *

+ * Attempt to load all resource definitions, ensure they are valid, then set the original file's + * path. + * + * @return the top-level importable data instance + * + * @throws DataImportException + * if loading or verifying the data fails + */ + public Resources load( ) + throws DataImportException + { + Resources resources = this.loadXMLFile( ); + resources.verifyData( ); + resources.setReadFrom( this.file ); + return resources; + } +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/BasicResource.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/BasicResource.java new file mode 100644 index 0000000..551ff47 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/BasicResource.java @@ -0,0 +1,105 @@ +package com.deepclone.lw.cli.xmlimport.data.resources; + + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.ImportableData; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + + + +/** + * A Basic Resource + * + *

+ * This class is the base for all resource classes providing a name, description, category and + * weight. In certain cases the category can be null. It provides a method to verify that data has + * been imported successfully. + * + * @author T. Rosser + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "basic-resource" ) +public class BasicResource + extends ImportableData +{ + /** The resource's name */ + @XStreamAsAttribute + private String name; + + /** The resource's description */ + @XStreamAsAttribute + private String description; + + /** + * The resource's category + * + *

+ * Can be null for resources that do not belong to a category + */ + @XStreamAsAttribute + private String category; + + /** + * The resource's weight + * + *

+ * Used when sorting resources and resource categories. + */ + @XStreamAsAttribute + private Integer weight; // Is int enough? + + + /** + * Check a resource's data + * + *

+ * Make sure that a resource's properties are both present and valid. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.name == null || "".equals( this.name.trim( ) ) ) { + throw new DataImportException( "Missing name string" ); + } + if ( this.description == null || "".equals( this.description.trim( ) ) ) { + throw new DataImportException( "Missing name string" ); + } + if ( this.category != null && "".equals( this.description.trim( ) ) ) { + throw new DataImportException( "Category invalid" ); + } + if ( this.weight == null ) { + throw new DataImportException( "Missing weight" ); + } + } + + + /** @return the resource's name */ + public String getName( ) + { + return this.name; + } + + + /** @return the resource's description */ + public String getDescription( ) + { + return this.description; + } + + + /** @return the resource's category */ + public String getCategory( ) + { + return this.category; + } + + + /** @return the resource's weight */ + public Integer getWeight( ) + { + return this.weight; + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/NaturalResource.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/NaturalResource.java new file mode 100644 index 0000000..7929d13 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/NaturalResource.java @@ -0,0 +1,119 @@ +package com.deepclone.lw.cli.xmlimport.data.resources; + + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + + + +/** + * A Natural Resource + * + *

+ * This class represents naturally occurring resources. As well as extending {@link BasicResource} + * it contains the presence-probability, quantity, difficulty and recovery of a resource and a + * method to verify the data of these properties. + * + * @author T. Rosser + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "natural-resource" ) +public class NaturalResource + extends BasicResource +{ + /** + * The resource's presence-probability + * + *

+ * Used by the universe generator to decide whether to add this type of resource to a particular + * planet or not. + */ + @XStreamAlias( "presence-probability" ) + @XStreamAsAttribute + private Double presenceProbability; + + /** The resource's quantity */ + @XStreamAlias( "quantity" ) + private ResourceParameter quantity; + + /** + * The resource's difficulty + * + *

+ * The difficulty of extracting the resource. + */ + @XStreamAlias( "difficulty" ) + private ResourceParameter difficulty; + + /** + * The resource's recovery + * + *

+ * The recovery rate of the resource. + */ + @XStreamAlias( "recovery" ) + private ResourceParameter recovery; + + + /** + * Check a resource's data + * + *

+ * Make sure that a resource's properties are both present and valid. Calls the base class' + * verifyData() method to check all of the data. + */ + @Override + public void verifyData( ) + throws DataImportException + { + super.verifyData( ); + + if ( this.presenceProbability == null ) { + throw new DataImportException( "Missing presence-probability" ); + } + if ( this.quantity == null ) { + throw new DataImportException( "Missing quantity" ); + } + this.quantity.verifyData( "quantity" ); + + if ( this.difficulty == null ) { + throw new DataImportException( "Missing difficulty" ); + } + this.difficulty.verifyData( "difficulty" ); + + if ( this.recovery == null ) { + throw new DataImportException( "Missing recovery" ); + } + this.recovery.verifyData( "recovery" ); + + } + + + /** @return the resource's presence-probability */ + public Double getPresenceProbability( ) + { + return this.presenceProbability; + } + + + /** @return the resource's quantity */ + public ResourceParameter getQuantity( ) + { + return this.quantity; + } + + + /** @return the resource's difficulty */ + public ResourceParameter getDifficulty( ) + { + return this.difficulty; + } + + + /** @return the resource's recovery */ + public ResourceParameter getRecovery( ) + { + return this.recovery; + } +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/ResourceParameter.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/ResourceParameter.java new file mode 100644 index 0000000..6d7c6fe --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/ResourceParameter.java @@ -0,0 +1,65 @@ +package com.deepclone.lw.cli.xmlimport.data.resources; + + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.ImportableData; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + + + +/** + * Resource property parameter + * + *

+ * This class represents the average and deviation of various properties of a resource. + * + * @author T. Rosser + */ +@SuppressWarnings( "serial" ) +public class ResourceParameter + extends ImportableData +{ + /** The property's average */ + @XStreamAsAttribute + private Double average; + + /** The property's deviation */ + @XStreamAsAttribute + private Double deviation; + + + /** + * Check the property's data + * + *

+ * Make sure that a average and deviation are present. + * + * @param property + * the name of the property + */ + public void verifyData( String property ) + throws DataImportException + { + if ( this.average == null ) { + throw new DataImportException( "Missing average for " + property ); + } + if ( this.deviation == null ) { + throw new DataImportException( "Missing deviation for " + property ); + } + } + + + /** @return the property's average */ + public Double getAverage( ) + { + return this.average; + } + + + /** @return the property's deviation */ + public Double getDeviation( ) + { + return this.deviation; + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/Resources.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/Resources.java new file mode 100644 index 0000000..ad8b907 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/Resources.java @@ -0,0 +1,96 @@ +package com.deepclone.lw.cli.xmlimport.data.resources; + + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.ImportableData; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + + + +/** + * Resource Data + * + *

+ * This class represents the contents of the resource text file. It contains a list of resource + * definitions. + * + * @author T. Rosser + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "lw-resources" ) +public class Resources + extends ImportableData + implements Iterable< BasicResource > +{ + /** All present resource definitions */ + @XStreamImplicit + private final List< BasicResource > resources = new LinkedList< BasicResource >( ); + + + /** + * Checks the resource data + * + *

+ * Checks each definition and ensures they are all unique. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.resources == null ) { + throw new DataImportException( "No resource definitions" ); + } + + HashSet< String > names = new HashSet< String >( ); + for ( BasicResource resource : this.resources ) { + resource.verifyData( ); + this.checkUniqueItem( names , resource.getName( ) ); + } + } + + + /** + * Checks that the name of the resource is unique + * + *

+ * This helper method is used by {@link #verifyData()} to make sure resource names are unique. + * + * @param existing + * the existing set of items + * @param value + * the item's value + * + * @throws DataImportException + * if the item's value is already present in the set of existing items + */ + public void checkUniqueItem( HashSet< String > existing , String value ) + throws DataImportException + { + if ( existing.contains( value ) ) { + throw new DataImportException( "Duplicate resource name '" + value + "'" ); + } + existing.add( value ); + } + + + /** + * Resource definition iterator + * + *

+ * Grant access to the list of resources in read-only mode. + * + * @return a read-only iterator on the list of resources. + */ + @Override + public Iterator< BasicResource > iterator( ) + { + return Collections.unmodifiableList( this.resources ).iterator( ); + } +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/UOCResourceErrorCode.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/UOCResourceErrorCode.java new file mode 100644 index 0000000..47dbed7 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/resources/UOCResourceErrorCode.java @@ -0,0 +1,20 @@ +package com.deepclone.lw.cli.xmlimport.data.resources; + +import com.deepclone.lw.cli.ImportResources; + + +/** + * Enum representing the error codes returned by the uoc_* stored procedures in + * {@link ImportResources} + * + * @author T. Rosser + * + */ +public enum UOCResourceErrorCode { + CREATED , + UPDATED , + BAD_TYPE , + BAD_STRINGS , + BAD_VALUE , + DUP_DESCR; +} diff --git a/legacyworlds-server-tests/TestFiles/resource-loader/bad-contents.xml b/legacyworlds-server-tests/TestFiles/resource-loader/bad-contents.xml new file mode 100644 index 0000000..262c241 --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/resource-loader/bad-contents.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/resource-loader/bad-data.xml b/legacyworlds-server-tests/TestFiles/resource-loader/bad-data.xml new file mode 100644 index 0000000..2209c73 --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/resource-loader/bad-data.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/legacyworlds-server-tests/TestFiles/resource-loader/bad-xml.xml b/legacyworlds-server-tests/TestFiles/resource-loader/bad-xml.xml new file mode 100644 index 0000000..ee1d0ed --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/resource-loader/bad-xml.xml @@ -0,0 +1,2 @@ +This is not an XML file, obviously. +We'll make that even more confusing: <<<<<< & >>!!! \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/resource-loader/good-data.xml b/legacyworlds-server-tests/TestFiles/resource-loader/good-data.xml new file mode 100644 index 0000000..77a3e62 --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/resource-loader/good-data.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestResourceLoader.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestResourceLoader.java new file mode 100644 index 0000000..d49210d --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestResourceLoader.java @@ -0,0 +1,155 @@ +package com.deepclone.lw.cli.xmlimport; + + +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 java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.resources.BasicResource; +import com.deepclone.lw.cli.xmlimport.data.resources.NaturalResource; +import com.deepclone.lw.cli.xmlimport.data.resources.Resources; +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.io.StreamException; + + + +/** + * Unit tests for the {@link ResourceLoader} class. + * + * @author T. Rosser + * + */ +public class TestResourceLoader +{ + /** + * Try initialising the loader with a null file instance. + * + * @throws NullPointerException + * when the constructor is executed + */ + @Test( expected = NullPointerException.class ) + public void testNullFile( ) + throws NullPointerException + { + new ResourceLoader( null ); + } + + + /** Try loading a file that does not exist */ + @Test + public void testMissingFile( ) + { + ResourceLoader loader = new ResourceLoader( new File( "does-not-exist" ) ); + try { + loader.load( ); + } catch ( DataImportException e ) { + assertTrue( "cause is a" + e.getCause( ).getClass( ).getName( ) , e.getCause( ) instanceof IOException ); + return; + } + fail( "no exception after trying to load a missing file" ); + } + + + /** Try loading a file that contains something that is not XML. */ + @Test + public void testBadXML( ) + { + ResourceLoader loader = new ResourceLoader( new File( "TestFiles/resource-loader/bad-xml.xml" ) ); + try { + loader.load( ); + } catch ( DataImportException e ) { + assertTrue( "cause is a " + e.getCause( ).getClass( ).getName( ) , e.getCause( ) instanceof StreamException ); + return; + } + fail( "no exception after loading stuff that isn't XML" ); + } + + + /** + * Test loading a file that contains XML but which cannot be deserialised to an + * {@link Resources} instance. + */ + @Test + public void testBadContents( ) + { + ResourceLoader loader = new ResourceLoader( new File( "TestFiles/resource-loader/bad-contents.xml" ) ); + try { + loader.load( ); + } catch ( DataImportException e ) { + assertTrue( "cause is a " + e.getCause( ).getClass( ).getName( ) , + e.getCause( ) instanceof ConversionException ); + return; + } + fail( "no exception after loading bad XML" ); + } + + + /** + * Try loading a file that contains valid XML for a {@link ResourceLoader} instance with + * semantic errors. + */ + @Test + public void testBadData( ) + { + ResourceLoader loader = new ResourceLoader( new File( "TestFiles/resource-loader/bad-data.xml" ) ); + try { + loader.load( ); + } catch ( DataImportException e ) { + assertNull( e.getCause( ) ); + return; + } + fail( "no exception after loading bad data" ); + } + + + /** Try loading valid data, make sure that it contains exactly what is expected */ + @Test + public void testGoodData( ) + { + ResourceLoader loader = new ResourceLoader( new File( "TestFiles/resource-loader/good-data.xml" ) ); + Resources resources; + try { + resources = loader.load( ); + } catch ( DataImportException e ) { + fail( "could not load valid file" ); + return; + } + assertNotNull( resources ); + + // Not sure if this is the best way to code for the two different resource types... + int rCount = 0; + + for ( BasicResource br : resources ) { + if ( rCount == 0 ) { + assertEquals( "money" , br.getName( ) ); + assertEquals( "moneyDescription" , br.getDescription( ) ); + assertEquals( new Integer( 0 ) , br.getWeight( ) ); + assertEquals( null , br.getCategory( ) ); + } else if ( rCount == 1 ) { + // This isn't retarded is it? + NaturalResource nr = (NaturalResource) br; + assertEquals( "titanium" , nr.getName( ) ); + assertEquals( "titaniumDescription" , nr.getDescription( ) ); + assertEquals( new Integer( 1 ) , nr.getWeight( ) ); + assertEquals( "minerals" , nr.getCategory( ) ); + assertEquals( new Double( 0.8 ) , nr.getPresenceProbability( ) ); + assertEquals( new Double( 5000 ) , nr.getQuantity( ).getAverage( ) ); + assertEquals( new Double( 1500 ) , nr.getQuantity( ).getDeviation( ) ); + assertEquals( new Double( 0.1 ) , nr.getDifficulty( ).getAverage( ) ); + assertEquals( new Double( 0.05 ) , nr.getDifficulty( ).getDeviation( ) ); + assertEquals( new Double( 0.4 ) , nr.getRecovery( ).getAverage( ) ); + assertEquals( new Double( 0.05 ) , nr.getRecovery( ).getDeviation( ) ); + } + rCount++; + } + assertEquals( 2 , rCount ); + } +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/BaseTest.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/BaseTest.java new file mode 100644 index 0000000..74a88d2 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/BaseTest.java @@ -0,0 +1,267 @@ +package com.deepclone.lw.cli.xmlimport.resources; + + +import com.deepclone.lw.cli.xmlimport.data.ImportableData; +import com.deepclone.lw.cli.xmlimport.data.resources.BasicResource; +import com.deepclone.lw.cli.xmlimport.data.resources.NaturalResource; +import com.deepclone.lw.cli.xmlimport.data.resources.ResourceParameter; +import com.deepclone.lw.cli.xmlimport.data.resources.Resources; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.XStreamException; + + + +/** + * Base class for testing resource importation + * + *

+ * This class is used as a parent for tests of the resource import structures. It includes the code + * used to actually create these resources, as this can normally only be done through XStream. + * + * @author T. Rosser + * + */ + +abstract public class BaseTest +{ + /** + * Escape &, < and > in XML strings + * + * @param string + * the string to escape + * + * @return the escaped string + */ + private String xmlString( String string ) + { + return string.replace( "&" , "&" ).replace( "<" , "<" ).replace( ">" , ">" ); + } + + + /** + * Escape &, < and >, ' and " in XML strings + * + * @param string + * the string to escape + * + * @return the escaped string + */ + private String quoteString( String string ) + { + return "\"" + this.xmlString( string ).replace( "\"" , """ ).replace( "'" , "'" ) + "\""; + } + + + /** + * Create a {@link BasicResource} definition. + * + *

+ * Generate the XML code that corresponds to a basic resource definition. + * + * @param name + * The resources name, or null to omit the name. + * @param description + * The resources description, or null to omit the description. + * @param weight + * The resources weight, or null to omit the weight. + * @param category + * The resources category, or null to omit the category. + * + * @return the XML code corresponding to the resource definition. + */ + protected String createBasicResourceDefinition( String name , String description , String weight , String category ) + { + StringBuilder str = new StringBuilder( ); + + str.append( "" ); + + return str.toString( ); + } + + + /** + * Create the a {@link NaturalResource} definition. + * + *

+ * Create the XML code that corresponds to a natural resource definition. + * + * @param name + * The resources name, or null to omit the name. + * @param description + * The resources description, or null to omit the description. + * @param weight + * The resources weight, or null to omit the weight. + * @param category + * The resources category, or null to omit the category. + * @param probability + * The resources presence-probability, or null to omit the probability. + * @param quantity + * The XML code representing the {@link ResourceParameter} giving the resources + * quantity average and deviation or null to omit the quantity. + * @param difficulty + * The XML code representing the {@link ResourceParameter} giving the resources + * difficulty average and deviation or null to omit the difficulty. + * @param recovery + * The XML code representing the {@link ResourceParameter} giving the resources + * recovery average and deviation or null to omit the recovery. + * + * @return the XML code corresponding to the natural resource definition + */ + protected String createNaturalResourceDefinition( String name , String description , String weight , + String category , String probability , String quantity , String difficulty , String recovery ) + { + StringBuilder str = new StringBuilder( ); + + str.append( "" ); + + if ( quantity != null ) { + str.append( quantity ); + } + if ( difficulty != null ) { + str.append( difficulty ); + } + if ( recovery != null ) { + str.append( recovery ); + } + + str.append( "" ); + + return str.toString( ); + } + + + /** + * Create a {@link ResourceParameter} definition + * + *

+ * Generates the XML code the corresponds to a resource parameter definition. + * + * @param type + * Gives the type of resource parameter to create (quantity/difficulty/recovery) + * @param average + * The parameters average. + * @param deviation + * The parameters deviation. + * + * @return the XML code corresponding to the resource parameter definition + */ + protected String createResourceParameterDefinition( String type , String average , String deviation ) + { + StringBuilder str = new StringBuilder( ); + + str.append( "<" ); + + if ( type != null ) { + str.append( type ); + } + if ( average != null ) { + str.append( " average=" ).append( this.quoteString( average ) ); + } + if ( deviation != null ) { + str.append( " deviation=" ).append( this.quoteString( deviation ) ); + } + + str.append( " />" ); + + return str.toString( ); + } + + + /** + * Creates the XML code for a top-level resource definition element + * + * @param resources + * XML definitions of resources + * + * @return the top-level elements XML code + */ + protected String createTopLevel( String... resources ) + { + StringBuilder str = new StringBuilder( ); + str.append( "" ); + for ( String resource : resources ) { + str.append( resource ); + } + str.append( "" ); + return str.toString( ); + } + + + /** + * Create the necessary XStream instance + * + *

+ * Initialise an XStream instance and set it up so it can process resource data definitions. + * Unlike {@link ResourceLoader}, this version also registers multiple aliases for + * ResourceParameter. This is because ResourceParameter is usually only used inside a + * NaturalResource, but when testing may need to be created independently. + */ + private XStream createXStreamInstance( ) + { + XStream xstream = new XStream( ); + xstream.processAnnotations( Resources.class ); + xstream.processAnnotations( NaturalResource.class ); + xstream.alias( "quantity" , ResourceParameter.class ); + xstream.alias( "difficulty" , ResourceParameter.class ); + xstream.alias( "recovery" , ResourceParameter.class ); + xstream.alias( "test" , ResourceParameter.class ); // Just for unit tests + xstream.alias( "lw-resources" , Resources.class ); + + return xstream; + } + + + /** + * Create a resource object from its XML code + * + * @param xml + * the XML code + * @param cls + * the class of the object + * + * @return the object that was created from the code + * + * @throws ClassCastException + * if the code corresponds to some other type of object + * @throws XStreamException + * if some error occurred while deserialising the object + */ + protected < T extends ImportableData > T createObject( String xml , Class< T > cls ) + throws ClassCastException , XStreamException + { + return cls.cast( this.createXStreamInstance( ).fromXML( xml ) ); + } +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/TestBasicResource.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/TestBasicResource.java new file mode 100644 index 0000000..a6bb6b3 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/resources/TestBasicResource.java @@ -0,0 +1,203 @@ +package com.deepclone.lw.cli.xmlimport.resources; + + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.resources.BasicResource; +import com.thoughtworks.xstream.converters.ConversionException; + + + +/** + * Unit tests for {@link BasicResource}. + * + * @author