Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrating GraphQL into project #152

Merged
merged 27 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5fa623a
Adding `graphql` and `webflux` as dependencies
dabico Aug 6, 2023
3d40da5
Renamed `client` to `httpClient`
dabico Aug 6, 2023
2d70480
Added GraphQL configuration
dabico Aug 6, 2023
ba4b1b8
Improved the `GraphQLConfig` to support header attachment
dabico Aug 7, 2023
e0d7f11
Adding converter that goes from `String`-keyed maps into `JsonObject`
dabico Aug 7, 2023
45240ae
Added GSON configuration to `application.properties`
dabico Aug 7, 2023
11da140
Adding GraphQL configuration, schema and query files
dabico Aug 7, 2023
74c819d
Adding basic GraphQL support to `GitHubAPIConnector`
dabico Aug 7, 2023
975e5b7
`fetchRepoInfo` now uses the newly added GraphQL endpoint
dabico Aug 7, 2023
69278dc
Added a `Result` abstraction to decrease amount of duplicate code
dabico Aug 7, 2023
2f42360
Added converter from Java's list to Gson's `JsonArray`
dabico Aug 8, 2023
28a2362
Renaming for consistency with the rest of the Spring GraphQL stuff
dabico Aug 8, 2023
4148ca8
Generifying the `Map` to `JsonObject` converter
dabico Aug 8, 2023
9f26067
Added `GraphQlErrorResponse`
dabico Aug 8, 2023
79722d7
Added converter for assigning error messages to a specific category
dabico Aug 8, 2023
79845ea
Adding very basic converter: `JsonObject` -> `SourceLocation`
dabico Aug 8, 2023
a609f3d
Adding converter for our new `GraphQlErrorResponse` class
dabico Aug 8, 2023
7003daa
Updating the `GitHubAPIConnector` to make use of new class
dabico Aug 8, 2023
8ca80ec
Renaming `ErrorResponse` to `RestErrorResponse` to be more specific
dabico Aug 8, 2023
683d45a
Fixing checkstyle violation: `WhitespaceAroundCheck`
dabico Aug 8, 2023
59f6434
Simplifying
dabico Aug 8, 2023
1ef124d
Fixing checkstyle violation: `WhitespaceAroundCheck`
dabico Aug 8, 2023
5c3c4cf
Fixing checkstyle violation: `RedundantModifierCheck`
dabico Aug 8, 2023
7d0f0c3
Parametrized document selection for GraphQL calls
dabico Aug 8, 2023
8c844d1
Updating the documentation
dabico Aug 9, 2023
f59ba16
`CrawlProjectsJob` will no longer start if there are no access tokens
dabico Aug 9, 2023
4cb98ac
Changed no-token notification message in `GitHubTokenManager`
dabico Aug 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ gzcat < gse.sql.gz | mysql -u gseadmin -pLugano2020 gse

### Server

Before attempting to run the server, I advise you generate your own GitHub personal access token (PAT).
Said token should include the `repo` scope, in order for it to effectively crawl the GitHub API.
While the token is not mandatory, the impact its presence has on the mining speed can not be understated.
Before attempting to run the server, you must generate your own GitHub personal access token (PAT).
GHS relies on the GraphQL API, which is inaccessible without authentication.
The token must include the `repo` scope, in order for it to access the information present in the GitHub API.

Once that is done, you can run the server locally using Maven:

Expand Down
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/usi/si/seart/config/GraphQlConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package usi.si.seart.config;

import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.graphql.client.HttpGraphQlClient;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import usi.si.seart.github.Endpoint;
import usi.si.seart.github.GitHubTokenManager;

@Configuration
@AllArgsConstructor(onConstructor_ = @Autowired)
public class GraphQlConfig {

GitHubTokenManager gitHubTokenManager;

@Bean
public GraphQlClient graphQlClient() {
return HttpGraphQlClient.create(webClient());
}

@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl(Endpoint.GRAPH_QL.toString())
.defaultHeader("X-GitHub-Api-Version", "2022-11-28")
.filter(exchangeFilterFunction())
.build();
}

