/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library 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 2.1 of the License, or (at your option)
* any later version.
*
* This library 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.
*/
const ALTER_RETURN = 'alterReturn';
const HALT = 'halt';
const METAL_AOP = '__METAL_AOP__';
const PREVENT = 'prevent';
/**
* AOP class
*/
class AOP {
/**
* Constructor for AOP class.
* @param {!Object} obj The object containing the displaced function.
* @param {!string} fnName The name of the displaced function.
*/
constructor(obj, fnName) {
/**
* Array of listeners that will invoke after the displaced function.
* @type {!Array}
* @protected
*/
this.after_ = [];
/**
* Array of listeners that will invoke before the displaced function.
* @type {!Array}
* @protected
*/
this.before_ = [];
/**
* The name of the displaced function.
* @type {string}
* @protected
*/
this.fnName_ = fnName;
/**
* The displaced function.
* @type {Function}
* @protected
*/
this.fn_ = obj[fnName];
/**
* The object hosting the method to displace.
* @type {Object}
* @protected
*/
this.obj_ = obj;
}
/**
* Creates handle for detaching listener from displaced function.
* @param {!Function} fn The listener
* @param {!boolean} before Determines when listener fires
* @return {Object}
*/
createHandle(fn, before) {
return {
detach: this.detach_.bind(this, fn, before),
};
}
/**
* Detaches listener from displaced function.
* @param {!Function} fn The listener
* @param {!boolean} before Determines when listener fires
*/
detach_(fn, before) {
const listenerArray = before ? this.before_ : this.after_;
listenerArray.splice(listenerArray.indexOf(fn), 1);
}
/**
*
* @param {any} args* Arguments are passed to the wrapping and wrapped functions.
* @return {any} Return value of wrapped function.
*/
exec(...args) {
let listenerRetVal;
let prevented = false;
let retVal;
for (let i = 0; i < this.before_.length; i++) {
listenerRetVal = this.before_[i].apply(this.obj_, args);
if (listenerRetVal && listenerRetVal.type) {
if (listenerRetVal.type === HALT) {
return listenerRetVal.value;
}
else if (listenerRetVal.type === PREVENT) {
prevented = true;
}
}
}
if (!prevented) {
retVal = this.fn_.apply(this.obj_, args);
}
AOP.currentRetVal = retVal;
AOP.originalRetVal = retVal;
for (let i = 0; i < this.after_.length; i++) {
listenerRetVal = this.after_[i].apply(this.obj_, args);
if (listenerRetVal && listenerRetVal.type) {
if (listenerRetVal.type === HALT) {
return listenerRetVal.value;
}
else if (listenerRetVal.type === ALTER_RETURN) {
retVal = listenerRetVal.value;
AOP.currentRetVal = retVal;
}
}
}
return retVal;
}
/**
* Registers an AOP listener.
*
* @param {!Function} fn the function to execute.
* @param {boolean} before determines when the listener is invoked.
* @return {EventHandle} Can be used to remove the listener.
*/
register(fn, before) {
if (before) {
this.before_.push(fn);
}
else {
this.after_.push(fn);
}
return this.createHandle(fn, before);
}
/**
* Executes the supplied method after the specified function.
*
* @param {!Function} fn the function to execute.
* @param {!Object} obj the object hosting the method to displace.
* @param {!string} fnName the name of the method to displace.
* @return {EventHandle} Can be used to remove the listener.
*/
static after(fn, obj, fnName) {
return AOP.inject(false, fn, obj, fnName);
}
/**
* Return an alterReturn object when you want to change the result returned
* from the core method to the caller.
*
* @param {any} value Return value passed to code that invoked the wrapped
* function.
* @return {Object}
*/
static alterReturn(value) {
return AOP.modify_(ALTER_RETURN, value);
}
/**
* Executes the supplied method before the specified function.
*
* @param {!Function} fn the function to execute.
* @param {!Object} obj the object hosting the method to displace.
* @param {!string} fnName the name of the method to displace.
* @return {EventHandle} Can be used to remove the listener.
*/
static before(fn, obj, fnName) {
return AOP.inject(true, fn, obj, fnName);
}
/**
* Return a halt object when you want to terminate the execution
* of all subsequent subscribers as well as the wrapped method
* if it has not executed yet.
*
* @param {any} value Return value passed to code that invoked the wrapped
* function.
* @return {Object}
*/
static halt(value) {
return AOP.modify_(HALT, value);
}
/**
* Executes the supplied method before or after the specified function.
*
* @param {boolean} before determines when the listener is invoked.
* @param {!Function} fn the function to execute.
* @param {!Object} obj the object hosting the method to displace.
* @param {!string} fnName the name of the method to displace.
* @return {EventHandle} Can be used to remove the listener.
*/
static inject(before, fn, obj, fnName) {
let aopObj = obj[METAL_AOP];
if (!aopObj) {
aopObj = obj[METAL_AOP] = {};
}
if (!aopObj[fnName]) {
aopObj[fnName] = new AOP(obj, fnName);
obj[fnName] = function (...args) {
return aopObj[fnName].exec(...args);
};
}
return aopObj[fnName].register(fn, before);
}
/**
* Returns object which instructs `exec` method to modify the return
* value or prevent default behavior of wrapped function.
*
* @param {!string} type The type of modification to be made
* @param {any} value Return value passed to code that invoked the wrapped
* function.
* @return {Object}
*/
static modify_(type, value) {
return {
type,
value,
};
}
/**
* Return a prevent object when you want to prevent the wrapped function
* from executing, but want the remaining listeners to execute.
* @return {Object}
*/
static prevent() {
return AOP.modify_(PREVENT);
}
}
export default AOP;
export {AOP};