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.
This commit is contained in:
Emmanuel BENOîT 2011-12-17 12:37:01 +01:00
parent be3106c463
commit 631f49fb86
57 changed files with 2295 additions and 200 deletions

View file

@ -21,7 +21,6 @@
<dependency> <dependency>
<groupId>com.deepclone.lw</groupId> <groupId>com.deepclone.lw</groupId>
<artifactId>legacyworlds-server-main</artifactId> <artifactId>legacyworlds-server-main</artifactId>
<version>${project.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>

View file

@ -56,8 +56,8 @@
<directory>../legacyworlds-server-main/data</directory> <directory>../legacyworlds-server-main/data</directory>
<outputDirectory>data</outputDirectory> <outputDirectory>data</outputDirectory>
<includes> <includes>
<include>*.txt</include> <include>i18n/**</include>
<include>*.xml</include> <include>**.xml</include>
</includes> </includes>
</fileSet> </fileSet>

View file

@ -1,23 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text" <lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text i18n-text.xsd">
i18n-text.xsd">
<language id="en" name="English"> <language id="en" name="English">
<from-file id="registrationMail" source="data/registrationMail-en.txt" /> <from-file id="registrationMail" source="i18n/en/registrationMail.txt" />
<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-en.txt" /> <from-file id="passwordRecoveryMail" source="i18n/en/passwordRecoveryMail.txt" />
<from-file id="reactivationMail" source="data/reactivationMail-en.txt" /> <from-file id="reactivationMail" source="i18n/en/reactivationMail.txt" />
<from-file id="addressChangeMail" source="data/addressChangeMail-en.txt" /> <from-file id="addressChangeMail" source="i18n/en/addressChangeMail.txt" />
<from-file id="adminRecapMail" source="data/adminRecapMail.txt" /> <from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
<from-file id="messageMail" source="data/messageMail-en.txt" /> <from-file id="messageMail" source="i18n/en/messageMail.txt" />
<from-file id="recapMail" source="data/recapMail-en.txt" /> <from-file id="recapMail" source="i18n/en/recapMail.txt" />
<from-file id="quitMail" source="data/quitMail-en.txt" /> <from-file id="quitMail" source="i18n/en/quitMail.txt" />
<from-file id="bannedMail" source="data/bannedMail-en.txt" /> <from-file id="bannedMail" source="i18n/en/bannedMail.txt" />
<from-file id="banLiftedMail" source="data/banLiftedMail-en.txt" /> <from-file id="banLiftedMail" source="i18n/en/banLiftedMail.txt" />
<from-file id="adminErrorMail" source="data/adminErrorMail.txt" /> <from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-en.txt" /> <from-file id="inactivityWarningMail" source="i18n/en/inactivityWarningMail.txt" />
<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-en.txt" /> <from-file id="inactivityQuitMail" source="i18n/en/inactivityQuitMail.txt" />
<inline-string id="instantNotification"> <inline-string id="instantNotification">
<value> <value>
@ -518,19 +517,20 @@ It was disbanded.</value>
</language> </language>
<language id="fr" name="Français"> <language id="fr" name="Français">
<from-file id="registrationMail" source="data/registrationMail-fr.txt" /> <from-file id="registrationMail" source="i18n/fr/registrationMail.txt" />
<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-fr.txt" /> <from-file id="passwordRecoveryMail" source="i18n/fr/passwordRecoveryMail.txt" />
<from-file id="reactivationMail" source="data/reactivationMail-fr.txt" /> <from-file id="reactivationMail" source="i18n/fr/reactivationMail.txt" />
<from-file id="addressChangeMail" source="data/addressChangeMail-fr.txt" /> <from-file id="addressChangeMail" source="i18n/fr/addressChangeMail.txt" />
<from-file id="adminRecapMail" source="data/adminRecapMail.txt" /> <from-file id="messageMail" source="i18n/fr/messageMail.txt" />
<from-file id="messageMail" source="data/messageMail-fr.txt" /> <from-file id="recapMail" source="i18n/fr/recapMail.txt" />
<from-file id="recapMail" source="data/recapMail-fr.txt" /> <from-file id="quitMail" source="i18n/fr/quitMail.txt" />
<from-file id="quitMail" source="data/quitMail-fr.txt" /> <from-file id="bannedMail" source="i18n/fr/bannedMail.txt" />
<from-file id="bannedMail" source="data/bannedMail-fr.txt" /> <from-file id="banLiftedMail" source="i18n/fr/banLiftedMail.txt" />
<from-file id="banLiftedMail" source="data/banLiftedMail-fr.txt" /> <from-file id="inactivityWarningMail" source="i18n/fr/inactivityWarningMail.txt" />
<from-file id="adminErrorMail" source="data/adminErrorMail.txt" /> <from-file id="inactivityQuitMail" source="i18n/fr/inactivityQuitMail.txt" />
<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-fr.txt" />
<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-fr.txt" /> <from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
<from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
<inline-string id="instantNotification"> <inline-string id="instantNotification">
<value> <value>

View file

@ -1,9 +1,7 @@
package com.deepclone.lw.cli; package com.deepclone.lw.cli;
import java.io.*; import java.io.File;
import java.util.LinkedList;
import java.util.List;
import javax.sql.DataSource; import javax.sql.DataSource;
@ -12,230 +10,186 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext; 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.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import com.thoughtworks.xstream.XStream; import com.deepclone.lw.cli.xmlimport.I18NLoader;
import com.thoughtworks.xstream.annotations.XStreamAlias; import com.deepclone.lw.cli.xmlimport.data.DataImportException;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute; import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText;
import com.thoughtworks.xstream.annotations.XStreamImplicit; 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
*
* <p>
* 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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
public class ImportText public class ImportText
extends CLITool extends CLITool
{ {
/** Logging system */
private final Logger logger = Logger.getLogger( ImportText.class ); private final Logger logger = Logger.getLogger( ImportText.class );
@SuppressWarnings( "serial" ) /** File to read the definitions from */
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 >( );
}
private File file; private File file;
/** Spring transaction template */
private TransactionTemplate tTemplate; private TransactionTemplate tTemplate;
private SimpleJdbcCall uocTranslation;
private SimpleJdbcCall uocLanguage; /** Language creation or update stored procedure */
private StoredProc uocLanguage;
/** Translation creation or update stored procedure */
private XStream initXStream( ) private StoredProc uocTranslation;
{
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
}
}
}
/**
* Create the Spring context
*
* <p>
* 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( ) private ClassPathXmlApplicationContext createContext( )
{ {
// Load data source and Hibernate properties FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( new String[] {
String[] dataConfig = { this.getDataSource( )
this.getDataSource( ) , } );
};
FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( dataConfig );
ctx.refresh( ); ctx.refresh( );
// Load transaction manager bean return new ClassPathXmlApplicationContext( new String[] {
String[] cfg = {
"configuration/transaction-bean.xml" "configuration/transaction-bean.xml"
}; } , true , ctx );
return new ClassPathXmlApplicationContext( cfg , true , ctx );
} }
/**
* Create database access templates
*
* <p>
* Initialise the transaction template as well as both stored procedure definitions.
*
* @param ctx
* the Spring application context
*/
private void createTemplates( ApplicationContext ctx ) private void createTemplates( ApplicationContext ctx )
{ {
DataSource dSource = ctx.getBean( DataSource.class ); DataSource dSource = ctx.getBean( DataSource.class );
PlatformTransactionManager tManager = ctx.getBean( PlatformTransactionManager.class ); PlatformTransactionManager tManager = ctx.getBean( PlatformTransactionManager.class );
this.uocLanguage = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName( "uoc_language" ); this.uocLanguage = new StoredProc( dSource , "defs" , "uoc_language" ).addParameter( "lid" ,
this.uocLanguage.withoutProcedureColumnMetaDataAccess( ); java.sql.Types.VARCHAR ).addParameter( "lname" , java.sql.Types.VARCHAR );
this.uocLanguage.declareParameters( new SqlParameter( "lid" , java.sql.Types.VARCHAR ) , new SqlParameter(
"lname" , java.sql.Types.VARCHAR ) );
this.uocTranslation = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName( this.uocTranslation = new StoredProc( dSource , "defs" , "uoc_translation" )
"uoc_translation" ); .addParameter( "lid" , java.sql.Types.VARCHAR ).addParameter( "sid" , java.sql.Types.VARCHAR )
this.uocTranslation.withoutProcedureColumnMetaDataAccess( ); .addParameter( "trans" , java.sql.Types.VARCHAR );
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.tTemplate = new TransactionTemplate( tManager ); this.tTemplate = new TransactionTemplate( tManager );
} }
private void importText( TextData data ) /**
* Import all languages and strings
*
* <p>
* 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 ); this.importLanguage( ld );
} }
} }
private void importLanguage( LanguageData ld ) /**
* Import a language definition and the translations it contains
*
* <p>
* 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 // Try creating or updating the language
this.uocLanguage.execute( ld.id , ld.name ); this.uocLanguage.execute( ld.getId( ) , ld.getName( ) );
// Import translations // Import translations
for ( StringData sd : ld.strings ) { for ( StringDefinition sd : ld ) {
this.uocTranslation.execute( ld.id , sd.id , sd.getString( ) ); this.uocTranslation.execute( ld.getId( ) , sd.getId( ) , sd.getString( ) );
} }
} }
/**
* Run the I18N definitions import tool
*
* <p>
* Load the data file, then connects to the database and create or update all definitions.
*/
@Override @Override
public void run( ) public void run( )
{ {
final TextData data = this.loadData( ); I18NText data;
if ( data == null ) { try {
System.err.println( "could not read data" ); 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; return;
} }
AbstractApplicationContext ctx = this.createContext( ); AbstractApplicationContext ctx = this.createContext( );
this.createTemplates( ctx ); this.createTemplates( ctx );
this.executeImportTransaction( data );
ToolBase.destroyContext( ctx );
}
/**
* Execute the I18N definitions importation transaction
*
* <p>
* 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 >( ) { boolean rv = this.tTemplate.execute( new TransactionCallback< Boolean >( ) {
@Override @Override
public Boolean doInTransaction( TransactionStatus status ) public Boolean doInTransaction( TransactionStatus status )
{ {
boolean rv; boolean rv = ImportText.this.doTransaction( data );
try {
importText( data );
rv = true;
} catch ( RuntimeException e ) {
logger.error( "Caught runtime exception" , e );
rv = false;
}
if ( !rv ) { if ( !rv ) {
status.setRollbackOnly( ); status.setRollbackOnly( );
} }
@ -243,15 +197,43 @@ public class ImportText
} }
} ); } );
if ( rv ) { if ( rv ) {
this.logger.info( "Text import successful" ); this.logger.info( "Text import successful" );
} }
ToolBase.destroyContext( ctx );
} }
/**
* Import transaction body
*
* <p>
* Import all definitions and handle exceptions.
*
* @param data
* the I18N definitions instance
*
* @return <code>true</code> on success, <code>false</code> 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
*
* <p>
* Check the command line options, setting the definitions file accordingly.
*/
@Override @Override
public boolean setOptions( String... options ) public boolean setOptions( String... options )
{ {

View file

@ -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
*
* <p>
* 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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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;
}
}

View file

@ -0,0 +1,43 @@
package com.deepclone.lw.cli.xmlimport.data;
/**
* Data import exception
*
* <p>
* This exception is thrown by importable data classes when some error occurs while loading, or when
* verification fails.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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 );
}
}

View file

@ -0,0 +1,66 @@
package com.deepclone.lw.cli.xmlimport.data;
import java.io.File;
import java.io.Serializable;
/**
* Base class for importable data
*
* <p>
* This abstract class serves as the base for all classes that represent data imported from XML
* files during the database's initialisation.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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
*
* <p>
* 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
*
* <p>
* 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;
}
}

View file

@ -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
*
* <p>
* This class corresponds to I18N string definitions which use external files to store the actual
* string.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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( );
}
}

View file

@ -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
*
* <p>
* This class represents the contents of the I18N text definitions file. It contains a list of
* language definitions.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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( );
}
}

View file

@ -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
*
* <p>
* This class corresponds to string definitions which are stored directly inside the XML data file.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@SuppressWarnings( "serial" )
@XStreamAlias( "inline-string" )
public class InlineString
extends StringDefinition
{
/** The string's text */
private String value;
/**
* Verify the in-line string definition
*
* <p>
* 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;
}
}

View file

@ -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
*
* <p>
* Language definitions for the I18N text importer possess an identifier and a name. In addition,
* they may contain any amount of string definitions.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* This method grants access to the list of string definitions in read-only mode.
*
* <p>
* <strong>Warning:</strong> this method should not be called if {@link #containsStrings()}
* returns <code>false</code>.
*
* @return the read-only string definition iterator
*/
@Override
public Iterator< StringDefinition > iterator( )
{
return Collections.unmodifiableList( this.strings ).iterator( );
}
}

