2022-05-01 22:37:01 +02:00
/ *
* This file is a part of the SteamWar software .
*
* Copyright ( C ) 2020 SteamWar . de - Serverteam
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program 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 Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
* /
2022-04-21 22:43:14 +02:00
package de.steamwar.command ;
import java.lang.annotation.* ;
import java.lang.reflect.Method ;
import java.lang.reflect.Parameter ;
import java.util.* ;
import java.util.function.BiConsumer ;
import java.util.function.IntPredicate ;
import java.util.function.Supplier ;
import java.util.stream.Collectors ;
public abstract class AbstractSWCommand < T > {
private Class < ? > clazz ; // This is used in createMappings()
private boolean initialized = false ;
protected final List < SubCommand < T > > commandList = new ArrayList < > ( ) ;
protected final List < SubCommand < T > > commandHelpList = new ArrayList < > ( ) ;
private final Map < String , AbstractTypeMapper < T , ? > > localTypeMapper = new HashMap < > ( ) ;
private final Map < String , AbstractGuardChecker < T > > localGuardChecker = new HashMap < > ( ) ;
protected AbstractSWCommand ( Class < T > clazz , String command ) {
this ( clazz , command , new String [ 0 ] ) ;
}
protected AbstractSWCommand ( Class < T > clazz , String command , String [ ] aliases ) {
this . clazz = clazz ;
createAndSafeCommand ( command , aliases ) ;
unregister ( ) ;
register ( ) ;
}
protected abstract void createAndSafeCommand ( String command , String [ ] aliases ) ;
public abstract void unregister ( ) ;
public abstract void register ( ) ;
protected void commandSystemError ( T sender , CommandFrameworkException e ) {
e . printStackTrace ( ) ;
}
protected void commandSystemWarning ( Supplier < String > message ) {
System . out . println ( message . get ( ) ) ;
}
protected final void execute ( T sender , String alias , String [ ] args ) {
initialize ( ) ;
try {
if ( ! commandList . stream ( ) . anyMatch ( s - > s . invoke ( sender , alias , args ) ) ) {
commandHelpList . stream ( ) . anyMatch ( s - > s . invoke ( sender , alias , args ) ) ;
}
} catch ( CommandNoHelpException e ) {
// Ignored
} catch ( CommandFrameworkException e ) {
commandSystemError ( sender , e ) ;
throw e ;
}
}
protected final List < String > tabComplete ( T sender , String alias , String [ ] args ) throws IllegalArgumentException {
initialize ( ) ;
String string = args [ args . length - 1 ] . toLowerCase ( ) ;
return commandList . stream ( )
. filter ( s - > ! s . noTabComplete )
. map ( s - > s . tabComplete ( sender , args ) )
. filter ( Objects : : nonNull )
. flatMap ( Collection : : stream )
. filter ( s - > ! s . isEmpty ( ) )
. filter ( s - > s . toLowerCase ( ) . startsWith ( string ) )
. collect ( Collectors . toList ( ) ) ;
}
private void initialize ( ) {
if ( initialized ) return ;
createMapping ( ) ;
}
private synchronized void createMapping ( ) {
List < Method > methods = methods ( ) ;
for ( Method method : methods ) {
addMapper ( Mapper . class , method , i - > i = = 0 , false , AbstractTypeMapper . class , ( anno , typeMapper ) - > {
if ( anno . local ( ) ) {
localTypeMapper . putIfAbsent ( anno . value ( ) , ( AbstractTypeMapper < T , ? > ) typeMapper ) ;
} else {
SWCommandUtils . getMAPPER_FUNCTIONS ( ) . putIfAbsent ( anno . value ( ) , typeMapper ) ;
}
} ) ;
addMapper ( ClassMapper . class , method , i - > i = = 0 , false , AbstractTypeMapper . class , ( anno , typeMapper ) - > {
if ( anno . local ( ) ) {
localTypeMapper . putIfAbsent ( anno . value ( ) . getTypeName ( ) , ( AbstractTypeMapper < T , ? > ) typeMapper ) ;
} else {
SWCommandUtils . getMAPPER_FUNCTIONS ( ) . putIfAbsent ( anno . value ( ) . getTypeName ( ) , typeMapper ) ;
}
} ) ;
addGuard ( Guard . class , method , i - > i = = 0 , false , AbstractGuardChecker . class , ( anno , guardChecker ) - > {
if ( anno . local ( ) ) {
localGuardChecker . putIfAbsent ( anno . value ( ) , ( AbstractGuardChecker < T > ) guardChecker ) ;
} else {
SWCommandUtils . getGUARD_FUNCTIONS ( ) . putIfAbsent ( anno . value ( ) , guardChecker ) ;
}
} ) ;
addGuard ( ClassGuard . class , method , i - > i = = 0 , false , AbstractGuardChecker . class , ( anno , guardChecker ) - > {
if ( anno . local ( ) ) {
localGuardChecker . putIfAbsent ( anno . value ( ) . getTypeName ( ) , ( AbstractGuardChecker < T > ) guardChecker ) ;
} else {
SWCommandUtils . getGUARD_FUNCTIONS ( ) . putIfAbsent ( anno . value ( ) . getTypeName ( ) , guardChecker ) ;
}
} ) ;
add ( Register . class , method , i - > i > 0 , true , null , ( anno , parameters ) - > {
if ( ! anno . help ( ) ) return ;
if ( parameters . length ! = 2 ) {
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking parameters or has too many " ) ;
}
if ( ! parameters [ parameters . length - 1 ] . isVarArgs ( ) ) {
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking the varArgs parameters as last Argument " ) ;
}
if ( parameters [ parameters . length - 1 ] . getType ( ) . getComponentType ( ) ! = String . class ) {
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking the varArgs parameters of type ' " + String . class . getTypeName ( ) + " ' as last Argument " ) ;
return ;
}
commandHelpList . add ( new SubCommand ( this , method , anno . value ( ) , new HashMap < > ( ) , localGuardChecker , true , null , anno . noTabComplete ( ) ) ) ;
} ) ;
}
for ( Method method : methods ) {
add ( Register . class , method , i - > i > 0 , true , null , ( anno , parameters ) - > {
if ( anno . help ( ) ) return ;
for ( int i = 1 ; i < parameters . length ; i + + ) {
Parameter parameter = parameters [ i ] ;
Class < ? > clazz = parameter . getType ( ) ;
if ( parameter . isVarArgs ( ) & & i = = parameters . length - 1 ) {
clazz = parameter . getType ( ) . getComponentType ( ) ;
}
Mapper mapper = parameter . getAnnotation ( Mapper . class ) ;
if ( clazz . isEnum ( ) & & mapper = = null & & ! SWCommandUtils . getMAPPER_FUNCTIONS ( ) . containsKey ( clazz . getTypeName ( ) ) ) {
continue ;
}
String name = mapper ! = null ? mapper . value ( ) : clazz . getTypeName ( ) ;
if ( ! SWCommandUtils . getMAPPER_FUNCTIONS ( ) . containsKey ( name ) & & ! localTypeMapper . containsKey ( name ) ) {
commandSystemWarning ( ( ) - > " The parameter ' " + parameter . toString ( ) + " ' is using an unsupported Mapper of type ' " + name + " ' " ) ;
return ;
}
}
commandList . add ( new SubCommand ( this , method , anno . value ( ) , localTypeMapper , localGuardChecker , false , anno . description ( ) , anno . noTabComplete ( ) ) ) ;
} ) ;
this . commandList . sort ( ( o1 , o2 ) - > {
int compare = Integer . compare ( - o1 . subCommand . length , - o2 . subCommand . length ) ;
if ( compare ! = 0 ) {
return compare ;
} else {
2022-05-10 09:19:30 +02:00
return Double . compare ( o1 . comparableValue , o2 . comparableValue ) ;
2022-04-21 22:43:14 +02:00
}
} ) ;
commandHelpList . sort ( ( o1 , o2 ) - > {
int compare = Integer . compare ( - o1 . subCommand . length , - o2 . subCommand . length ) ;
if ( compare ! = 0 ) {
return compare ;
} else {
return Integer . compare ( o1 . method . getDeclaringClass ( ) = = AbstractSWCommand . class ? 1 : 0 ,
o2 . method . getDeclaringClass ( ) = = AbstractSWCommand . class ? 1 : 0 ) ;
}
} ) ;
}
initialized = true ;
}
private < T extends Annotation > void add ( Class < T > annotation , Method method , IntPredicate parameterTester , boolean firstParameter , Class < ? > returnType , BiConsumer < T , Parameter [ ] > consumer ) {
T [ ] anno = SWCommandUtils . getAnnotation ( method , annotation ) ;
if ( anno = = null | | anno . length = = 0 ) return ;
Parameter [ ] parameters = method . getParameters ( ) ;
if ( ! parameterTester . test ( parameters . length ) ) {
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking parameters or has too many " ) ;
return ;
}
if ( firstParameter & & ! clazz . isAssignableFrom ( parameters [ 0 ] . getType ( ) ) ) {
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking the first parameter of type ' " + clazz . getTypeName ( ) + " ' " ) ;
return ;
}
2022-04-21 22:58:45 +02:00
if ( returnType ! = null & & ! method . getReturnType ( ) . isAssignableFrom ( returnType ) ) {
2022-04-21 22:43:14 +02:00
commandSystemWarning ( ( ) - > " The method ' " + method . toString ( ) + " ' is lacking the desired return type ' " + returnType . getTypeName ( ) + " ' " ) ;
return ;
}
Arrays . stream ( anno ) . forEach ( t - > consumer . accept ( t , parameters ) ) ;
}
private < T extends Annotation > void addMapper ( Class < T > annotation , Method method , IntPredicate parameterTester , boolean firstParameter , Class < ? > returnType , BiConsumer < T , AbstractTypeMapper < ? , ? > > consumer ) {
add ( annotation , method , parameterTester , firstParameter , returnType , ( anno , parameters ) - > {
try {
method . setAccessible ( true ) ;
consumer . accept ( anno , ( AbstractTypeMapper < T , ? > ) method . invoke ( this ) ) ;
} catch ( Exception e ) {
throw new SecurityException ( e . getMessage ( ) , e ) ;
}
} ) ;
}
private < T extends Annotation > void addGuard ( Class < T > annotation , Method method , IntPredicate parameterTester , boolean firstParameter , Class < ? > returnType , BiConsumer < T , AbstractGuardChecker < ? > > consumer ) {
add ( annotation , method , parameterTester , firstParameter , returnType , ( anno , parameters ) - > {
try {
method . setAccessible ( true ) ;
consumer . accept ( anno , ( AbstractGuardChecker < T > ) method . invoke ( this ) ) ;
} catch ( Exception e ) {
throw new SecurityException ( e . getMessage ( ) , e ) ;
}
} ) ;
}
// TODO: Implement this when Message System is ready
/ *
public void addDefaultHelpMessage ( String message ) {
defaultHelpMessages . add ( message ) ;
}
* /
private List < Method > methods ( ) {
List < Method > methods = new ArrayList < > ( ) ;
Class < ? > current = getClass ( ) ;
while ( current . getSuperclass ( ) ! = AbstractSWCommand . class ) {
methods . addAll ( Arrays . asList ( current . getDeclaredMethods ( ) ) ) ;
current = current . getSuperclass ( ) ;
}
return methods ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . METHOD } )
@Repeatable ( Register . Registeres . class )
protected @interface Register {
String [ ] value ( ) default { } ;
boolean help ( ) default false ;
String [ ] description ( ) default { } ;
boolean noTabComplete ( ) default false ;
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . METHOD } )
@interface Registeres {
Register [ ] value ( ) ;
}
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . PARAMETER , ElementType . METHOD } )
protected @interface Mapper {
String value ( ) ;
boolean local ( ) default false ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . METHOD } )
protected @interface ClassMapper {
Class < ? > value ( ) ;
boolean local ( ) default false ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . PARAMETER , ElementType . METHOD } )
protected @interface Guard {
String value ( ) default " " ;
boolean local ( ) default false ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . METHOD } )
protected @interface ClassGuard {
Class < ? > value ( ) ;
boolean local ( ) default false ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . PARAMETER } )
protected @interface StaticValue {
String [ ] value ( ) ;
/ * *
* This is the short form for ' allowImplicitSwitchExpressions '
* and can be set to true if you want to allow int as well as boolean as annotated parameter types .
* The value array needs to be at least 2 long for this flag to be considered .
* While using an int , the value will represent the index into the value array .
* While using a boolean , the value array must only be 2 long and the value will be { @code false }
* for the first index and { @code true } for the second index .
* /
boolean allowISE ( ) default false ;
}
@Retention ( RetentionPolicy . RUNTIME )
@Target ( { ElementType . PARAMETER } )
protected @interface OptionalValue {
/ * *
* Will pe parsed against the TypeMapper specified by the parameter or annotation .
* /
String value ( ) ;
}
}