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:
parent
be3106c463
commit
631f49fb86
57 changed files with 2295 additions and 200 deletions
legacyworlds-server-main
data
i18n-text.xml
i18n
src/main
|
@ -1,23 +1,22 @@
|
|||
<?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
|
||||
i18n-text.xsd">
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text i18n-text.xsd">
|
||||
|
||||
<language id="en" name="English">
|
||||
<from-file id="registrationMail" source="data/registrationMail-en.txt" />
|
||||
<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-en.txt" />
|
||||
<from-file id="reactivationMail" source="data/reactivationMail-en.txt" />
|
||||
<from-file id="addressChangeMail" source="data/addressChangeMail-en.txt" />
|
||||
<from-file id="adminRecapMail" source="data/adminRecapMail.txt" />
|
||||
<from-file id="messageMail" source="data/messageMail-en.txt" />
|
||||
<from-file id="recapMail" source="data/recapMail-en.txt" />
|
||||
<from-file id="quitMail" source="data/quitMail-en.txt" />
|
||||
<from-file id="bannedMail" source="data/bannedMail-en.txt" />
|
||||
<from-file id="banLiftedMail" source="data/banLiftedMail-en.txt" />
|
||||
<from-file id="adminErrorMail" source="data/adminErrorMail.txt" />
|
||||
<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-en.txt" />
|
||||
<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-en.txt" />
|
||||
<from-file id="registrationMail" source="i18n/en/registrationMail.txt" />
|
||||
<from-file id="passwordRecoveryMail" source="i18n/en/passwordRecoveryMail.txt" />
|
||||
<from-file id="reactivationMail" source="i18n/en/reactivationMail.txt" />
|
||||
<from-file id="addressChangeMail" source="i18n/en/addressChangeMail.txt" />
|
||||
<from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
|
||||
<from-file id="messageMail" source="i18n/en/messageMail.txt" />
|
||||
<from-file id="recapMail" source="i18n/en/recapMail.txt" />
|
||||
<from-file id="quitMail" source="i18n/en/quitMail.txt" />
|
||||
<from-file id="bannedMail" source="i18n/en/bannedMail.txt" />
|
||||
<from-file id="banLiftedMail" source="i18n/en/banLiftedMail.txt" />
|
||||
<from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
|
||||
<from-file id="inactivityWarningMail" source="i18n/en/inactivityWarningMail.txt" />
|
||||
<from-file id="inactivityQuitMail" source="i18n/en/inactivityQuitMail.txt" />
|
||||
|
||||
<inline-string id="instantNotification">
|
||||
<value>
|
||||
|
@ -518,19 +517,20 @@ It was disbanded.</value>
|
|||
</language>
|
||||
|
||||
<language id="fr" name="Français">
|
||||
<from-file id="registrationMail" source="data/registrationMail-fr.txt" />
|
||||
<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-fr.txt" />
|
||||
<from-file id="reactivationMail" source="data/reactivationMail-fr.txt" />
|
||||
<from-file id="addressChangeMail" source="data/addressChangeMail-fr.txt" />
|
||||
<from-file id="adminRecapMail" source="data/adminRecapMail.txt" />
|
||||
<from-file id="messageMail" source="data/messageMail-fr.txt" />
|
||||
<from-file id="recapMail" source="data/recapMail-fr.txt" />
|
||||
<from-file id="quitMail" source="data/quitMail-fr.txt" />
|
||||
<from-file id="bannedMail" source="data/bannedMail-fr.txt" />
|
||||
<from-file id="banLiftedMail" source="data/banLiftedMail-fr.txt" />
|
||||
<from-file id="adminErrorMail" source="data/adminErrorMail.txt" />
|
||||
<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-fr.txt" />
|
||||
<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-fr.txt" />
|
||||
<from-file id="registrationMail" source="i18n/fr/registrationMail.txt" />
|
||||
<from-file id="passwordRecoveryMail" source="i18n/fr/passwordRecoveryMail.txt" />
|
||||
<from-file id="reactivationMail" source="i18n/fr/reactivationMail.txt" />
|
||||
<from-file id="addressChangeMail" source="i18n/fr/addressChangeMail.txt" />
|
||||
<from-file id="messageMail" source="i18n/fr/messageMail.txt" />
|
||||
<from-file id="recapMail" source="i18n/fr/recapMail.txt" />
|
||||
<from-file id="quitMail" source="i18n/fr/quitMail.txt" />
|
||||
<from-file id="bannedMail" source="i18n/fr/bannedMail.txt" />
|
||||
<from-file id="banLiftedMail" source="i18n/fr/banLiftedMail.txt" />
|
||||
<from-file id="inactivityWarningMail" source="i18n/fr/inactivityWarningMail.txt" />
|
||||
<from-file id="inactivityQuitMail" source="i18n/fr/inactivityQuitMail.txt" />
|
||||
|
||||
<from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
|
||||
<from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
|
||||
|
||||
<inline-string id="instantNotification">
|
||||
<value>
|
||||
|
|
|
@ -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
|
||||
*
|
||||
* <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
|
||||
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
|
||||
*
|
||||
* <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( )
|
||||
{
|
||||
// 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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <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 >( ) {
|
||||
|
||||
@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
|
||||
*
|
||||
* <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
|
||||
public boolean setOptions( String... options )
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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( );
|
||||
}
|
||||
}
|
|
@ -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( );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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( );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Reference in a new issue