View file

@ -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
*
* <p>
* 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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@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
*
* <p>
* 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;
}

View file

@ -36,3 +36,4 @@ log4j.logger.org.springframework=WARN, server
log4j.logger.org.springframework=INFO, fullDebug log4j.logger.org.springframework=INFO, fullDebug
log4j.logger.com.deepclone.lw=DEBUG, fullDebug log4j.logger.com.deepclone.lw=DEBUG, fullDebug
log4j.logger.com.deepclone.lw.interfaces.eventlog=DEBUG, server log4j.logger.com.deepclone.lw.interfaces.eventlog=DEBUG, server
log4j.logger.com.deepclone.lw.cli=INFO, stdout

View file

@ -0,0 +1 @@
This is a test.

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lw-text-data>
<does-not-exist />
</lw-text-data>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text ../../../legacyworlds-server-main/data/i18n-text.xsd">
<language name="test" id="test">
<inline-string id="test">
<value />
</inline-string>
</language>
</lw-text-data>

View file

@ -0,0 +1,2 @@
This is not an XML file, obviously.
We'll make that even more confusing: <<<<<< & >>!!!

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text ../../../legacyworlds-server-main/data/i18n-text.xsd">
<language name="test" id="test">
<from-file id="test" source="test.txt" />
</language>
</lw-text-data>

