ServerException.java

package com.surrealdb;

import java.util.Map;

// Note: java.lang.Object is used explicitly throughout because
// com.surrealdb.Object shadows it in this package.

/**
 * Base class for all exceptions originating from the SurrealDB server.
 *
 * <p>Carries structured error information: a machine-readable {@link #getKind() kind},
 * optional {@link #getDetails() details} using the {@code {kind, details?}} wire format,
 * and an optional typed {@link #getServerCause() cause} chain.
 *
 * <p>Details follow the internally-tagged format where each detail object has a
 * {@code "kind"} field and an optional {@code "details"} field:
 * <ul>
 *   <li>Unit variant: {@code {"kind": "Parse"}}</li>
 *   <li>Newtype variant: {@code {"kind": "Auth", "details": {"kind": "TokenExpired"}}}</li>
 *   <li>Struct variant: {@code {"kind": "Table", "details": {"name": "users"}}}</li>
 * </ul>
 *
 * <p>For backward compatibility with older servers, the legacy externally-tagged
 * format ({@code "Parse"}, {@code {"Auth": "TokenExpired"}},
 * {@code {"Table": {"name": "users"}}}) is also supported by all detail helpers.
 *
 * <p>Specific error kinds are represented by subclasses (e.g. {@link NotAllowedException},
 * {@link NotFoundException}). When the server returns an unknown kind, a plain
 * {@code ServerException} is used (not {@link InternalException}) to preserve forward
 * compatibility.
 *
 * @see ErrorKind
 */
public class ServerException extends SurrealException {

	private final ErrorKind kindEnum;
	private final String rawKind;
	private final java.lang.Object details;
	private final ServerException serverCause;

	/**
	 * Constructs a {@code ServerException} from an {@link ErrorKind} and optional raw kind string.
	 * Used by the JNI bridge when the native side passes the enum. When {@code kind} is
	 * {@link ErrorKind#UNKNOWN}, {@code rawKindIfUnknown} must be the wire string.
	 *
	 * @param kind             the error kind enum (from the Rust SDK's ErrorDetails)
	 * @param rawKindIfUnknown when {@code kind} is {@link ErrorKind#UNKNOWN}, the wire string; otherwise {@code null}
	 */
	ServerException(ErrorKind kind, String rawKindIfUnknown, String message, java.lang.Object details, ServerException cause) {
		super(message, cause);
		this.kindEnum = kind;
		this.rawKind = kind == ErrorKind.UNKNOWN ? rawKindIfUnknown : null;
		this.details = details;
		this.serverCause = cause;
	}

	/**
	 * Constructs a {@code ServerException} from a kind string (for subclasses and tests).
	 * The kind is resolved to an {@link ErrorKind} via {@link ErrorKind#fromString(String)}.
	 */
	ServerException(String kind, String message, java.lang.Object details, ServerException cause) {
		super(message, cause);
		this.kindEnum = ErrorKind.fromString(kind);
		this.rawKind = kindEnum == ErrorKind.UNKNOWN ? kind : null;
		this.details = details;
		this.serverCause = cause;
	}

	/**
	 * Extracts the {@code "kind"} string from a {@code {kind, details?}} detail
	 * object. Returns {@code null} if the details are not in internally-tagged format.
	 *
	 * @param details the details object (may be {@code null})
	 * @return the kind string, or {@code null}
	 */
	@SuppressWarnings("unchecked")
	static String detailKind(java.lang.Object details) {
		if (details instanceof Map) {
			java.lang.Object k = ((Map<String, java.lang.Object>) details).get("kind");
			if (k instanceof String) {
				return (String) k;
			}
		}
		return null;
	}

	/**
	 * Extracts the {@code "details"} value from a {@code {kind, details?}} detail
	 * object. Returns {@code null} if not present or not in internally-tagged format.
	 *
	 * @param details the details object (may be {@code null})
	 * @return the inner details value, or {@code null}
	 */
	@SuppressWarnings("unchecked")
	static java.lang.Object detailInner(java.lang.Object details) {
		if (details instanceof Map) {
			return ((Map<String, java.lang.Object>) details).get("details");
		}
		return null;
	}

