All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.blazebit.query.connector.gitlab.GitlabGraphQlClient Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright Blazebit
 */
package com.blazebit.query.connector.gitlab;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Martijn Sprengers
 * @since 1.0.4
 */
public class GitlabGraphQlClient {

	private static final ObjectMapper MAPPER = ObjectMappers.getInstance();
	private static final int DEFAULT_PAGE_SIZE = 100; // GitLab's default pagination size

	private final HttpClient httpClient;
	private final String gitlabApiUrl;
	private final String authToken;

	public GitlabGraphQlClient(String host, String gitlabToken) {
		this.httpClient = HttpClient.newHttpClient();
		this.gitlabApiUrl = host + "/api/graphql";
		this.authToken = gitlabToken;
	}

	private static List extractUsersFromProjects(JsonNode rootNode) {
		List users = new ArrayList<>();

		// Traverse edges -> node -> projectMembers -> edges -> node -> user
		for ( JsonNode projectEdge : rootNode.path( "edges" ) ) {
			JsonNode projectNode = projectEdge.path( "node" );
			JsonNode projectMembers = projectNode.path( "projectMembers" );

			for ( JsonNode memberEdge : projectMembers.path( "edges" ) ) {
				JsonNode userNode = memberEdge.path( "node" ).path( "user" );
				if ( !userNode.isMissingNode() ) {
					users.add( GitlabUser.fromJson( userNode.toString() ) );
				}
			}
		}
		return users;
	}

	private static List extractUsersFromGroups(JsonNode rootNode) {
		List users = new ArrayList<>();

		// Traverse edges -> node -> groupMembers -> edges -> node -> user
		for ( JsonNode groupEdge : rootNode.path( "edges" ) ) {
			JsonNode groupNode = groupEdge.path( "node" );
			JsonNode groupMembers = groupNode.path( "groupMembers" );

			for ( JsonNode memberEdge : groupMembers.path( "edges" ) ) {
				JsonNode userNode = memberEdge.path( "node" ).path( "user" );
				if ( !userNode.isMissingNode() ) {
					users.add( GitlabUser.fromJson( userNode.toString() ) );
				}
			}
		}
		return users;
	}

	private static List extractProjects(JsonNode rootNode) {
		List projects = new ArrayList<>();

		for ( JsonNode projectEdge : rootNode.path( "edges" ) ) {
			JsonNode projectNode = projectEdge.path( "node" );
			if ( !projectNode.isMissingNode() ) {
				projects.add( GitlabProject.fromJson( projectNode.toString() ) );
			}
		}

		return projects;
	}

	private static List extractGroups(JsonNode rootNode) {
		List groups = new ArrayList<>();

		for ( JsonNode groupEdge : rootNode.path( "edges" ) ) {
			JsonNode groupNode = groupEdge.path( "node" );
			if ( !groupNode.isMissingNode() ) {
				groups.add( GitlabGroup.fromJson( groupNode.toString() ) );
			}
		}

		return groups;
	}

	public List fetchUsers(List userIds) {
		Map variables = new HashMap<>();
		variables.put( "ids", userIds );

		String query = """
					query ($ids: [ID!]) {
						users(ids: $ids) {
							nodes { id name username lastActivityOn active avatarUrl bio bot commitEmail createdAt discord gitpodEnabled groupCount human jobTitle linkedin location organization pronouns publicEmail twitter webPath webUrl }
						}
					}
				""";

		return executeQuery( query, variables, "users", GitlabUser::fromJson );
	}

	public List fetchUsersFromProjects(boolean membership) {
		Map variables = new HashMap<>();
		variables.put( "membership", membership );

		String query = """
					query ($membership: Boolean, $first: Int, $cursor: String) {
						projects(membership: $membership, first: $first, after: $cursor) {
							pageInfo { endCursor hasNextPage }
							edges {
								node {
									projectMembers {
										edges {
											node {
												user {
													id
													name
													username
													lastActivityOn
													avatarUrl
													publicEmail
												}
											}
										}
									}
								}
							}
						}
					}
				""";

		return executePaginatedQuery( query, variables, "projects", GitlabGraphQlClient::extractUsersFromProjects );
	}

	public List fetchUsersFromGroups(boolean membership) {
		Map variables = new HashMap<>();
		variables.put( "membership", membership );

		String query = """
					query ($membership: Boolean, $first: Int, $cursor: String) {
						groups(ownedOnly: $membership, first: $first, after: $cursor) {
							pageInfo { endCursor hasNextPage }
							edges {
								node {
									groupMembers {
										edges {
											node {
												user {
													id
													name
													username
													lastActivityOn
													avatarUrl
													publicEmail
												}
											}
										}
									}
								}
							}
						}
					}
				""";

		return executePaginatedQuery( query, variables, "groups", GitlabGraphQlClient::extractUsersFromGroups );
	}

	public List fetchUsersFromProjectsAndGroups(boolean membership) {
		List projectUsers = fetchUsersFromProjects( membership );
		List groupUsers = fetchUsersFromGroups( membership );

		// Merge results, removing duplicates
		Set uniqueIds = new HashSet<>();
		List allUsers = new ArrayList<>();

		for ( GitlabUser user : projectUsers ) {
			if ( uniqueIds.add( user.id() ) ) {
				allUsers.add( user );
			}
		}
		for ( GitlabUser user : groupUsers ) {
			if ( uniqueIds.add( user.id() ) ) {
				allUsers.add( user );
			}
		}

		return allUsers;
	}

