+ * This class defines the body of the I18N text import tool. It loads the data file specified on the + * command line, validates it, then proceeds to loading the languages and strings to the database. + * + * @author E. Benoît + * + */ public class ImportText extends CLITool { + /** Logging system */ private final Logger logger = Logger.getLogger( ImportText.class ); - @SuppressWarnings( "serial" ) - public abstract static class StringData - implements Serializable - { - @XStreamAsAttribute - public String id; - - - public abstract String getString( ); - } - - @SuppressWarnings( "serial" ) - @XStreamAlias( "inline-string" ) - public static class InlineString - extends StringData - { - public String value; - - - @Override - public String getString( ) - { - return this.value; - } - } - - @SuppressWarnings( "serial" ) - @XStreamAlias( "from-file" ) - public static class FileString - extends StringData - { - @XStreamAsAttribute - public String source; - - - @Override - public String getString( ) - { - StringBuilder sBuilder = new StringBuilder( ); - try { - BufferedReader in = new BufferedReader( new FileReader( source ) ); - String str; - while ( ( str = in.readLine( ) ) != null ) { - sBuilder.append( str ); - sBuilder.append( "\n" ); - } - in.close( ); - } catch ( IOException e ) { - throw new RuntimeException( "Could not read " + source ); - } - - return sBuilder.toString( ); - } - } - - @SuppressWarnings( "serial" ) - public static class LanguageData - implements Serializable - { - @XStreamAsAttribute - public String id; - - @XStreamAsAttribute - public String name; - - @XStreamImplicit - public List< StringData > strings = new LinkedList< StringData >( ); - } - - @SuppressWarnings( "serial" ) - @XStreamAlias( "lw-text-data" ) - public static class TextData - implements Serializable - { - @XStreamImplicit( itemFieldName = "language" ) - public List< LanguageData > languages = new LinkedList< LanguageData >( ); - } - + /** File to read the definitions from */ private File file; + + /** Spring transaction template */ private TransactionTemplate tTemplate; - private SimpleJdbcCall uocTranslation; - private SimpleJdbcCall uocLanguage; - - - private XStream initXStream( ) - { - XStream xstream = new XStream( ); - xstream.processAnnotations( TextData.class ); - xstream.processAnnotations( InlineString.class ); - xstream.processAnnotations( FileString.class ); - return xstream; - } - - - private TextData loadData( ) - { - FileInputStream fis; - try { - fis = new FileInputStream( this.file ); - } catch ( FileNotFoundException e ) { - return null; - } - - try { - XStream xstream = this.initXStream( ); - return (TextData) xstream.fromXML( fis ); - } catch ( Exception e ) { - e.printStackTrace( ); - return null; - } finally { - try { - fis.close( ); - } catch ( IOException e ) { - // EMPTY - } - } - } + + /** Language creation or update stored procedure */ + private StoredProc uocLanguage; + /** Translation creation or update stored procedure */ + private StoredProc uocTranslation; + /** + * 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( ) { - // Load data source and Hibernate properties - String[] dataConfig = { - this.getDataSource( ) , - }; - FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( dataConfig ); + FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( new String[] { + this.getDataSource( ) + } ); ctx.refresh( ); - // Load transaction manager bean - String[] cfg = { + return new ClassPathXmlApplicationContext( new String[] { "configuration/transaction-bean.xml" - }; - return new ClassPathXmlApplicationContext( cfg , true , ctx ); + } , true , ctx ); } + /** + * Create database access templates + * + *
+ * Initialise the transaction template as well as both 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.uocLanguage = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName( "uoc_language" ); - this.uocLanguage.withoutProcedureColumnMetaDataAccess( ); - this.uocLanguage.declareParameters( new SqlParameter( "lid" , java.sql.Types.VARCHAR ) , new SqlParameter( - "lname" , java.sql.Types.VARCHAR ) ); + this.uocLanguage = new StoredProc( dSource , "defs" , "uoc_language" ).addParameter( "lid" , + java.sql.Types.VARCHAR ).addParameter( "lname" , java.sql.Types.VARCHAR ); - this.uocTranslation = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName( - "uoc_translation" ); - this.uocTranslation.withoutProcedureColumnMetaDataAccess( ); - this.uocTranslation.declareParameters( new SqlParameter( "lid" , java.sql.Types.VARCHAR ) , new SqlParameter( - "sid" , java.sql.Types.VARCHAR ) , new SqlParameter( "trans" , java.sql.Types.VARCHAR ) ); + this.uocTranslation = new StoredProc( dSource , "defs" , "uoc_translation" ) + .addParameter( "lid" , java.sql.Types.VARCHAR ).addParameter( "sid" , java.sql.Types.VARCHAR ) + .addParameter( "trans" , java.sql.Types.VARCHAR ); this.tTemplate = new TransactionTemplate( tManager ); } - private void importText( TextData data ) + /** + * Import all languages and strings + * + *
+ * Import all language and string definitions from the top-level I18N data instance. + * + * @param data + * the top-level I18N data instance + * + * @throws DataImportException + * when some external string definition fails to load + */ + private void importText( I18NText data ) + throws DataImportException { - for ( LanguageData ld : data.languages ) { + for ( LanguageDefinition ld : data ) { this.importLanguage( ld ); } } - private void importLanguage( LanguageData ld ) + /** + * Import a language definition and the translations it contains + * + *
+ * Import the language's definition, then iterate over all string definitions and import them as + * well. + * + * @param ld + * the language's definition data + * + * @throws DataImportException + * when some external string definition fails to load + */ + private void importLanguage( LanguageDefinition ld ) + throws DataImportException { - if ( ld.strings == null ) { - return; - } - // Try creating or updating the language - this.uocLanguage.execute( ld.id , ld.name ); + this.uocLanguage.execute( ld.getId( ) , ld.getName( ) ); // Import translations - for ( StringData sd : ld.strings ) { - this.uocTranslation.execute( ld.id , sd.id , sd.getString( ) ); + for ( StringDefinition sd : ld ) { + this.uocTranslation.execute( ld.getId( ) , sd.getId( ) , sd.getString( ) ); } } + /** + * Run the I18N definitions import tool + * + *
+ * Load the data file, then connects to the database and create or update all definitions. + */ @Override public void run( ) { - final TextData data = this.loadData( ); - if ( data == null ) { - System.err.println( "could not read data" ); + I18NText data; + try { + data = new I18NLoader( this.file ).load( ); + } catch ( DataImportException e ) { + System.err.println( "Error while loading '" + this.file + "': " + e.getMessage( ) ); + this.logger.error( "Error while loading I18N data" , e ); return; } AbstractApplicationContext ctx = this.createContext( ); this.createTemplates( ctx ); + this.executeImportTransaction( data ); + ToolBase.destroyContext( ctx ); + } + + + /** + * Execute the I18N definitions importation transaction + * + *
+ * Run a transaction and execute the importation code inside it. Roll back if anything goes + * awry. + * + * @param data + * the I18N definitions instance + */ + private void executeImportTransaction( final I18NText data ) + { boolean rv = this.tTemplate.execute( new TransactionCallback< Boolean >( ) { @Override public Boolean doInTransaction( TransactionStatus status ) { - boolean rv; - try { - importText( data ); - rv = true; - } catch ( RuntimeException e ) { - logger.error( "Caught runtime exception" , e ); - rv = false; - } + boolean rv = ImportText.this.doTransaction( data ); if ( !rv ) { status.setRollbackOnly( ); } @@ -243,15 +197,43 @@ public class ImportText } } ); - if ( rv ) { this.logger.info( "Text import successful" ); } - - ToolBase.destroyContext( ctx ); } + /** + * Import transaction body + * + *
+ * Import all definitions and handle exceptions.
+ *
+ * @param data
+ * the I18N definitions instance
+ *
+ * @return true
on success, false
otherwise
+ */
+ private boolean doTransaction( I18NText data )
+ {
+ try {
+ ImportText.this.importText( data );
+ return true;
+ } catch ( RuntimeException e ) {
+ ImportText.this.logger.error( "Caught runtime exception" , e );
+ } catch ( DataImportException e ) {
+ ImportText.this.logger.error( "Error while importing external strings" , 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 ) { diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java new file mode 100644 index 0000000..2932549 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java @@ -0,0 +1,121 @@ +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.i18n.FileString; +import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText; +import com.deepclone.lw.cli.xmlimport.data.i18n.InlineString; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.XStreamException; + + + +/** + * I18N text definitions loader + * + *
+ * This class can be used to load all I18N definitions. It will extract them from the XML file, + * verify them and set their origin. + * + * @author E. Benoît + */ +public class I18NLoader +{ + /** The file to read the XML from */ + private final File file; + + + /** + * Initialise the loader + * + * @param file + * the XML file that contains the definitions + */ + public I18NLoader( File file ) + { + this.file = file.getAbsoluteFile( ); + } + + + /** + * Initialise the necessary XStream instance + * + *
+ * Initialise the XStream instance by processing annotations in all I18N importable data + * classes. + * + * @return the XStream instance to use when loading the data + */ + private XStream initXStream( ) + { + XStream xstream = new XStream( ); + xstream.processAnnotations( I18NText.class ); + xstream.processAnnotations( InlineString.class ); + xstream.processAnnotations( FileString.class ); + return xstream; + } + + + /** + * Load the I18N definitions + * + *
+ * Load the XML file and process the definitions file using XStream. + * + * @return the top-level importable data instance + * + * @throws DataImportException + * if reading from the file or parsing its contents fail + */ + private I18NText loadXMLFile( ) + throws DataImportException + { + FileInputStream fis; + try { + fis = new FileInputStream( this.file ); + } catch ( FileNotFoundException e ) { + throw new DataImportException( "Unable to load I18N definitions" , e ); + } + + try { + try { + XStream xstream = this.initXStream( ); + return (I18NText) xstream.fromXML( fis ); + } finally { + fis.close( ); + } + } catch ( IOException e ) { + throw new DataImportException( "Input error while loading I18N definitions" , e ); + } catch ( XStreamException e ) { + throw new DataImportException( "XML error while loading I18N definitions" , e ); + } + } + + + /** + * Load and process I18N definition + * + *
+ * Attempt to load all I18N definitions, make sure 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 I18NText load( ) + throws DataImportException + { + I18NText text = this.loadXMLFile( ); + text.verifyData( ); + text.setReadFrom( this.file ); + return text; + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java new file mode 100644 index 0000000..7c70c71 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java @@ -0,0 +1,43 @@ +package com.deepclone.lw.cli.xmlimport.data; + + +/** + * Data import exception + * + *
+ * This exception is thrown by importable data classes when some error occurs while loading, or when + * verification fails. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public class DataImportException + extends Exception +{ + + /** + * Initialise a data import exception using an error message + * + * @param message + * the error message + */ + public DataImportException( String message ) + { + super( message ); + } + + + /** + * Initialise a data import exception using an message and a cause + * + * @param message + * the error message + * @param cause + * the exception that caused this exception to be thrown + */ + public DataImportException( String message , Throwable cause ) + { + super( message , cause ); + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java new file mode 100644 index 0000000..fae56bd --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java @@ -0,0 +1,66 @@ +package com.deepclone.lw.cli.xmlimport.data; + + +import java.io.File; +import java.io.Serializable; + + + +/** + * Base class for importable data + * + *
+ * This abstract class serves as the base for all classes that represent data imported from XML + * files during the database's initialisation. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public abstract class ImportableData + implements Serializable +{ + + /** The file these data were read from */ + private transient File readFrom; + + + /** @return the file these data were read from */ + public final File getReadFrom( ) + { + return this.readFrom; + } + + + /** + * Validate the item + * + *
+ * This method may be overridden to implement some form of validation of the loaded data. + * + * @throws DataImportException + * if data validation failed + */ + public void verifyData( ) + throws DataImportException + { + // EMPTY + } + + + /** + * Set the source file + * + *
+ * This method sets the file instance returned by {@link #getReadFrom()}. For classes that + * represent collections of data, this method should be overridden in order to update all + * contained items. + * + * @param readFrom + * the file the data were read from + */ + public void setReadFrom( File readFrom ) + { + this.readFrom = readFrom; + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java new file mode 100644 index 0000000..4645170 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java @@ -0,0 +1,104 @@ +package com.deepclone.lw.cli.xmlimport.data.i18n; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamAsAttribute; + + + +/** + * An external string definition + * + *
+ * This class corresponds to I18N string definitions which use external files to store the actual + * string. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "from-file" ) +public class FileString + extends StringDefinition +{ + /** The name of the file which contains the string's text */ + @XStreamAsAttribute + private String source; + + + /** + * Verify an external string definition + * + *
+ * Make sure that the definition actually includes the name of the file to load the string from. + */ + @Override + public void verifyData( ) + throws DataImportException + { + super.verifyData( ); + if ( this.source == null || "".equals( this.source.trim( ) ) ) { + throw new DataImportException( "Missing file name for string definition '" + this.getId( ) + "'" ); + } + } + + + /** + * Load the string + * + *
+ * This implementation opens the file defined as the source of the string's text, reads it then + * returns it. + * + * @throws DataImportException + * if the file cannot be opened or read from + */ + @Override + public String getString( ) + throws DataImportException + { + File source = getSourceFile( ); + + StringBuilder sBuilder = new StringBuilder( ); + try { + BufferedReader in = new BufferedReader( new FileReader( source ) ); + try { + String str; + while ( ( str = in.readLine( ) ) != null ) { + sBuilder.append( str ); + sBuilder.append( "\n" ); + } + } finally { + in.close( ); + } + } catch ( IOException e ) { + throw new DataImportException( "Could not read from " + source.getPath( ) , e ); + } + + return sBuilder.toString( ); + } + + + /** + * Access the source file + * + *
+ * Create the source file instance using the path to the data file the current instance was read + * from as the base directory. + * + * @return the source file + */ + private File getSourceFile( ) + { + File parentDirectory = this.getReadFrom( ).getParentFile( ); + if ( parentDirectory == null ) { + parentDirectory = new File( "." ); + } + return new File( parentDirectory , this.source ).getAbsoluteFile( ); + } +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java new file mode 100644 index 0000000..13cf4da --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java @@ -0,0 +1,120 @@ +package com.deepclone.lw.cli.xmlimport.data.i18n; + + +import java.io.File; +import java.io.Serializable; +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; + + + +/** + * I18N text data + * + *
+ * This class represents the contents of the I18N text definitions file. It contains a list of + * language definitions. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "lw-text-data" ) +public class I18NText + extends ImportableData + implements Serializable , Iterable< LanguageDefinition > +{ + /** All present language definitions */ + @XStreamImplicit( itemFieldName = "language" ) + private List< LanguageDefinition > languages = new LinkedList< LanguageDefinition >( ); + + + /** + * Set the source file + * + *
+ * Update the definition's own source file, then update all language definitions. + */ + @Override + public void setReadFrom( File readFrom ) + { + super.setReadFrom( readFrom ); + for ( LanguageDefinition languageDefinition : this.languages ) { + languageDefinition.setReadFrom( readFrom ); + } + } + + + /** + * Verify I18N text data + * + *
+ * Check each definition, then make sure both identifiers and language names are unique. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.languages == null || this.languages.isEmpty( ) ) { + throw new DataImportException( "No language definitions" ); + } + + HashSet< String > identifiers = new HashSet< String >( ); + HashSet< String > names = new HashSet< String >( ); + for ( LanguageDefinition definition : this.languages ) { + definition.verifyData( ); + this.checkUniqueItem( "identifier" , identifiers , definition.getId( ) ); + this.checkUniqueItem( "name" , names , definition.getName( ) ); + } + } + + + /** + * Check that some part of the definition is unique + * + *
+ * This helper method is used by {@link #verifyData()} to make sure that names and identifiers + * are unique. + * + * @param exceptionText + * the name of the item + * @param existing + * the set of existing items + * @param value + * the item's value + * + * @throws DataImportException + * if the item's value is already present in the set of existing items + */ + private void checkUniqueItem( String exceptionText , HashSet< String > existing , String value ) + throws DataImportException + { + if ( existing.contains( value ) ) { + throw new DataImportException( "Duplicate language " + exceptionText + " '" + value + "'" ); + } + existing.add( value ); + } + + + /** + * Language definition iterator + * + *
+ * Grant access to the list of languages in read-only mode. + * + * @return a read-only iterator on the list of languages. + */ + @Override + public Iterator< LanguageDefinition > iterator( ) + { + return Collections.unmodifiableList( this.languages ).iterator( ); + } + +} diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java new file mode 100644 index 0000000..01e497e --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java @@ -0,0 +1,55 @@ +package com.deepclone.lw.cli.xmlimport.data.i18n; + + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.thoughtworks.xstream.annotations.XStreamAlias; + + + +/** + * An in-line string definition + * + *
+ * This class corresponds to string definitions which are stored directly inside the XML data file. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +@XStreamAlias( "inline-string" ) +public class InlineString + extends StringDefinition +{ + + /** The string's text */ + private String value; + + + /** + * Verify the in-line string definition + * + *
+ * Make sure that the definition actually contains a string. + */ + @Override + public void verifyData( ) + throws DataImportException + { + super.verifyData( ); + + if ( this.value == null || "".equals( this.value.trim( ) ) ) { + throw new DataImportException( "Missing or empty in-line string definition '" + this.getId( ) + "'" ); + } + } + + + /* + * (non-Javadoc) + * + * @see StringDefinition#getString() + */ + @Override + public String getString( ) + { + return this.value; + } +} \ No newline at end of file diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java new file mode 100644 index 0000000..77b17d1 --- /dev/null +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java @@ -0,0 +1,145 @@ +package com.deepclone.lw.cli.xmlimport.data.i18n; + + +import java.io.File; +import java.io.Serializable; +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.XStreamAsAttribute; +import com.thoughtworks.xstream.annotations.XStreamImplicit; + + + +/** + * A language definition + * + *
+ * Language definitions for the I18N text importer possess an identifier and a name. In addition, + * they may contain any amount of string definitions. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public class LanguageDefinition + extends ImportableData + implements Serializable , Iterable< StringDefinition > +{ + /** The language's identifier */ + @XStreamAsAttribute + private String id; + + /** The language's name */ + @XStreamAsAttribute + private String name; + + /** All strings defined for this language */ + @XStreamImplicit + private final List< StringDefinition > strings = new LinkedList< StringDefinition >( ); + + + /** + * Set the source file + * + *
+ * Update the definition's own source file, then update all string definitions. + */ + @Override + public void setReadFrom( File readFrom ) + { + super.setReadFrom( readFrom ); + for ( StringDefinition definition : this.strings ) { + definition.setReadFrom( readFrom ); + } + } + + + /** + * Validate the language definition + * + *
+ * Make sure that all definition characteristics are properly defined, then check all strings + * and make sure they are unique. + */ + @Override + public void verifyData( ) + throws DataImportException + { + if ( this.id == null || "".equals( this.id.trim( ) ) ) { + throw new DataImportException( "Missing language identifier" ); + } + if ( this.name == null || "".equals( this.name.trim( ) ) ) { + throw new DataImportException( "Missing language name for identifier '" + this.id + "'" ); + } + if ( this.strings == null || this.strings.isEmpty( ) ) { + throw new DataImportException( "No strings defined for language '" + this.id + "'" ); + } + + this.checkStringDefinitions( ); + } + + + /** + * Check all string definitions + * + *
+ * Iterate over all included string definitions, validating them and making sure they all have + * unique identifiers. + * + * @throws DataImportException + * if a string definition is invalid or if string identifiers contain duplicates. + */ + private void checkStringDefinitions( ) + throws DataImportException + { + HashSet< String > strings = new HashSet< String >( ); + for ( StringDefinition definition : this.strings ) { + definition.verifyData( ); + + String stringId = definition.getId( ); + if ( strings.contains( stringId ) ) { + throw new DataImportException( "In language '" + this.id + "': duplicate string '" + stringId + "'" ); + } + strings.add( stringId ); + } + } + + + /** @return the language's identifier */ + public String getId( ) + { + return this.id; + } + + + /** @return the language's name */ + public String getName( ) + { + return this.name; + } + + + /** + * Iterate over all string definitions + * + *
+ * This method grants access to the list of string definitions in read-only mode. + * + *
+ * Warning: this method should not be called if {@link #containsStrings()}
+ * returns false
.
+ *
+ * @return the read-only string definition iterator
+ */
+ @Override
+ public Iterator< StringDefinition > iterator( )
+ {
+ return Collections.unmodifiableList( this.strings ).iterator( );
+ }
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java
new file mode 100644
index 0000000..1e2701a
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java
@@ -0,0 +1,73 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+
+
+/**
+ * A string definition
+ *
+ *
+ * This class is the base for both types of string definitions used by the initial I18N text + * importer. String definitions always contain an identifier, and it is always possible to obtain + * the actual text they contain. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public abstract class StringDefinition + extends ImportableData + implements Serializable +{ + /** Pattern followed by string identifiers */ + private static final Pattern identifierCheck = Pattern.compile( "^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$" ); + + /** The string's identifier */ + @XStreamAsAttribute + private String id; + + + /** + * Check a string definition's identifier + * + *
+ * Make sure that a string definition's identifier is both present and valid.
+ */
+ @Override
+ public void verifyData( )
+ throws DataImportException
+ {
+ if ( this.id == null ) {
+ throw new DataImportException( "Missing string identifier" );
+ } else if ( !identifierCheck.matcher( this.id ).find( ) ) {
+ throw new DataImportException( "Invalid string identifier '" + this.id + "'" );
+ }
+ }
+
+
+ /** @return the string's identifier */
+ public final String getId( )
+ {
+ return this.id;
+ }
+
+
+ /**
+ * This method must be overridden to provide a way of reading the actual text of the string that
+ * is being defined.
+ *
+ * @return the string's text
+ *
+ * @throws DataImportException
+ * if accessing the string's actual text fails
+ */
+ public abstract String getString( )
+ throws DataImportException;
+
+}
diff --git a/legacyworlds-server-main/src/main/resources/log4j.properties b/legacyworlds-server-main/src/main/resources/log4j.properties
index 994c335..066fac3 100644
--- a/legacyworlds-server-main/src/main/resources/log4j.properties
+++ b/legacyworlds-server-main/src/main/resources/log4j.properties
@@ -36,3 +36,4 @@ log4j.logger.org.springframework=WARN, server
log4j.logger.org.springframework=INFO, fullDebug
log4j.logger.com.deepclone.lw=DEBUG, fullDebug
log4j.logger.com.deepclone.lw.interfaces.eventlog=DEBUG, server
+log4j.logger.com.deepclone.lw.cli=INFO, stdout
diff --git a/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt b/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt
new file mode 100644
index 0000000..273c1a9
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt
@@ -0,0 +1 @@
+This is a test.
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml
new file mode 100644
index 0000000..f33b821
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml
@@ -0,0 +1,6 @@
+
+
+ * This class is used in the test to have access to an importable data instance that corresponds
+ * exactly to the base class.
+ *
+ * @author E. Benoît
+ */
+ @SuppressWarnings( "serial" )
+ private static class DummyData
+ extends ImportableData
+ {
+ // EMPTY
+ }
+
+
+ /** Make sure that {@link ImportableData#getReadFrom()} returns
+ * This class is used as a parent by tests of the I18N import structures. It includes the code
+ * required to actually create such resources, 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 an in-line string definition
+ *
+ *
+ * Generate the XML code that corresponds to an in-line string definition.
+ *
+ * @param identifier
+ * the string's identifier, or null
file instance.
+ *
+ * @throws NullPointerException
+ * when the constructor is executed
+ */
+ @Test( expected = NullPointerException.class )
+ public void testNullFile( )
+ throws NullPointerException
+ {
+ new I18NLoader( null );
+ }
+
+
+ /**
+ * Try loading a file that does not exist
+ */
+ @Test
+ public void testMissingFile( )
+ {
+ I18NLoader loader = new I18NLoader( 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 definitely isn't XML.
+ */
+ @Test
+ public void testBadXML( )
+ {
+ I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-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" );
+ }
+
+
+ /**
+ * Try loading a file that contains XML but which cannot be desertialised to an {@link I18NText}
+ * instance.
+ */
+ @Test
+ public void testBadContents( )
+ {
+ I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-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 an {@link I18NText} instance with semantic
+ * errors.
+ */
+ @Test
+ public void testBadData( )
+ {
+ I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-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, and that it
+ * loads external strings from the appropriate directory.
+ */
+ @Test
+ public void testGoodData( )
+ {
+ I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-loader/good.xml" ) );
+ I18NText text;
+ try {
+ text = loader.load( );
+ } catch ( DataImportException e ) {
+ fail( "could not load valid file" );
+ return;
+ }
+ assertNotNull( text );
+
+ int lCount = 0;
+ for ( LanguageDefinition ld : text ) {
+ assertEquals( "test" , ld.getId( ) );
+ assertEquals( "test" , ld.getName( ) );
+
+ int tCount = 0;
+ for ( StringDefinition sd : ld ) {
+ assertEquals( "test" , sd.getId( ) );
+ try {
+ assertEquals( "test" , sd.getString( ).trim( ) );
+ } catch ( DataImportException e ) {
+ fail( "could not load string: " + e.getMessage( ) );
+ }
+ tCount++;
+ }
+
+ assertEquals( 1 , tCount );
+ lCount++;
+ }
+ assertEquals( 1 , lCount );
+ }
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java
new file mode 100644
index 0000000..f8f5880
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java
@@ -0,0 +1,73 @@
+package com.deepclone.lw.cli.xmlimport.data;
+
+
+import java.io.File;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+
+/**
+ * Unit tests for the {@link ImportableData} class
+ *
+ * @author E. Benoît
+ */
+public class TestImportableData
+{
+
+ /**
+ * Dummy importable data
+ *
+ * null
by default */
+ @Test
+ public void testUninitialisedReadFrom( )
+ {
+ DummyData data = new DummyData( );
+ assertNull( data.getReadFrom( ) );
+ }
+
+
+ /** Checks the default implementation of {@link ImportableData#setReadFrom(File)} */
+ @Test
+ public void testSetReadFrom( )
+ {
+ DummyData data = new DummyData( );
+ File file = new File( "test" );
+
+ data.setReadFrom( file );
+ assertNotNull( data.getReadFrom( ) );
+ assertEquals( "test" , data.getReadFrom( ).getPath( ) );
+ }
+
+
+ /**
+ * Make sure that the default implementation of {@link ImportableData#verifyData()} always
+ * succeeds
+ *
+ * @throws DataImportException
+ * if the default implementation fails somehow
+ */
+ @Test
+ public void testDefaultVerify( )
+ throws DataImportException
+ {
+ DummyData data = new DummyData( );
+ data.verifyData( );
+ }
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java
new file mode 100644
index 0000000..1daeca8
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java
@@ -0,0 +1,205 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import com.deepclone.lw.cli.xmlimport.I18NLoader;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
+
+
+
+/**
+ * Base class for I18N import structures
+ *
+ * null
to omit the identifier
+ * @param contents
+ * the string's contents, or null
to omit the contents.
+ *
+ * @return the XML code corresponding to the string definition
+ */
+ protected String createInlineStringDefinition( String identifier , String contents )
+ {
+ StringBuilder str = new StringBuilder( );
+ str.append( "
+ * Generate the XML code that corresponds to an external string definition.
+ *
+ * @param identifier
+ * the string's identifier, or null
to omit the identifier
+ * @param source
+ * the string's source file, or null
to omit the source.
+ *
+ * @return the XML code corresponding to the string definition
+ */
+ protected String createExternalStringDefinition( String identifier , String source )
+ {
+ StringBuilder str = new StringBuilder( );
+ str.append( "null
if the identifier is to be omitted
+ * @param name
+ * the language's name, or null
if the name is to be omitted
+ * @param stringDefinitions
+ * XML definitions returned by {@link #createInlineStringDefinition(String, String)}
+ * or {@link #createExternalStringDefinition(String, String)}
+ *
+ * @return the XML code corresponding to the language definition
+ */
+ protected String createLanguageDefinition( String identifier , String name , String... stringDefinitions )
+ {
+ StringBuilder str = new StringBuilder( );
+
+ str.append( "
+ * Initialise an XStream instance and set it up so it can process I18N data definitions. Unlike
+ * the XStream instance generated by {@link I18NLoader}, this version also registers the alias
+ * for language definitions.
+ *
+ * @return the initialised XStream instance.
+ */
+ private XStream createXStreamInstance( )
+ {
+ XStream xstream = new XStream( );
+ xstream.processAnnotations( I18NText.class );
+ xstream.processAnnotations( InlineString.class );
+ xstream.processAnnotations( FileString.class );
+ xstream.alias( "language" , LanguageDefinition.class );
+ return xstream;
+ }
+
+
+ /**
+ * Create an I18N definition 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 unserialising 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/i18n/TestExternalString.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java
new file mode 100644
index 0000000..6970672
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java
@@ -0,0 +1,307 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link FileString} class
+ *
+ * @author E. Benoît
+ */
+public class TestExternalString
+ extends BaseTest
+{
+
+ /**
+ * Test loading a valid external string record and the corresponding contents
+ *
+ * @throws DataImportException
+ * if a failure occurs while reading the source file.
+ */
+ @Test
+ public void testLoadStringOK( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+ assertEquals( "test" , instance.getId( ) );
+ assertEquals( "This is a test." , instance.getString( ).trim( ) );
+ }
+
+
+ /**
+ * Test loading an external string record with no identifier
+ *
+ * @throws DataImportException
+ * if a failure occurs while reading the source file.
+ */
+ @Test
+ public void testLoadStringNullIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( null , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+ assertNull( instance.getId( ) );
+ assertEquals( "This is a test." , instance.getString( ).trim( ) );
+ }
+
+
+ /**
+ * Test loading an external string record with no source file
+ *
+ * @throws DataImportException
+ * if a failure occurs while reading the source file
+ * @throws NullPointerException
+ * when {@link FileString#getString()} is called while its source is
+ * null
+ */
+ @Test( expected = NullPointerException.class )
+ public void testLoadStringNullSource( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , null );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+ assertEquals( "test" , instance.getId( ) );
+ instance.getString( );
+ }
+
+
+ /**
+ * Test loading a valid external string record and obtaining the contents without setting the
+ * path to the data file first.
+ *
+ * @throws DataImportException
+ * if a failure occurs while reading the source file.
+ * @throws NullPointerException
+ * when {@link FileString#getString()} is called while its original path is
+ * null
+ */
+ @Test( expected = NullPointerException.class )
+ public void testLoadStringNoPath( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.getString( );
+ }
+
+
+ /**
+ * Test loading a valid external string record and obtaining the contents when the source file
+ * does not exist.
+ *
+ * @throws DataImportException
+ * when a failure occurs while reading the source file.
+ */
+ @Test( expected = DataImportException.class )
+ public void testLoadStringBadSource( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "does-not-exist.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+ instance.getString( );
+ }
+
+
+ /**
+ * Test loading a valid external string record and obtaining the contents when the source file
+ * is a directory.
+ *
+ * @throws DataImportException
+ * when a failure occurs while reading the source file.
+ */
+ @Test( expected = DataImportException.class )
+ public void testLoadStringSourceIsDirectory( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "." );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+ instance.getString( );
+ }
+
+
+ /**
+ * Test loading a valid external string record and obtaining the contents when the original
+ * file's path is incorrect.
+ *
+ * @throws DataImportException
+ * when a failure occurs while reading the source file.
+ */
+ @Test( expected = DataImportException.class )
+ public void testLoadStringBadParent( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "does-not-exist/does-not-exist" ) );
+ instance.getString( );
+ }
+
+
+ /**
+ * Test loading a valid external string record and obtaining the contents when the original
+ * file's path is a single file name.
+ *
+ * @throws DataImportException
+ * if a failure occurs while reading the source file.
+ */
+ @Test
+ public void testLoadStringParentIsCurrentDirectory( )
+ throws DataImportException , NullPointerException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "TestFiles/i18n-external-file/test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.setReadFrom( new File( "does-not-exist" ) );
+ instance.getString( );
+ }
+
+
+
+ /** Test validating a file string record with a short identifier and a source */
+ @Test
+ public void testValidateSimple( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test validating an external string record with a camel case identifier and a source */
+ @Test
+ public void testValidateCamelCaseIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "testCamelCase" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test validating an external string record with an identifier that includes a sequence of
+ * upper-case letters and a source
+ */
+ @Test
+ public void testValidateCapsIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "testCAPSIdentifier" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test validating an external string record with an identifier that includes numbers and some
+ * contents
+ */
+ @Test
+ public void testValidateNumbersIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test123Numbers123" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an external string record with a null
identifier and a source
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( null , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an external string record with an identifier that starts with a bad character */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadFirstCharacterIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( " test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an external string record with an identifier that starts with an upper-case
+ * letter
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateFirstCharacterWrongCaseIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "Test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an external string record with an identifier that includes "bad" characters */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadMiddleCharacterIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test Test" , "test.txt" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an external record with null
contents */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullSource( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test" , null );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an external record with empty contents */
+ @Test( expected = DataImportException.class )
+ public void testValidateEmptySource( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test" , "" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an external record with contents the include only white space (spaces, tabs,
+ * new line, line feed)
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateSpacesOnlySource( )
+ throws DataImportException
+ {
+ String definition = this.createExternalStringDefinition( "test" , " \t\r\n" );
+ FileString instance = this.createObject( definition , FileString.class );
+ instance.verifyData( );
+ }
+
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java
new file mode 100644
index 0000000..f158d91
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java
@@ -0,0 +1,134 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link I18NText} class
+ *
+ * @author E. Benoît
+ */
+public class TestI18NText
+ extends BaseTest
+{
+ /** The language definition that will be used in most tests */
+ private String languageDefinition;
+
+
+ /** Initialise a language definition to use while testing */
+ @Before
+ public void setUp( )
+ {
+ String xml = this.createInlineStringDefinition( "test" , "test" );
+ this.languageDefinition = this.createLanguageDefinition( "test" , "test" , xml );
+ }
+
+
+ /**
+ * Try loading a valid top-level I18N definition and make sure its contents are correct
+ *
+ * @throws DataImportException
+ * if the language definition check fails for some unknown reason
+ */
+ @Test
+ public void testLoadValidDefinitions( )
+ throws DataImportException
+ {
+ String xml = this.createTopLevel( this.languageDefinition );
+ I18NText text = this.createObject( xml , I18NText.class );
+
+ int count = 0;
+ for ( LanguageDefinition definition : text ) {
+ definition.verifyData( );
+ count++;
+ }
+ assertEquals( 1 , count );
+ }
+
+
+ /**
+ * Try loading an empty top-level element
+ *
+ * @throws NullPointerException
+ * when the iterator is accessed
+ */
+ @Test( expected = NullPointerException.class )
+ public void testLoadEmpty( )
+ throws NullPointerException
+ {
+ String xml = this.createTopLevel( );
+ I18NText text = this.createObject( xml , I18NText.class );
+ text.iterator( );
+ }
+
+
+ /**
+ * Try validating a correct top-level element
+ *
+ * @throws DataImportException
+ * if the check fails
+ */
+ @Test
+ public void testValidateOK( )
+ throws DataImportException
+ {
+ String xml = this.createTopLevel( this.languageDefinition );
+ I18NText text = this.createObject( xml , I18NText.class );
+ text.verifyData( );
+ }
+
+
+ /**
+ * Try rejecting a top-level element that does not contain any language definition
+ *
+ * @throws DataImportException
+ * when the check fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateEmpty( )
+ throws DataImportException
+ {
+ String xml = this.createTopLevel( );
+ I18NText text = this.createObject( xml , I18NText.class );
+ text.verifyData( );
+ }
+
+
+ /**
+ * Try rejecting a top-level element that contains an invalid language definition
+ *
+ * @throws DataImportException
+ * when the check fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadDefinition( )
+ throws DataImportException
+ {
+ String xml = this.createTopLevel( this.createLanguageDefinition( null , null ) );
+ I18NText text = this.createObject( xml , I18NText.class );
+ text.verifyData( );
+ }
+
+
+ /**
+ * Try rejecting a top-level element that contains two valid but duplicate language definitions
+ *
+ * @throws DataImportException
+ * when the check fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateDuplicateDefinition( )
+ throws DataImportException
+ {
+ String xml = this.createTopLevel( this.languageDefinition , this.languageDefinition );
+ I18NText text = this.createObject( xml , I18NText.class );
+ text.verifyData( );
+ }
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java
new file mode 100644
index 0000000..501ce92
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java
@@ -0,0 +1,210 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+
+import static org.junit.Assert.*;
+
+
+
+/**
+ * Tests for the {@link InlineString} class
+ *
+ * @author E. Benoît
+ */
+public class TestInlineString
+ extends BaseTest
+{
+
+ /** Test loading a valid in-line string record */
+ @Test
+ public void testLoadStringOK( )
+ {
+ String definition = this.createInlineStringDefinition( "test" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ assertEquals( "test" , instance.getId( ) );
+ assertEquals( "Test" , instance.getString( ) );
+ }
+
+
+ /** Test loading an in-line string record without identifier */
+ @Test
+ public void testLoadStringNoIdentifier( )
+ {
+ String definition = this.createInlineStringDefinition( null , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ assertNull( instance.getId( ) );
+ assertEquals( "Test" , instance.getString( ) );
+ }
+
+
+ /** Test loading an in-line string record without contents */
+ @Test
+ public void testLoadStringNoContents( )
+ {
+ String definition = this.createInlineStringDefinition( "test" , null );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ assertEquals( "test" , instance.getId( ) );
+ assertNull( instance.getString( ) );
+ }
+
+
+ /** Test validating an in-line string record with a short identifier and some contents */
+ @Test
+ public void testValidateSimple( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test validating an in-line string record with a camel case identifier and some contents */
+ @Test
+ public void testValidateCamelCaseIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "testCamelCase" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test validating an in-line string record with an identifier that includes a sequence of
+ * upper-case letters and some contents
+ */
+ @Test
+ public void testValidateCapsIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "testCAPSIdentifier" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test validating an in-line string record with an identifier that includes numbers and some
+ * contents
+ */
+ @Test
+ public void testValidateNumbersIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test123Numbers123" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an in-line string record with a null
identifier and some contents
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( null , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an in-line string record with an identifier that starts with a bad character */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadFirstCharacterIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( " test" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an in-line string record with an identifier that starts with an upper-case
+ * letter
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateFirstCharacterWrongCaseIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "Test" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an in-line string record with an identifier that includes "bad" characters */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadMiddleCharacterIdentifier( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test Test" , "Test" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an in-line record with null
contents */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullContents( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test" , null );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /** Test rejecting an in-line record with empty contents */
+ @Test( expected = DataImportException.class )
+ public void testValidateEmptyContents( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test" , "" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Test rejecting an in-line record with contents the include only white space (spaces, tabs,
+ * new line, line feed)
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateSpacesOnlyContents( )
+ throws DataImportException
+ {
+ String definition = this.createInlineStringDefinition( "test" , " \t\r\n" );
+ InlineString instance = this.createObject( definition , InlineString.class );
+ instance.verifyData( );
+ }
+
+
+ /**
+ * Make sure the class does not override {@link ImportableData#setReadFrom(java.io.File)}; if it
+ * does, new tests must be written.
+ *
+ * @throws NoSuchMethodException
+ * if the method has been deleted
+ * @throws SecurityException
+ * if the JVM set-up is getting in the wayy
+ */
+ @Test
+ public void testNoSetReadFromOverride( )
+ throws SecurityException , NoSuchMethodException
+ {
+ Class< InlineString > cls = InlineString.class;
+ Method method = cls.getMethod( "setReadFrom" , File.class );
+ assertEquals( ImportableData.class , method.getDeclaringClass( ) );
+ }
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java
new file mode 100644
index 0000000..d9b18ef
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java
@@ -0,0 +1,264 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link LanguageDefinition} class
+ *
+ * @author E. Benoît
+ */
+public class TestLanguageDefinition
+ extends BaseTest
+{
+
+ /** The in-line string definition that will be used in most tests */
+ private String inlineDefinition;
+
+
+ /**
+ * Create a valid in-line string definition to be appended to language definitions.
+ */
+ @Before
+ public void setUp( )
+ {
+ this.inlineDefinition = this.createInlineStringDefinition( "test" , "test" );
+ }
+
+
+ /**
+ * Test loading a valid language definition and make sure that all fields were set
+ * appropriately.
+ *
+ * @throws DataImportException
+ * if accessing the test string fails.
+ */
+ @Test
+ public void testLoadValidDefinition( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ assertEquals( "t" , language.getId( ) );
+ assertEquals( "Test" , language.getName( ) );
+
+ int count = 0;
+ for ( StringDefinition stringDefinition : language ) {
+ assertEquals( InlineString.class , stringDefinition.getClass( ) );
+ assertEquals( "test" , stringDefinition.getId( ) );
+ assertEquals( "test" , stringDefinition.getString( ) );
+ count++;
+ }
+ assertEquals( 1 , count );
+ }
+
+
+ /**
+ * Test loading a language definition that has no identifier.
+ */
+ @Test
+ public void testLoadNullIdentifier( )
+ {
+ String xml = this.createLanguageDefinition( null , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ assertNull( language.getId( ) );
+ }
+
+
+ /**
+ * Test loading a language definition that has no name
+ */
+ @Test
+ public void testLoadNullName( )
+ {
+ String xml = this.createLanguageDefinition( "test" , null , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ assertNull( language.getName( ) );
+ }
+
+
+ /**
+ * Test loading a language definition that has no contents
+ *
+ * @throws NullPointerException
+ * when the language definition's iterator is accessed, as its contents will be
+ * null
+ */
+ @Test( expected = NullPointerException.class )
+ public void testLoadNullContents( )
+ throws NullPointerException
+ {
+ String xml = this.createLanguageDefinition( "test" , "test" );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.iterator( );
+ }
+
+
+ /**
+ * Test validating a correct language definition.
+ *
+ * @throws DataImportException
+ * if validating the definition fails
+ */
+ @Test
+ public void testValidateOK( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with a null
identifier.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullIdentifier( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( null , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with an empty identifier.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateEmptyIdentifier( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "" , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with an identifier that contains white space only.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateWhiteSpaceIdentifier( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( " \t\n\r" , "Test" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with a null
name.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateNullName( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , null , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with an empty name.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateEmptyName( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , "" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with a name that contains white space only.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateWhiteSpaceName( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , " \t\n\r" , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with no contents.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateNoContents( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , "Test" );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with invalid contents.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateBadContents( )
+ throws DataImportException
+ {
+ String strXML = this.createInlineStringDefinition( null , "test" );
+ String xml = this.createLanguageDefinition( "t" , "Test" , strXML );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+
+ /**
+ * Test validating a language definition with duplicate contents.
+ *
+ * @throws DataImportException
+ * when validation fails
+ */
+ @Test( expected = DataImportException.class )
+ public void testValidateDuplicateContents( )
+ throws DataImportException
+ {
+ String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition , this.inlineDefinition );
+ LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+ language.verifyData( );
+ }
+
+}
diff --git a/legacyworlds-server-tests/src/test/resources/.empty b/legacyworlds-server-tests/src/test/resources/.empty
new file mode 100644
index 0000000..e69de29
diff --git a/legacyworlds/doc/TODO.txt b/legacyworlds/doc/TODO.txt
index f1fc671..448622a 100644
--- a/legacyworlds/doc/TODO.txt
+++ b/legacyworlds/doc/TODO.txt
@@ -20,8 +20,6 @@ SERVER & DATABASE:
* Add a tool to initialise the database
- * I18N loader: improve text file loading (use relative paths)
-
* Replace current authentication information (pair of hashes) with a
salted SHA512 hash.
-> Make sure it is still possible to import old passwords using the
diff --git a/legacyworlds/pom.xml b/legacyworlds/pom.xml
index fd00897..773e818 100644
--- a/legacyworlds/pom.xml
+++ b/legacyworlds/pom.xml
@@ -177,6 +177,11 @@