@Bean
public ExchangeFilterFunction exchangeFilterFunction() {
return new ExchangeFilterFunction() {

@NotNull
@Override
public Mono<ClientResponse> filter(@NotNull ClientRequest original, @NotNull ExchangeFunction next) {
ClientRequest modified = ClientRequest.from(original)
.headers(headers -> {
String token = gitHubTokenManager.getCurrentToken();
if (token != null)
headers.setBearerAuth(token);
})
.build();
return next.exchange(modified);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
import com.google.gson.JsonObject;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import usi.si.seart.github.ErrorResponse;
import usi.si.seart.github.RestErrorResponse;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;
import java.util.stream.StreamSupport;

public class JsonObjectToErrorResponseConverter implements Converter<JsonObject, ErrorResponse> {
public class JsonObjectToErrorResponseConverter implements Converter<JsonObject, RestErrorResponse> {

@Override
@NonNull
public ErrorResponse convert(@NonNull JsonObject source) {
ErrorResponse.ErrorResponseBuilder builder = ErrorResponse.builder();
public RestErrorResponse convert(@NonNull JsonObject source) {
RestErrorResponse.RestErrorResponseBuilder builder = RestErrorResponse.builder();

builder.message(source.get("message").getAsString());

Expand Down Expand Up @@ -48,7 +48,7 @@ public ErrorResponse convert(@NonNull JsonObject source) {
String codeName = Optional.ofNullable(object.get("code"))
.map(JsonElement::getAsString)
.orElse(null);
return new ErrorResponse.Error(resource, field, codeName, message);
return new RestErrorResponse.Error(resource, field, codeName, message);
}).forEach(builder::error);

return builder.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package usi.si.seart.converter;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import graphql.ErrorClassification;
import graphql.language.SourceLocation;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import usi.si.seart.github.GraphQlErrorResponse;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Component
@AllArgsConstructor(onConstructor_ = @Autowired)
public class JsonObjectToGraphQlErrorResponse implements Converter<JsonObject, GraphQlErrorResponse> {

Gson gson;
JsonObjectToSourceLocationConverter sourceLocationConverter;
StringToGraphQlErrorResponseErrorTypeConverter errorTypeConverter;

@Override
@NotNull
public GraphQlErrorResponse convert(@NotNull JsonObject source) {
GraphQlErrorResponse.GraphQlErrorResponseBuilder builder = GraphQlErrorResponse.builder();

String message = source.getAsJsonPrimitive("message").getAsString();
builder.message(message);
ErrorClassification errorType = errorTypeConverter.convert(message);
builder.errorType(errorType);

if (source.has("path")) {
JsonArray array = source.getAsJsonArray("path");
Type type = new TypeToken<List<Object>>() { }.getType();
List<Object> parsedPath = gson.fromJson(array, type);
builder.parsedPath(parsedPath);
}

if (source.has("locations")) {
JsonArray array = source.getAsJsonArray("locations");
List<SourceLocation> locations = StreamSupport.stream(array.spliterator(), true)
.map(JsonElement::getAsJsonObject)
.map(sourceLocationConverter::convert)
.collect(Collectors.toList());
builder.locations(locations);
}

if (source.has("extensions")) {
JsonObject object = source.getAsJsonObject("extensions");
Type type = new TypeToken<Map<String, Object>>() { }.getType();
Map<String, Object> extensions = gson.fromJson(object, type);
builder.extensions(extensions);
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package usi.si.seart.converter;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import graphql.language.SourceLocation;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
@AllArgsConstructor(onConstructor_ = @Autowired)
public class JsonObjectToSourceLocationConverter implements Converter<JsonObject, SourceLocation> {

private final Gson gson;

@Override
@NotNull
public SourceLocation convert(@NotNull JsonObject source) {
return gson.fromJson(source, SourceLocation.class);
}
}
25 changes: 25 additions & 0 deletions src/main/java/usi/si/seart/converter/ListToJsonArrayConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package usi.si.seart.converter;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@AllArgsConstructor(onConstructor_ = @Autowired)
public class ListToJsonArrayConverter implements Converter<List<?>, JsonArray> {

private final Gson gson;

@Override
@NotNull
public JsonArray convert(@NotNull List<?> source) {
String string = gson.toJson(source);
return gson.fromJson(string, JsonArray.class);
}
}
25 changes: 25 additions & 0 deletions src/main/java/usi/si/seart/converter/MapToJsonObjectConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package usi.si.seart.converter;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@AllArgsConstructor(onConstructor_ = @Autowired)
public class MapToJsonObjectConverter implements Converter<Map<String, ?>, JsonObject> {

private final Gson gson;

@Override
@NotNull
public JsonObject convert(@NotNull Map<String, ?> source) {
String string = gson.toJson(source);
return gson.fromJson(string, JsonObject.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package usi.si.seart.converter;

import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import usi.si.seart.github.GraphQlErrorResponse;

import java.util.regex.Pattern;

@Component
public class StringToGraphQlErrorResponseErrorTypeConverter implements Converter<String, GraphQlErrorResponse.ErrorType>
{
// https://www.debuggex.com/r/kzZ6PfNvkQLAg3qG
private static final Pattern RATE_LIMITED_PATTERN = Pattern.compile(
"^API rate limit exceeded for user ID (\\d+)\\.$"
);

// https://www.debuggex.com/r/1unTNcRjYb3M8TrH
private static final Pattern FIELD_ERROR_PATTERN = Pattern.compile(
"^Field '([^']+)' doesn't exist on type '([^']+)'$"
);

// https://www.debuggex.com/r/yK-tNA559lGPF0aL
private static final Pattern NOT_FOUND_PATTERN = Pattern.compile(
"^Could not resolve to an? (\\w+) with the (\\w+) (?:of )?'([^']+)'\\.$"
);

// https://www.debuggex.com/r/6BjTNEeyleQXfBjx
private static final Pattern ARGUMENT_TYPE_ERROR_PATTERN = Pattern.compile(
"^Argument '([^']+)' on Field '([^']+)' has an invalid value \\(([^)]+)\\). Expected type '([^']+)'\\.$"
);

// https://www.debuggex.com/r/mcZ2rcy61FSqVpBO
private static final Pattern ARGUMENT_MISSING_ERROR_PATTERN = Pattern.compile(
"^Field '([^']+)' is missing required arguments?: (\\w+(?:,\\s*\\w+)*)$"
);

// https://www.debuggex.com/r/emiQbdZlfCYhAlhi
private static final Pattern ARGUMENT_UNKNOWN_ERROR_PATTERN = Pattern.compile(
"^Field '([^']+)' doesn't accept argument '([^']+)'$"
);

// https://www.debuggex.com/r/_WAiksCCoBiSq9nw
private static final Pattern VARIABLE_VALUE_ERROR_PATTERN = Pattern.compile(
"^Variable \\$(\\w+) of type (\\w+!?) was provided invalid value$"
);

// https://www.debuggex.com/r/OqQixKpVA8MogqMH
private static final Pattern VARIABLE_UNUSED_ERROR_PATTERN = Pattern.compile(
"^Variable \\$(\\w+) is declared by (\\w+)(?: query)? but not used$"
);

@Override
@Nullable
public GraphQlErrorResponse.ErrorType convert(@NotNull String source) {
if (source.equals("A query attribute must be specified and must be a string."))
return GraphQlErrorResponse.ErrorType.EMPTY_QUERY;
if (source.equals("Unexpected end of document"))
return GraphQlErrorResponse.ErrorType.EARLY_EOF;
if (source.startsWith("Parse error"))
return GraphQlErrorResponse.ErrorType.PARSE_ERROR;
if (RATE_LIMITED_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.RATE_LIMITED;
if (FIELD_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.FIELD_ERROR;
if (NOT_FOUND_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.NOT_FOUND;
if (ARGUMENT_TYPE_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.ARGUMENT_TYPE_ERROR;
if (ARGUMENT_MISSING_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.ARGUMENT_MISSING_ERROR;
if (ARGUMENT_UNKNOWN_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.ARGUMENT_UNKNOWN_ERROR;
if (VARIABLE_VALUE_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.VARIABLE_VALUE_ERROR;
if (VARIABLE_UNUSED_ERROR_PATTERN.matcher(source).matches())
return GraphQlErrorResponse.ErrorType.VARIABLE_UNUSED_ERROR;
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package usi.si.seart.exception.github;

import lombok.experimental.StandardException;
import usi.si.seart.github.ErrorResponse;
import usi.si.seart.github.RestErrorResponse;

@StandardException
public class GitHubAPIException extends RuntimeException {

public GitHubAPIException(ErrorResponse errorResponse) {
public GitHubAPIException(RestErrorResponse errorResponse) {
this(errorResponse.toString());
}
}
Loading