From 631f49fb863fb31ebe556e26fc33f54c7f2fdd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= Date: Sat, 17 Dec 2011 12:37:01 +0100 Subject: [PATCH] Command line tools * Added base classes for all importable data. These new classes should be used for all future loaders; all existing loaders that are modified should be updated. * I18N loader rewritten to make use of the new base classes. External strings are now read using the XML data file's path as the base directory. * Updated all external I18N definitions and moved the existing files around in an attempt to make the data directory somewhat more livable. * Added dependency management entry for the server's main package to the root project, updated server distribution package accordingly. Added dependency on the server's main package to the server's testing package. --- legacyworlds-server-DIST/pom.xml | 1 - legacyworlds-server-DIST/src/server.xml | 4 +- legacyworlds-server-main/data/i18n-text.xml | 58 ++-- .../data/{ => i18n}/adminErrorMail.txt | 0 .../data/{ => i18n}/adminRecapMail.txt | 0 .../en/addressChangeMail.txt} | 0 .../en/banLiftedMail.txt} | 0 .../en/bannedMail.txt} | 0 .../en/inactivityQuitMail.txt} | 0 .../en/inactivityWarningMail.txt} | 0 .../en/messageMail.txt} | 0 .../en/passwordRecoveryMail.txt} | 0 .../{quitMail-en.txt => i18n/en/quitMail.txt} | 0 .../en/reactivationMail.txt} | 0 .../en/recapMail.txt} | 0 .../en/registrationMail.txt} | 0 .../fr/addressChangeMail.txt} | 0 .../fr/banLiftedMail.txt} | 0 .../fr/bannedMail.txt} | 0 .../fr/inactivityQuitMail.txt} | 0 .../fr/inactivityWarningMail.txt} | 0 .../fr/messageMail.txt} | 0 .../fr/passwordRecoveryMail.txt} | 0 .../{quitMail-fr.txt => i18n/fr/quitMail.txt} | 0 .../fr/reactivationMail.txt} | 0 .../fr/recapMail.txt} | 0 .../fr/registrationMail.txt} | 0 .../java/com/deepclone/lw/cli/ImportText.java | 314 +++++++++--------- .../lw/cli/xmlimport/I18NLoader.java | 121 +++++++ .../xmlimport/data/DataImportException.java | 43 +++ .../lw/cli/xmlimport/data/ImportableData.java | 66 ++++ .../cli/xmlimport/data/i18n/FileString.java | 104 ++++++ .../lw/cli/xmlimport/data/i18n/I18NText.java | 120 +++++++ .../cli/xmlimport/data/i18n/InlineString.java | 55 +++ .../data/i18n/LanguageDefinition.java | 145 ++++++++ .../xmlimport/data/i18n/StringDefinition.java | 73 ++++ .../src/main/resources/log4j.properties | 1 + .../TestFiles/i18n-external-file/test.txt | 1 + .../TestFiles/i18n-loader/bad-contents.xml | 6 + .../TestFiles/i18n-loader/bad-data.xml | 12 + .../TestFiles/i18n-loader/bad-xml.xml | 2 + .../TestFiles/i18n-loader/good.xml | 12 + .../TestFiles/i18n-loader/test.txt | 1 + legacyworlds-server-tests/pom.xml | 4 + .../src/main/java/.empty | 0 .../src/main/resources/.empty | 0 .../src/test/java/.empty | 0 .../lw/cli/xmlimport/TestI18NLoader.java | 152 +++++++++ .../xmlimport/data/TestImportableData.java | 73 ++++ .../lw/cli/xmlimport/data/i18n/BaseTest.java | 205 ++++++++++++ .../data/i18n/TestExternalString.java | 307 +++++++++++++++++ .../cli/xmlimport/data/i18n/TestI18NText.java | 134 ++++++++ .../xmlimport/data/i18n/TestInlineString.java | 210 ++++++++++++ .../data/i18n/TestLanguageDefinition.java | 264 +++++++++++++++ .../src/test/resources/.empty | 0 legacyworlds/doc/TODO.txt | 2 - legacyworlds/pom.xml | 5 + 57 files changed, 2295 insertions(+), 200 deletions(-) rename legacyworlds-server-main/data/{ => i18n}/adminErrorMail.txt (100%) rename legacyworlds-server-main/data/{ => i18n}/adminRecapMail.txt (100%) rename legacyworlds-server-main/data/{addressChangeMail-en.txt => i18n/en/addressChangeMail.txt} (100%) rename legacyworlds-server-main/data/{banLiftedMail-en.txt => i18n/en/banLiftedMail.txt} (100%) rename legacyworlds-server-main/data/{bannedMail-en.txt => i18n/en/bannedMail.txt} (100%) rename legacyworlds-server-main/data/{inactivityQuitMail-en.txt => i18n/en/inactivityQuitMail.txt} (100%) rename legacyworlds-server-main/data/{inactivityWarningMail-en.txt => i18n/en/inactivityWarningMail.txt} (100%) rename legacyworlds-server-main/data/{messageMail-en.txt => i18n/en/messageMail.txt} (100%) rename legacyworlds-server-main/data/{passwordRecoveryMail-en.txt => i18n/en/passwordRecoveryMail.txt} (100%) rename legacyworlds-server-main/data/{quitMail-en.txt => i18n/en/quitMail.txt} (100%) rename legacyworlds-server-main/data/{reactivationMail-en.txt => i18n/en/reactivationMail.txt} (100%) rename legacyworlds-server-main/data/{recapMail-en.txt => i18n/en/recapMail.txt} (100%) rename legacyworlds-server-main/data/{registrationMail-en.txt => i18n/en/registrationMail.txt} (100%) rename legacyworlds-server-main/data/{addressChangeMail-fr.txt => i18n/fr/addressChangeMail.txt} (100%) rename legacyworlds-server-main/data/{banLiftedMail-fr.txt => i18n/fr/banLiftedMail.txt} (100%) rename legacyworlds-server-main/data/{bannedMail-fr.txt => i18n/fr/bannedMail.txt} (100%) rename legacyworlds-server-main/data/{inactivityQuitMail-fr.txt => i18n/fr/inactivityQuitMail.txt} (100%) rename legacyworlds-server-main/data/{inactivityWarningMail-fr.txt => i18n/fr/inactivityWarningMail.txt} (100%) rename legacyworlds-server-main/data/{messageMail-fr.txt => i18n/fr/messageMail.txt} (100%) rename legacyworlds-server-main/data/{passwordRecoveryMail-fr.txt => i18n/fr/passwordRecoveryMail.txt} (100%) rename legacyworlds-server-main/data/{quitMail-fr.txt => i18n/fr/quitMail.txt} (100%) rename legacyworlds-server-main/data/{reactivationMail-fr.txt => i18n/fr/reactivationMail.txt} (100%) rename legacyworlds-server-main/data/{recapMail-fr.txt => i18n/fr/recapMail.txt} (100%) rename legacyworlds-server-main/data/{registrationMail-fr.txt => i18n/fr/registrationMail.txt} (100%) create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java create mode 100644 legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java create mode 100644 legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt create mode 100644 legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml create mode 100644 legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml create mode 100644 legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml create mode 100644 legacyworlds-server-tests/TestFiles/i18n-loader/good.xml create mode 100644 legacyworlds-server-tests/TestFiles/i18n-loader/test.txt create mode 100644 legacyworlds-server-tests/src/main/java/.empty create mode 100644 legacyworlds-server-tests/src/main/resources/.empty create mode 100644 legacyworlds-server-tests/src/test/java/.empty create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java create mode 100644 legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java create mode 100644 legacyworlds-server-tests/src/test/resources/.empty diff --git a/legacyworlds-server-DIST/pom.xml b/legacyworlds-server-DIST/pom.xml index a2fc403..fc85884 100644 --- a/legacyworlds-server-DIST/pom.xml +++ b/legacyworlds-server-DIST/pom.xml @@ -21,7 +21,6 @@ com.deepclone.lw legacyworlds-server-main - ${project.version} diff --git a/legacyworlds-server-DIST/src/server.xml b/legacyworlds-server-DIST/src/server.xml index 20a5255..875f9b7 100644 --- a/legacyworlds-server-DIST/src/server.xml +++ b/legacyworlds-server-DIST/src/server.xml @@ -56,8 +56,8 @@ ../legacyworlds-server-main/data data - *.txt - *.xml + i18n/** + **.xml diff --git a/legacyworlds-server-main/data/i18n-text.xml b/legacyworlds-server-main/data/i18n-text.xml index c2ef4e9..b148a45 100644 --- a/legacyworlds-server-main/data/i18n-text.xml +++ b/legacyworlds-server-main/data/i18n-text.xml @@ -1,23 +1,22 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text i18n-text.xsd"> - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -518,19 +517,20 @@ It was disbanded. - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/legacyworlds-server-main/data/adminErrorMail.txt b/legacyworlds-server-main/data/i18n/adminErrorMail.txt similarity index 100% rename from legacyworlds-server-main/data/adminErrorMail.txt rename to legacyworlds-server-main/data/i18n/adminErrorMail.txt diff --git a/legacyworlds-server-main/data/adminRecapMail.txt b/legacyworlds-server-main/data/i18n/adminRecapMail.txt similarity index 100% rename from legacyworlds-server-main/data/adminRecapMail.txt rename to legacyworlds-server-main/data/i18n/adminRecapMail.txt diff --git a/legacyworlds-server-main/data/addressChangeMail-en.txt b/legacyworlds-server-main/data/i18n/en/addressChangeMail.txt similarity index 100% rename from legacyworlds-server-main/data/addressChangeMail-en.txt rename to legacyworlds-server-main/data/i18n/en/addressChangeMail.txt diff --git a/legacyworlds-server-main/data/banLiftedMail-en.txt b/legacyworlds-server-main/data/i18n/en/banLiftedMail.txt similarity index 100% rename from legacyworlds-server-main/data/banLiftedMail-en.txt rename to legacyworlds-server-main/data/i18n/en/banLiftedMail.txt diff --git a/legacyworlds-server-main/data/bannedMail-en.txt b/legacyworlds-server-main/data/i18n/en/bannedMail.txt similarity index 100% rename from legacyworlds-server-main/data/bannedMail-en.txt rename to legacyworlds-server-main/data/i18n/en/bannedMail.txt diff --git a/legacyworlds-server-main/data/inactivityQuitMail-en.txt b/legacyworlds-server-main/data/i18n/en/inactivityQuitMail.txt similarity index 100% rename from legacyworlds-server-main/data/inactivityQuitMail-en.txt rename to legacyworlds-server-main/data/i18n/en/inactivityQuitMail.txt diff --git a/legacyworlds-server-main/data/inactivityWarningMail-en.txt b/legacyworlds-server-main/data/i18n/en/inactivityWarningMail.txt similarity index 100% rename from legacyworlds-server-main/data/inactivityWarningMail-en.txt rename to legacyworlds-server-main/data/i18n/en/inactivityWarningMail.txt diff --git a/legacyworlds-server-main/data/messageMail-en.txt b/legacyworlds-server-main/data/i18n/en/messageMail.txt similarity index 100% rename from legacyworlds-server-main/data/messageMail-en.txt rename to legacyworlds-server-main/data/i18n/en/messageMail.txt diff --git a/legacyworlds-server-main/data/passwordRecoveryMail-en.txt b/legacyworlds-server-main/data/i18n/en/passwordRecoveryMail.txt similarity index 100% rename from legacyworlds-server-main/data/passwordRecoveryMail-en.txt rename to legacyworlds-server-main/data/i18n/en/passwordRecoveryMail.txt diff --git a/legacyworlds-server-main/data/quitMail-en.txt b/legacyworlds-server-main/data/i18n/en/quitMail.txt similarity index 100% rename from legacyworlds-server-main/data/quitMail-en.txt rename to legacyworlds-server-main/data/i18n/en/quitMail.txt diff --git a/legacyworlds-server-main/data/reactivationMail-en.txt b/legacyworlds-server-main/data/i18n/en/reactivationMail.txt similarity index 100% rename from legacyworlds-server-main/data/reactivationMail-en.txt rename to legacyworlds-server-main/data/i18n/en/reactivationMail.txt diff --git a/legacyworlds-server-main/data/recapMail-en.txt b/legacyworlds-server-main/data/i18n/en/recapMail.txt similarity index 100% rename from legacyworlds-server-main/data/recapMail-en.txt rename to legacyworlds-server-main/data/i18n/en/recapMail.txt diff --git a/legacyworlds-server-main/data/registrationMail-en.txt b/legacyworlds-server-main/data/i18n/en/registrationMail.txt similarity index 100% rename from legacyworlds-server-main/data/registrationMail-en.txt rename to legacyworlds-server-main/data/i18n/en/registrationMail.txt diff --git a/legacyworlds-server-main/data/addressChangeMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/addressChangeMail.txt similarity index 100% rename from legacyworlds-server-main/data/addressChangeMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/addressChangeMail.txt diff --git a/legacyworlds-server-main/data/banLiftedMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/banLiftedMail.txt similarity index 100% rename from legacyworlds-server-main/data/banLiftedMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/banLiftedMail.txt diff --git a/legacyworlds-server-main/data/bannedMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/bannedMail.txt similarity index 100% rename from legacyworlds-server-main/data/bannedMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/bannedMail.txt diff --git a/legacyworlds-server-main/data/inactivityQuitMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/inactivityQuitMail.txt similarity index 100% rename from legacyworlds-server-main/data/inactivityQuitMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/inactivityQuitMail.txt diff --git a/legacyworlds-server-main/data/inactivityWarningMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/inactivityWarningMail.txt similarity index 100% rename from legacyworlds-server-main/data/inactivityWarningMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/inactivityWarningMail.txt diff --git a/legacyworlds-server-main/data/messageMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/messageMail.txt similarity index 100% rename from legacyworlds-server-main/data/messageMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/messageMail.txt diff --git a/legacyworlds-server-main/data/passwordRecoveryMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/passwordRecoveryMail.txt similarity index 100% rename from legacyworlds-server-main/data/passwordRecoveryMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/passwordRecoveryMail.txt diff --git a/legacyworlds-server-main/data/quitMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/quitMail.txt similarity index 100% rename from legacyworlds-server-main/data/quitMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/quitMail.txt diff --git a/legacyworlds-server-main/data/reactivationMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/reactivationMail.txt similarity index 100% rename from legacyworlds-server-main/data/reactivationMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/reactivationMail.txt diff --git a/legacyworlds-server-main/data/recapMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/recapMail.txt similarity index 100% rename from legacyworlds-server-main/data/recapMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/recapMail.txt diff --git a/legacyworlds-server-main/data/registrationMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/registrationMail.txt similarity index 100% rename from legacyworlds-server-main/data/registrationMail-fr.txt rename to legacyworlds-server-main/data/i18n/fr/registrationMail.txt diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java index c3952ed..8d6e856 100644 --- a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java +++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java @@ -1,9 +1,7 @@ package com.deepclone.lw.cli; -import java.io.*; -import java.util.LinkedList; -import java.util.List; +import java.io.File; import javax.sql.DataSource; @@ -12,230 +10,186 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; -import org.springframework.jdbc.core.SqlParameter; -import org.springframework.jdbc.core.simple.SimpleJdbcCall; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; -import com.thoughtworks.xstream.XStream; -import com.thoughtworks.xstream.annotations.XStreamAlias; -import com.thoughtworks.xstream.annotations.XStreamAsAttribute; -import com.thoughtworks.xstream.annotations.XStreamImplicit; +import com.deepclone.lw.cli.xmlimport.I18NLoader; +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText; +import com.deepclone.lw.cli.xmlimport.data.i18n.LanguageDefinition; +import com.deepclone.lw.cli.xmlimport.data.i18n.StringDefinition; +import com.deepclone.lw.utils.StoredProc; +/** + * I18N text import tool + * + *

+ * 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 @@ + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml new file mode 100644 index 0000000..aec5ebc --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml new file mode 100644 index 0000000..ee1d0ed --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml @@ -0,0 +1,2 @@ +This is not an XML file, obviously. +We'll make that even more confusing: <<<<<< & >>!!! \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml new file mode 100644 index 0000000..f1ca627 --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt b/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/legacyworlds-server-tests/pom.xml b/legacyworlds-server-tests/pom.xml index d1b1438..c9cd3a4 100644 --- a/legacyworlds-server-tests/pom.xml +++ b/legacyworlds-server-tests/pom.xml @@ -50,6 +50,10 @@ legacyworlds-server-beans-user com.deepclone.lw + + legacyworlds-server-main + com.deepclone.lw + junit diff --git a/legacyworlds-server-tests/src/main/java/.empty b/legacyworlds-server-tests/src/main/java/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-tests/src/main/resources/.empty b/legacyworlds-server-tests/src/main/resources/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-tests/src/test/java/.empty b/legacyworlds-server-tests/src/test/java/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java new file mode 100644 index 0000000..0b2ac67 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java @@ -0,0 +1,152 @@ +package com.deepclone.lw.cli.xmlimport; + + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import com.deepclone.lw.cli.xmlimport.data.DataImportException; +import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText; +import com.deepclone.lw.cli.xmlimport.data.i18n.LanguageDefinition; +import com.deepclone.lw.cli.xmlimport.data.i18n.StringDefinition; +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.io.StreamException; + + + +/** + * Unit tests for the {@link I18NLoader} class. + * + * @author E. Benoît + * + */ +public class TestI18NLoader +{ + + /** + * Try initialising the loader with a null file instance. + * + * @throws NullPointerException + * when the constructor is executed + */ + @Test( expected = NullPointerException.class ) + public void testNullFile( ) + throws NullPointerException + { + new 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 + * + *

+ * 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 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 + * + *

+ * 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 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( "" ).append( this.xmlString( contents ) ).append( "" ); + } + str.append( "" ); + return str.toString( ); + } + + + /** + * Create an external string definition + * + *

+ * 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( "" ); + return str.toString( ); + } + + + /** + * Create a language definition + * + * @param identifier + * the language's identifier, or 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( "" ); + for ( String definition : stringDefinitions ) { + str.append( definition ); + } + str.append( "" ); + + return str.toString( ); + } + + + /** + * Create the XML code for a top-level I18N definition element + * + * @param languages + * XML definitions of languages, as returned by + * {@link #createLanguageDefinition(String, String, String...)} + * + * @return the top-level element's XML code + */ + protected String createTopLevel( String... languages ) + { + StringBuilder str = new StringBuilder( ); + str.append( "" ); + for ( String language : languages ) { + str.append( language ); + } + str.append( "" ); + return str.toString( ); + } + + + /** + * Create the necessary XStream instance + * + *

+ * 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 @@ legacyworlds-server-interfaces ${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build} + + com.deepclone.lw + legacyworlds-server-main + ${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build} + com.deepclone.lw legacyworlds-session