DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world
Auto-test For Equals Function In Java Classes Through Annotations
See comments and http://stackoverflow.com/questions/190007
/**
*
*/
package test;
import static test.MyClass.GenericConstants.COLON;
import static test.MyClass.GenericConstants.DOT;
import static test.MyClass.GenericConstants.EXCLAMATION_MARK;
import static test.MyClass.GenericConstants.SIMPLE_QUOTE;
import static test.MyClass.GenericConstants.SPACE;
import static test.MyClass.EqualsInstancesDirective.InstanceStatus.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import junit.framework.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.runners.TextListener;
import org.junit.runner.JUnitCore;
import test.MyClass.ArrayStringParser.ArrayDimension;
/**
* Class with overridden hash() and equals() method to be automatically tested. <br />
* The appropriate parameters for instances to be tested for equality
* are stored in annotations just above the equals() method itself. <br />
* JDK6, JUnit4.4, this class is also a test case, and its main function will launch JUnit on itself.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
* @see <a href="http://stackoverflow.com/questions/190007/automagic-unit-tests-for-upholding-object-method-contracts-in-java">
* Automagic unit tests for upholding Object method contracts in Java?</a>
*/
public class MyClass {
/*
* ########################################################################
* Private attributes for this object.
* They will play a role in the equal() and hash() methods
* They also involve a non-empty constructor
* ########################################################################
*/
private String string = null;
private int integer = 0;
private boolean check = false;
/*
* ########################################################################
* Constructors to be used in equals() tests
* They may be a default constructor, but it is not always the case
* ########################################################################
*/
/**
* Default constructor, which must always, since it is declared, be used for equals() test. <br />
* By default, the string is null, integer 0 and check false
*/
public MyClass() {/**/}
/**
* Constructor with parameter, to be used if equals() comes with the right annotations. <br />
* That is annotations representing the right Api
* @param aString
* @param anInteger
* @param aCheck
*/
public MyClass(final String aString, final int anInteger, final boolean aCheck)
{
this.string = aString;
this.integer = anInteger;
this.check = aCheck;
}
/*
* ########################################################################
* Definition of the runtime annotation used to specify valid equality tests
* The directive is about how to build the right valid instances
* ########################################################################
*/
/**
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Directives {
/** array of directives. */
EqualsInstancesDirective[] value();
}
/**
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface EqualsInstancesDirective{
/** Parameters for the constructors.*/
String parameters();
/** Checks if those parameters builds a default value or not (by default, not). */
InstanceStatus status() default NOT_DEFAULT;
/** The instance can either be equal to default value or not. */
public static enum InstanceStatus { DEFAULT, NOT_DEFAULT }
}
/*
* ########################################################################
* Hash() and equals() overridden methods
* They should always be defined: if only one is overridden,
* any equals() test must fail immediately
* ########################################################################
*/
/**
* Basic equals() function: if all private instances are equal, the objects are equal. <br />
* The trick is to know how to build the right instances, hence the EqualsInstancesDirective annotation
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
@Directives({
@EqualsInstancesDirective(parameters = "\"test1\", -1, true"),
@EqualsInstancesDirective(parameters = "\"test2\", 2, false"),
@EqualsInstancesDirective(parameters = "null, 0, false", status=DEFAULT)
})
public final boolean equals(final Object obj) {
boolean res = false;
if(obj != null && obj instanceof MyClass)
{
final MyClass aMyClass = (MyClass)obj;
if(this.integer == aMyClass.integer && this.check == aMyClass.check)
{
if(this.string == null && aMyClass.string == null)
{
res = true;
}
else if(this.string != null && aMyClass.string != null && this.string.equals(aMyClass.string))
{
res = true;
}
}
}
return res;
}
/**
* Computes hash for this objects, based on Based on a FNV hash.
* @see java.lang.Object#hashCode()
*/
@Override
public final int hashCode() {
String aString = Integer.toString(this.integer) + "~" + Boolean.toString(this.check);
if(this.string != null)
{
aString = aString + "~" + this.string;
}
final int anHashCode = (int)FnvHash.fnvHashCode(aString);
return anHashCode;
}
/**
* @see java.lang.Object#toString()
*/
@Override
public final String toString()
{
return this.string + ": " + this.integer + "(" + this.check + ")";
}
/*
* ########################################################################
* The following classes should be separate, in a JUNIT package section.
* They are here to keep this test self-contained
* ########################################################################
*/
/**
* @throws java.lang.Exception
*/
@Before
public final void setUp() throws Exception {
System.out.println("setup");
}
private void testClassesWithEquals(final List<Class<?>> someClasses)
{
for (final Class<?> aClass : someClasses)
{
final Constructor<?>[] someConstructors = aClass.getDeclaredConstructors();
if(someConstructors.length > 1)
{
final List<Object> someValues = new ArrayList<Object>();
final List<Object> someDefaultValues = new ArrayList<Object>();
final InstanceBuilder aDefaultInstanceBuilder = new InstanceBuilder(aClass, null);
someDefaultValues.add(aDefaultInstanceBuilder.build());
Method anEqualMethod;
try {
anEqualMethod = aClass.getDeclaredMethod("equals", Object.class);
fillValues(aClass, someValues, someDefaultValues, anEqualMethod);
testObjectsWithEquals(someDefaultValues, someValues);
} catch (final SecurityException e) {
e.printStackTrace();
} catch (final NoSuchMethodException e) {
e.printStackTrace();
}
}
}
}
private void fillValues(final Class<?> aClass,
final List<Object> someValues,
final List<Object> someDefaultValues, final Method anEqualMethod)
{
final Annotation[] someAnotations = anEqualMethod.getAnnotations();
if(someAnotations != null && someAnotations.length > 0)
{
for (final Annotation anAnnotation : someAnotations) {
if(anAnnotation.annotationType().getName().equals(Directives.class.getName()))
{
final EqualsInstancesDirective[] someDirectives = ((Directives)anAnnotation).value();
for (final EqualsInstancesDirective anEqualsInstancesDirective : someDirectives) {
final InstanceBuilder anInstanceBuilder = new InstanceBuilder(aClass, anEqualsInstancesDirective);
if(anEqualsInstancesDirective.status().equals(EqualsInstancesDirective.InstanceStatus.DEFAULT))
{
someDefaultValues.add(anInstanceBuilder.build());
}
else
{
someValues.add(anInstanceBuilder.build());
}
}
}
}
}
}
private void testObjectsWithEquals(final List<Object> someDefaultValues, final List<Object> someValues) {
for (final Object aDefaultValue : someDefaultValues) {
testObject(aDefaultValue, someDefaultValues, true);
testObject(aDefaultValue, someValues, false);
}
for (final Object aValue : someValues) {
testObject(aValue, someValues, false);
}
}
private void testObject(final Object aValue, final List<Object> someValues, final boolean isEqual) {
for (final Object anotherValue : someValues) {
if(isEqual || aValue != anotherValue)
{
if(aValue.equals(anotherValue) != isEqual)
{
Assert.failNotSame("Equality test fails between '"+aValue.toString()
+ "' and '"+anotherValue.toString()+"'",
Boolean.valueOf(isEqual), Boolean.valueOf(!isEqual));
}
}
}
}
/**
* @throws MyException
*
*/
@Test
public final void testEquals() throws MyException
{
System.out.println("testEquals");
final ClassWithAnnotedEqualsDetector aDetector = new ClassWithAnnotedEqualsDetector("test");
final List<Class<?>> someClasses = aDetector.getClasses();
for (final Class<?> aClass : someClasses) {
System.out.println("Will test equals on: " + aClass.getName());
}
testClassesWithEquals(someClasses);
}
/**
* @throws java.lang.Exception
*/
@After
public final void tearDown() throws Exception {
System.out.println("tearDown");
}
/**
* Launches the JUnit test on itself. <br />
* Self-contained test
* @param args ignored
*/
public static void main(final String... args)
{
final JUnitCore aRunner = new JUnitCore();
final TextListener aTextListener = new TextListener();
aRunner.addListener(aTextListener);
aRunner.run(MyClass.class);
}
/*
* ########################################################################
* The following classes should be in their on java files
* with their own packages.
* They are here to keep this test self-contained
* ########################################################################
*/
/**
* Computes a FNV hash. <br />
* Used for this short key example test class.
* @see <a href="http://stackoverflow.com/questions/114085/what-is-a-performant-string-hashing-function-that-results-in-a-32-bit-integer-w#114102">
* What is a performant string hashing function that results in a 32 bit integer with low collision rates? </a>
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class FnvHash {
private FnvHash() {/* */}
private static final int K_FNV_OFFSET = (int) 2166136261L;
private static final int K_FNV_PRIME = 16777619;
/**
* Computes a Fowler/Noll/Vo (FNV) hash. <br />
* Useful for short key
* From http://forums.devx.com/showthread.php?t=141108
* @param str identifier of the object (MUST NOT BE NULL)
* @return hash value, always defined
*/
public static long fnvHashCode(final String str) {
final int anHashShift = 0x0000ffff;
final long anHashLongShift = 0x00000000ffffffffL;
int hash = K_FNV_OFFSET;
for (int i = 0; i < str.length(); i++) {
hash ^= (anHashShift & str.charAt(i));
hash *= K_FNV_PRIME;
}
return anHashLongShift & hash;
}
}
/*
* ########################################################################
* Parse all classpath locations to seek eligible classes to test.
* Meaning: classes with a criteria making them eligible
* Hence the Eligible interface
* ########################################################################
*/
/**
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static class ClassWithAnnotedEqualsDetector
{
private String packageName = null;
/**
* @param aPackageName
* @throws MyException
*/
public ClassWithAnnotedEqualsDetector(final String aPackageName) throws MyException
{
this.packageName = aPackageName;
if(StringUtils.isEmpty(this.packageName)) { throw new MyException("a package must be provided"); }
}
/**
* @return list of classes with annotated equals, empty if none, never null
* @throws MyException
*/
public final List<Class<?>> getClasses() throws MyException
{
final List<Class<?>> someClasses = ReflectionUtils.getClasses(this.packageName);
final List<Class<?>> someClassesWithEquals = new ArrayList<Class<?>>();
for (final Class<?> aClass : someClasses) {
try {
final Method anEqualMethod = aClass.getDeclaredMethod("equals", Object.class);
// must check if hashCode is also defined
final Method anHashMethod = aClass.getDeclaredMethod("hashCode");
// but there is no usage for that variable
JavaUtils.mockAction(anEqualMethod);
JavaUtils.mockAction(anHashMethod);
someClassesWithEquals.add(aClass);
} catch (final SecurityException e) {
// nothing to do
JavaUtils.mockAction();
} catch (final NoSuchMethodException e) {
// nothing to do
JavaUtils.mockAction();
}
}
return someClassesWithEquals;
}
}
/**
* Utilities based on reflection java method. <br />
* Used to examine dynamic java instances
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class ReflectionUtils
{
private ReflectionUtils() {/* */}
private static final String POURCENT_20 = "%20";
private static final String DOT_CLASS = ".class";
private static final int DOT_CLASS_LENGTH = 6;
//private static final String DOT_JAVA = ".java";
//private static final int DOT_JAVA_LENGTH = 5;
private static final String DOT_JAR = ".jar";
private static final String UNABLE_TO_FIND_CLASS_NAMED_QUOTE = "Unable to find class named '";
/**
* Scans all classes accessible from the context class loader which belong to the given package and sub-packages. <br />
* Look within directories resources and within jar resources
* @param packageName The base package
* @return The classes found, empty list if none found
* @throws MyException if problem during class detection
* @throws Error if unable to access resources of class loader, to look within a jar resource or to find a class
*/
public static List<Class<?>> getClasses(final String packageName) throws MyException
{
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//assert classLoader != null;
final String path = packageName.replace('.', '/');
final List<Class<?>> classes = new ArrayList<Class<?>>();
Enumeration<URL> anEnumerator = null;
try
{
anEnumerator = classLoader.getResources(path);
}
catch(final IOException ioe)
{
throw new MyException("Unable to access resources of current thread class loader for path '" + path + SIMPLE_QUOTE, ioe);
}
if (anEnumerator != null)
{
while (anEnumerator.hasMoreElements())
{
String filePath = anEnumerator.nextElement().getFile();
// WINDOWS HACK
if(StringUtils.strictContains(filePath, POURCENT_20))
{
filePath = filePath.replaceAll(POURCENT_20, SPACE);
}
if (filePath != null)
{
if (StringUtils.strictContains(filePath,EXCLAMATION_MARK) && StringUtils.strictContains(filePath,DOT_JAR))
{
String jarPath = filePath.substring(0, filePath.indexOf(EXCLAMATION_MARK)).substring(filePath.indexOf(COLON) + 1);
// WINDOWS HACK
if (jarPath.indexOf(COLON) >= 0) { jarPath = jarPath.substring(1); }
classes.addAll(getClassesFromJARFile(jarPath, path));
}
else
{
classes.addAll(getClassesFromDirectory(new File(filePath), packageName));
}
}
}
}
return classes;
}
private static List<Class<?>> getClassesFromJARFile(final String jar, final String packageName) throws MyException
{
final List<Class<?>> classes = new ArrayList<Class<?>>();
JarInputStream jarFile = null;
final StringBuffer anExceptionMessage = new StringBuffer();
try
{
jarFile = new JarInputStream(new FileInputStream(jar));
JarEntry jarEntry;
do
{
try
{
jarEntry = jarFile.getNextJarEntry();
}
catch(final IOException ioe)
{
throw new MyException(
anExceptionMessage.append("Unable to get next jar entry from jar file '").
append(jar).append(SIMPLE_QUOTE).toString(), ioe);
}
finally
{
//closeJarFile(jarFile);
JavaUtils.mockAction();
}
if (jarEntry != null)
{
extractClassFromJar(jar, packageName, classes, jarEntry);
}
} while (jarEntry != null);
closeJarFile(jarFile);
}
catch(final IOException ioe)
{
throw new MyException(
anExceptionMessage.append("Unable to get Jar input stream from '").append(jar).append(SIMPLE_QUOTE).toString(), ioe);
}
finally
{
closeJarFile(jarFile);
}
return classes;
}
/**
* @param jar
* @param packageName
* @param classes
* @param jarEntry
* @throws Error
*/
private static void extractClassFromJar(final String jar, final String packageName,
final List<Class<?>> classes, final JarEntry jarEntry) throws MyException
{
String className = jarEntry.getName();
if (className.endsWith(DOT_CLASS))
{
className = className.substring(0, className.length() - DOT_CLASS_LENGTH);
if (className.startsWith(packageName))
{
try
{
classes.add(Class.forName(className.replace('/', '.')));
} catch (final ClassNotFoundException cnfe)
{
throw new MyException(UNABLE_TO_FIND_CLASS_NAMED_QUOTE + className.replace('/', '.')
+ "' within jar '" + jar + SIMPLE_QUOTE, cnfe);
}
}
}
}
/**
* @param jarFile
*/
private static void closeJarFile(final JarInputStream jarFile)
{
if(jarFile != null)
{
try
{
jarFile.close();
}
catch(final IOException ioe)
{
JavaUtils.mockAction();
}
}
}
/**
* Recursive method used to find all classes in a given directory and subdirs.
*
* @param directory The base directory
* @param packageName The package name for classes found inside the base directory
* @return The classes (empty list if none found)
* @throws ClassNotFoundException
*/
private static List<Class<?>> getClassesFromDirectory(final File directory, final String packageName) throws MyException
{
final List<Class<?>> classes = new ArrayList<Class<?>>();
if (directory.exists())
{
final File[] files = directory.listFiles();
for (int iFiles = 0; iFiles < files.length; iFiles++)
{
final File file = files[iFiles];
if (file.isDirectory() && file.getName().indexOf(DOT) == -1) {
//assert ;
classes.addAll(getClassesFromDirectory(file,
new StringBuffer().append(packageName).append(DOT).append(file.getName()).toString()));
} else if (file.getName().endsWith(DOT_CLASS)) {
final String aClassName =
new StringBuffer().append(packageName).append('.').
append(file.getName().substring(0, file.getName().length() - DOT_CLASS_LENGTH)).toString();
try
{
classes.add(Class.forName(aClassName));
} catch (final ClassNotFoundException cnfe)
{
throw new MyException(
new StringBuffer().append(UNABLE_TO_FIND_CLASS_NAMED_QUOTE).append(aClassName).
append("' within directory '").append(directory.getAbsolutePath()).
append(SIMPLE_QUOTE).toString(), cnfe);
}
}
}
}
return classes;
}
/**
* Check if aClass is an subclass of anotherClass. <br />
* USefull when the Type is not statically known
* @param aClass class to check (regarding its superclass)
* @param anotherClass reference class
* @return false if one of the parameters is null, if no relation is found. True otherwise (subclass or equality)
*/
public static boolean isAsSubclassOf(final Class<?> aClass, final Class<?> anotherClass)
{
boolean res = false;
if(aClass != null && anotherClass != null)
{
if(aClass == anotherClass) { res = true; }
else
{
try
{
aClass.asSubclass(anotherClass);
res = true;
}
catch(final ClassCastException e)
{
JavaUtils.mockAction(e);
res = false;
}
}
}
return res;
}
/**
* Get the number of dimensions represented by the java array. <br />
* int[][][] will gives 3
* @param anArrayClass Class representing an array of a class, can be null
* @return 0 if not an array, class otherwise
*/
public static int getArrayClassDimensionsNumber(final Class<?> anArrayClass) {
int anArrayClassDimensionsNumber = 0;
if (anArrayClass != null && anArrayClass.isArray())
{
try
{
Class<?> anArrayActualClass = anArrayClass;
while (anArrayActualClass.isArray())
{
anArrayClassDimensionsNumber = anArrayClassDimensionsNumber + 1;
anArrayActualClass = anArrayActualClass.getComponentType();
}
} catch (final Throwable e) { /*FALLTHRU*/ JavaUtils.mockAction(); }
}
return anArrayClassDimensionsNumber;
}
/**
* Get the Class represented by the java array. <br />
* int[][][] will gives Integer
* @param anArrayClass Class representing an array of a class, can be null
* @return null if not an array, class otherwise
*/
public static Class<?> getArrayClass(final Class<?> anArrayClass) {
Class<?> anArrayActualClass = null;
if (anArrayClass != null && anArrayClass.isArray())
{
try
{
anArrayActualClass = anArrayClass;
while (anArrayActualClass.isArray())
{
anArrayActualClass = anArrayActualClass.getComponentType();
}
} catch (final Throwable e) { /*FALLTHRU*/ JavaUtils.mockAction(); }
}
return anArrayActualClass;
}
}
/**
* Applicative Exception to encapsulate all internal explicit exceptions encountered. <br />
* Used for applicative static exception container.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class MyException extends Exception
{
private static final long serialVersionUID = -8257625811507034547L;
/**
* Constructs a new exception with the specified detail message.
* @param message the detail message
* @see java.lang.Exception#Exception(java.lang.String)
*/
public MyException(final String message) {
super(message);
}
/**
* Constructs a new exception with the specified detail message and
* cause.
* @param message the detail message
* @param cause the cause
* @see java.lang.Exception#Exception(java.lang.String, java.lang.Throwable)
*/
public MyException(final String message, final Throwable cause) {
super(message, cause);
}
/**
* Constructs a new exception with the specified cause and a detail
* message.
* @param cause the cause
* @see java.lang.Exception#Exception(java.lang.Throwable)
*
*/
public MyException(final Throwable cause) {
super(cause);
}
}
/**
* Generic constants that can be used anywhere. <br />
* Mostly String constants.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class GenericConstants
{
private GenericConstants() {/* */}
/**
* space ' ' (no quotes).
*/
public static final String SPACE= " ";
/**
* Exclamation mark !.
*/
public static final String EXCLAMATION_MARK = "!";
/**
* colon ':'.
*/
public static final String COLON = ":";
/**
* Simple quote '.
*/
public static final String SIMPLE_QUOTE = "'";
/**
* dot '.'.
*/
public static final String DOT = ".";
/**
* Closing round bracket ).
*/
public static final String CLOSING_ROUND_BRACKET= ")";
}
/**
* Generic java utilities functions. <br />
* Encapsulate basic operations.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class JavaUtils
{
private JavaUtils() {/* */}
/**
* Mock Action, do nothing. <br />
* Used for FindBugs workaround: <br />
* DLS: Dead store of class literal
* This instruction assigns a class literal to a variable and then never uses it.
* @param anObject an object (ignored if null)
*/
static void mockAction(final Object anObject)
{
if(returnsFalse() && anObject != null)
{
JavaUtils.mockAction(); //System. out.println("do nothing on " + o.toString());
}
}
/**
* Mock Action, do nothing. <br />
* Used for CheckStyle workaround: <br />
* "This block should not be empty".
*/
static void mockAction()
{
if(returnsFalse())
{
JavaUtils.mockAction(); //System. out.println("do nothing");
}
}
/**
* Returns boolean false. <br />
* Used for FindBugs workaround like a method not called: <br />
* CN: Class implements Cloneable but does not define or use clone method
* @return false
*/
private static boolean returnsFalse()
{
return false;
}
}
/**
* Generic String utilities functions. <br />
* Encapsulate basic operations.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class StringUtils
{
private StringUtils() {/* */}
/**
* Check if a string is empty. <br />
* Meaning null or length == 0 (empty string)
* @param s string to be tested
* @return true if empty, false otherwise
*/
public static boolean isEmpty(final String s)
{
return !isNotEmpty(s);
}
/**
* Check if a string is not empty. <br />
* Meaning not null and length > 0
* @param s string to be tested
* @return true if not empty, false otherwise
*/
public static boolean isNotEmpty(final String s)
{
final boolean notempty = (s != null) && s.length() > 0;
return notempty;
}
/**
* Check if a string contains another string at a position greater than zero<br />
* Means that a string "strictly" contains another if and only if the substring is found
* but not at the beginning of the string<br />
* "abcd", "ab" => false. <br />
* "abcd", "bc" => true. <br />
* Avoid "findBugs warning" <b>Method checks to see if result of String.indexOf is positive</b> <br />
* "The method invokes String.indexOf and checks to see if the result is positive or non-positive.
* It is much more typical to check to see if the result is negative or non-negative.
* It is positive only if the substring checked for occurs
* at some place other than at the beginning of the String."
* @param aString string containing or not the substring
* @param amsg substring looked for in 'aString'
* @return true if substring found, false otherwise (null parameters, ...)
*/
static boolean strictContains(final String aString, final String amsg)
{
return strictContains(aString, amsg, 0);
}
/**
* Check if a string contains another string at a position greater than a positive index<br />
* Means that a string "strictly" contains another if and only if the substring is found
* but not at the beginning of the string<br />
* "abcd", "ab" => false. <br />
* "abcd", "c" => true if positive index equals '1'. <br />
* Avoid "findBugs warning" <b>Method checks to see if result of String.indexOf is positive</b> <br />
* "The method invokes String.indexOf and checks to see if the result is positive or non-positive.
* It is much more typical to check to see if the result is negative or non-negative.
* It is positive only if the substring checked for occurs
* at some place other than at the beginning of the String."
* @param aString string containing or not the substring
* @param aMsg substring looked for in 'aString'
* @param anIndex positive index
* @return true if substring found, false otherwise (null parameters, ...)
*/
private static boolean strictContains(final String aString, final String aMsg, final int anIndex)
{
boolean res = false;
if(aString != null && aMsg != null && aMsg.length() > 0)
{
final int i = aString.indexOf(aMsg);
final boolean notcontains = i < 0;
if(!notcontains && i > anIndex)
{
res = true;
}
}
return res;
}
}
/*
* ########################################################################
* The following methods are used to find the right constructor for a given set of parameters.
* They could be grouped in a separated static class
* ########################################################################
*/
/**
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static final class InstanceBuilder
{
private Class<?> myClass = null;
private EqualsInstancesDirective directive = null;
/**
* @param aClass
* @param anEqualsInstancesDirective
*/
public InstanceBuilder(final Class<?> aClass, final EqualsInstancesDirective anEqualsInstancesDirective)
{
this.myClass = aClass;
this.directive = anEqualsInstancesDirective;
}
Object build() {
Object aValue = null;
String[] someArgs = new String[] {};
if(this.directive != null)
{
someArgs = this.directive.parameters().split(",\\s*");
}
final Object[] someParameters = new Object[someArgs.length];
final Constructor<?> aConstructor = findConstructor(this.myClass, someArgs, someParameters);
if(aConstructor == null)
{
Assert.fail("Unable to find constructor for class '" + this.myClass.getName() + "' directive: " + this.directive);
}
else
{
Exception anException = null;
try {
aValue = aConstructor.newInstance(someParameters);
} catch (final IllegalArgumentException e) {
anException = e;
} catch (final InstantiationException e) {
anException = e;
} catch (final IllegalAccessException e) {
anException = e;
} catch (final InvocationTargetException e) {
anException = e;
}
if(anException != null)
{
Assert.fail("Unable to build instance from constructor for class '" + this.myClass.getName()
+ "' because of '" + anException.getMessage());
}
}
return aValue;
}
private static Constructor<?> findConstructor(final Class<?> aClass, final String[] someArgs, final Object[] someParameters)
{
Constructor<?> aConstructor = null;
final Constructor<?>[] someDeclaredConstructors = aClass.getDeclaredConstructors();
for (final Constructor<?> aDeclaredConstructor : someDeclaredConstructors)
{
final Class<?>[] someConstructorParameters = aDeclaredConstructor.getParameterTypes();
if(someConstructorParameters.length == someArgs.length)
{
final boolean paramsOk = fillParameters(someArgs, someParameters, someConstructorParameters);
if(paramsOk)
{
aConstructor = aDeclaredConstructor;
break;
}
}
}
return aConstructor;
}
private static boolean fillParameters(final String[] someArgs, final Object[] someParameters,
final Class<?>[] someConstructorParameters)
{
boolean paramsOk = true;
int iConstructorParam = 0;
for (final Class<?> aConstructorParamClass : someConstructorParameters)
{
if(ReflectionUtils.isAsSubclassOf(aConstructorParamClass, String.class)== false)
{
if(aConstructorParamClass.isPrimitive())
{
if(fillParameter(aConstructorParamClass, iConstructorParam, someArgs[iConstructorParam], someParameters) == false)
{
paramsOk = false;
break;
}
}
else if(aConstructorParamClass.isArray())
{
if(fillArrayParameter(aConstructorParamClass, iConstructorParam, someArgs[iConstructorParam], someParameters) == false)
{
paramsOk = false;
break;
}
}
else
{
paramsOk = false;
break;
}
}
else
{
someParameters[iConstructorParam] = someArgs[iConstructorParam];
if(someArgs[iConstructorParam].equals("null"))
{
someParameters[iConstructorParam] = null;
}
}
iConstructorParam++;
}
return paramsOk;
}
private static boolean fillParameter(final Class<?> aConstructorParamClass, final int anIndexConstructorParams,
final String aParameter, final Object[] someParameters)
{
Object aParameterValue = null;
try
{
aParameterValue = getParameterObjFromPrimitiveArg(aConstructorParamClass, aParameter);
}
catch(final NumberFormatException nfe)
{
//isParameterSucessfullyFilled = false;
JavaUtils.mockAction();
}
if(aParameterValue!= null)
{
someParameters[anIndexConstructorParams] = aParameterValue;
}
return aParameterValue != null;
}
private static Object getParameterObjFromPrimitiveArg(final Class<?> methodParamClass, final String aParameter)
{
Object aParameterValue = null;
if(isInteger(methodParamClass))
{
aParameterValue = Integer.valueOf(aParameter);
}
else if(isBoolean(methodParamClass))
{
aParameterValue = Boolean.valueOf(aParameter);
}
else if(isCharacter(methodParamClass))
{
if(aParameter.length() == 1)
{
aParameterValue = Character.valueOf(aParameter.charAt(0));
}
}
else if(isLong(methodParamClass))
{
aParameterValue = Long.valueOf(aParameter);
}
else if(isDouble(methodParamClass))
{
aParameterValue = Double.valueOf(aParameter);
}
else if(isFloat(methodParamClass))
{
aParameterValue = Float.valueOf(aParameter);
}
else if(isByte(methodParamClass))
{
aParameterValue = Byte.valueOf(aParameter);
}
else if(isShort(methodParamClass))
{
aParameterValue = Short.valueOf(aParameter);
}
return aParameterValue;
}
private static boolean isInteger(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Integer.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Integer.TYPE); }
private static boolean isBoolean(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Boolean.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Boolean.TYPE); }
private static boolean isCharacter(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Character.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Character.TYPE); }
private static boolean isLong(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Long.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Long.TYPE); }
private static boolean isDouble(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Double.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Double.TYPE); }
private static boolean isFloat(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Float.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Float.TYPE); }
private static boolean isByte(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Byte.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Byte.TYPE); }
private static boolean isShort(final Class<?> methodParamClass) {
return ReflectionUtils.isAsSubclassOf(methodParamClass, Short.class) || ReflectionUtils.isAsSubclassOf(methodParamClass, Short.TYPE); }
private static boolean fillArrayParameter(final Class<?> methodParamClass, final int indexMethodParams,
final String aParameter, final Object[] someParameters)
{
boolean isParameterSucessfullyFilled = true;
final int anArrayDimensionNumber = ReflectionUtils.getArrayClassDimensionsNumber(methodParamClass);
final ArrayStringParser anArrayStringParser = new ArrayStringParser(aParameter);
int anArrayStringDepth = -1;
try
{
anArrayStringDepth = anArrayStringParser.getNbDimensions();
if(anArrayStringDepth != anArrayDimensionNumber)
{
isParameterSucessfullyFilled = false;
}
final Class<?> anArrayClass = ReflectionUtils.getArrayClass(methodParamClass);
final Object anArray = getParameterArrayObjFromPrimArg(anArrayClass, methodParamClass, anArrayStringParser.getMainArrayDimension());
if(anArray != null)
{
someParameters[indexMethodParams] = anArray;
}
else
{
isParameterSucessfullyFilled = false;
}
}
catch(final MyException cce)
{
isParameterSucessfullyFilled = false;
//LOG.warning("Method " + aMethodName + " is rejected because parameter number "
//+ indexMethodParams + " can not be parsed: " + cce.getMessage());
}
return isParameterSucessfullyFilled;
}
private static Object getParameterArrayObjFromPrimArg(final Class<?> anArrayClass, final Class<?> anArrayClassArray,
final ArrayDimension anArrayDimension) throws MyException
{
Object anArray = null;
final boolean hasValues = anArrayDimension.hasValues();
final int aNumberOfElements = anArrayDimension.getNbElements();
final Class<?> aSubArrayClass = anArrayClassArray.getComponentType();
final int[] someDimensions = new int[] { aNumberOfElements } ;
anArray = Array.newInstance(aSubArrayClass, someDimensions);
for(int jNbElems = 0; jNbElems < aNumberOfElements; jNbElems++)
{
Object aParameter = null;
if(hasValues)
{
final String aValue = anArrayDimension.getValue(jNbElems);
aParameter = getParameterObjFromPrimitiveArg(anArrayClass, aValue);
}
else
{
aParameter = getParameterArrayObjFromPrimArg(anArrayClass, aSubArrayClass, anArrayDimension.getChild(jNbElems));
}
Array.set(anArray, jNbElems, aParameter);
}
return anArray;
}
}
/**
* Able to parse a string representation of an array. <br />
* Check if well-formed and returns java array of string values (one array by dimension detected). <br />
* Can be a string representation with various dimension and value delimiters, and separators (for value and delimiters), like in:
* <pre>[ { "first value first dimension", 'second value first dimension" ] ,
* { 'first value second dimension'; 'second value second dimension" } }</pre>
* (here, the dimension delimiters are '{' and '[', and they do not necessary matches,
* the value delimiters are " and ', and they do not necessary matches,
* the value separators are ',' or ';'. <br />
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static class ArrayStringParser
{
private final char[] dimensionOpeningDelimiters = new char[] {'[' };
private final char[] dimensionEndingDelimiters = new char[] {']' };
private final char[] valueDelimiters = new char[] {'\"' };
private final char[] dimensionSeparator = new char[] {',' };
private final char[] valueSeparator = new char[] {',' };
private String array = null;
private ArrayDimension mainDimension = null;
private ArrayDimension currentDimension = null;
private StringBuffer currentValue = null;
private int nbDimensions = 0;
/**
* Build a parser with default dimension and value delimiters, and default dimension and value separators. <br />
* Means '[', '"' and ',' are used.
* @param anArray String representation of an array, can be null or empty
*/
public ArrayStringParser(final String anArray)
{
this.array = anArray;
}
private static final int DIMENSION_EXPECTED = 0;
private static final int DIMENSION_OR_VALUE_EXPECTED = 1;
private static final int END_VALUE_EXPECTED = 2;
private static final int END_DIMENSION_OR_VALUE_SEPARATOR_EXPECTED = 3;
private static final int END_DIMENSION_OR_DIMENSION_SEPARATOR_EXPECTED = 4;
private static final int VALUE_EXPECTED = 5;
private int mode = DIMENSION_EXPECTED;
private int parserIndex = 0;
private Boolean hasBeenParsed = Boolean.FALSE;
private static final Object GUARD = new Object();
/**
* Parse the memorize String representation of an array. <br />
* Will ignore any first or trailing spaces, tabs or newline characters (ignored also between values)
* @throws MyException if problem during parsing
*/
public final void parse() throws MyException
{
synchronized (GUARD)
{
if(this.hasBeenParsed.booleanValue() == false)
{
this.currentDimension = null;
this.mainDimension = null;
this.mode = DIMENSION_EXPECTED;
this.parserIndex = 0;
if(StringUtils.isNotEmpty(this.array))
{
while(this.parserIndex < this.array.length())
{
if(this.mode == DIMENSION_EXPECTED) { parseDimensionExpected(); }
if(this.mode == DIMENSION_OR_VALUE_EXPECTED) { parseDimensionOrValueExpected(); }
if(this.mode == END_VALUE_EXPECTED) { parseEndValueExpected(); }
if(this.mode == END_DIMENSION_OR_VALUE_SEPARATOR_EXPECTED) { parseEndDimOrValueSeparExpected(); }
if(this.mode == END_DIMENSION_OR_DIMENSION_SEPARATOR_EXPECTED) { parseEndDimOrDimSeparExpected(); }
if(this.mode == VALUE_EXPECTED) { parseValueExpected(); }
}
this.nbDimensions = checkArrayDimensionNumber(this.mainDimension);
}
this.hasBeenParsed = Boolean.TRUE;
}
}
}
/**
* Get the number of dimensions of this array, based on number of dimensions parsed. <br />
* If the String has not been parsed yet, this call will trigger the parse() method automatically (only once).
* @return number of dimensions detected, 1 or more
* @throws MyException if trouble during parsing
*/
public final int getNbDimensions() throws MyException
{
parse();
return this.nbDimensions;
}
/**
* Get main Array Dimension as parsed. <br />
* If the String has not been parsed yet, this call will trigger the parse() method automatically (only once).
* @return main Array Dimension, never null
* @throws MyException if trouble during parsing
*/
public final ArrayDimension getMainArrayDimension() throws MyException
{
parse();
return this.mainDimension;
}
private int checkArrayDimensionNumber(final ArrayDimension aDimension) throws MyException
{
int aNbDimFound = 0;
if(aDimension != null)
{
int aChildDepth = 0;
for (final ArrayDimension aChildArrayDimension : aDimension.getChildren())
{
aChildDepth = checkArrayDimensionNumber(aChildArrayDimension);
if(aChildDepth != aNbDimFound && aNbDimFound > 0)
{
throw new MyException(new StringBuffer().append("Incoherent depth found on child number ").
append(aDimension.getChildren().indexOf(aChildArrayDimension)).toString());
}
aNbDimFound = aChildDepth;
}
if(aDimension.getChildren().size() == 0)
{
aNbDimFound = aDimension.getDepth() + 1;
}
}
return aNbDimFound;
}
/**
* ' \t\n\r\x0B\f'.
* @param c char to check
*/
private boolean isWhiteSpace(final char c)
{
if(c == ' ' || c == '\t' || c == '\n')
{
return true;
}
if(c == '\r' || c == '\f' || c == '\u000B')
{
return true;
}
return false;
}
private boolean isOpeningDimension(final char c)
{
return isPartOf(this.dimensionOpeningDelimiters, c);
}
private boolean isEndingDimension(final char c)
{
return isPartOf(this.dimensionEndingDelimiters, c);
}
private boolean isValueDelimiter(final char c)
{
return isPartOf(this.valueDelimiters, c);
}
private boolean isValueSeparator(final char c)
{
return isPartOf(this.valueSeparator, c);
}
private boolean isDimensionSeparator(final char c)
{
return isPartOf(this.dimensionSeparator, c);
}
private boolean isPartOf(final char[] someChars, final char aChar)
{
for (int ichars = 0; ichars < someChars.length; ichars++)
{
final char c = someChars[ichars];
if(c == aChar)
{
return true;
}
}
return false;
}
private String displayCharArray(final char[] someChars, final boolean isShort)
{
final StringBuffer msg = new StringBuffer("");
if(someChars.length == 1)
{
if(isShort) { msg.append("the following character "); }
msg.append('\'').append(String.valueOf(someChars[0])).append('\'');
}
else
{
if(isShort) { msg.append("one of the following characters: "); }
msg.append('\'');
for (int iChars = 0; iChars < someChars.length; iChars++)
{
final char c = someChars[iChars];
msg.append(String.valueOf(c)).append("'");
if(iChars < (someChars.length - 1))
{
msg.append(", '");
}
}
}
return msg.toString();
}
private String getUnableToParseMessage()
{
final StringBuffer aMsg = new StringBuffer("Unable to parse '");
aMsg.append("': ");
if(this.parserIndex < this.array.length())
{
if(this.parserIndex> 0)
{
aMsg.append(this.array.substring(0, this.parserIndex -1)).append('\'');
}
aMsg.append(">>>").append(this.array.charAt(this.parserIndex)).append("<<<");
if(this.parserIndex < (this.array.length() - 1))
{
aMsg.append('\'').append(this.array.substring(this.parserIndex + 1)).append('\'').append("");
}
}
else
{
aMsg.append(this.array).append('\'').append(">>><<<");
}
return aMsg.toString();
}
private void parseDimensionExpected() throws MyException
{
boolean isDimensionFound = false;
boolean isCharProcessed = false;
while(isDimensionFound == false)
{
final char c = this.array.charAt(this.parserIndex);
isCharProcessed = false;
if(isWhiteSpace(c))
{
isCharProcessed = true; // char ignored
}
else if(isOpeningDimension(c))
{
this.mode = DIMENSION_OR_VALUE_EXPECTED;
isDimensionFound = true;
this.currentDimension = new ArrayDimension(this.currentDimension);
if(this.mainDimension == null) { this.mainDimension = this.currentDimension; }
isCharProcessed = true;
}
if(isCharProcessed == false || (isDimensionFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects opening dimension char (").
append(displayCharArray(this.dimensionOpeningDelimiters, false)).append(")").toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
private void parseDimensionOrValueExpected() throws MyException
{
boolean isDimensionOrValueFound = false;
boolean isCharProcessed = false;
while(isDimensionOrValueFound == false)
{
final char c = this.array.charAt(this.parserIndex);
isCharProcessed = false;
if(isWhiteSpace(c))
{
isCharProcessed = true; // char ignored
}
else if(isOpeningDimension(c))
{
this.currentDimension = new ArrayDimension(this.currentDimension);
isDimensionOrValueFound = true;
isCharProcessed = true;
}
else if(isValueDelimiter(c))
{
this.currentValue = new StringBuffer();
this.mode = END_VALUE_EXPECTED;
isDimensionOrValueFound = true;
isCharProcessed = true;
}
if(isCharProcessed == false || (isDimensionOrValueFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects opening dimension (").
append(displayCharArray(this.dimensionOpeningDelimiters, true)).append(") ").
append("or a value delimiter (").append(displayCharArray(this.valueDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
private void parseValueExpected() throws MyException
{
boolean isValueFound = false;
boolean isCharProcessed = false;
while(isValueFound == false)
{
final char c = this.array.charAt(this.parserIndex);
isCharProcessed = false;
if(isWhiteSpace(c))
{
isCharProcessed = true; // char ignored
}
else if(isValueDelimiter(c))
{
this.currentValue = new StringBuffer();
this.mode = END_VALUE_EXPECTED;
isValueFound = true;
isCharProcessed = true;
}
if(isCharProcessed == false || (isValueFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects a value delimiter (").append(displayCharArray(this.valueDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
private boolean canParseNextChar()
{
return this.parserIndex < (this.array.length() - 1);
}
private void parseEndValueExpected() throws MyException
{
boolean isEndValueFound = false;
boolean isCharProcessed = false;
while(isEndValueFound == false)
{
final char c = this.array.charAt(this.parserIndex);
if(isValueDelimiter(c))
{
this.currentDimension.getValues().add(this.currentValue.toString());
this.currentValue = null;
this.mode = END_DIMENSION_OR_VALUE_SEPARATOR_EXPECTED;
isEndValueFound = true;
isCharProcessed = true;
}
else if(c == '\\')
{
this.currentValue.append(c);
if(canParseNextChar())
{
this.parserIndex = this.parserIndex + 1;
this.currentValue.append(this.array.charAt(this.parserIndex));
}
isCharProcessed = true;
}
else
{
this.currentValue.append(c);
isCharProcessed = true;
}
if(isCharProcessed == false || (isEndValueFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects end of value delimiter (").
append(displayCharArray(this.valueDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
private void parseEndDimOrValueSeparExpected() throws MyException
{
boolean isEndDimOrValueSeparatorFound = false;
boolean isCharProcessed = false;
while(isEndDimOrValueSeparatorFound == false)
{
final char c = this.array.charAt(this.parserIndex);
if(isWhiteSpace(c))
{
isCharProcessed = true; // char ignored
}
else if(isValueSeparator(c))
{
this.mode = VALUE_EXPECTED;
isEndDimOrValueSeparatorFound = true;
isCharProcessed = true;
}
else if(isEndingDimension(c))
{
this.mode = END_DIMENSION_OR_DIMENSION_SEPARATOR_EXPECTED;
this.currentDimension = this.currentDimension.getParent();
if(this.currentDimension == null)
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("one too many ending dimension character found (").
append(displayCharArray(this.dimensionEndingDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
isEndDimOrValueSeparatorFound = true;
isCharProcessed = true;
}
if(isCharProcessed == false || (isEndDimOrValueSeparatorFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects value separator (").
append(displayCharArray(this.valueSeparator, true)).append("), ").
append("or an ending dimension char (").append(displayCharArray(this.valueSeparator, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
private void parseEndDimOrDimSeparExpected() throws MyException
{
boolean isEndDimOrDimSeparatorFound = false;
boolean isCharProcessed = false;
while(isEndDimOrDimSeparatorFound == false)
{
final char c = this.array.charAt(this.parserIndex);
if(isWhiteSpace(c))
{
isCharProcessed = true; // char ignored
}
else if(isDimensionSeparator(c))
{
if(this.currentDimension == null)
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("the string representation of the array should be finished, no more ending dimension char (").
append(displayCharArray(this.dimensionEndingDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).append(" expected.").toString());
}
this.mode = DIMENSION_EXPECTED;
isEndDimOrDimSeparatorFound = true;
isCharProcessed = true;
}
else if(isEndingDimension(c))
{
if(this.currentDimension == null)
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("one too many ending dimension char found (").
append(displayCharArray(this.dimensionEndingDelimiters, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.mode = END_DIMENSION_OR_DIMENSION_SEPARATOR_EXPECTED;
this.currentDimension = this.currentDimension.getParent();
isEndDimOrDimSeparatorFound = true;
isCharProcessed = true;
}
if(isCharProcessed == false || (isEndDimOrDimSeparatorFound == false && canParseNextChar() == false))
{
throw new MyException(new StringBuffer().append(getUnableToParseMessage()).
append("expects dimension separator (").
append(displayCharArray(this.valueSeparator, true)).append("),").
append(' ').append("or an ending dimension character (").
append(displayCharArray(this.valueSeparator, true)).
append(GenericConstants.CLOSING_ROUND_BRACKET).toString());
}
this.parserIndex = this.parserIndex + 1;
}
}
/**
* Represent the dimension of an Array. <br />
* Can contains values if mono-dimensional.
* @author <a href="http://stackoverflow.com/users/6309/vonc">VonC</a>
*/
public static class ArrayDimension
{
private ArrayDimension parent = null;
private final ArrayList<ArrayDimension> children = new ArrayList<ArrayDimension>();
private final ArrayList<String> values = new ArrayList<String>();
final ArrayDimension getParent() { return this.parent; }
final ArrayList<String> getValues() { return this.values; }
final ArrayList<ArrayDimension> getChildren() { return this.children; }
ArrayDimension(final ArrayDimension aParent)
{
this.parent = aParent;
if(aParent != null)
{
aParent.children.add(this);
}
}
/**
* Check if the given dimension represents a value container. <br />
* @return true if has values, false otherwise
*/
public final boolean hasValues() /*throws MyException*/
{
boolean res = false;
if(this.children.size() == 0) { res = true; }
return res;
}
/**
* Get sub-dimension array. <br />
* Does not check if there is value or not
* @param anIndex MUST be POSITIVE and < number of children 0-based
* @return sub-array dimension
*/
public final ArrayDimension getChild(final int anIndex)
{
return this.children.get(anIndex);
}
/**
* 0-based index . <br />
* base on position within dimensions
* @return 0 or more, never negative
*/
public final int getDepth()
{
int aDepth = 0;
ArrayDimension aParent = this.parent;
while(aParent != null)
{
aDepth = aDepth + 1;
aParent = aParent.parent;
}
return aDepth;
}
/**
* Check if the given depth, gives the numbers of sub-dimensions or values. <br />
* If the String has not been parsed yet, this call will trigger the parse() method automatically (only once).
* @return Number of values if has values, number of children otherwise
*/
public final int getNbElements()
{
int nbElements = 0;
if(this.values.size() > 0)
{
nbElements = this.values.size();
}
else
{
nbElements = this.children.size();
}
return nbElements;
}
/**
* Get value for a given index. <br />
* index must take into account dimensions
* @param anIndex 0-based index, multi-dimension wide
* @return String value, never null, can be empty
*/
public final String getValue(final int anIndex)
{
String aValue = null;
if(this.children.size() == 0)
{
aValue = this.values.get(anIndex);
}
return aValue;
}
}
}
}





