package de.skuzzle.polly.sdk;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import de.skuzzle.polly.sdk.Types.AnyType;
import de.skuzzle.polly.sdk.Types.BooleanType;
import de.skuzzle.polly.sdk.Types.ChannelType;
import de.skuzzle.polly.sdk.Types.DateType;
import de.skuzzle.polly.sdk.Types.ListType;
import de.skuzzle.polly.sdk.Types.NumberType;
import de.skuzzle.polly.sdk.Types.StringType;
import de.skuzzle.polly.sdk.Types.UserType;
/**
* <p>This class represents a signature for a {@link Command}. It is used for formal and
* actual signatures. That means, any Command may register a set of different formal
* signatures that they are compatible with.</p>
*
* <p>If the command is executed, each formal signature is compared to the actual
* signature that was entered by the user to resolve which action the command shall
* execute.</p>
*
* <p>The resolved signature gets passed to the
* {@link Command#executeOnChannel(User, String, Signature)} or
* {@link Command#executeOnQuery(User, Signature)} method.
* The {@link Command} can distinguish the actual passed signatures by their id. The
* formal signatures are numbered consecutively in order of their creation beginning at
* 0.</p>
*
* <p>Canonical signatures are those, that only have parameters of distinct types. If
* a command has registered a canonical signature, it does not matter in which order the
* user provides the parameters when calling the command, as they are being automatically
* reordered to match the formal signature.</p>
*
* @author Simon
* @since zero day
* @version RC 1.0
*/
public class Signature {
/**
* The actual or formal parameters for this signature.
*/
private List<Types> parameters;
/**
* The command name that this signatures is for.
*/
private String name;
/**
* The id of this signature.
*/
private int signatureId;
/**
* This attributes is set to true if this signature only contains distinct
* types.
*/
private boolean canonical;
/**
* Creates a new signature with no required permissions.
*
* @param name The name of the command that this signature is for. The name must
* exactly equal the commands name.
* @param id The id of this signature. Commands distinguish actual signatures by
* this id.
* @param parameters The parameters of this signature. If this is a formal signature,
* the values of the parameter types are ignored. This parameter can be empty.
*/
public Signature(String name, int id, Types...parameters) {
this(name, id, Arrays.asList(parameters));
}
/**
* Creates a new signature.
*
* @param name The name of the command that this signature is for. The name must
* exactly equal the commands name.
* @param id The id of this signature. Commands distinguish actual signatures by
* this id.
* @param parameters The parameters of this signature. If this is a formal signature,
* the values of the parameter types are ignored. This list can be empty.
*/
public Signature(String name, int id, List<Types> parameters) {
this.name = name;
this.signatureId = id;
this.parameters = parameters;
this.canonical = this.checkCanonical(parameters);
}
/**
* Gets whether this signature is canonical, i.e. it only contains distinct
* types.
*
* @return <code>true</code> if this signature only contains distinct types
* since 0.8
*/
public boolean isCanonical() {
return this.canonical;
}
/**
* Returns this signatures parameters.
*
* @return The parameters.
* @since 0.8
*/
public List<Types> getParameters() {
return this.parameters;
}
/**
* Returns the name of the command that this signature is for.
* @return The commands name.
*/
public String getName() {
return this.name;
}
/**
* Returns the id of this signature.
* @return The signature id.
*/
public int getId() {
return this.signatureId;
}
/**
* Sets the id for this signature. Its highly discouraged to use this method. It is
* used by {@link CommandManager#getCommand(Signature)} to set the formal id
* to the actual signature if it was resolved.
*
* @param id The new id for this signature.
*/
public void setId(int id) {
this.signatureId = id;
}
/**
* Returns a parameter of this signature. You can access the actual values of the
* signature via this method. You must cast the returnvalue to your expected
* {@link Types} instance.
*
* @param paramId The index of the parameter to retrieve.
* @return The {@link Types} instance at the given index.
*/
public Types getValue(int paramId) {
return this.parameters.get(paramId);
}
/**
* This is a convenience method for accessing a number parameter. Note that this
* method will crash if the parameter with given id is no {@link NumberType}.
*
* @param paramId The parameter whose value shall be resolved.
* @return The parameters number value.
*/
public double getNumberValue(int paramId) {
NumberType nt = (NumberType) this.parameters.get(paramId);
return nt.getValue();
}
/**
* This is a convenience method for accessing a data parameter. Note that this
* method will crash if the parameter with given id is no {@link DateType}.
*
* @param paramId The parameter whose value shall be resolved.
* @return The parameters date value.
*/
public Date getDateValue(int paramId) {
DateType dt = (DateType) this.parameters.get(paramId);
return dt.getValue();
}
/**
* This is a convenience method for accessing a string parameter. It returns the
* String values for any of these types: {@link StringType}, {@link UserType},
* , {@link BooleanType}, {@link NumberType} and
* {@link ChannelType}.
*
* @param paramId The parameter whose value shall be resolved.
* @return The parameters string value.
* @throws IllegalArgumentException If the parameter is none of the above mentioned
* types.
*/
public String getStringValue(int paramId) {
Types t = this.parameters.get(paramId);
if (t instanceof StringType) {
return ((StringType) t).getValue();
} else if (t instanceof UserType) {
return ((UserType) t).getValue();
} else if (t instanceof ChannelType) {
return ((ChannelType) t).getValue();
} else if (t instanceof BooleanType) {
return Boolean.toString(((BooleanType) t).getValue());
} else if (t instanceof NumberType) {
return Double.toString(((NumberType) t).getValue());
}
throw new IllegalArgumentException("No String-type parameter"); //$NON-NLS-1$
}
/**
* This is a convenience method for accessing a boolean parameter. Note that this
* method will crash if the parameter with given id is no {@link BooleanType}.
*
* @param paramId The parameter whose value shall be resolved.
* @return The parameters boolean value.
* @since Beta 0.2
*/
public boolean getBooleanValue(int paramId) {
BooleanType bt = (BooleanType) this.parameters.get(paramId);
return bt.getValue();
}
/**
* This is a convenience method for accessing list parameters. It returns a list
* with the ListTypes elements.
*
* @param subType The type of the elements that are contained in the list.
* @param paramId The id of the list parameter. Note that calling this method
* for a parameter which is no ListType will result in a
* {@link ClassCastException}.
* @return A new List with correct generic type, containing the ListTypes elements.
*/
public <T extends Types> List<T> getListValue(Class<T> subType, int paramId) {
ListType lt = (ListType) this.parameters.get(paramId);
List<T> result = new ArrayList<T>(lt.getElements().size());
for (Types t : lt.getElements()) {
result.add(subType.cast(t));
}
return result;
}
/**
* Matches two signatures against each others. They match if their parameter
* types equal pairwise.
*
* If both, this and the other signature are canonical as determined by
* {@link #isCanonical()}, this method first tries to rearrange the parameters
* of the other signature to match the sequence of this signature.
*
* @param other The signature to match against this signature.
* @return This signature if the signatures matches or <code>null</code> if they
* do not match.
*/
public Signature match(Signature other) {
if (other.parameters.size() != this.parameters.size()) {
return null;
}
if (this.isCanonical() && other.isCanonical()) {
// if both are canonical, we try to rearrange it to increase
// chance of matching.
// Additional, its of high importance that the parameter
// sequence of both signatures match if the result is
// positive
this.rearrange(other);
}
Iterator<Types> formal = this.parameters.iterator();
Iterator<Types> actual = other.parameters.iterator();
while (formal.hasNext()) {
if (!formal.next().check(actual.next())) {
return null;
}
}
return this;
}
private void rearrange(Signature other) {
List<Types> formal = this.parameters;
List<Types> actual = other.parameters;
for (int i = 0; i < formal.size(); ++i) {
for (int j = i; j < actual.size(); ++j) {
Types formalCls = formal.get(i);
Types actualCls = actual.get(j);
if(i != j && formalCls.check(actualCls)) {
this.swap(actual, i, j);
}
}
}
}
private void swap(List<Types> list, int i, int j) {
Types tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
/**
* Checks whether the given parameter list is canonical.
*
* @param parameters List of parameters to check.
* @return If the list is canonical.
*/
private boolean checkCanonical(List<Types> parameters) {
for (int i = 0; i < parameters.size(); ++i) {
// signatures that contain the Any-Type can't be canonical
if (parameters.get(i) instanceof AnyType) {
return false;
}
for (int j = i; j < parameters.size(); ++j) {
Types formalCls = parameters.get(i);
Types actualCls = parameters.get(j);
if(i != j && formalCls.check(actualCls)) {
return false;
}
}
}
return true;
}
@Override
public int hashCode() {
// HACK: this method does not completely conform to equals() as equals tries to
// rearrange signatures to increase the possibility of a match and thus
// produces no canonical result
final int prime = 31;
int result = 1;
result = prime * result + (this.canonical ? 1231 : 1237);
result = prime * result
+ ((this.parameters == null) ? 0 : this.parameters.hashCode());
return result;
}
/**
* Two signatures are considered equals if they match each other.
*
* @param obj The other signature to compare this one with.
* @return <code>true</code> if the signatures match, <code>false</code> otherwise.
* @see #match(Signature)
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (obj == this) {
return true;
} else if (!(obj instanceof Signature)) {
return false;
}
Signature other = (Signature) obj;
return this.match(other) != null;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append(":"); //$NON-NLS-1$
b.append(this.getName());
if (this.parameters.isEmpty()) {
return b.toString();
}
b.append(" "); //$NON-NLS-1$
Iterator<Types> it = this.parameters.iterator();
while (it.hasNext()) {
b.append(it.next().toString());
if (it.hasNext()) {
b.append(" "); //$NON-NLS-1$
}
}
return b.toString();
}
}