/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package leafchat.core.api;
import java.lang.reflect.*;
import java.util.*;
/**
* Provides information about a particular class of Msg. Subclasses of this
* should be provided by each message class. This information is used to
* present a view of the message hierarchy for the scripting user interface.
* To provide this for a message class, create a method - public static MessageInfo
* getInfo() - in the class.
* <p>
* Note: This implements {@link Comparable}<Object> for compatibility reasons;
* implementing {@link Comparable}<MessageInfo> would remove binary
* compatibility with earlier versions.
*/
public class MessageInfo implements Comparable<Object>
{
private Class<? extends Msg> c;
private String name;
private String description;
private MessageInfo superclass;
private TreeSet<MessageInfo> subclasses=new TreeSet<MessageInfo>();
private Variables v=null;
/** Class representing the list of available scripting variables */
public class Variables
{
/**
* Add a variable to the list. If the variable is called 'frog' then
* its type will be determined by the getFrog() method in the message,
* which will also be used to define the variable.
* @param variable Variable name
* @throws BugException If the method isn't properly defined
*/
public void add(String variable)
{
String methodName="get"+
variable.substring(0,1).toUpperCase()+
variable.substring(1);
Class<?> type;
try
{
Method m=c.getMethod(methodName);
if(m.getReturnType()!=int.class && m.getReturnType()!=String.class)
throw new BugException("Scripting variable method must return int or String: "+methodName);
if(!Modifier.isPublic(m.getModifiers()))
throw new BugException("Scripting variable method must be public: "+methodName);
type=m.getReturnType();
}
catch(NoSuchMethodException e)
{
throw new BugException("Scripting variable method does not exist: "+methodName);
}
String code=(type==int.class ? "int" : "String")+" "+variable+"=msg."+methodName+"();";
add(variable,type,code);
}
class VariableDetails
{
Class<?> type;
String code;
public VariableDetails(Class<?> type,String code)
{
this.type=type;
this.code=code;
}
}
private TreeMap<String, VariableDetails> variables =
new TreeMap<String, VariableDetails>();
/**
* Adds a custom variable to the list.
* @param variable Variable name
* @param type Type (should be int.class or String.class)
* @param code Code to define variable, which can assume the existance
* of a msg variable of the message type, e.g. "int frog=msg.getFrog();"
*/
public void add(String variable,Class<?> type,String code)
{
variables.put(variable,new VariableDetails(type,code));
}
/**
* @return Sorted list of all variable names
*/
public String[] getNames()
{
return variables.keySet().toArray(new String[variables.keySet().size()]);
}
/**
* Obtains the code needed to define a variable, for example:
* "String myVariable=msg.getMyVariable();"
* @param variable Variable
* @return Code to define variable
*/
public String getDefinition(String variable)
{
VariableDetails v=variables.get(variable);
return v.code;
}
/**
* Obtains the type of a named variable. Default is to look for the get method
* (as per getVariableType) and check its return value.
* @param variable Variablename
* @return Type of variable (should be either int.class or String.class)
* @throws BugException If variable method does not exist or there's something
* wrong with it
*/
public Class<?> getType(String variable)
{
VariableDetails v=variables.get(variable);
return v.type;
}
/**
* Removes a variable from the list.
* @param variable Variable to remove
*/
public void remove(String variable)
{
variables.remove(variable);
}
}
/**
* Constructs with a particular message class and a default name (the
* class name without Msg).
* @param c Class of message to which this info applies
*/
public MessageInfo(Class<? extends Msg> c)
{
this(c,c.getName().replaceAll("^.*\\.","").replaceAll("Msg$",""),null);
}
/**
* @param obj Comparison object
* @return True if the object is a MessageInfo referring to same class
*/
@Override
public boolean equals(Object obj)
{
return obj instanceof MessageInfo && c==((MessageInfo)obj).c;
}
/**
* Constructs with a particular message class.
* @param c Class of message to which this info applies
* @param name Display name for message
* @param description Description of message
*/
public MessageInfo(Class<? extends Msg> c, String name, String description)
{
this.c=c;
this.name=name;
this.description=description;
}
/**
* Messages can supply code that initialises the context if needed, to be used
* when the message is received. Message will be in 'msg' variable. Default
* version just calls superclass or returns nothing.
* @return Java code
*/
public String getContextInit()
{
if(superclass==null) return "";
return superclass.getContextInit();
}
/**
* @return List of scripting variables automatically provided by the message
*/
public Variables getVariables()
{
if(v==null)
{
v=new Variables();
listScriptingVariables(v);
}
return v;
}
/** @return Class of message */
public Class<? extends Msg> getMessageClass()
{
return c;
}
/** @return Display name */
public final String getName()
{
return name;
}
/** @return Description for display, null if none */
public final String getDescription()
{
return description;
}
@Override
public int compareTo(Object obj)
{
MessageInfo other = (MessageInfo)obj;
return getName().compareTo(other.getName());
}
/**
* @return True if messages of this type are never sent; default is
* true if the message class is abstract
*/
public boolean isAbstract()
{
return Modifier.isAbstract(c.getModifiers());
}
/**
* @return True if this message appears in the scripting 'events' section,
* false if it (and all subclasses) doesn't; default taken from superclass
* (root default is false)
*/
public boolean allowScripting()
{
if(superclass==null) return false;
return superclass.allowScripting();
}
/**
* Adds scripting variables to a list. Default just adds variables from
* superclass MessageInfo.
* @param v List to add variables to
*/
protected void listScriptingVariables(Variables v)
{
if(superclass==null) return;
superclass.listScriptingVariables(v);
}
/**
* @return List of message filter classes that may be appropriate for this
* message type
*/
public final FilterInfo[] getAppropriateFilters()
{
List<FilterInfo> l = new LinkedList<FilterInfo>();
listAppropriateFilters(l);
return l.toArray(new FilterInfo[l.size()]);
}
/**
* Called by system to add a subclass to this info.
* @param subclass Subclass info
*/
public final void addSubclass(MessageInfo subclass)
{
subclasses.add(subclass);
}
/**
* Called by system to remove a subclass from this info.
* @param subclass Subclass info
*/
public final void removeSubclass(MessageInfo subclass)
{
subclasses.remove(subclass);
}
/**
* Called by system to set the superclass for this info
* @param superclass Superclass info
*/
public final void setSuperclass(MessageInfo superclass)
{
this.superclass=superclass;
}
/**
* Called by system to close this info; removes the info from its superclass,
* if any.
*/
public final void close()
{
if(superclass!=null) superclass.removeSubclass(this);
}
/**
* Adds permitted filters to a list. Default version just adds filters from
* superclass MessageInfo, or nothing if none.
* @param list List to add filters to
*/
protected void listAppropriateFilters(Collection<FilterInfo> list)
{
if(superclass==null)
{
return;
}
superclass.listAppropriateFilters(list);
}
/**
* @return List of subclasses of this message type
*/
public MessageInfo[] getSubclasses()
{
return subclasses.toArray(new MessageInfo[subclasses.size()]);
}
/**
* @return Superclass of this message type
*/
public MessageInfo getSuperclass()
{
return superclass;
}
}