CloudhopperRetryablePredicates.java

package fr.sii.ogham.sms.builder.cloudhopper;

import static com.cloudhopper.smpp.SmppConstants.STATUS_ALYBND;
import static com.cloudhopper.smpp.SmppConstants.STATUS_INVPASWD;
import static com.cloudhopper.smpp.SmppConstants.STATUS_INVSERTYP;
import static com.cloudhopper.smpp.SmppConstants.STATUS_INVSYSID;
import static fr.sii.ogham.core.util.ExceptionUtils.fatalJvmError;
import static fr.sii.ogham.core.util.ExceptionUtils.hasAnyCause;
import static java.util.Arrays.asList;

import java.util.function.Predicate;

import com.cloudhopper.commons.gsm.DataCoding;
import com.cloudhopper.smpp.type.SmppBindException;

import fr.sii.ogham.sms.exception.message.EncodingException;
import fr.sii.ogham.sms.sender.impl.cloudhopper.exception.DataCodingException;
import fr.sii.ogham.sms.sender.impl.cloudhopper.exception.MessagePreparationException;

/**
 * 
 * @author Aurélien Baudet
 *
 */
public final class CloudhopperRetryablePredicates {

	private CloudhopperRetryablePredicates() {
		super();
	}

	/**
	 * Default predicate used to indicate if the error raised by Cloudhopper is
	 * fatal or not. It returns {@code true} if the error is not fatal (means
	 * that a new retry can be attempted). It returns {@code false} if the error
	 * is fatal and no retry must be attempted.
	 * 
	 * <p>
	 * Here is the list of cases where there should have no retries:
	 * <ul>
	 * <li>There is a fatal JVM {@link Error} (like {@link OutOfMemoryError} for
	 * example).</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that the credentials are invalid</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that the {@code system_type} is invalid</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that client is already bound</li>
	 * </ul>
	 * 
	 * 
	 * @param error
	 *            the error to analyze
	 * @return true if a connect may be retried
	 */
	@SuppressWarnings("squid:S1126")
	public static boolean canRetryConnecting(Throwable error) {
		if (fatalJvmError(error) || invalidCredentials(error) || invalidSystemType(error) || alreadyBound(error)) {
			return false;
		}
		return true;
	}

	/**
	 * Checks whether the error has been raised because the SMSC has sent a
	 * response to a bind request indicating that the credentials are invalid
	 * (wrong {@code system_id} or {@code password}).
	 * 
	 * <p>
	 * If the credentials are invalid, there is no point in retrying to connect.
	 * 
	 * @param error
	 *            the raised error
	 * @return true if the error is issued due to a bind failure (wrong
	 *         credentials)
	 */
	public static boolean invalidCredentials(Throwable error) {
		if (error instanceof SmppBindException) {
			return isCommandStatus((SmppBindException) error, STATUS_INVPASWD, STATUS_INVSYSID);
		}
		return false;
	}

	/**
	 * Checks whether the error has been raised because the SMSC has sent a
	 * response to a bind request indicating that the {@code system_type} field
	 * is invalid.
	 * 
	 * <p>
	 * If the {@code system_type} field is invalid, there is no point in
	 * retrying to connect.
	 * 
	 * @param error
	 *            the raised error
	 * @return true if the error is issued due to a bind failure (wrong
	 *         {@code system_type})
	 */
	public static boolean invalidSystemType(Throwable error) {
		if (error instanceof SmppBindException) {
			return isCommandStatus((SmppBindException) error, STATUS_INVSERTYP);
		}
		return false;
	}

	/**
	 * Checks whether the error has been raised because the SMSC has sent a
	 * response to a bind request indicating that the client is already bound.
	 * 
	 * <p>
	 * If the client is already bound, there is no point in retrying to connect.
	 * 
	 * @param error
	 *            the raised error
	 * @return true if the error is issued due to a bind failure (already bound)
	 */
	public static boolean alreadyBound(Throwable error) {
		if (error instanceof SmppBindException) {
			return isCommandStatus((SmppBindException) error, STATUS_ALYBND);
		}
		return false;
	}

