AbstractListenerObjectExecutor.java

/*
 * Copyright (C) 2012-2024 RRiBbit.org
 *
 * 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
 *
 * https://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 org.rribbit.execution;

import org.rribbit.ListenerObject;
import org.rribbit.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

/**
 * This {@link ListenerObjectExecutor} provides a blueprint for the execution of a {@link Collection} of {@link ListenerObject}s and the processing of their results. The
 * only thing left to implementation subclasses is to actually use the methods provided in this class to actually do the execution. The implementations can decide on, for example, sequential
 * or multi-threaded execution of the different {@link ListenerObject}s.
 * <p />
 * Please note that this class uses the java method {@link Method}.invoke() and this method does NOT work with varargs. So, if you want to call a listener method that accepts varargs, you MUST
 * explicitly pass an array where the varargs are expected, even if that array is empty, otherwise, the method will NOT match and will NOT be executed.
 *
 * @author G.J. Schouten
 *
 */
public abstract class AbstractListenerObjectExecutor implements ListenerObjectExecutor {

	private static final Logger log = LoggerFactory.getLogger(AbstractListenerObjectExecutor.class);

	@Override
	public <T> Response<T> executeListeners(Collection<ListenerObject> listenerObjects, Object... parameters) {

		Collection<T> results = new ArrayList<>();
		Collection<Throwable> throwables = new ArrayList<>();

		log.debug("Executing ListenerObjects");
		for(ExecutionResult executionResult : this.doExecuteListeners(listenerObjects, parameters)) {
			if(executionResult != null && !(executionResult instanceof VoidResult)) {
				if(executionResult instanceof ThrowableResult) {
					throwables.add(((ThrowableResult) executionResult).getThrowable());
				} else {
					results.add((T) (((ObjectResult) executionResult).getResult()));
				}
			}
		}
		return new Response<>(results, throwables);
	}

	/**
	 * This method executes a single {@link ListenerObject} and returns the appropriate {@link ExecutionResult}. It should be used by subclasses when excuting the {@link ListenerObject}s.
	 *
	 * @param listenerObject
	 * @param parameters
	 * @return the {@link ExecutionResult} of the execution of the {@link ListenerObject}, or null if the {@link ListenerObject} did not match the parameters
	 */
	protected ExecutionResult executeSingleListenerObject(ListenerObject listenerObject, Object... parameters) {

		try {
			Object returnValue = listenerObject.getMethod().invoke(listenerObject.getTarget(), parameters);
			if(listenerObject.getMethod().getReturnType().equals(void.class)) { //Nothing to return
				log.debug("ListenerObject '{}' successfully executed, return value was void", listenerObject);
				return new VoidResult();
			} else {
				log.debug("ListenerObject '{}' successfully executed, return value was object", listenerObject);
				return new ObjectResult(returnValue);
			}
		} catch(InvocationTargetException e) {
			log.debug("Underlying method of ListenerObject '{}' threw Throwable", listenerObject);
			//Caused by the underlying method throwing a Throwable. Rethrowing...
			return new ThrowableResult(e.getCause());
		} catch(Exception e) {
			//Note: In Java 18, the JVM sometimes throws an InvocationTargetException instead of an IllegalArgumentException
			//if the parameters don't match the Method signature, thereby ending up in the above catch clause instead of this
			//one. This seems to be fixed in Java 21, but if it occurs again, the solution is to return null from the
			//above catch clause if the cause of the InvocationTargetException is actually mismatching parameters, in order
			//to match the handling of that scenario in this catch clause.

			log.trace("Method of ListenerObject '" + listenerObject + "' did not match parameters, ignoring", e);
			//Probably caused by parameters not matching Method signature. Ignoring...
			return null;
		}
	}

	/**
	 * This method should call {@link #executeSingleListenerObject(ListenerObject, Object...)} on each {@link ListenerObject}, accumulate the results, and return. Typical implementations
	 * do this either sequentially, or multi-threaded.
	 *
	 * @param listenerObjects
	 * @param parameters
	 * @return a {@link Collection} with the {@link ExecutionResult}s of the given {@link ListenerObject}s or an empty {@link Collection} if there are no results, never returns null
	 */
	protected abstract Collection<ExecutionResult> doExecuteListeners(Collection<ListenerObject> listenerObjects, Object... parameters);

	/**
	 * This class represents the outcome of the execution of a {@link ListenerObject}.
	 *
	 * @author G.J. Schouten
	 *
	 */
	protected abstract static class ExecutionResult {}

	/**
	 * This {@link ExecutionResult} represents a successful invocation, but no result (method return type was 'void').
	 *
	 * @author G.J. Schouten
	 *
	 */
	protected static class VoidResult extends ExecutionResult {}

	/**
	 * This {@link ExecutionResult} represents an unsuccessful invocation, where the method threw a {@link Throwable}.
	 *
	 * @author G.J. Schouten
	 *
	 */
	protected static class ThrowableResult extends ExecutionResult {

		private Throwable throwable;

		public ThrowableResult(Throwable throwable) {
			this.throwable = throwable;
		}

		public Throwable getThrowable() {
			return throwable;
		}
	}

	/**
	 * This {@link ExecutionResult} represents a successful invocation, along with its result.
	 *
	 * @author G.J. Schouten
	 */
	protected static class ObjectResult extends ExecutionResult {

		private Object result;

		public ObjectResult(Object result) {
			this.result = result;
		}

		public Object getResult() {
			return result;
		}
	}
}