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

@ -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>

View file

@ -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 )
{

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.com.deepclone.lw=DEBUG, fullDebug
log4j.logger.com.deepclone.lw.interfaces.eventlog=DEBUG, server
log4j.logger.com.deepclone.lw.cli=INFO, stdout