	public List fetchProjects(boolean membership) {
		Map variables = new HashMap<>();
		variables.put( "membership", membership );

		String query = """
					query ($membership: Boolean, $first: Int, $cursor: String) {
						projects(membership: $membership, first: $first, after: $cursor) {
							pageInfo { endCursor hasNextPage }
							edges {
								node {
									id
									name
									archived
									avatarUrl
									createdAt
									description
									lastActivityAt
									path
									updatedAt
									group { id }
									repository { rootRef }
									mergeRequestsEnabled
									branchRules {
										edges {
											node {
												id
												name
												isDefault
												isProtected
												branchProtection {
													allowForcePush
													codeOwnerApprovalRequired
												}
											}
										}
									}
								}
							}
						}
					}
				""";

		return executePaginatedQuery( query, variables, "projects", GitlabGraphQlClient::extractProjects );
	}

	public List fetchGroups(boolean ownedOnly) {
		Map variables = new HashMap<>();
		variables.put( "ownedOnly", ownedOnly );

		String query = """
					query ($ownedOnly: Boolean, $first: Int, $cursor: String) {
						groups(ownedOnly: $ownedOnly, first: $first, after: $cursor) {
							pageInfo { endCursor hasNextPage }
							edges {
								node {
									id
									name
									path
									requireTwoFactorAuthentication
									twoFactorGracePeriod
								}
							}
						}
					}
				""";

		return executePaginatedQuery( query, variables, "groups", GitlabGraphQlClient::extractGroups );
	}

	private  List executePaginatedQuery(
			String query,
			Map variables,
			String rootNode,
			JsonPathExtractor extractor
	) {
		List allResults = new ArrayList<>();
		String cursor = null;
		boolean hasNextPage;

		do {
			variables.put( "cursor", cursor );
			variables.put( "first", DEFAULT_PAGE_SIZE );

			String requestBody = createJsonRequest( query, variables );

			try {
				HttpRequest request = HttpRequest.newBuilder()
						.uri( URI.create( gitlabApiUrl ) )
						.header( "Authorization", "Bearer " + authToken )
						.header( "Content-Type", "application/json" )
						.POST( HttpRequest.BodyPublishers.ofString( requestBody ) )
						.build();

				HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofString() );

				if ( response.statusCode() != 200 ) {
					throw new RuntimeException( "GitLab API error: " + response.body() );
				}

				// Parse JSON response using Jackson
				JsonNode jsonResponse = MAPPER.readTree( response.body() );
				JsonNode data = jsonResponse.path( "data" ).path( rootNode );
				JsonNode pageInfo = data.path( "pageInfo" );

				// Use the provided extractor function to get results
				List extractedResults = extractor.extract( data );
				allResults.addAll( extractedResults );

				cursor = pageInfo.path( "endCursor" ).asText( null );
				hasNextPage = pageInfo.path( "hasNextPage" ).asBoolean( false );

			}
			catch (Exception e) {
				throw new RuntimeException( "Failed to fetch " + rootNode + " from GitLab GraphQL API", e );
			}

		}
		while ( hasNextPage && cursor != null );

		return allResults;
	}

	private  List executeQuery(String query, Map variables, String rootNode, JsonParser parser) {
		try {
			String requestBody = createJsonRequest( query, variables );

			HttpRequest request = HttpRequest.newBuilder()
					.uri( URI.create( gitlabApiUrl ) )
					.header( "Authorization", "Bearer " + authToken )
					.header( "Content-Type", "application/json" )
					.POST( HttpRequest.BodyPublishers.ofString( requestBody ) )
					.build();

			HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofString() );

			if ( response.statusCode() != 200 ) {
				throw new RuntimeException( "GitLab API error: " + response.body() );
			}

			// Parse JSON response using Jackson
			JsonNode jsonResponse = MAPPER.readTree( response.body() );

			JsonNode dataNode = jsonResponse.path( "data" ).path( rootNode ).path( "nodes" );
			if ( !dataNode.isArray() ) {
				throw new RuntimeException( "Unexpected response structure: " + response.body() );
			}

			List resultList = new ArrayList<>();
			for ( JsonNode node : dataNode ) {
				resultList.add( parser.parse( node.toString() ) ); // Convert JSON node to String and parse
			}

			return resultList;
		}
		catch (Exception e) {
			throw new RuntimeException( "Failed to fetch " + rootNode + " from GitLab GraphQL API", e );
		}
	}

	private String createJsonRequest(String query, Map variables) {
		try {
			Map requestMap = Map.of(
					"query", query,
					"variables", variables
			);

			return MAPPER.writeValueAsString( requestMap );
		}
		catch (Exception e) {
			throw new RuntimeException( "Failed to create JSON request", e );
		}
	}

	@FunctionalInterface
	interface JsonParser {
		T parse(String json);
	}

	@FunctionalInterface
	interface JsonPathExtractor {
		List extract(JsonNode rootNode);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy