/*
* Copyright 2013-2014 headius.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.headius.invokebinder;
import java.lang.invoke.
MethodHandle;
import java.lang.invoke.
MethodHandles;
import java.lang.invoke.
MethodType;
import java.lang.reflect.
Array;
import java.util.
ArrayList;
import java.util.
Arrays;
import java.util.
List;
import java.util.regex.
Matcher;
import java.util.regex.
Pattern;
/**
* Signature represents a series of method arguments plus their symbolic names.
*
* In order to make it easier to permute arguments, track their flow, and debug
* cases where reordering or permuting fails to work properly, the Signature
* class also tracks symbolic names for all arguments. This allows permuting
* by name or by name pattern, avoiding the error-prone juggling of int[] for
* the standard MethodHandles.permuteArguments call.
*
* A Signature is created starting using #thatReturns method, and expanded using
* #withArgument for each named argument in sequence. Order is preserved.
*
* A Signature can be mutated into another by manipuating the argument list as
* with java.lang.invoke.MethodType, but using argument names and name patterns
* instead of integer offsets.
*
* Two signatures can be used to produce a permute array suitable for use in
* java.lang.invoke.MethodHandles#permuteArguments using the #to methods. The
* #to method can also accept a list of argument names, as a shortcut.
*
* @author headius
*/
public class
Signature {
private final
MethodType methodType;
private final
String[]
argNames;
/**
* Construct a new signature with the given return value.
*
* @param retval the return value for the new signature
*/
Signature(
Class<?>
retval) {
this(
MethodType.
methodType(
retval));
}
/**
* Construct a new signature with the given return value, argument types,
* and argument names.
*
* @param retval the return value for the new signature
* @param argTypes the argument types for the new signature
* @param argNames the argument names for the new signature
*/
Signature(
Class<?>
retval,
Class<?>[]
argTypes,
String...
argNames) {
this(
MethodType.
methodType(
retval,
argTypes),
argNames);
}
/**
* Construct a new signature with the given return value, argument types,
* and argument names.
*
* @param retval the return value for the new signature
* @param firstArg the first argument type, often the receiver of an instance method
* @param restArgs the remaining argument types for the new signature
* @param argNames the argument names for the new signature
*/
Signature(
Class<?>
retval,
Class<?>
firstArg,
Class<?>[]
restArgs,
String...
argNames) {
this(
MethodType.
methodType(
retval,
firstArg,
restArgs),
argNames);
}
/**
* Construct a new signature with the given method type and argument names.
*
* @param methodType the method type for the new signature
* @param argNames the argument names for the new signature
*/
Signature(
MethodType methodType,
String...
argNames) {
assert
methodType.
parameterCount() ==
argNames.length : "arg name count " +
argNames.length + " does not match parameter count " +
methodType.
parameterCount();
this.
methodType =
methodType;
this.
argNames =
argNames;
}
/**
* Construct a new signature with the given method type and argument names.
*
* @param methodType the method type for the new signature
* @param firstName the first argument name for the new signature; for eventual instance methods, it can be "this"
* @param restNames the remaining argument names for the new signature
*/
Signature(
MethodType methodType,
String firstName,
String...
restNames) {
assert
methodType.
parameterCount() == (
restNames.length + 1) : "arg name count " + (
restNames.length + 1) + " does not match parameter count " +
methodType.
parameterCount();
this.
methodType =
methodType;
this.
argNames = new
String[
restNames.length + 1];
this.
argNames[0] =
firstName;
System.
arraycopy(
restNames, 0, this.
argNames, 1,
restNames.length);
}
/**
* Produce a human-readable representation of this signature. This
* representation uses Class#getSimpleName to improve readability.
*
* @return a human-readable representation of the signature
*/
public
String toString() {
StringBuilder sb = new
StringBuilder("(");
for (int
i = 0;
i <
argNames.length;
i++) {
sb.
append(
methodType.
parameterType(
i).
getSimpleName()).
append(' ').
append(
argNames[
i]);
if (
i + 1 <
argNames.length) {
sb.
append(", ");
}
}
sb.
append(")").
append(
methodType.
returnType().
getSimpleName());
return
sb.
toString();
}
/**
* Create a new signature returning the given type.
*
* @param retval the return type for the new signature
* @return the new signature
*/
public static
Signature returning(
Class<?>
retval) {
Signature sig = new
Signature(
retval);
return
sig;
}
/**
* Create a new signature based on the given return value, argument types, and argument names
*
* @param retval the type of the return value
* @param argTypes the types of the arguments
* @param argNames the names of the arguments
* @return a new Signature
*/
public static
Signature from(
Class<?>
retval,
Class<?>[]
argTypes,
String...
argNames) {
assert
argTypes.length ==
argNames.length;
return new
Signature(
retval,
argTypes,
argNames);
}
/**
* Create a new signature based on this one with a different return type.
*
* @param retval the class for the new signature's return type
* @return the new signature with modified return value
*/
public
Signature changeReturn(
Class<?>
retval) {
return new
Signature(
methodType.
changeReturnType(
retval),
argNames);
}
/**
* Produce a new signature based on this one with a different return type.
*
* @param retval the new return type for the new signature
* @return a new signature with the added argument
*/
public
Signature asFold(
Class<?>
retval) {
return new
Signature(
methodType.
changeReturnType(
retval),
argNames);
}
/**
* Append an argument (name + type) to the signature.
*
* @param name the name of the argument
* @param type the type of the argument
* @return a new signature with the added arguments
*/
public
Signature appendArg(
String name,
Class<?>
type) {
String[]
newArgNames = new
String[
argNames.length + 1];
System.
arraycopy(
argNames, 0,
newArgNames, 0,
argNames.length);
newArgNames[
argNames.length] =
name;
MethodType newMethodType =
methodType.
appendParameterTypes(
type);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Append an argument (name + type) to the signature.
*
* @param names the names of the arguments
* @param types the types of the argument
* @return a new signature with the added arguments
*/
public
Signature appendArgs(
String[]
names,
Class<?>...
types) {
assert
names.length ==
types.length : "names and types must be of the same length";
String[]
newArgNames = new
String[
argNames.length +
names.length];
System.
arraycopy(
argNames, 0,
newArgNames, 0,
argNames.length);
System.
arraycopy(
names, 0,
newArgNames,
argNames.length,
names.length);
MethodType newMethodType =
methodType.
appendParameterTypes(
types);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Prepend an argument (name + type) to the signature.
*
* @param name the name of the argument
* @param type the type of the argument
* @return a new signature with the added arguments
*/
public
Signature prependArg(
String name,
Class<?>
type) {
String[]
newArgNames = new
String[
argNames.length + 1];
System.
arraycopy(
argNames, 0,
newArgNames, 1,
argNames.length);
newArgNames[0] =
name;
MethodType newMethodType =
methodType.
insertParameterTypes(0,
type);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Prepend arguments (names + types) to the signature.
*
* @param names the names of the arguments
* @param types the types of the arguments
* @return a new signature with the added arguments
*/
public
Signature prependArgs(
String[]
names,
Class<?>...
types) {
String[]
newArgNames = new
String[
argNames.length +
names.length];
System.
arraycopy(
argNames, 0,
newArgNames,
names.length,
argNames.length);
System.
arraycopy(
names, 0,
newArgNames, 0,
names.length);
MethodType newMethodType =
methodType.
insertParameterTypes(0,
types);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Insert an argument (name + type) into the signature.
*
* @param index the index at which to insert
* @param name the name of the new argument
* @param type the type of the new argument
* @return a new signature with the added arguments
*/
public
Signature insertArg(int
index,
String name,
Class<?>
type) {
return
insertArgs(
index, new
String[]{
name}, new
Class<?>[]{
type});
}
/**
* Insert an argument (name + type) into the signature before the argument
* with the given name.
*
* @param beforeName the name of the argument before which to insert
* @param name the name of the new argument
* @param type the type of the new argument
* @return a new signature with the added arguments
*/
public
Signature insertArg(
String beforeName,
String name,
Class<?>
type) {
return
insertArgs(
argOffset(
beforeName), new
String[]{
name}, new
Class<?>[]{
type});
}
/**
* Insert arguments (names + types) into the signature.
*
* @param index the index at which to insert
* @param names the names of the new arguments
* @param types the types of the new arguments
* @return a new signature with the added arguments
*/
public
Signature insertArgs(int
index,
String[]
names,
Class<?>...
types) {
assert
names.length ==
types.length : "names and types must be of the same length";
String[]
newArgNames = new
String[
argNames.length +
names.length];
System.
arraycopy(
names, 0,
newArgNames,
index,
names.length);
if (
index != 0)
System.
arraycopy(
argNames, 0,
newArgNames, 0,
index);
if (
argNames.length -
index != 0)
System.
arraycopy(
argNames,
index,
newArgNames,
index +
names.length,
argNames.length -
index);
MethodType newMethodType =
methodType.
insertParameterTypes(
index,
types);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Insert arguments (names + types) into the signature before the argument
* with the given name.
*
* @param beforeName the name of the argument before which to insert
* @param names the names of the new arguments
* @param types the types of the new arguments
* @return a new Signature with the added arguments
*/
public
Signature insertArgs(
String beforeName,
String[]
names,
Class<?>...
types) {
return
insertArgs(
argOffset(
beforeName),
names,
types);
}
/**
* Drops the first argument with the given name.
*
* @param name the name of the argument to drop
* @return a new signature
*/
public
Signature dropArg(
String name) {
String[]
newArgNames = new
String[
argNames.length - 1];
MethodType newType =
methodType;
for (int
i = 0,
j = 0;
i <
argNames.length;
i++) {
if (
argNames[
i].
equals(
name)) {
newType =
newType.
dropParameterTypes(
j,
j + 1);
continue;
}
newArgNames[
j++] =
argNames[
i];
}
if (
newType == null) {
// arg name not found; should we error?
return this;
}
return new
Signature(
newType,
newArgNames);
}
/**
* Drops the argument at the given index.
*
* @param index the index of the argument to drop
* @return a new signature
*/
public
Signature dropArg(int
index) {
assert
index <
argNames.length;
String[]
newArgNames = new
String[
argNames.length - 1];
if (
index > 0)
System.
arraycopy(
argNames, 0,
newArgNames, 0,
index);
if (
index <
argNames.length - 1)
System.
arraycopy(
argNames,
index + 1,
newArgNames,
index,
argNames.length - (
index + 1));
MethodType newType =
methodType.
dropParameterTypes(
index,
index + 1);
return new
Signature(
newType,
newArgNames);
}
/**
* Drop the last argument from this signature.
*
* @return a new signature
*/
public
Signature dropLast() {
return
dropLast(1);
}
/**
* Drop the specified number of last arguments from this signature.
*
* @param n number of arguments to drop
* @return a new signature
*/
public
Signature dropLast(int
n) {
return new
Signature(
methodType.
dropParameterTypes(
methodType.
parameterCount() -
n,
methodType.
parameterCount()),
Arrays.
copyOfRange(
argNames, 0,
argNames.length -
n));
}
/**
* Drop the first argument from this signature.
*
* @return a new signature
*/
public
Signature dropFirst() {
return
dropFirst(1);
}
/**
* Drop the specified number of first arguments from this signature.
*
* @param n number of arguments to drop
* @return a new signature
*/
public
Signature dropFirst(int
n) {
return new
Signature(
methodType.
dropParameterTypes(0,
n),
Arrays.
copyOfRange(
argNames,
n,
argNames.length));
}
/**
* Replace the named argument with a new name and type.
*
* @param oldName the old name of the argument
* @param newName the new name of the argument; can be the same as old
* @param newType the new type of the argument; can be the same as old
* @return a new signature with the modified argument
*/
public
Signature replaceArg(
String oldName,
String newName,
Class<?>
newType) {
int
offset =
argOffset(
oldName);
String[]
newArgNames =
argNames;
if (!
oldName.
equals(
newName)) {
newArgNames =
Arrays.
copyOf(
argNames,
argNames.length);
newArgNames[
offset] =
newName;
}
Class<?>
oldType =
methodType.
parameterType(
offset);
MethodType newMethodType =
methodType;
if (!
oldType.
equals(
newType))
newMethodType =
methodType.
changeParameterType(
offset,
newType);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Spread the trailing [] argument into its component type assigning given names.
*
* @param names names to use for the decomposed arguments
* @param types types to use for the decomposed arguments
* @return a new signature with decomposed arguments in place of the trailing array
*/
public
Signature spread(
String[]
names,
Class<?>...
types) {
assert
names.length ==
types.length : "names and types must be of the same length";
String[]
newArgNames = new
String[
argNames.length - 1 +
names.length];
System.
arraycopy(
names, 0,
newArgNames,
newArgNames.length -
names.length,
names.length);
System.
arraycopy(
argNames, 0,
newArgNames, 0,
argNames.length - 1);
MethodType newMethodType =
methodType
.
dropParameterTypes(
methodType.
parameterCount() - 1,
methodType.
parameterCount())
.
appendParameterTypes(
types);
return new
Signature(
newMethodType,
newArgNames);
}
/**
* Spread the trailing [] argument into its component type assigning given names.
*
* @param names names to use for the decomposed arguments
* @return a new signature with decomposed arguments in place of the trailing array
*/
public
Signature spread(
String...
names) {
Class<?>
aryType =
lastArgType();
assert
lastArgType().
isArray();
Class<?>[]
newTypes = new
Class<?>[
names.length];
Arrays.
fill(
newTypes,
aryType.
getComponentType());
return
spread(
names,
newTypes);
}
/**
* Spread the trailing [] argument into its component type assigning given names.
*
* @param baseName base name of the spread arguments
* @param count number of arguments into which the last argument will decompose
* @return a new signature with decomposed arguments in place of the trailing array
*/
public
Signature spread(
String baseName, int
count) {
String[]
spreadNames = new
String[
count];
for (int
i = 0;
i <
count;
i++)
spreadNames[
i] =
baseName +
i;
return
spread(
spreadNames);
}
/**
* Collect sequential arguments matching pattern into an array. They must have the same type.
*
* @param newName the name of the new array argument
* @param oldPattern the pattern of arguments to collect
* @return a new signature with an array argument where the collected arguments were
*/
public
Signature collect(
String newName,
String oldPattern) {
int
start = -1;
int
newCount = 0;
int
gatherCount = 0;
Class<?>
type = null;
Pattern pattern =
Pattern.
compile(
oldPattern);
MethodType newType =
type();
for (int
i = 0;
i <
argNames.length;
i++) {
if (
pattern.
matcher(
argName(
i)).
matches()) {
gatherCount++;
newType =
newType.
dropParameterTypes(
newCount,
newCount + 1);
Class<?>
argType =
argType(
i);
if (
start == -1)
start =
i;
if (
type == null) {
type =
argType;
} else {
if (
argType !=
type) {
throw new
InvalidTransformException("arguments matching " +
pattern + " are not all of the same type");
}
}
} else {
newCount++;
}
}
if (
start != -1) {
String[]
newNames = new
String[
newCount + 1];
// pre
System.
arraycopy(
argNames, 0,
newNames, 0,
start);
// vararg
newNames[
start] =
newName;
newType =
newType.
insertParameterTypes(
start,
Array.
newInstance(
type, 0).
getClass());
// post
if (
newCount + 1 >
start) { // args not at end
System.
arraycopy(
argNames,
start +
gatherCount,
newNames,
start + 1,
newCount -
start);
}
return new
Signature(
newType,
newNames);
}
return this;
}
/**
* The current java.lang.invoke.MethodType for this Signature.
*
* @return the current method type
*/
public
MethodType type() {
return
methodType;
}
/**
* The current argument count.
*
* @return argument count of this signature
*/
public int
argCount() {
return
argNames.length;
}
/**
* The current argument names for this signature.
*
* @return the current argument names
*/
public
String[]
argNames() {
return
argNames;
}
/**
* Retrieve the name of the argument at the given index.
*
* @param index the index from which to get the argument name
* @return the argument name
*/
public
String argName(int
index) {
return
argNames[
index];
}
/**
* Retrieve the offset of the given argument name in this signature's
* arguments. If the argument name is not in the argument list, returns -1.
*
* @param name the argument name to search for
* @return the offset at which the argument name was found or -1
*/
public int
argOffset(
String name) {
for (int
i = 0;
i <
argNames.length;
i++) {
if (
argNames[
i].
equals(
name)) return
i;
}
return -1;
}
/**
* Retrieve the offset of the given argument name in this signature's
* arguments. If the argument name is not in the argument list, returns -1.
*
* @param pattern the argument name to search for
* @return the offset at which the argument name was found or -1
*/
public int
argOffsets(
String pattern) {
for (int
i = 0;
i <
argNames.length;
i++) {
if (
Pattern.
compile(
pattern).
matcher(
argNames[
i]).
find()) return
i;
}
return -1;
}
/**
* Get the first argument name.
*
* @return the first argument name
*/
public
String firstArgName() {
return
argNames[0];
}
/**
* Get the last argument name.
*
* @return the last argument name
*/
public
String lastArgName() {
return
argNames[
argNames.length - 1];
}
/**
* Set the argument name at the given index.
*
* @param index the index at which to set the argument name
* @param name the name to set
* @return a new signature with the given name at the given index
*/
public
Signature argName(int
index,
String name) {
String[]
argNames =
Arrays.
copyOf(
argNames(),
argNames().length);
argNames[
index] =
name;
return new
Signature(
type(),
argNames);
}
/**
* Get the argument type at the given index.
*
* @param index the index from which to get the argument type
* @return the argument type
*/
public
Class<?>
argType(int
index) {
return
methodType.
parameterType(
index);
}
/**
* Get the first argument type.
*
* @return the first argument type
*/
public
Class<?>
firstArgType() {
return
methodType.
parameterType(0);
}
/**
* Get the last argument type.
*
* @return the last argument type
*/
public
Class<?>
lastArgType() {
return
argType(
methodType.
parameterCount() - 1);
}
/**
* Set the argument type at the given index.
*
* @param index the index at which to set the argument type
* @param type the type to set
* @return a new signature with the given type at the given index
*/
public
Signature argType(int
index,
Class<?>
type) {
return new
Signature(
type().
changeParameterType(
index,
type),
argNames());
}
/**
* Create a new signature containing the same return value as this one, but
* only the specified arguments.
*
* @param permuteArgs the names of the arguments to preserve
* @return the new signature
*/
public
Signature permute(
String...
permuteArgs) {
Pattern[]
patterns = new
Pattern[
permuteArgs.length];
for (int
i = 0;
i <
permuteArgs.length;
i++)
patterns[
i] =
Pattern.
compile(
permuteArgs[
i]);
List<
Class<?>>
types = new
ArrayList<>(
argNames.length);
List<
String>
names = new
ArrayList<>(
argNames.length);
for (
Pattern pattern :
patterns) {
for (int
argOffset = 0;
argOffset <
argNames.length;
argOffset++) {
String arg =
argNames[
argOffset];
Matcher matcher =
pattern.
matcher(
arg);
if (
matcher.
find()) {
types.
add(
methodType.
parameterType(
argOffset));
names.
add(
arg);
}
}
}
return new
Signature(
MethodType.
methodType(
methodType.
returnType(),
types.
toArray(new
Class<?>[0])),
names.
toArray(new
String[0]));
}
/**
* Create a new signature containing the same return value as this one, but
* omitting the specified arguments. Blacklisting to #permute's whitelisting.
*
* @param excludeArgs the names of the arguments to exclude
* @return the new signature
*/
public
Signature exclude(
String...
excludeArgs) {
Pattern[]
patterns = new
Pattern[
excludeArgs.length];
for (int
i = 0;
i <
excludeArgs.length;
i++)
patterns[
i] =
Pattern.
compile(
excludeArgs[
i]);
List<
Class<?>>
types = new
ArrayList<>(
argNames.length);
List<
String>
names = new
ArrayList<>(
argNames.length);
OUTER:
for (int
argOffset = 0;
argOffset <
argNames.length;
argOffset++) {
String arg =
argNames[
argOffset];
for (
Pattern pattern :
patterns) {
Matcher matcher =
pattern.
matcher(
arg);
if (
matcher.
find()) continue
OUTER;
}
// no matches, include
types.
add(
methodType.
parameterType(
argOffset));
names.
add(
arg);
}
return new
Signature(
MethodType.
methodType(
methodType.
returnType(),
types.
toArray(new
Class<?>[0])),
names.
toArray(new
String[0]));
}
/**
* Produce a method handle permuting the arguments in this signature using
* the given permute arguments and targeting the given java.lang.invoke.MethodHandle.
*
* Example:
*
* <pre>
* Signature sig = Signature.returning(String.class).appendArg("a", int.class).appendArg("b", int.class);
* MethodHandle handle = handleThatTakesOneInt();
* MethodHandle newHandle = sig.permuteTo(handle, "b");
* </pre>
*
* @param target the method handle to target
* @param permuteArgs the arguments to permute
* @return a new handle that permutes appropriate positions based on the
* given permute args
*/
public
MethodHandle permuteWith(
MethodHandle target,
String...
permuteArgs) {
return
MethodHandles.
permuteArguments(
target,
methodType,
to(
permute(
permuteArgs)));
}
/**
* Produce a new SmartHandle by permuting this Signature's arguments to the
* Signature of a target SmartHandle. The new SmartHandle's signature will
* match this one, permuting those arguments and invoking the target handle.
*
* @param target the SmartHandle to use as a permutation target
* @return a new SmartHandle that permutes this Signature's args into a call
* to the target SmartHandle.
* @see Signature#permuteWith(java.lang.invoke.MethodHandle, java.lang.String[])
*/
public
SmartHandle permuteWith(
SmartHandle target) {
String[]
argNames =
target.
signature().
argNames();
return new
SmartHandle(this,
permuteWith(
target.
handle(),
argNames));
}
/**
* Generate an array of argument offsets based on permuting this signature
* to the given signature.
*
* @param other the signature to target
* @return an array of argument offsets that will permute to the given
* signature
*/
public int[]
to(
Signature other) {
return
nonMatchingTo(
other.
argNames);
}
/**
* Generate an array of argument offsets based on permuting this signature
* to the given signature. Repeats are permitted, and the patterns will be
* matched against actual argument names using regex matching.
*
* @param otherArgPatterns the argument name patterns to permute
* @return an array of argument offsets that will permute to the matching
* argument names
*/
public int[]
to(
String...
otherArgPatterns) {
return
to(
permute(
otherArgPatterns));
}
private int[]
nonMatchingTo(
String...
otherArgNames) {
int[]
offsets = new int[
otherArgNames.length];
int
i = 0;
for (
String arg :
otherArgNames) {
int
pos = -1;
for (int
offset = 0;
offset <
argNames.length;
offset++) {
if (
argNames[
offset].
equals(
arg)) {
pos =
offset;
break;
}
}
assert
pos >= 0 : "argument not found: \"" +
arg + "\"";
offsets[
i++] =
pos;
}
return
offsets;
}
@
Override
public boolean
equals(
Object o) {
if (this ==
o) return true;
if (
o == null ||
getClass() !=
o.
getClass()) return false;
Signature signature = (
Signature)
o;
if (!
methodType.
equals(
signature.
methodType)) return false;
return
Arrays.
equals(
argNames,
signature.
argNames);
}
@
Override
public int
hashCode() {
int
result =
methodType.
hashCode();
result = 31 *
result +
Arrays.
hashCode(
argNames);
return
result;
}
}