/**
* Copyright (C) 2012-2013 Selventa, Inc.
*
* This file is part of the OpenBEL Framework.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The OpenBEL Framework 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms under LGPL v3:
*
* This license does not authorize you and you are prohibited from using the
* name, trademarks, service marks, logos or similar indicia of Selventa, Inc.,
* or, in the discretion of other licensors or authors of the program, the
* name, trademarks, service marks, logos or similar indicia of such authors or
* licensors, in any marketing or advertising materials relating to your
* distribution of the program or any covered product. This restriction does
* not waive or limit your obligation to keep intact all copyright notices set
* forth in the program as delivered to you.
*
* If you distribute the program in whole or in part, or any modified version
* of the program, and you assume contractual liability to the recipient with
* respect to the program or modified version, then you will indemnify the
* authors and licensors of the program for any liabilities that these
* contractual assumptions directly impose on those licensors and authors.
*/
package org.openbel.framework.common.lang;
import static java.lang.String.valueOf;
import static java.util.regex.Pattern.compile;
import static org.openbel.framework.common.BELUtilities.noItems;
import static org.openbel.framework.common.BELUtilities.noLength;
import static org.openbel.framework.common.enums.FunctionEnum.getFunctionEnum;
import static org.openbel.framework.common.enums.SemanticStatus.*;
import static org.openbel.framework.common.enums.ValueEncoding.getValueEncoding;
import java.io.Serializable;
import java.util.regex.Pattern;
import org.openbel.framework.common.InvalidArgument;
import org.openbel.framework.common.enums.FunctionEnum;
import org.openbel.framework.common.enums.ReturnType;
import org.openbel.framework.common.enums.SemanticStatus;
import org.openbel.framework.common.enums.ValueEncoding;
/**
* Represents a BEL function signature.
* <p>
* A BEL function signature has the following format:
*
* <pre>
* <i>A</i>([<i>arg1</i>,<i>arg2</i>,...,<i>argN</i>{@code [...]}])<i>B</i>
* </pre>
*
* where <i>A</i> is a BEL function enum and <i>B</i> is a BEL return type. The
* following rules concerning arguments are:
* <ol>
* <li>0 or more arguments are required</li>
* <li>The last argument may be a variable length argument where one or more is
* included with a function</li>
* </ol>
* The <i>arg</i> values refer to BEL function parameters and are represented in
* one of two ways:
*
* <pre>
* F:<i>A</i>
* </pre>
*
* where <i>A</i> is a return type. Or in the second format:
*
* <pre>
* E:<i>B</i>
* </pre>
*
* where <i>B</i> is a value encoding. In both cases, what comes after the colon
* defines what is a valid parameter at that position in the function.
* </p>
*
* @see FunctionEnum BEL function definitions
* @see ReturnType BEL function return types
* @see ValueEncoding BEL function parameter encodings
*/
public final class Signature implements Serializable {
// TODO: provide signature-based support for additional argument types (not
// a namespace value, not a function) to better support
// modification/substitution/truncation/fusion
public final static String WILDCARD_ENCODING = "*";
private static final long serialVersionUID = -9134224994229618223L;
private final static String BAD_SIG;
private final static Pattern ARGS_REGEX;
private final static Pattern PARAMS_REGEX;
private final static String ENCODE_PREFIX = "E:";
private final static String FUNC_PREFIX = "F:";
private final static String VARARGS_SUFFIX = "...";
static {
BAD_SIG = "Bad signature";
ARGS_REGEX = compile("[()]");
PARAMS_REGEX = compile("[:]");
}
private final String value;
private final int hash;
private final String numArgs;
/**
* Creates a signature with the provided value.
*
* @param value Signature value
* @throws InvalidArgument Thrown if the signature is not in the correct
* format
*/
public Signature(final String value) {
if (noLength(value) || !validFormat(value)) {
final String err = BAD_SIG.concat(", " + value);
throw new InvalidArgument(err);
}
this.value = value;
hash = value.hashCode();
numArgs = countArgs(value);
}
/**
* Returns the number of arguments for this signature.
*
* @return {@link String}
*/
public String getNumberOfArguments() {
return numArgs;
}
/**
* Returns this signature's return type.
*
* @return {@link ReturnType}
*/
public ReturnType getReturnType() {
return returnType(value);
}
/**
* Returns this signature's function enum.
*
* @return {@link FunctionEnum}
*/
public FunctionEnum getFunction() {
return function(value);
}
/**
* Returns this signature's value.
*
* @return {@link String}
*/
public String getValue() {
return value;
}
/**
* Returns {@code true} if the string {@code s} adheres to the signature
* format, {@code false} otherwise.
*
* @param s String
* @return boolean
*/
private boolean validFormat(final String s) {
String[] args = ARGS_REGEX.split(s);
if (noItems(args)) return false;
if (args.length < 3) return false;
// Check for the presence of a function name
String name = args[0];
if (name.isEmpty()) return false;
if (!validFunction(name)) return false;
// Check for the presence of a return type
String returntype = args[args.length - 1];
if (returntype.isEmpty()) return false;
if (!validReturnType(returntype)) return false;
// Check for zero or more arguments
String arglist = args[1];
if (arglist.isEmpty()) return true;
// Split parameters on comma
String[] argsplit = arglist.split(",");
for (final String arg : argsplit) {
if (!validParameter(arg)) return false;
}
return true;
}
/**
* Returns {@code true} if the string {@code s} adheres to the signature
* parameter format, {@code false} otherwise.
*
* @param s String
* @return boolean
*/
private boolean validParameter(final String s) {
final String[] tokens = PARAMS_REGEX.split(s);
if (tokens.length != 2) {
return false;
}
final char first = s.charAt(0);
String second = tokens[1];
if (second.endsWith(VARARGS_SUFFIX)) {
// value encoding / return type var args, mangle argument
second = varargsRoot(second);
}
if (first == 'E') {
if (isWildcard(second)) {
return true;
}
// Verify value encoding exists as an enum
if (getValueEncoding(second) != null) {
return true;
}
// Fallback to verification based on all character discriminators
for (final Character c : second.toCharArray()) {
if (getValueEncoding(c) == null) {
return false;
}
}
return true;
} else if (first == 'F') {
// Verify return type exists as an enum
if (ReturnType.getReturnType(second) != null) {
return true;
}
}
return false;
}
/**
* Returns {@code true} if the string {@code s} is a valid function,
* {@code false} otherwise.
*
* @param s String
* @return boolean
*/
private boolean validFunction(final String s) {
FunctionEnum fe = getFunctionEnum(s);
if (fe != null) return true;
return false;
}
/**
* Returns {@code true} if the string {@code s} is a valid return type,
* {@code false} otherwise.
*
* @param s String
* @return boolean
*/
private boolean validReturnType(final String s) {
ReturnType rt = ReturnType.getReturnType(s);
if (rt != null) return true;
return false;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return hash;
}
/**
* Returns the {@link SemanticStatus semantic status} of the supplied
* signature against this one.
*
* @param other {@link Signature}
* @return SemanticStatus, which may be null
*/
public SemanticStatus matches(final Signature other) {
if (other == null) return null;
// Check simple function/return types first
if (getFunction() != other.getFunction()) return INVALID_FUNCTION;
if (getReturnType() != other.getReturnType())
return INVALID_RETURN_TYPE;
String[] myargs = this.getArguments();
String[] otherargs = other.getArguments();
return argumentsMatch(myargs, otherargs);
}
/**
* Returns {@code true} if the object {@code o} is this object,
* {@code false} otherwise.
*
* @return boolean
*/
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof Signature)) return false;
final Signature s = (Signature) o;
if (hash == s.hash) return true;
return value.equals(s.value);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(value);
return builder.toString();
}
/**
* Returns the argument tokens, split by {@code ,}.
*
* @return {@link String} array
*/
private String[] getArguments() {
String[] args = ARGS_REGEX.split(value);
return args[1].split(",");
}
/**
* Returns the {@link SemanticStatus semantic status} of the supplied
* argument lists.
*
* @param args1 Argument list
* @param args2 Argument list
* @return SemanticStatus, which may be null
*/
private SemanticStatus argumentsMatch(final String[] myargs,
final String[] other) {
String my_arg, arg;
boolean varargs = false;
for (int i = 0, j = 0;; i++, j++) {
boolean my_last = (i == (myargs.length - 1));
boolean other_last = (j == (other.length - 1));
if (myargs[i].endsWith(VARARGS_SUFFIX)) {
my_arg = varargsRoot(myargs[i]);
// stay at position in argument list
i = i - 1;
varargs = true;
} else {
my_arg = myargs[i];
}
arg = other[j];
final SemanticStatus status = argumentsMatch(my_arg, arg);
if (status != VALID) return status;
if (my_last && other_last) {
break;
} else if (my_last && !varargs) {
// You have arguments left to match, I don't.
return TOO_MANY_ARGUMENTS;
} else if (other_last) {
// I have arguments left to match, you don't.
return TOO_FEW_ARGUMENTS;
}
}
return VALID;
}
/**
* Returns the {@link SemanticStatus semantic status} of the supplied
* arguments.
*
* @param arg1 Argument list
* @param arg2 Argument list
* @return SemanticStatus, which may be null
*/
private SemanticStatus argumentsMatch(final String arg1, final String arg2) {
if (arg1.startsWith(ENCODE_PREFIX)) {
if (arg2.startsWith(FUNC_PREFIX)) {
return INVALID_RETURN_TYPE_ARGUMENT;
}
if (!arg2.startsWith(ENCODE_PREFIX)) {
return INVALID_ENCODING_ARGUMENT;
}
final String[] tokens1 = PARAMS_REGEX.split(arg1);
if (tokens1.length != 2) {
return INVALID_ENCODING_ARGUMENT;
}
final String[] tokens2 = PARAMS_REGEX.split(arg2);
if (tokens2.length != 2) {
return INVALID_ENCODING_ARGUMENT;
}
// If either token is the wildcard, any encoding is valid.
if (isWildcard(tokens1[1]) || isWildcard(tokens2[1])) {
return VALID;
}
final ValueEncoding ve1 = getValueEncoding(tokens1[1]);
ValueEncoding ve2 = getValueEncoding(tokens2[1]);
if (ve2 != null) {
if (ve1.isAssignableFrom(ve2)) {
return VALID;
}
return INVALID_ENCODING_ARGUMENT;
}
// Fallback to verification based on all character discriminators
for (final Character c : tokens2[1].toCharArray()) {
ve2 = getValueEncoding(c);
if (ve1.isAssignableFrom(ve2)) {
return VALID;
}
}
return INVALID_ENCODING_ARGUMENT;
}
if (arg1.startsWith(FUNC_PREFIX)) {
if (arg2.startsWith(ENCODE_PREFIX))
return INVALID_RETURN_TYPE_ARGUMENT;
if (!arg2.startsWith(FUNC_PREFIX))
return INVALID_RETURN_TYPE_ARGUMENT;
final String[] tokens1 = PARAMS_REGEX.split(arg1);
if (tokens1.length != 2)
return INVALID_RETURN_TYPE_ARGUMENT;
final String[] tokens2 = PARAMS_REGEX.split(arg2);
if (tokens2.length != 2)
return INVALID_RETURN_TYPE_ARGUMENT;
final ReturnType re1 = ReturnType.getReturnType(tokens1[1]);
final ReturnType re2 = ReturnType.getReturnType(tokens2[1]);
if (re1 != re2 && !re1.isAssignableFrom(re2))
return INVALID_RETURN_TYPE_ARGUMENT;
return VALID;
}
return null;
}
/**
* Encodes the supplied value encoding as
*
* <pre>
* E:<i>encoding</i>
* </pre>
*
* @param encoding String encoding
* @return {@link String}
*/
public static String encode(final String encoding) {
return ENCODE_PREFIX.concat(encoding);
}
/**
* Encodes the supplied return type encoding as
*
* <pre>
* F:{@link ReturnType#getDisplayValue() DISPLAY_VALUE}
* </pre>
*
* @param rt {@link ReturnType}
* @return {@link String}
*/
public static String encode(final ReturnType rt) {
return FUNC_PREFIX.concat(rt.getDisplayValue());
}
/**
* Returns the function enum for the provided string.
*
* @param s String
* @return {@link FunctionEnum}
*/
private static FunctionEnum function(final String s) {
String[] args = ARGS_REGEX.split(s);
return getFunctionEnum(args[0]);
}
/**
* Returns the return type for the provided string.
*
* @param s String
* @return {@link ReturnType}
*/
private static ReturnType returnType(final String s) {
String[] args = ARGS_REGEX.split(s);
return ReturnType.getReturnType(args[args.length - 1]);
}
/**
* Returns the root string of a varargs argument.
* <p>
* For example, for the string {@code F:myFunction...}, this method returns
* {@code F:myFunction}.
* </p>
*
* @param s {@link String}
* @return {@link String}
*/
private static String varargsRoot(final String s) {
return s.substring(0, s.length() - (VARARGS_SUFFIX.length()));
}
/**
* Returns the number of arguments for the provided string.
*
* @param s {@link String}
* @return {@link String}
*/
private static String countArgs(final String s) {
String[] args = ARGS_REGEX.split(s);
if ("".equals(args[1])) return "0";
String[] argarray = args[1].split(",");
for (final String arg : argarray)
if (arg.contains(VARARGS_SUFFIX)) return "1 or more";
return valueOf(argarray.length);
}
/**
* Returns {@code true} if {@code s} is the wildcard encoding, {@code false}
* otherwise.
*
* @param s {@link String}
* @return boolean
*/
private static boolean isWildcard(final String s) {
return WILDCARD_ENCODING.equals(s);
}
}