	/**
	 * Checks whether the details object matches a given variant key.
	 * Supports both new and old formats:
	 * <ul>
	 *   <li>New: {@code {"kind": "Parse"}} -- checks if {@code kind} equals key</li>
	 *   <li>Old: {@code "Parse"} -- checks if the string equals key</li>
	 *   <li>Old: {@code {"Parse": ...}} -- checks if the map contains key</li>
	 * </ul>
	 *
	 * @param details the details object (may be {@code null})
	 * @param key     the key to look for
	 * @return {@code true} if the key is present
	 */
	@SuppressWarnings("unchecked")
	static boolean hasDetailKey(java.lang.Object details, String key) {
		if (details == null) {
			return false;
		}
		// New format: {"kind": "Parse"}
		String dk = detailKind(details);
		if (dk != null) {
			return dk.equals(key);
		}
		// Old format: "Parse" (bare string)
		if (details instanceof String) {
			return ((String) details).equals(key);
		}
		// Old format: {"Parse": ...} (map key)
		if (details instanceof Map) {
			return ((Map<String, java.lang.Object>) details).containsKey(key);
		}
		return false;
	}

	/**
	 * Extracts the inner value for a given variant key.
	 * Supports both new and old formats:
	 * <ul>
	 *   <li>New: if {@code kind} equals key, returns {@code details["details"]}</li>
	 *   <li>Old: if details is a map, returns {@code details[key]}</li>
	 *   <li>Old: if details is a string matching key, returns {@code null} (unit variant)</li>
	 * </ul>
	 *
	 * @param details the details object (may be {@code null})
	 * @param key     the key to extract
	 * @return the value, or {@code null}
	 */
	@SuppressWarnings("unchecked")
	static java.lang.Object getDetailValue(java.lang.Object details, String key) {
		if (details == null) {
			return null;
		}
		// New format: {"kind": "Auth", "details": {"kind": "TokenExpired"}}
		String dk = detailKind(details);
		if (dk != null) {
			if (dk.equals(key)) {
				return detailInner(details);
			}
			return null;
		}
		// Old format: unit variant string has no value
		if (details instanceof String) {
			return null;
		}
		// Old format: {"Auth": "TokenExpired"} or {"Table": {"name": "users"}}
		if (details instanceof Map) {
			return ((Map<String, java.lang.Object>) details).get(key);
		}
		return null;
	}

	/**
	 * Extracts a string field from a variant's inner details.
	 * Supports both new and old formats.
	 *
	 * <p>New: {@code {"kind": "Table", "details": {"name": "users"}}}
	 * <br>Old: {@code {"Table": {"name": "users"}}}
	 * <br>Both: {@code detailField(details, "Table", "name")} returns {@code "users"}.
	 *
	 * @param details the details object
	 * @param key     the variant key
	 * @param field   the field name inside the inner object
	 * @return the string value, or {@code null}
	 */
	@SuppressWarnings("unchecked")
	static String detailField(java.lang.Object details, String key, String field) {
		java.lang.Object inner = getDetailValue(details, key);
		if (inner instanceof Map) {
			java.lang.Object value = ((Map<String, java.lang.Object>) inner).get(field);
			return value instanceof String ? (String) value : null;
		}
		return null;
	}

	/**
	 * Extracts a string from the inner details of a newtype variant.
	 * Supports both new and old formats:
	 * <ul>
	 *   <li>New: {@code {"kind": "Auth", "details": {"kind": "TokenExpired"}}}
	 *       -- returns {@code "TokenExpired"}</li>
	 *   <li>Old: {@code {"Auth": "TokenExpired"}} -- returns {@code "TokenExpired"}</li>
	 * </ul>
	 *
	 * @param details the details object
	 * @param key     the variant key
	 * @return the string value, or {@code null}
	 */
	static String getDetailString(java.lang.Object details, String key) {
		java.lang.Object inner = getDetailValue(details, key);
		if (inner == null) {
			return null;
		}
		// New format: inner is {"kind": "TokenExpired"}
		String dk = detailKind(inner);
		if (dk != null) {
			return dk;
		}
		// Old format: inner is "TokenExpired"
		if (inner instanceof String) {
			return (String) inner;
		}
		return null;
	}

