HealthResource.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.eclipse.jgit.server.rest;

import java.io.IOException;
import java.io.PrintWriter;

import org.eclipse.jgit.storage.hibernate.config.HibernateSessionFactoryProvider;
import org.eclipse.jgit.storage.hibernate.entity.GitCommitIndex;
import org.hibernate.Session;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * REST endpoint for server health checks.
 * <p>
 * Returns the health status of the application and its database connection.
 * <ul>
 * <li>{@code GET /api/health} — returns health status as JSON</li>
 * </ul>
 */
public class HealthResource extends HttpServlet {

	private static final long serialVersionUID = 1L;

	private final HibernateSessionFactoryProvider provider;

	/**
	 * Create a health check endpoint.
	 *
	 * @param provider
	 *            the session factory provider for database health checks
	 */
	public HealthResource(HibernateSessionFactoryProvider provider) {
		this.provider = provider;
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
		resp.setContentType("application/json"); //$NON-NLS-1$
		resp.setCharacterEncoding("UTF-8"); //$NON-NLS-1$

		boolean dbOk = false;
		boolean searchOk = false;
		String dbError = null;
		String searchError = null;

		// Check database connectivity
		try (Session session = provider.getSessionFactory().openSession()) {
			session.createNativeQuery("SELECT 1", Integer.class) //$NON-NLS-1$
					.uniqueResult();
			dbOk = true;
		} catch (Exception e) {
			dbError = e.getMessage();
		}

		// Check search backend
		try (Session session = provider.getSessionFactory().openSession()) {
			SearchSession searchSession = Search.session(session);
			searchSession.search(GitCommitIndex.class)
					.where(f -> f.matchAll())
					.fetch(0, 0);
			searchOk = true;
		} catch (Exception e) {
			searchError = e.getMessage();
		}

		boolean allOk = dbOk && searchOk;
		resp.setStatus(allOk ? HttpServletResponse.SC_OK
				: HttpServletResponse.SC_SERVICE_UNAVAILABLE);

		try (PrintWriter w = resp.getWriter()) {
			StringBuilder json = new StringBuilder();
			json.append("{\"status\":\""); //$NON-NLS-1$
			json.append(allOk ? "UP" : "DOWN"); //$NON-NLS-1$ //$NON-NLS-2$
			json.append("\",\"database\":{\"status\":\""); //$NON-NLS-1$
			json.append(dbOk ? "UP" : "DOWN"); //$NON-NLS-1$ //$NON-NLS-2$
			json.append("\""); //$NON-NLS-1$
			if (dbError != null) {
				json.append(",\"error\":\""); //$NON-NLS-1$
				json.append(escapeJson(dbError));
				json.append("\""); //$NON-NLS-1$
			}
			json.append("},\"search\":{\"status\":\""); //$NON-NLS-1$
			json.append(searchOk ? "UP" : "DOWN"); //$NON-NLS-1$ //$NON-NLS-2$
			json.append("\""); //$NON-NLS-1$
			if (searchError != null) {
				json.append(",\"error\":\""); //$NON-NLS-1$
				json.append(escapeJson(searchError));
				json.append("\""); //$NON-NLS-1$
			}
			json.append("}}"); //$NON-NLS-1$
			w.write(json.toString());
		}
	}

	private static String escapeJson(String s) {
		StringBuilder sb = new StringBuilder(s.length());
		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			switch (c) {
			case '"':
				sb.append("\\\""); //$NON-NLS-1$
				break;
			case '\\':
				sb.append("\\\\"); //$NON-NLS-1$
				break;
			case '\n':
				sb.append("\\n"); //$NON-NLS-1$
				break;
			case '\r':
				sb.append("\\r"); //$NON-NLS-1$
				break;
			case '\t':
				sb.append("\\t"); //$NON-NLS-1$
				break;
			default:
				if (c < 0x20) {
					sb.append(String.format("\\u%04x", (int) c)); //$NON-NLS-1$
				} else {
					sb.append(c);
				}
				break;
			}
		}
		return sb.toString();
	}
}