another.im-ios/Monal/Classes/MLHandler.m
2024-11-18 15:53:52 +01:00

213 lines
8.7 KiB
Objective-C

//
// MLHandler.m
// monalxmpp
//
// Created by Thilo Molitor on 29.10.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLConstants.h"
#import "MLHandler.h"
#import "HelperTools.h"
#define HANDLER_VERSION 1
@interface MLHandler ()
{
NSMutableDictionary* _internalData;
BOOL _invalidated;
}
@end
NSString* type_to_classname(NSString* type)
{
return [type componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"*< "]][0];
}
@implementation MLHandler
-(instancetype) init
{
self = [super init];
return self;
}
-(instancetype) initWithDelegate:(id) delegate handlerName:(NSString*) handlerName andBoundArguments:(NSDictionary*) args
{
self = [self init];
return [self INTERNALinitWithDelegate:delegate handlerName:handlerName invalidationHandlerName:nil andBoundArguments:args];
}
-(instancetype) initWithDelegate:(id) delegate handlerName:(NSString*) handlerName invalidationHandlerName:(NSString*) invalidationHandlerName andBoundArguments:(NSDictionary*) args
{
self = [self init];
return [self INTERNALinitWithDelegate:delegate handlerName:handlerName invalidationHandlerName:invalidationHandlerName andBoundArguments:args];
}
-(instancetype) INTERNALinitWithDelegate:(id) delegate handlerName:(NSString*) handlerName invalidationHandlerName:(NSString* _Nullable) invalidationHandlerName andBoundArguments:(NSDictionary* _Nullable) args
{
if(![delegate respondsToSelector:[self handlerNameToSelector:handlerName]])
@throw [NSException exceptionWithName:@"RuntimeException" reason:[NSString stringWithFormat:@"Class '%@' does not provide handler implementation '%@'!", NSStringFromClass(delegate), handlerName] userInfo:@{
@"delegate": NSStringFromClass(delegate),
@"handlerSelector": NSStringFromSelector([self handlerNameToSelector:handlerName]),
}];
if(invalidationHandlerName && ![delegate respondsToSelector:[self handlerNameToSelector:invalidationHandlerName]])
@throw [NSException exceptionWithName:@"RuntimeException" reason:[NSString stringWithFormat:@"Class '%@' does not provide invalidation implementation '%@'!", NSStringFromClass(delegate), invalidationHandlerName] userInfo:@{
@"delegate": NSStringFromClass(delegate),
@"handlerSelector": NSStringFromSelector([self handlerNameToSelector:handlerName]),
@"invalidationSelector": NSStringFromSelector([self handlerNameToSelector:invalidationHandlerName]),
}];
_internalData = [NSMutableDictionary new];
_invalidated = NO;
[_internalData addEntriesFromDictionary:@{
@"version": @(HANDLER_VERSION),
@"delegate": NSStringFromClass(delegate),
@"handlerName": handlerName,
}];
if(invalidationHandlerName)
_internalData[@"invalidationName"] = invalidationHandlerName;
[self bindArguments:args];
return self;
}
-(void) bindArguments:(NSDictionary* _Nullable) args
{
[self checkInvalidation];
_internalData[@"boundArguments"] = [self sanitizeArguments:args];
}
-(void) callWithArguments:(NSDictionary* _Nullable) args
{
MLAssert(_internalData[@"delegate"] && _internalData[@"handlerName"], @"Tried to call MLHandler while delegate and/or handlerName was not set!", @{@"handler": _internalData});
[self checkInvalidation];
args = [self sanitizeArguments:args];
id delegate = NSClassFromString(_internalData[@"delegate"]);
SEL sel = [self handlerNameToSelector:_internalData[@"handlerName"]];
if(![delegate respondsToSelector:sel])
@throw [NSException exceptionWithName:@"RuntimeException" reason:[NSString stringWithFormat:@"Class '%@' does not provide handler implementation '%@'!", _internalData[@"delegate"], _internalData[@"handlerName"]] userInfo:@{
@"delegate": _internalData[@"delegate"],
@"handlerSelector": NSStringFromSelector(sel),
}];
DDLogVerbose(@"Calling handler %@...", self);
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:sel]];
[inv setTarget:delegate];
[inv setSelector:sel];
//arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
//default arguments of the caller
[inv setArgument:(void* _Nonnull)&args atIndex:2];
//bound arguments of the handler
//make sure we use a copy because we don't want to leak changes of our bound arguments dict into already running invocations
NSDictionary* boundArgs = [_internalData[@"boundArguments"] copy];
[inv setArgument:(void* _Nonnull)&boundArgs atIndex:3];
//now call it
[inv invoke];
}
-(void) invalidateWithArguments:(NSDictionary* _Nullable) args
{
if(!(_internalData[@"delegate"] && _internalData[@"invalidationName"]))
return;
[self checkInvalidation];
args = [self sanitizeArguments:args];
id delegate = NSClassFromString(_internalData[@"delegate"]);
SEL sel = [self handlerNameToSelector:_internalData[@"invalidationName"]];
if(![delegate respondsToSelector:sel])
@throw [NSException exceptionWithName:@"RuntimeException" reason:[NSString stringWithFormat:@"Class '%@' does not provide invalidation implementation '%@'!", _internalData[@"delegate"], _internalData[@"invalidationName"]] userInfo:@{
@"delegate": _internalData[@"delegate"],
@"handlerSelector": NSStringFromSelector([self handlerNameToSelector:_internalData[@"handlerName"]]),
@"invalidationSelector": NSStringFromSelector(sel),
}];
DDLogVerbose(@"Calling invalidation %@...", self);
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:sel]];
[inv setTarget:delegate];
[inv setSelector:sel];
//arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
//default arguments of the caller
[inv setArgument:(void* _Nonnull)&args atIndex:2];
//bound arguments of the handler
NSDictionary* boundArgs = _internalData[@"boundArguments"];
[inv setArgument:(void* _Nonnull)&boundArgs atIndex:3];
//now call it
[inv invoke];
_invalidated = YES;
}
-(NSString*) id
{
if(!_internalData[@"delegate"] || !_internalData[@"handlerName"])
return @"{emptyHandler}";
NSString* extras = @"";
if(_internalData[@"invalidationName"])
extras = [NSString stringWithFormat:@"<%@>", _internalData[@"invalidationName"]];
return [NSString stringWithFormat:@"%@|%@%@", _internalData[@"delegate"], _internalData[@"handlerName"], extras];
}
-(NSString*) description
{
NSString* extras = @"";
if(_internalData[@"invalidationName"])
extras = [NSString stringWithFormat:@"<%@>", _internalData[@"invalidationName"]];
return [NSString stringWithFormat:@"{%@, %@%@}", _internalData[@"delegate"], _internalData[@"handlerName"], extras];
}
+(BOOL) supportsSecureCoding
{
return YES;
}
-(void) encodeWithCoder:(NSCoder*) coder
{
[coder encodeObject:_internalData forKey:@"internalData"];
[coder encodeBool:_invalidated forKey:@"invalidated"];
}
-(instancetype) initWithCoder:(NSCoder*) coder
{
self = [super init];
_internalData = [coder decodeObjectForKey:@"internalData"];
_invalidated = [coder decodeBoolForKey:@"invalidated"];
return self;
}
-(id) copyWithZone:(NSZone*) zone
{
MLHandler* copy = [[[self class] alloc] init];
copy->_internalData = [[NSMutableDictionary alloc] initWithDictionary:_internalData copyItems:YES];
copy->_invalidated = _invalidated;
return copy;
}
//this removes NSNull references from arguments altogether
-(NSMutableDictionary*) sanitizeArguments:(NSDictionary* _Nullable) args
{
NSMutableDictionary* retval = [NSMutableDictionary new];
if(args)
for(NSString* key in args)
if(args[key] != [NSNull null])
retval[key] = args[key];
return retval;
}
-(void) checkInvalidation
{
if(_invalidated)
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Tried to call or bind vars to already invalidated handler!" userInfo:@{
@"handler": _internalData,
}];
}
-(SEL) handlerNameToSelector:(NSString*) handlerName
{
return NSSelectorFromString([NSString stringWithFormat:@"MLHandler_%@_withArguments:andBoundArguments:", handlerName]);
}
+(void) throwDynamicExceptionForType:(NSString*) type andVar:(NSString*) varName andUserData:(id) userInfo andFile:(char*) file andLine:(int) line andFunc:(char*) func
{
NSString* text = [NSString stringWithFormat:@"Dynamic unpacking exception triggered for '%@' var '%@' at %@:%d in %s", type, varName, [HelperTools sanitizeFilePath:file], line, func];
DDLogError(@"%@", text);
@throw [NSException exceptionWithName:text reason:text userInfo:userInfo];
}
@end