diff --git a/build/post-build.d/20-import-tools.sh b/build/post-build.d/20-import-tools.sh
index 2b20f04..de69c46 100644
--- a/build/post-build.d/20-import-tools.sh
+++ b/build/post-build.d/20-import-tools.sh
@@ -33,6 +33,7 @@ EOF
java legacyworlds-server-main-*.jar --run-tool ImportText data/i18n-text.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportResources data/resources.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportTechs data/techs.xml || exit 1
+java legacyworlds-server-main-*.jar --run-tool ImportTechGraph data/tech-graph.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportBuildables data/buildables.xml || exit 1
java legacyworlds-server-main-*.jar &
diff --git a/legacyworlds-server-data/db-structure/parts/040-functions/030-tech.sql b/legacyworlds-server-data/db-structure/parts/040-functions/030-tech.sql
index 5b2e90b..2a1f4be 100644
--- a/legacyworlds-server-data/db-structure/parts/040-functions/030-tech.sql
+++ b/legacyworlds-server-data/db-structure/parts/040-functions/030-tech.sql
@@ -324,6 +324,47 @@ GRANT EXECUTE
+/*
+ * Remove all dependencies for a technology
+ * -----------------------------------------
+ *
+ * This stored procedure removes all dependencies and reverse dependencies for
+ * some technology.
+ *
+ * Parameters:
+ * _technology The name of the technology
+ */
+DROP FUNCTION IF EXISTS defs.techdep_clear( TEXT );
+CREATE FUNCTION defs.techdep_clear( _technology TEXT )
+ RETURNS VOID
+ LANGUAGE SQL
+ STRICT VOLATILE
+ SECURITY DEFINER
+AS $techdep_clear$
+
+ DELETE FROM defs.technology_dependencies
+ WHERE technology_name_id = (
+ SELECT id FROM defs.strings
+ WHERE name = $1
+ );
+
+ DELETE FROM defs.technology_dependencies
+ WHERE technology_name_id_depends = (
+ SELECT id FROM defs.strings
+ WHERE name = $1
+ );
+
+$techdep_clear$;
+
+REVOKE EXECUTE
+ ON FUNCTION defs.techdep_clear( TEXT )
+ FROM PUBLIC;
+GRANT EXECUTE
+ ON FUNCTION defs.techdep_clear( TEXT )
+ TO :dbuser;
+
+
+
-- ********************************************************
-- OLD CODE BELOW
-- ********************************************************
diff --git a/legacyworlds-server-data/db-structure/tests/admin/040-functions/030-tech/060-techdep-clear.sql b/legacyworlds-server-data/db-structure/tests/admin/040-functions/030-tech/060-techdep-clear.sql
new file mode 100644
index 0000000..ef2dacc
--- /dev/null
+++ b/legacyworlds-server-data/db-structure/tests/admin/040-functions/030-tech/060-techdep-clear.sql
@@ -0,0 +1,58 @@
+/*
+ * Test the defs.techdep_clear() function
+ */
+BEGIN;
+ \i utils/strings.sql
+ -- Make the columns we don't use in the technology definition table NULL-able
+ ALTER TABLE defs.technologies
+ ALTER technology_category_id DROP NOT NULL ,
+ ALTER technology_discovery_id DROP NOT NULL ,
+ ALTER technology_description_id DROP NOT NULL ,
+ ALTER technology_price DROP NOT NULL ,
+ ALTER technology_points DROP NOT NULL;
+
+ -- Create strings to use as the technologies' names
+ SELECT _create_test_strings( 4 , 'tech' );
+
+ -- Insert the technologies
+ INSERT INTO defs.technologies ( technology_name_id )
+ VALUES ( _get_string( 'tech1' ) ) ,
+ ( _get_string( 'tech2' ) ) ,
+ ( _get_string( 'tech3' ) ) ,
+ ( _get_string( 'tech4' ) );
+
+ -- Add a chain of dependencies
+ INSERT INTO defs.technology_dependencies(
+ technology_name_id , technology_name_id_depends
+ ) VALUES ( _get_string( 'tech2' ) , _get_string( 'tech1' ) ) ,
+ ( _get_string( 'tech3' ) , _get_string( 'tech2' ) ) ,
+ ( _get_string( 'tech4' ) , _get_string( 'tech3' ) );
+
+
+ -- ***** TESTS BEGIN HERE *****
+ SELECT plan( 4 );
+
+ SELECT diag_test_name( 'defs.techdep_clear() - No failure on invalid technology name' );
+ SELECT lives_ok( $$
+ SELECT defs.techdep_clear( 'does not exist' )
+ $$ );
+
+ SELECT diag_test_name( 'defs.techdep_clear() - Successful call' );
+ SELECT lives_ok( $$
+ SELECT defs.techdep_clear( 'tech2' )
+ $$ );
+
+ SELECT diag_test_name( 'defs.techdep_clear() - Cleared technology has no dependencies' );
+ SELECT is_empty( $$
+ SELECT * FROM defs.technology_dependencies
+ WHERE technology_name_id = _get_string( 'tech2' );
+ $$ );
+
+ SELECT diag_test_name( 'defs.techdep_clear() - Cleared technology has no reverse dependencies' );
+ SELECT is_empty( $$
+ SELECT * FROM defs.technology_dependencies
+ WHERE technology_name_id_depends = _get_string( 'tech2' );
+ $$ );
+
+ SELECT * FROM finish( );
+ROLLBACK;
\ No newline at end of file
diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/030-tech/040-techdep-clear.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/030-tech/040-techdep-clear.sql
new file mode 100644
index 0000000..fb9f70b
--- /dev/null
+++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/030-tech/040-techdep-clear.sql
@@ -0,0 +1,13 @@
+/*
+ * Test privileges on defs.techdep_remove()
+ */
+BEGIN;
+ SELECT plan( 1 );
+
+ SELECT diag_test_name( 'defs.techdep_clear() - EXECUTE privilege' );
+ SELECT lives_ok( $$
+ SELECT defs.techdep_clear( '' );
+ $$ );
+
+ SELECT * FROM finish( );
+ROLLBACK;
\ No newline at end of file
diff --git a/legacyworlds-server-main/data/tech-graph.xml b/legacyworlds-server-main/data/tech-graph.xml
new file mode 100644
index 0000000..4f54016
--- /dev/null
+++ b/legacyworlds-server-main/data/tech-graph.xml
@@ -0,0 +1,42 @@
+
+
+ * This class defines the body of the technology graph import tool. It loads the data file specified + * on the command line, validates it and then loads the technology definitions and dependencies into + * the database. + * + * @author E. Benoît + */ +public class ImportTechGraph + extends CLITool +{ + /** Logging system */ + private final Logger logger = Logger.getLogger( ImportTechGraph.class ); + + /** File to read the definitions from */ + private File file; + + /** Spring transaction template */ + private TransactionTemplate tTemplate; + + /** Technology creation or update stored procedure */ + private StoredProc fUocTechnology; + + /** Stored procedure that removes all dependencies from a technology */ + private StoredProc fTechdepClear; + + /** Stored procedure that adds a dependency to a technology */ + private StoredProc fTechdepAdd; + + + /** + * 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/transactions.xml" + } , true , ctx ); + } + + + /** + * Create database access templates + * + *
+ * Initialise the transaction template and the stored procedure wrappers. + * + * @param ctx + * the Spring application context + */ + private void createTemplates( ApplicationContext ctx ) + { + DataSource dSource = ctx.getBean( DataSource.class ); + PlatformTransactionManager tManager = ctx.getBean( PlatformTransactionManager.class ); + + this.fUocTechnology = new StoredProc( dSource , "defs" , "uoc_technology" ) + .addParameter( "_name" , java.sql.Types.VARCHAR ).addParameter( "_category" , java.sql.Types.VARCHAR ) + .addParameter( "_discovery" , java.sql.Types.VARCHAR ) + .addParameter( "_description" , java.sql.Types.VARCHAR ) + .addParameter( "_price" , java.sql.Types.BIGINT ).addParameter( "_points" , java.sql.Types.BIGINT ) + .addOutput( "_result" , java.sql.Types.OTHER ); + + this.fTechdepClear = new StoredProc( dSource , "defs" , "techdep_clear" ).addParameter( "_tech" , + java.sql.Types.VARCHAR ); + + this.fTechdepAdd = new StoredProc( dSource , "defs" , "techdep_add" ) + .addParameter( "_dependent" , java.sql.Types.VARCHAR ) + .addParameter( "_dependency" , java.sql.Types.VARCHAR ).addOutput( "_result" , java.sql.Types.OTHER ); + + this.tTemplate = new TransactionTemplate( tManager ); + } + + + /** + * Import all technology definitions + * + *
+ * Import all technology definitions from the top-level {@link Technologies}, creating a map of + * dependencies. Once all definitions have been imported, add all dependencies. + * + * @param data + * the top level {@link Technologies} data instance + * + * @throws DataImportException + * when some technology definition or dependency cannot be imported + */ + private void importTechnologies( Technologies data ) + throws DataImportException + { + Map< String , List< String > > dependencies = new HashMap< String , List< String > >( ); + for ( Technology technology : data ) { + this.importTechnologyDefinition( technology ); + dependencies.put( technology.getName( ) , technology.getDependencies( ) ); + } + + for ( Map.Entry< String , List< String > > techDeps : dependencies.entrySet( ) ) { + for ( String dependency : techDeps.getValue( ) ) { + this.importDependency( techDeps.getKey( ) , dependency ); + } + } + } + + + /** + * Import a technology definition + * + *
+ * Import the definition, and if there was already a definition by that name, clear its
+ * dependencies.
+ *
+ * @param technology
+ * the definition to import
+ * @throws DataImportException
+ * when the update or creation stored procedure returns anything other than
+ * CREATED
or UPDATED
+ */
+ private void importTechnologyDefinition( Technology technology )
+ throws DataImportException
+ {
+ this.logger.info( "Importing technology '" + technology.getName( ) + "'" );
+
+ TechnologyDefinitionResult result = this.getResultOf( this.fUocTechnology.execute( technology.getName( ) ,
+ technology.getCategory( ) , technology.getDiscovery( ) , technology.getDescription( ) ,
+ technology.getCost( ) , technology.getPoints( ) ) );
+ if ( result == TechnologyDefinitionResult.UPDATED ) {
+ this.fTechdepClear.execute( technology.getName( ) );
+ } else if ( result != TechnologyDefinitionResult.CREATED ) {
+ throw new DataImportException( "Error while importing technology '" + technology.getName( ) + "': "
+ + result );
+ }
+ }
+
+
+ /**
+ * Import a dependency between two technologies
+ *
+ *
+ * Attempt to add a dependency from a technology to another. + * + * @param dependent + * the dependent technology + * @param dependency + * the technology to depend on + * + * @throws DataImportException + * if dependency creation fails + */ + private void importDependency( String dependent , String dependency ) + throws DataImportException + { + this.logger.info( "Importing dependency from " + dependent + " to " + dependency ); + + TechnologyDefinitionResult result = this.getResultOf( this.fTechdepAdd.execute( dependent , dependency ) ); + switch ( result ) { + case CREATED: + return; + case REDUNDANT: + throw new DataImportException( "( " + dependent + " -> " + dependency + " ) is redundant" ); + case BAD_STRINGS: + throw new DataImportException( "( " + dependent + " -> " + dependency + " ): undefined technology" ); + case CYCLE: + throw new DataImportException( "( " + dependent + " -> " + dependency + " ) would create a cycle" ); + default: + throw new DataImportException( "( " + dependent + " -> " + dependency + " ): unexpected error code " + + result ); + } + } + + + /** + * Helper method for stored procedure results + * + *
+ * This method converts the map returned by one of the definition stored procedures into a + * {@link TechnologyDefinitionResult} value. + * + * @param spResult + * the return value of {@link StoredProc#execute(Object...)} + * + * @return the converted value of the _result field + */ + private TechnologyDefinitionResult getResultOf( Map< String , Object > spResult ) + { + return TechnologyDefinitionResult.valueOf( ( (PGobject) spResult.get( "_result" ) ).getValue( ) ); + } + + + /** + * Run the technology definitions import tool + * + *
+ * Loads the data file, the connects to the database and creates or updates all definitions. + */ + @Override + public void run( ) + { + Technologies data; + try { + data = new TechnologyLoader( this.file ).load( ); + } catch ( DataImportException e ) { + this.logger.error( "Error while loading technology definitions" , e ); + return; + } + + AbstractApplicationContext ctx = this.createContext( ); + this.createTemplates( ctx ); + this.executeImportTransaction( data ); + ToolBase.destroyContext( ctx ); + } + + + /** + * Execute the technology definitions importation transaction + * + *
+ * Run a transaction and execute the importation code inside it. Roll back if anything goes + * wrong. + * + * @param data + * the {@link Technologies} definitions instance + */ + private void executeImportTransaction( final Technologies data ) + { + boolean rv = this.tTemplate.execute( new TransactionCallback< Boolean >( ) { + @Override + public Boolean doInTransaction( TransactionStatus status ) + { + boolean rv = ImportTechGraph.this.doTransaction( data ); + if ( !rv ) { + status.setRollbackOnly( ); + } + return rv; + } + } ); + if ( rv ) { + this.logger.info( "Technology import successful" ); + } + } + + + /** + * Import transaction body + * + *
+ * Import all definitions and handle exceptions. + * + * @param data + * the {@link Technologies} definitions instance + * @return + */ + private boolean doTransaction( Technologies data ) + { + try { + this.importTechnologies( data ); + return true; + } catch ( RuntimeException e ) { + this.logger.error( "Caught runtime exception" , e ); + } catch ( DataImportException e ) { + this.logger.error( e.getMessage( ) ); + } + 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/xmlimport/TechnologyLoader.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/TechnologyLoader.java new file mode 100644 index 0000000..275a7cf --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/TechnologyLoader.java @@ -0,0 +1,116 @@ +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.techs.Technologies; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.XStreamException; + + + +/** + * Technology Loader + * + *
+ * This class can be used to load all technology definitions. It extracts them from the XML file and + * verifies them. + * + * @author E. Benoît + */ +public class TechnologyLoader +{ + /** The file to read the XML from */ + private final File file; + + + /** + * Initialise the loader + * + * @param file + * the XML file that contains the definitions + */ + public TechnologyLoader( File file ) + { + this.file = file.getAbsoluteFile( ); + } + + + /** + * Initialise the necessary XStream instance + * + *
+ * Initialise the XStream instance by processing the annotations of all the technology + * importable data classes. + * + * @return the XStream instance to use when loading the data + */ + private XStream initXStream( ) + { + XStream xstream = new XStream( ); + xstream.processAnnotations( Technologies.class ); + return xstream; + } + + + /** + * Load the technology 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 Technologies loadXMLFile( ) + throws DataImportException + { + FileInputStream fis; + try { + fis = new FileInputStream( this.file ); + } catch ( FileNotFoundException e ) { + throw new DataImportException( "Unable to load technology definitions" , e ); + } + + try { + try { + XStream xstream = this.initXStream( ); + return (Technologies) xstream.fromXML( fis ); + } finally { + fis.close( ); + } + } catch ( IOException e ) { + throw new DataImportException( "Input error while loading technology definitions" , e ); + } catch ( XStreamException e ) { + throw new DataImportException( "XML error while loading technology definitions" , e ); + } + } + + + /** + * Load and process technology definitions + * + *
+ * Attempt to load all technology 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 Technologies load( ) + throws DataImportException + { + Technologies techs = this.loadXMLFile( ); + techs.verifyData( ); + techs.setReadFrom( this.file ); + return techs; + } +} 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 index 551ff47..86a8442 100644 --- 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 @@ -64,9 +64,9 @@ public class BasicResource throw new DataImportException( "Missing name string" ); } if ( this.description == null || "".equals( this.description.trim( ) ) ) { - throw new DataImportException( "Missing name string" ); + throw new DataImportException( "Missing description string" ); } - if ( this.category != null && "".equals( this.description.trim( ) ) ) { + if ( this.category != null && "".equals( this.category.trim( ) ) ) { throw new DataImportException( "Category invalid" ); } if ( this.weight == null ) { diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technologies.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technologies.java new file mode 100644 index 0000000..842f64c --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technologies.java @@ -0,0 +1,98 @@ +package com.deepclone.lw.cli.xmlimport.data.techs; + + +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; + + + +/** + * Technology definitions data + * + *
+ * This class represents the contents of a technology definition XML file. It contains a list of + * technology definitions. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "lw-tech-graph" ) +public class Technologies + extends ImportableData + implements Iterable< Technology > +{ + /** All present technology definitions */ + @XStreamImplicit( itemFieldName = "technology" ) + private final List< Technology > technologies = new LinkedList< Technology >( ); + + + /** + * Check the technology data + * + *
+ * Check whether there is a list of technologies, then check each definition to make sure that + * it is valid and that its name is unique. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.technologies == null ) { + throw new DataImportException( "No technology definitions" ); + } + + HashSet< String > names = new HashSet< String >( ); + for ( Technology tech : this.technologies ) { + tech.verifyData( ); + this.checkUniqueItem( names , tech.getName( ) ); + } + } + + + /** + * Checks that the name of a technology is unique + * + *
+ * This helper method is used by {@link #verifyData()} to make sure technology names are unique. + * + * @param existing + * the existing set of names + * @param value + * the name to check + * + * @throws DataImportException + * if the name is already present in the set of existing names + */ + public void checkUniqueItem( HashSet< String > existing , String value ) + throws DataImportException + { + if ( existing.contains( value ) ) { + throw new DataImportException( "Duplicate technology name '" + value + "'" ); + } + existing.add( value ); + } + + + /** + * Technology definition iterator + * + *
+ * Grant access to the list of technologies in read-only mode. + * + * @return a read-only iterator on the list of technologies. + */ + @Override + public Iterator< Technology > iterator( ) + { + return Collections.unmodifiableList( this.technologies ).iterator( ); + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technology.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technology.java new file mode 100644 index 0000000..49b6368 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/Technology.java @@ -0,0 +1,183 @@ +package com.deepclone.lw.cli.xmlimport.data.techs; + + +import java.util.Collections; +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.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + + + +/** + * A technology definition + * + *
+ * This class represents a technology definition that can be imported from the data file. It + * contains all required technology attributes, as well as a list of strings for the dependencies. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public class Technology + extends ImportableData +{ + + /** Identifier of the name string */ + @XStreamAsAttribute + private String name; + + /** Identifier of the category string */ + @XStreamAsAttribute + private String category; + + /** Identifier of the description string */ + @XStreamAsAttribute + private String description; + + /** Identifier of the string to display on technology discovery */ + @XStreamAsAttribute + private String discovery; + + /** Monetary cost of the technology */ + @XStreamAsAttribute + private Long cost; + + /** Required amount of research points */ + @XStreamAsAttribute + private Long points; + + /** List of technology name string identifiers describing the dependencies */ + @XStreamImplicit( itemFieldName = "depends-on" ) + private final List< String > dependencies = new LinkedList< String >( ); + + + /** + * Technology definition verifier + * + *
+ * Make sure that all technology definition fields are both present and valid. If dependencies + * exist, also verify the names' validity. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.name == null || "".equals( this.name.trim( ) ) ) { + throw new DataImportException( "Missing name string" ); + } + if ( this.category == null || "".equals( this.category.trim( ) ) ) { + throw new DataImportException( "Missing category string in " + this.name ); + } + if ( this.description == null || "".equals( this.description.trim( ) ) ) { + throw new DataImportException( "Missing description string in " + this.name ); + } + if ( this.discovery == null || "".equals( this.discovery.trim( ) ) ) { + throw new DataImportException( "Missing discovery string in " + this.name ); + } + + if ( this.points == null ) { + throw new DataImportException( "Missing research points in " + this.name ); + } else if ( this.points <= 0 ) { + throw new DataImportException( "Invalid research points in " + this.name ); + } + + if ( this.cost == null ) { + throw new DataImportException( "Missing cost in " + this.name ); + } else if ( this.cost <= 0 ) { + throw new DataImportException( "Invalid cost in " + this.name ); + } + + if ( this.dependencies != null ) { + for ( String dep : this.dependencies ) { + if ( dep == null || "".equals( dep.trim( ) ) ) { + throw new DataImportException( "Empty dependency name in " + this.name ); + } + } + } + } + + + /** + * Gets the identifier of the name string. + * + * @return the identifier of the name string + */ + public String getName( ) + { + return this.name; + } + + + /** + * Gets the identifier of the category string. + * + * @return the identifier of the category string + */ + public String getCategory( ) + { + return this.category; + } + + + /** + * Gets the identifier of the description string. + * + * @return the identifier of the description string + */ + public String getDescription( ) + { + return this.description; + } + + + /** + * Gets the identifier of the string to display on technology discovery. + * + * @return the identifier of the string to display on technology discovery + */ + public String getDiscovery( ) + { + return this.discovery; + } + + + /** + * Gets the monetary cost of the technology. + * + * @return the monetary cost of the technology + */ + public Long getCost( ) + { + return this.cost; + } + + + /** + * Gets the required amount of research points. + * + * @return the required amount of research points + */ + public Long getPoints( ) + { + return this.points; + } + + + /** + * Gets the list of technology name string identifiers describing the dependencies. + * + * @return the list of technology name string identifiers describing the dependencies + */ + public List< String > getDependencies( ) + { + if ( this.dependencies == null ) { + return Collections.emptyList( ); + } + return Collections.unmodifiableList( this.dependencies ); + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/TechnologyDefinitionResult.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/TechnologyDefinitionResult.java new file mode 100644 index 0000000..e7491bb --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/techs/TechnologyDefinitionResult.java @@ -0,0 +1,47 @@ +package com.deepclone.lw.cli.xmlimport.data.techs; + + +/** + * Return codes for technology definition manipulation stored procedures + * + *
+ * This enumeration represents the various values which can be returned by the stored procedures
+ * which manipulate technology definitions and technology dependencies.
+ *
+ * @author E. Benoît
+ */
+public enum TechnologyDefinitionResult {
+
+ /** The technology definition or dependency was created */
+ CREATED ,
+
+ /** The technology definition was updated */
+ UPDATED ,
+
+ /** The dependency was deleted */
+ DELETED ,
+
+ /** The specified dependency does not exist */
+ MISSING ,
+
+ /** One (or more) of the numeric parameters is invalid */
+ BAD_VALUE ,
+
+ /**
+ * The name, description, discovery or category string identifiers were not valid string
+ * identifiers.
+ */
+ BAD_STRINGS ,
+
+ /**
+ * The specified description and/or discovery string was in use by another technology.
+ */
+ DUP_STRING ,
+
+ /** The dependency would cause a cycle */
+ CYCLE ,
+
+ /** The dependency would be redundant */
+ REDUNDANT
+
+}
diff --git a/legacyworlds-server-tests/TestFiles/technology-loader/bad-contents.xml b/legacyworlds-server-tests/TestFiles/technology-loader/bad-contents.xml
new file mode 100644
index 0000000..272dba0
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/technology-loader/bad-contents.xml
@@ -0,0 +1,6 @@
+
+null
file instance.
+ *
+ * @throws NullPointerException
+ * when the constructor is executed
+ */
+ @Test( expected = NullPointerException.class )
+ public void testNullFile( )
+ throws NullPointerException
+ {
+ new TechnologyLoader( null );
+ }
+
+
+ /** Try loading a file that does not exist */
+ @Test
+ public void testMissingFile( )
+ {
+ TechnologyLoader loader = new TechnologyLoader( 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( )
+ {
+ TechnologyLoader loader = new TechnologyLoader( new File( "TestFiles/technology-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 a
+ * {@link Technologies} instance.
+ */
+ @Test
+ public void testBadContents( )
+ {
+ TechnologyLoader loader = new TechnologyLoader( new File( "TestFiles/technology-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 Technologies} instance with semantic
+ * errors.
+ */
+ @Test
+ public void testBadData( )
+ {
+ TechnologyLoader loader = new TechnologyLoader( new File( "TestFiles/technology-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 one record */
+ @Test
+ public void testGoodData( )
+ {
+ TechnologyLoader loader = new TechnologyLoader( new File( "TestFiles/technology-loader/good-data.xml" ) );
+ Technologies technologies;
+ try {
+ technologies = loader.load( );
+ } catch ( DataImportException e ) {
+ fail( "could not load valid file" );
+ return;
+ }
+ assertNotNull( technologies );
+
+ int rCount = 0;
+ for ( @SuppressWarnings( "unused" )
+ Technology tech : technologies ) {
+ rCount++;
+ }
+ assertEquals( 1 , rCount );
+ }
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/resources/BaseTest.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/resources/BaseTest.java
index d578d51..9f2eb11 100644
--- a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/resources/BaseTest.java
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/resources/BaseTest.java
@@ -22,7 +22,7 @@ import com.thoughtworks.xstream.XStreamException;
*
*/
-abstract public class BaseTest
+abstract class BaseTest
{
/**
* Escape &, < and > in XML strings
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/BaseTest.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/BaseTest.java
new file mode 100644
index 0000000..2d70331
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/BaseTest.java
@@ -0,0 +1,175 @@
+package com.deepclone.lw.cli.xmlimport.data.techs;
+
+
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
+
+
+
+/**
+ * Base class for testing technology importation
+ *
+ *
+ * This class is used as a parent for tests of the technology import structures. It includes the + * code used to actually create these technology definitions, as this can normally only be done + * through XStream. + * + * @author E. Benoît + * + */ + +abstract 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 the XML code for a technology definition + * + *
+ * This method generates the XML code for a technology definition, which can then be imported
+ * using XStream into a {@link Technology} instance. It is capable of generating instances with
+ * various fields set to null
.
+ *
+ * @param name
+ * identifier of the technology's name string
+ * @param category
+ * identifier of the technology's category string
+ * @param discovery
+ * identifier of the technology's discovery string
+ * @param description
+ * identifier of the technology's description string
+ * @param cost
+ * monetary cost of implementing the technology
+ * @param points
+ * amount of research points required
+ * @param dependencies
+ * dependencies as an array of strings
+ * @return
+ */
+ protected String createTechnology( String name , String category , String discovery , String description ,
+ Long cost , Long points , String... dependencies )
+ {
+ StringBuilder builder = new StringBuilder( "
+ * Initialise an XStream instance and set it up so it can process technology data definitions. + * This includes annotation processing, of course, but also generating an alias allowing the + * <technology> tag to be recognised on its own. + */ + private XStream createXStreamInstance( ) + { + XStream xstream = new XStream( ); + xstream.processAnnotations( Technologies.class ); + xstream.alias( "technology" , Technology.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/data/techs/TestTechnologies.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/TestTechnologies.java new file mode 100644 index 0000000..bd5c6ac --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/TestTechnologies.java @@ -0,0 +1,96 @@ +package com.deepclone.lw.cli.xmlimport.data.techs; + + +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; + + + +/** + * Unit tests for the {@link Technologies} class + * + * @author E. Benoît + */ +public class TestTechnologies + extends BaseTest +{ + + /** + * Test loading an empty list of technologies + * + *
+ * {@link Technologies#iterator()} will throw {@link NullPointerException} when there are no + * definitions. Use that to determine if it is empty. + */ + @Test( expected = NullPointerException.class ) + public void testEmpty( ) + { + String defs = this.createTopLevel( ); + Technologies techs = this.createObject( defs , Technologies.class ); + techs.iterator( ); + } + + + /** + * Test loading a list of technologies + */ + @Test + public void testTechs( ) + { + String tDef = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L ); + String defs = this.createTopLevel( tDef ); + Technologies techs = this.createObject( defs , Technologies.class ); + + int i = 0; + for ( Technology technology : techs ) { + assertNotNull( technology ); + assertEquals( 0 , i ); + i++; + } + } + + + /** + * Test verifying an empty list of technologies + */ + @Test( expected = DataImportException.class ) + public void testVerifyEmpty( ) + throws DataImportException + { + String defs = this.createTopLevel( ); + Technologies techs = this.createObject( defs , Technologies.class ); + techs.verifyData( ); + } + + + /** + * Test verifying a list of technologies with an invalid technology in it + */ + @Test( expected = DataImportException.class ) + public void testVerifyInvalid( ) + throws DataImportException + { + String tDef = this.createTechnology( null , "category" , "discovery" , "description" , 12L , 34L ); + String defs = this.createTopLevel( tDef ); + Technologies techs = this.createObject( defs , Technologies.class ); + techs.verifyData( ); + } + + + /** + * Test verifying a valid list of technologies + */ + @Test + public void testVerifyOk( ) + throws DataImportException + { + String tDef = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L ); + String defs = this.createTopLevel( tDef ); + Technologies techs = this.createObject( defs , Technologies.class ); + techs.verifyData( ); + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/TestTechnology.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/TestTechnology.java new file mode 100644 index 0000000..4b76abc --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/techs/TestTechnology.java @@ -0,0 +1,329 @@ +package com.deepclone.lw.cli.xmlimport.data.techs; + + +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; + + + +/** + * Test the {@link Technology} class + * + * @author E. Benoît + */ +public class TestTechnology + extends BaseTest +{ + + /** + * Test loading a definition with no dependencies + */ + @Test + public void testLoadNoDeps( ) + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + + assertNotNull( technology ); + assertEquals( "name" , technology.getName( ) ); + assertEquals( "category" , technology.getCategory( ) ); + assertEquals( "discovery" , technology.getDiscovery( ) ); + assertEquals( "description" , technology.getDescription( ) ); + assertEquals( new Long( 12 ) , technology.getCost( ) ); + assertEquals( new Long( 34 ) , technology.getPoints( ) ); + + assertNotNull( technology.getDependencies( ) ); + assertTrue( technology.getDependencies( ).isEmpty( ) ); + } + + + /** + * Test loading a definition with dependencies + */ + @Test + public void testLoadDeps( ) + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L , + "dep1" , "dep2" ); + Technology technology = this.createObject( definition , Technology.class ); + + assertNotNull( technology ); + assertEquals( "name" , technology.getName( ) ); + assertEquals( "category" , technology.getCategory( ) ); + assertEquals( "discovery" , technology.getDiscovery( ) ); + assertEquals( "description" , technology.getDescription( ) ); + assertEquals( new Long( 12 ) , technology.getCost( ) ); + assertEquals( new Long( 34 ) , technology.getPoints( ) ); + + assertNotNull( technology.getDependencies( ) ); + assertEquals( 2 , technology.getDependencies( ).size( ) ); + assertEquals( "dep1" , technology.getDependencies( ).get( 0 ) ); + assertEquals( "dep2" , technology.getDependencies( ).get( 1 ) ); + } + + + /** + * Test {@link Technology#verifyData()} when the name is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyNameNull( ) + throws DataImportException + { + String definition = this.createTechnology( null , "category" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the name is empty + */ + @Test( expected = DataImportException.class ) + public void testVerifyNameEmpty( ) + throws DataImportException + { + String definition = this.createTechnology( "" , "category" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the name consists of spaces only + */ + @Test( expected = DataImportException.class ) + public void testVerifyNameSpaces( ) + throws DataImportException + { + String definition = this.createTechnology( " " , "category" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the category is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyCategoryNull( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , null , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the category is empty + */ + @Test( expected = DataImportException.class ) + public void testVerifyCategoryEmpty( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the category consists of spaces only + */ + @Test( expected = DataImportException.class ) + public void testVerifyCategorySpaces( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , " " , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the discovery string is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyDiscoveryNull( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , null , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the discovery string is empty + */ + @Test( expected = DataImportException.class ) + public void testVerifyDiscoveryEmpty( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the discovery string consists of spaces only + */ + @Test( expected = DataImportException.class ) + public void testVerifyDiscoverySpaces( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , " " , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the description is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyDescriptionNull( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , null , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the description is empty + */ + @Test( expected = DataImportException.class ) + public void testVerifyDescriptionEmpty( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the description consists of spaces only + */ + @Test( expected = DataImportException.class ) + public void testVerifyDescriptionSpaces( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , " " , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the cost is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyCostNull( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , null , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the cost is 0 + */ + @Test( expected = DataImportException.class ) + public void testVerifyCostZero( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 0L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the amount of points is null + */ + @Test( expected = DataImportException.class ) + public void testVerifyPointsNull( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , null ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when the amount of points is 0 + */ + @Test( expected = DataImportException.class ) + public void testVerifyPointsZero( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 0L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} on a valid definition with no dependencies + */ + @Test + public void testVerifyValidNoDeps( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when a dependency is empty + */ + @Test( expected = DataImportException.class ) + public void testVerifyDepsEmpty( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L , "" ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} when a dependency consists of spaces only + */ + @Test( expected = DataImportException.class ) + public void testVerifyDepsSpaces( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L , + " " ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + + + /** + * Test {@link Technology#verifyData()} on a valid definition with dependencies + */ + @Test + public void testVerifyValidDeps( ) + throws DataImportException + { + String definition = this.createTechnology( "name" , "category" , "discovery" , "description" , 12L , 34L , + "dep1" , "dep2" ); + Technology technology = this.createObject( definition , Technology.class ); + technology.verifyData( ); + } + +} diff --git a/legacyworlds/doc/local-deployment.txt b/legacyworlds/doc/local-deployment.txt index 24669c3..b31c0ff 100644 --- a/legacyworlds/doc/local-deployment.txt +++ b/legacyworlds/doc/local-deployment.txt @@ -41,6 +41,8 @@ from the root of the server's distribution: --run-tool ImportText data/i18n-text.xml java -jar legacyworlds-server-main-1.0.0-0.jar \ --run-tool ImportTechs data/techs.xml + java -jar legacyworlds-server-main-1.0.0-0.jar \ + --run-tool ImportTechGraph data/tech-graph.xml java -jar legacyworlds-server-main-1.0.0-0.jar \ --run-tool ImportResources data/resources.xml java -jar legacyworlds-server-main-1.0.0-0.jar \