	/**
	 * Default predicate used to indicate if the error raised by Cloudhopper is
	 * fatal or not. It returns {@code true} if the error is not fatal (means
	 * that a new retry can be attempted). It returns {@code false} if the error
	 * is fatal and no retry must be attempted.
	 * 
	 * <p>
	 * Here is the list of cases where there should have no retries:
	 * <ul>
	 * <li>There is a fatal JVM {@link Error} (like {@link OutOfMemoryError} for
	 * example).</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that the credentials are invalid</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that the {@code system_type} is invalid</li>
	 * <li>A bind request has been sent to the SMSC and it has responded with an
	 * error indicating that client is already bound</li>
	 * </ul>
	 * 
	 * @return the predicate
	 */
	public static Predicate<Throwable> canRetryConnecting() {
		return CloudhopperRetryablePredicates::canRetryConnecting;
	}

	/**
	 * Predicate that skip retry if one of theses condition is met:
	 * 
	 * If the error is due to a preparation error (not sending). In this case,
	 * retrying will result in the same behavior so it will fail again:
	 * <ul>
	 * <li>Data coding couldn't be determined</li>
	 * <li>Encoding couldn't be determined</li>
	 * <li>Message preparation has failed</li>
	 * </ul>
	 * 
	 * <p>
	 * In other situations, the message may be sent again.
	 * 
	 * 
	 * @param error
	 *            the error to analyze
	 * @return true if a connect may be retried
	 */
	@SuppressWarnings("squid:S1126")
	public static boolean canResendMessage(Throwable error) {
		if (isDataCodingError(error) || isEncodingError(error) || messagePreparationFailed(error)) {
			return false;
		}
		// @formatter:off
		if (hasAnyCause(error, CloudhopperRetryablePredicates::isDataCodingError)
				|| hasAnyCause(error, CloudhopperRetryablePredicates::isEncodingError)
				|| hasAnyCause(error, CloudhopperRetryablePredicates::messagePreparationFailed)) {
			return false;
		}
		// @formatter:on
		return true;
	}

	/**
	 * Indicates if the error is due to a {@link DataCoding} detection error.
	 * 
	 * <p>
	 * In this case, retrying will lead to the same error.
	 * 
	 * @param error
	 *            the error to analyze
	 * @return true if it is a data coding error
	 */
	public static boolean isDataCodingError(Throwable error) {
		return error instanceof DataCodingException;
	}

	/**
	 * Indicates if the error is due to an encoding detection error.
	 * 
	 * <p>
	 * In this case, retrying will lead to the same error.
	 * 
	 * @param error
	 *            the error to analyze
	 * @return true if it is an encoding error
	 */
	public static boolean isEncodingError(Throwable error) {
		return error instanceof EncodingException;
	}

	/**
	 * Indicates if the error is due to an error during preparation of the
	 * message.
	 * 
	 * <p>
	 * In this case, retrying will lead to the same error.
	 * 
	 * @param error
	 *            the error to analyze
	 * @return true if it is a preparation error
	 */
	public static boolean messagePreparationFailed(Throwable error) {
		return error instanceof MessagePreparationException;
	}

	/**
	 * Predicate that skip retry if one of theses condition is met:
	 * 
	 * If the error is due to a preparation error (not sending). In this case,
	 * retrying will result in the same behavior so it will fail again:
	 * <ul>
	 * <li>Data coding couldn't be determined</li>
	 * <li>Encoding couldn't be determined</li>
	 * <li>Message preparation has failed</li>
	 * </ul>
	 * 
	 * <p>
	 * In other situations, the message may be sent again.
	 * 
	 * 
	 * @return the predicate that indicates if the message can be sent again
	 */
	public static Predicate<Throwable> canResendMessage() {
		return CloudhopperRetryablePredicates::canResendMessage;
	}

	private static boolean isCommandStatus(SmppBindException e, Integer... statuses) {
		return asList(statuses).contains(e.getBindResponse().getCommandStatus());
	}
}