package ecologylab.serialization;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import ecologylab.serialization.annotations.simpl_collection;
import ecologylab.serialization.annotations.simpl_scalar;
public class EnumerationDescriptor extends DescriptorBase implements ISimplStringMarshaller {
/**
* Gets an enumeration description for a given class.
* @param enumerationClass The given class to describe.
* @return An EnumerationDescription
* @throws SIMPLDescriptionException Whenever the described class has invalid aspects
*/
public static EnumerationDescriptor get(Class<?> enumerationClass) throws SIMPLDescriptionException
{
// TODO: Add caching here at some point.
EnumerationDescriptor ed = new EnumerationDescriptor(enumerationClass);
if(isCustomValuedEnum(enumerationClass))
{
// Remember: An enum is a collection of static constants of a type...
Object[] enumerationConstants = enumerationClass.getEnumConstants();
for(Object o : enumerationConstants)
{
String enumEntryName = ((Enum<?>)o).toString();
Integer enumEntryValue;
try
{
// We marshal this value in; sometimes, we need to reset the accessibility
enumEntryValue = (Integer)ed.getEnumerationCustomValueField().get(o);
// Interesting aside: It seems that on the first entry of an enumeration, we /always/ need to set accessibility
// But not on the second. Weird!
}
catch (IllegalArgumentException e) {
throw new SIMPLDescriptionException("Illegal argument exception while attempting to marshal entry value for enum entry: " + enumEntryName, e);
}
catch (IllegalAccessException e)
{
try{
// Most of the time, we can set the field to accessible to overcome the IllegalAccessException
ed.getEnumerationCustomValueField().setAccessible(true);
enumEntryValue = (Integer)ed.getEnumerationCustomValueField().get(o);
}
catch(Exception w)
{
throw new SIMPLDescriptionException("Illegal access exception while attempting to marshal entry value for enum entry: " + enumEntryName, w);
}
}
ed.getEnumerationEntries().add(new EnumerationEntry(enumEntryName, enumEntryValue));
}
}
else
{
// Remember: An enum is a collection of static constants of a type...
Object[] enumerationConstants = enumerationClass.getEnumConstants();
for(Object o : enumerationConstants)
{
String enumEntryName = ((Enum<?>)o).toString();
ed.getEnumerationEntries().add(new EnumerationEntry(enumEntryName));
}
}
return ed;
}
/**
* Initialize the basic data structures in the EnumerationDescription
*/
private void basicInitialization()
{
this.enumerationEntries = new LinkedList<>();
this.metaInfo = new LinkedList<>();
this.otherTags = new ArrayList<>();
}
/**
* This constructor is primarily for the sake of SIMPL serialization; you probably don't want to use it.
*/
public EnumerationDescriptor()
{
basicInitialization();
}
/**
* Creates an EnumerationDescription from a Class<?>... Does not add the EnumerationEntries
* (Leave that to .Get() which will cache the EnumreationDescriptions)
* @param describedEnum
* @throws SIMPLDescriptionException
*/
private EnumerationDescriptor(Class<?> describedEnum) throws SIMPLDescriptionException
{
super(XMLTools.getXmlTagName(describedEnum, null), describedEnum.getSimpleName());
basicInitialization();
if(describedEnum.isEnum())
{
this.enumerationClass = describedEnum;
this.enumerationName = describedEnum.getName();
this.packageName = describedEnum.getPackage().getName();
if(isCustomValuedEnum(describedEnum))
{
List<Field> enumFields = getEnumerationFields(describedEnum);
if(enumFields.size() == 1)
{
Field theField = enumFields.get(0);
if(!(theField.getType().equals(Integer.class) || theField.getType().equals(int.class)))
{
throw new SIMPLDescriptionException("To facilitate cross-platform compatability, any custom valued enumeration must be an Integer or an int type.");
}
else
{
if(theField.getAnnotation(simpl_scalar.class) != null)
{
this.enumerationCustomValueField = theField;
}
else
{
throw new SIMPLDescriptionException("Error on: " + describedEnum.getName() + " The single field of an enumeration type should be annotated with the simpl_scalar type.");
}
}
}
else
{
// In java, our enumerations can be super fancy... not the case in most languages. (specifically statically typed languages w/ enum types)
throw new SIMPLDescriptionException("To facilitate cross-platform compatibility, any custom-valued enumeration must have only a single value field.");
}
}
}else{
throw new SIMPLDescriptionException("Cannot create an enumeration description for a non-enumeration type.");
}
}
@simpl_scalar
private String packageName;
@simpl_scalar
private String enumerationName;
/**
* The class that represents the enumeration class.
*/
private Class<?> enumerationClass;
/**
* The field that represents the custom value for this enumeration.
*/
private Field enumerationCustomValueField;
public Field getEnumerationCustomValueField() {
return enumerationCustomValueField;
}
/**
* A list of entires in this given enumeration.
*/
@simpl_collection("entry")
private List<EnumerationEntry> enumerationEntries;
public Class<?> getEnumerationClass() {
return enumerationClass;
}
public void setEnumerationClass(Class<?> enumerationClass) {
this.enumerationClass = enumerationClass;
}
public List<EnumerationEntry> getEnumerationEntries() {
return enumerationEntries;
}
public void setEnumerationEntries(List<EnumerationEntry> enumerationEntries) {
this.enumerationEntries = enumerationEntries;
}
@Override
public ArrayList<String> otherTags() {
// TODO Auto-generated method stub
return new ArrayList<String>();
}
@Override
public String getJavaTypeName() {
// TODO Auto-generated method stub
return enumerationName;
}
@Override
public String getCSharpTypeName() {
// TODO Auto-generated method stub
return enumerationName;
}
@Override
public String getCSharpNamespace() {
// TODO Auto-generated method stub
return packageName;
}
@Override
public String getObjectiveCTypeName() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getDbTypeName() {
// TODO Auto-generated method stub
return null ;
}
/**
* Determines if this enumeration contains an entry with the given entry name
* @param entryName Name of the entry (case sensitive)
* @return True if the entry is in the list of entries
*/
public boolean containsEntry(String entryName)
{
for(EnumerationEntry ee : this.enumerationEntries)
{
if(ee.getName().equals(entryName))
{
return true;
}
}
return false;
}
private Class<?> fetchEnumClass()
{
if(this.enumerationClass == null)
{
// let's fetch a class! :3
try{
Class<?> theClass = Class.forName(this.getJavaTypeName());
this.enumerationClass = theClass;
}
catch(Exception e)
{
return null;
}
}
// TODO: Standard simpl code for ressurecting the class of a simpl descriptor / field?
return this.enumerationClass;
}
private HashMap<String, Enum<?>> enumNameToEnumValueHash = null;
private HashMap<Integer, Enum<?>> enumIntegerToEnumValueHash = null;
/**
* Fetches the Enum Value from the name of a given enrty, or null if the given string does not correspond to a value.
* @param string The entry name to retrieve
* @return The Enum value for the entry name, or null if none exists.
*/
public Enum<?> getEntryEnumValue(String string) {
// lazy initialize some underlying hashes from the data we have.
if(enumNameToEnumValueHash == null)
{
if(this.enumerationClass == null)
{
this.enumerationClass = fetchEnumClass();
}
this.enumNameToEnumValueHash = fetchEnumNameToEnumValueHash();
}
return this.enumNameToEnumValueHash.get(string);
}
public Enum<?> getEntryEnumFromValue(Integer value)
{
return this.getEntryEnumValue(this.fetchEnumValueToEnumName().get(value));
}
/**
* Fetches a name to EnumValue hash, lazy initializes behind the scenes.
* @return
*/
private HashMap<String, Enum<?>> fetchEnumNameToEnumValueHash() {
if(this.enumerationClass == null)
{
this.enumerationClass = fetchEnumClass();
}
HashMap<String, Enum<?>> ourHash = new HashMap<>();
for(Object o : this.enumerationClass.getEnumConstants())
{
Enum<?> aValue = (Enum<?>)o;
String aName = aValue.toString();
ourHash.put(aName, aValue);
}
return ourHash;
}
private HashMap<String, Integer> fetchEnumNameToEnumIntegerValue() {
HashMap<String, Integer> ourHash = new HashMap<>();
if(isCustomValuedEnum(fetchEnumClass()))
{
for(EnumerationEntry ee : this.enumerationEntries)
{
ourHash.put(ee.getName(), ee.getValue());
}
}
return ourHash;
}
private HashMap<Integer, String> fetchEnumValueToEnumName()
{
HashMap<Integer, String> ourHash = new HashMap<>();
if(isCustomValuedEnum(fetchEnumClass()))
{
for(EnumerationEntry ee : this.enumerationEntries)
{
ourHash.put(ee.getValue(), ee.getName());
}
}
return ourHash;
}
/**
* Filters out all extraneous fields and gets only enumeration-entry level fields
* @param enumClass
* @return
*/
private static List<Field> getEnumerationFields(Class<?> enumClass)
{
List<Field> filteredFields = new LinkedList<Field>();
for(Field f:enumClass.getDeclaredFields())
{
if(f.isEnumConstant())
{
continue; // exclude enum constants
}
if(Modifier.isStatic(f.getModifiers()))
{
continue; // exclude static
}
filteredFields.add(f);
}
return filteredFields;
}
/**
* Determines if a given class represents a "Custom Valued" enumeration
* @param enumClass
* @return
*/
public static boolean isCustomValuedEnum(Class<?> enumClass) {
if(enumClass.isEnum())
{
// So, an enumeration creates public static final fields for each entry
// and a private static final values() field...
// any additional fields have different signatures; so let's filter out all of those other fields and procede accordingly.
List<Field> filteredFields = getEnumerationFields(enumClass);
if(filteredFields.isEmpty())
{
return false;
}
else
{
return true;
}
}else{
return false; // not even an enum to begin with!
}
}
/**
* Gets the integer value that corresponds to a custom-valued enumeration
* @param string Entry name
* @return Custom value
*/
public Integer getEntryEnumIntegerValue(String string) {
return fetchEnumNameToEnumIntegerValue().get(string);
}
@Override
/**
* Marshals a given Object (an enumeration, in our case) to a string representation.
*/
public String marshal(Object object) throws SIMPLTranslationException{
// TODO Auto-generated method stub
if(object == null)
{
throw new SIMPLTranslationException(new SimplIssue("Should not attempt to marshal null values.", null, object));
}
if(object.getClass().isEnum())
{
if(object.getClass().equals(this.fetchEnumClass()))
{
//We have a valid object! Let's do this!
return object.toString(); // Hehehe. This should do the trick.
// Marshalling enums here is super trivial because toString() is guarenteed to return the name.
// Unmarshalling will be a bit more tricksy... but not by much.
}
else
{
throw new SIMPLTranslationException(new SimplIssue("Could not marshal because enumeration class was not the same as that in description. Was: "+ object.getClass().getName(), null, object));
}
}
else
{
throw new SIMPLTranslationException(new SimplIssue("Could not marshal a non-enumeration type... was: " + object.getClass().getName(), null, object));
}
}
@Override
public Object unmarshal(String string) throws SIMPLTranslationException{
// TODO Auto-generated method stub
if(string == null || string.isEmpty())
{
throw new SIMPLTranslationException(new SimplIssue("Could not unmarshal a null or empty string!", string, null));
}
if(this.containsEntry(string))
{
// We have this entry! Let's try to get it.
return this.getEntryEnumValue(string);
}
else
{
// Can we convert the string to an integer? If yes: try to marshal by value...
// otherwise, it's not in here!
try
{
Integer enumValue = Integer.parseInt(string);
Object value = this.getEntryEnumFromValue(enumValue);
if(value == null)
{
throw new SIMPLTranslationException(new SimplIssue("No enumeration entry exists with given value!", string, null));
}
else
{
return value;
}
}
catch(NumberFormatException nfe)
{
throw new SIMPLTranslationException(new SimplIssue("Could not find the string value in the enumeration!", string, null));
}
}
}
}