View file

@ -0,0 +1 @@
test

View file

@ -50,6 +50,10 @@
<artifactId>legacyworlds-server-beans-user</artifactId> <artifactId>legacyworlds-server-beans-user</artifactId>
<groupId>com.deepclone.lw</groupId> <groupId>com.deepclone.lw</groupId>
</dependency> </dependency>
<dependency>
<artifactId>legacyworlds-server-main</artifactId>
<groupId>com.deepclone.lw</groupId>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
public class TestI18NLoader
{
/**
* Try initialising the loader with a <code>null</code> 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 );
}
}

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class TestImportableData
{
/**
* Dummy importable data
*
* <p>
* This class is used in the test to have access to an importable data instance that corresponds
* exactly to the base class.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@SuppressWarnings( "serial" )
private static class DummyData
extends ImportableData
{
// EMPTY
}
/** Make sure that {@link ImportableData#getReadFrom()} returns <code>null</code> 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( );
}
}

View file

@ -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
*
* <p>
* 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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
abstract class BaseTest
{
/**
* Escape &amp;, &lt; and &gt; in XML strings
*
* @param string
* the string to escape
*
* @return the escaped string
*/
private String xmlString( String string )
{
return string.replace( "&" , "&amp;" ).replace( "<" , "&lt;" ).replace( ">" , "&gt;" );
}
/**
* Escape &amp;, &lt; and &gt;, ' and " in XML strings
*
* @param string
* the string to escape
*
* @return the escaped string
*/
private String quoteString( String string )
{
return "\"" + this.xmlString( string ).replace( "\"" , "&quot;" ).replace( "'" , "&apos;" ) + "\"";
}
/**
* Create an in-line string definition
*
* <p>
* Generate the XML code that corresponds to an in-line string definition.
*
* @param identifier
* the string's identifier, or <code>null</code> to omit the identifier
* @param contents
* the string's contents, or <code>null</code> 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( "<inline-string" );
if ( identifier != null ) {
str.append( " id=" ).append( this.quoteString( identifier ) );
}
str.append( '>' );
if ( contents != null ) {
str.append( "<value>" ).append( this.xmlString( contents ) ).append( "</value>" );
}
str.append( "</inline-string>" );
return str.toString( );
}
/**
* Create an external string definition
*
* <p>
* Generate the XML code that corresponds to an external string definition.
*
* @param identifier
* the string's identifier, or <code>null</code> to omit the identifier
* @param source
* the string's source file, or <code>null</code> 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( "<from-file" );
if ( identifier != null ) {
str.append( " id=" ).append( this.quoteString( identifier ) );
}
if ( source != null ) {
str.append( " source=" ).append( this.quoteString( source ) );
}
str.append( " />" );
return str.toString( );
}
/**
* Create a language definition
*
* @param identifier
* the language's identifier, or <code>null</code> if the identifier is to be omitted
* @param name
* the language's name, or <code>null</code> 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( "<language" );
if ( identifier != null ) {
str.append( " id=" ).append( this.quoteString( identifier ) );
}
if ( name != null ) {
str.append( " name=" ).append( this.quoteString( name ) );
}
str.append( ">" );
for ( String definition : stringDefinitions ) {
str.append( definition );
}
str.append( "</language>" );
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( "<lw-text-data>" );
for ( String language : languages ) {
str.append( language );
}
str.append( "</lw-text-data>" );
return str.toString( );
}
/**
* Create the necessary XStream instance
*
* <p>
* 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 ) );
}
}

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
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
* <code>null</code>
*/
@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
* <code>null</code>
*/
@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 <code>null</code> 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 <code>null</code> 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( );
}
}

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
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( );
}
}

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
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 <code>null</code> 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 <code>null</code> 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 <strong>must</strong> 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( ) );
}
}

View file

@ -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 <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
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
* <code>null</code>
*/
@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 <code>null</code> 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 <code>null</code> 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( );
}
}

View file

@ -20,8 +20,6 @@ SERVER & DATABASE:
* Add a tool to initialise the 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 * Replace current authentication information (pair of hashes) with a
salted SHA512 hash. salted SHA512 hash.
-> Make sure it is still possible to import old passwords using the -> Make sure it is still possible to import old passwords using the

View file

@ -177,6 +177,11 @@
<artifactId>legacyworlds-server-interfaces</artifactId> <artifactId>legacyworlds-server-interfaces</artifactId>
<version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version> <version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version>
</dependency> </dependency>
<dependency>
<groupId>com.deepclone.lw</groupId>
<artifactId>legacyworlds-server-main</artifactId>
<version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version>
</dependency>
<dependency> <dependency>
<groupId>com.deepclone.lw</groupId> <groupId>com.deepclone.lw</groupId>
<artifactId>legacyworlds-session</artifactId> <artifactId>legacyworlds-session</artifactId>