	/**
	 * @deprecated Use {@link #detailField(java.lang.Object, String, String)} instead.
	 */
	@SuppressWarnings("unchecked")
	static String getNestedString(java.lang.Object details, String outerKey, String innerKey) {
		return detailField(details, outerKey, innerKey);
	}

	/**
	 * @deprecated Use {@link #getDetailString(java.lang.Object, String)} with an equality check instead.
	 */
	static boolean isNewtypeValue(java.lang.Object details, String outerKey, String value) {
		return value.equals(getDetailString(details, outerKey));
	}

	// ---- Detail navigation helpers ----
	//
	// SurrealDB v3 uses a recursive { "kind": "...", "details": ... } format
	// for error details (internally-tagged). Older servers used serde's
	// externally-tagged format ("Parse" / {"Auth": "TokenExpired"} / {"Table": {"name": "users"}}).
	//
	// All helpers support both formats for backward compatibility.

	/**
	 * Returns the machine-readable error kind string (e.g. {@code "NotAllowed"}).
	 * For unknown kinds this is the wire string; otherwise it matches {@link #getKindEnum()}{@code .getRaw()}.
	 *
	 * @return the error kind string, never {@code null}
	 */
	public String getKind() {
		return rawKind != null ? rawKind : kindEnum.getRaw();
	}

	/**
	 * Returns the error kind as an enum for type-safe matching.
	 * Unknown kinds from newer servers map to {@link ErrorKind#UNKNOWN}; the raw string is in {@link #getKind()}.
	 *
	 * @return the error kind enum, never {@code null}
	 */
	public ErrorKind getKindEnum() {
		return kindEnum;
	}

	/**
	 * Returns the optional structured details for this error.
	 *
	 * <p>Details use the {@code {kind, details?}} wire format. The value is either:
	 * <ul>
	 *   <li>{@code null} -- no details</li>
	 *   <li>a {@code Map<String, Object>} with a {@code "kind"} key and optional
	 *       {@code "details"} key (new internally-tagged format)</li>
	 *   <li>a {@link String} -- a unit variant in the legacy format (e.g. {@code "Parse"})</li>
	 *   <li>a {@code Map<String, Object>} without {@code "kind"} -- legacy externally-tagged format</li>
	 * </ul>
	 *
	 * <p>Prefer the typed convenience getters on subclasses (e.g.
	 * {@link NotFoundException#getTableName()}) over inspecting details directly.
	 *
	 * @return the details object, or {@code null}
	 */
	public java.lang.Object getDetails() {
		return details;
	}

	/**
	 * Returns the typed server-side cause of this error, if any.
	 *
	 * <p>This is equivalent to calling {@link #getCause()} and casting to
	 * {@code ServerException}, but avoids the cast.
	 *
	 * @return the server cause, or {@code null}
	 */
	public ServerException getServerCause() {
		return serverCause;
	}

	/**
	 * Checks whether this error or any error in its cause chain has the given
	 * {@link ErrorKind kind}.
	 *
	 * @param kind the kind to look for
	 * @return {@code true} if any error in the chain matches
	 */
	public boolean hasKind(String kind) {
		return findCause(kind) != null;
	}

	/**
	 * Checks whether this error or any error in its cause chain has the given {@link ErrorKind}.
	 */
	public boolean hasKind(ErrorKind kind) {
		return findCause(kind) != null;
	}

	// ---- Legacy helpers (delegate to new ones for backward source compat) ----

	/**
	 * Finds the first error in the cause chain (including this error) that has
	 * the given {@link ErrorKind kind}.
	 *
	 * @param kind the kind to look for
	 * @return the matching {@code ServerException}, or {@code null}
	 */
	public ServerException findCause(String kind) {
		ServerException current = this;
		while (current != null) {
			if (kind.equals(current.getKind())) {
				return current;
			}
			current = current.serverCause;
		}
		return null;
	}

	/**
	 * Finds the first error in the cause chain (including this error) that has the given {@link ErrorKind}.
	 */
	public ServerException findCause(ErrorKind kind) {
		ServerException current = this;
		while (current != null) {
			if (kind == current.kindEnum) {
				return current;
			}
			current = current.serverCause;
		}
		return null;
	}
}