diff --git a/pom.xml b/pom.xml
index 9376808..104bd05 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,7 @@
UTF-8
1.5.4
+ 1.7.25
@@ -25,6 +26,26 @@
ratpack-test
${ratpack.version}
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ 2.10.0
+
+
+ junit
+ junit
+ RELEASE
+ test
+
+
+
+ com.github.tomakehurst
+ wiremock
+ 2.15.0
+ pom
+ test
+
@@ -38,6 +59,29 @@
1.8
+
+ maven-assembly-plugin
+ 3.1.0
+
+
+
+ edu.allegro.exercise.Main
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ install
+
+ single
+
+
+
+
diff --git a/src/main/java/edu/allegro/exercise/Constant.java b/src/main/java/edu/allegro/exercise/Constant.java
new file mode 100644
index 0000000..3db1110
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/Constant.java
@@ -0,0 +1,28 @@
+package edu.allegro.exercise;
+
+public enum Constant {
+ APP_CONFIG("/app"),
+ GITHUB_USER_AGENT_HEADER("githubUserAgent"),
+ GITHUB_ACCEPT_HEADER("githubAcept"),
+ GITHUB_REPOSITORY_OWNER_INFO("githubResourceV3"),
+ ACCEPT("Accept"),
+ USER_AGENT("User-Agent"),
+ CONFIG_YAML("config.yaml"),
+ GITHUB_USER("githubUser"),
+ GITHUB_PASSWORD("githubPassword"), CLIENT_POOL_SIZE("clientPoolSize"), CLIENT_TIMEOUT("clientTimeout");
+
+ private final String prop;
+
+ Constant(String prop) {
+ this.prop = prop;
+ }
+
+ public String getName() {
+ return prop;
+ }
+
+ @Override
+ public String toString() {
+ return prop;
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/Main.java b/src/main/java/edu/allegro/exercise/Main.java
new file mode 100644
index 0000000..9720faa
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/Main.java
@@ -0,0 +1,28 @@
+package edu.allegro.exercise;
+
+import edu.allegro.exercise.handlers.GetUserRepoInfoService;
+import edu.allegro.exercise.handlers.RepositoryInfo;
+import edu.allegro.exercise.util.ServerUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ratpack.server.ServerConfig;
+
+import java.util.Map;
+
+
+public class Main {
+ static final private Logger log = LoggerFactory.getLogger(Main.class);
+
+ public static void main(String[] args) throws Exception {
+ ServerConfig config = ServerConfig
+ .builder()
+ .yaml(ServerUtil.getConfigurationFile())
+ .onError(throwable -> log.error("Cannot load config file", throwable))
+ .build();
+
+ Map configuration = ServerUtil.getConfiguration(config);
+ RepositoryInfo info = new RepositoryInfo(configuration);
+ GetUserRepoInfoService getUserRepoInfoService = new GetUserRepoInfoService(info);
+ Server.start(config, getUserRepoInfoService);
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/ResultCodes.java b/src/main/java/edu/allegro/exercise/ResultCodes.java
new file mode 100644
index 0000000..1d4743e
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/ResultCodes.java
@@ -0,0 +1,27 @@
+package edu.allegro.exercise;
+
+import edu.allegro.exercise.model.github.ErrorResponse;
+
+public enum ResultCodes {
+ BAD_REQUEST(400, 500, "Internal error - Bad github request"),
+ NOT_FOUND(404, 404, "Not found - Given repository or owner doesn't exist");
+
+ private final int statusCode;
+ private final int internalCode;
+ private final String statusDescription;
+
+ ResultCodes(int statusCode, int internalCode, String statusDescription) {
+ this.statusCode = statusCode;
+ this.statusDescription = statusDescription;
+ this.internalCode = internalCode;
+ }
+
+ public static ErrorResponse fromCode(int statusCode) {
+ for (ResultCodes resultCodes : ResultCodes.values()) {
+ if (resultCodes.statusCode == statusCode) {
+ return new ErrorResponse(resultCodes.internalCode, resultCodes.statusDescription);
+ }
+ }
+ return new ErrorResponse(500, "Server internal error");
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/Server.java b/src/main/java/edu/allegro/exercise/Server.java
new file mode 100644
index 0000000..20b0b62
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/Server.java
@@ -0,0 +1,37 @@
+package edu.allegro.exercise;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import edu.allegro.exercise.handlers.GetUserRepoInfoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ratpack.error.ClientErrorHandler;
+import ratpack.error.ServerErrorHandler;
+import ratpack.error.internal.DefaultProductionErrorHandler;
+import ratpack.handling.RequestLogger;
+import ratpack.registry.Registry;
+import ratpack.server.RatpackServer;
+import ratpack.server.ServerConfig;
+
+public class Server {
+ private static final Logger log = LoggerFactory.getLogger(Server.class);
+
+ static void start(ServerConfig config, GetUserRepoInfoService userRepoInfo) throws Exception {
+ RatpackServer.start(
+ ratpackServerSpec -> ratpackServerSpec
+ .serverConfig(config)
+ .registry(Registry.builder()
+ .add(ClientErrorHandler.class, new DefaultProductionErrorHandler())
+ .add(ServerErrorHandler.class, new DefaultProductionErrorHandler())
+ .with(registrySpec ->
+ registrySpec.add(new ObjectMapper().registerModule(new Jdk8Module())))
+ .build())
+ .handlers(userRepoInfo.create().append(
+ chain ->
+ chain.all((RequestLogger.ncsa()))
+ )
+ )
+ );
+ }
+
+}
diff --git a/src/main/java/edu/allegro/exercise/handlers/GetUserRepoInfoService.java b/src/main/java/edu/allegro/exercise/handlers/GetUserRepoInfoService.java
new file mode 100644
index 0000000..eee2417
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/handlers/GetUserRepoInfoService.java
@@ -0,0 +1,81 @@
+package edu.allegro.exercise.handlers;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import edu.allegro.exercise.ResultCodes;
+import edu.allegro.exercise.model.github.ErrorResponse;
+import edu.allegro.exercise.model.github.UserRepoInfo;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import ratpack.exec.Promise;
+import ratpack.func.Action;
+import ratpack.func.Function;
+import ratpack.handling.Chain;
+import ratpack.handling.Context;
+import ratpack.http.client.ReceivedResponse;
+import ratpack.jackson.Jackson;
+import ratpack.jackson.JsonRender;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static ratpack.jackson.Jackson.json;
+
+public class GetUserRepoInfoService {
+ private static final String OWNER = "owner";
+ private static final String REPO = "repo";
+ private static final String PATH = ":" + OWNER + "/:" + REPO;
+ private static final String REPOSITORIES = "repositories";
+ private final RepositoryInfo repositoryInfo;
+ private final ObjectMapper mapper;
+
+ public GetUserRepoInfoService(RepositoryInfo repositoryInfo) {
+ this.repositoryInfo = repositoryInfo;
+ this.mapper = new ObjectMapper();
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ public Action create() {
+ return chain ->
+ chain.prefix(REPOSITORIES, action ->
+ action.get(PATH, ctx -> {
+ Promise> optionalPromise =
+ repositoryInfo.getInfo(getToken(ctx, OWNER), getToken(ctx, REPO))
+ .mapIf(this::ifSuccess, handleOK(), handleNOK(ctx));
+
+ Promise response = optionalPromise
+ .map(responseOp -> responseOp.map(this::getUserRepoInfo))
+ .map(Jackson::json);
+ ctx.render(response);
+ }));
+ }
+
+ private boolean ifSuccess(ReceivedResponse response) {
+ return response.getStatusCode() == HttpResponseStatus.OK.code();
+ }
+
+ private Function> handleOK() {
+ return Optional::of;
+ }
+
+ private Function> handleNOK(Context ctx) {
+ return response -> {
+ ErrorResponse errorResponse = ResultCodes.fromCode(response.getStatusCode());
+ ctx.getResponse().status(errorResponse.getStatusCode());
+ ctx.render(json(errorResponse));
+ return Optional.empty();
+ };
+ }
+
+ private String getToken(Context ctx, String token) {
+ return ctx.getPathTokens().get(token);
+ }
+
+ private Optional getUserRepoInfo(ReceivedResponse res) {
+ try {
+ return Optional.of(mapper.readValue(res.getBody().getText(), UserRepoInfo.class));
+ } catch (IOException e) {
+ return Optional.empty();
+ }
+
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/handlers/RepositoryInfo.java b/src/main/java/edu/allegro/exercise/handlers/RepositoryInfo.java
new file mode 100644
index 0000000..17c0fff
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/handlers/RepositoryInfo.java
@@ -0,0 +1,79 @@
+package edu.allegro.exercise.handlers;
+
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.util.internal.StringUtil;
+import ratpack.exec.Promise;
+import ratpack.http.Headers;
+import ratpack.http.client.HttpClient;
+import ratpack.http.client.ReceivedResponse;
+import ratpack.http.client.RequestSpec;
+import ratpack.http.internal.NettyHeadersBackedHeaders;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Map;
+
+import static edu.allegro.exercise.Constant.*;
+
+public class RepositoryInfo {
+
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String BASIC = "Basic ";
+ private final HttpClient client;
+ private final String resourceLink;
+ private final Headers headers;
+
+ public RepositoryInfo(Map configuration) throws Exception {
+ Integer poolSize = (Integer) configuration.get(CLIENT_POOL_SIZE.getName());
+ Integer timeout = (Integer) configuration.get(CLIENT_TIMEOUT.getName());
+ this.client = initHttpClient(poolSize, timeout);
+ this.resourceLink = (String) configuration.get(GITHUB_REPOSITORY_OWNER_INFO.getName());
+ this.headers = setHeaders(configuration);
+ }
+
+ private HttpClient initHttpClient(int poolSize, int timeout) throws Exception {
+ return HttpClient.of(httpClientSpec ->
+ httpClientSpec
+ .poolSize(poolSize)
+ .connectTimeout(Duration.ofSeconds(timeout))
+ .readTimeout(Duration.ofSeconds(timeout))
+ );
+
+ }
+
+ public Promise getInfo(String owner, String repo) throws URISyntaxException {
+ StringBuilder urlBuilder = new StringBuilder(resourceLink)
+ .append(owner)
+ .append("/")
+ .append(repo);
+
+ return client.get(new URI(urlBuilder.toString()),
+ this::setHeaders);
+ }
+
+ private void setHeaders(RequestSpec requestSpec) {
+ requestSpec.getHeaders().copy(headers);
+ }
+
+
+ private Headers setHeaders(Map configuration) {
+ DefaultHttpHeaders defaultHttpHeaders = new DefaultHttpHeaders();
+ Headers headers = new NettyHeadersBackedHeaders(defaultHttpHeaders);
+ defaultHttpHeaders.set(ACCEPT.getName(), configuration.get(GITHUB_ACCEPT_HEADER.getName()));
+ defaultHttpHeaders.set(USER_AGENT.getName(), configuration.get(GITHUB_USER_AGENT_HEADER.getName()));
+ setAuthorizationHeader(configuration, defaultHttpHeaders);
+ return headers;
+ }
+
+ private void setAuthorizationHeader(Map configuration, DefaultHttpHeaders defaultHttpHeaders) {
+ String githubUser = (String) configuration.get(GITHUB_USER.getName());
+ String githubPassword = (String) configuration.get(GITHUB_PASSWORD.getName());
+ if (!StringUtil.isNullOrEmpty(githubPassword) && !StringUtil.isNullOrEmpty(githubPassword)) {
+ String credentials = githubUser + ":" + githubPassword;
+ byte[] encode = Base64.getEncoder().encode(credentials.getBytes());
+ defaultHttpHeaders.set(AUTHORIZATION, BASIC + new String(encode));
+ }
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/model/github/ErrorResponse.java b/src/main/java/edu/allegro/exercise/model/github/ErrorResponse.java
new file mode 100644
index 0000000..f536d00
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/model/github/ErrorResponse.java
@@ -0,0 +1,19 @@
+package edu.allegro.exercise.model.github;
+
+public class ErrorResponse {
+ private int StatusCode;
+ private String StatusDescription;
+
+ public ErrorResponse(int statusCode, String statusDescription) {
+ StatusCode = statusCode;
+ StatusDescription = statusDescription;
+ }
+
+ public int getStatusCode() {
+ return StatusCode;
+ }
+
+ public String getStatusDescription() {
+ return StatusDescription;
+ }
+}
diff --git a/src/main/java/edu/allegro/exercise/model/github/UserRepoInfo.java b/src/main/java/edu/allegro/exercise/model/github/UserRepoInfo.java
new file mode 100644
index 0000000..59694b8
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/model/github/UserRepoInfo.java
@@ -0,0 +1,30 @@
+package edu.allegro.exercise.model.github;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({
+ "full_name",
+ "description",
+ "created_at",
+ "clone_url",
+ "stargazers_count",
+})
+public class UserRepoInfo {
+
+ @JsonProperty("full_name")
+ private String fullName;
+ @JsonProperty("description")
+ private String description;
+ @JsonProperty("created_at")
+ private String createdAt;
+ @JsonProperty("clone_url")
+ private String cloneUrl;
+ @JsonProperty("stargazers_count")
+ private Integer stargazersCount;
+
+}
+
+
diff --git a/src/main/java/edu/allegro/exercise/util/ServerUtil.java b/src/main/java/edu/allegro/exercise/util/ServerUtil.java
new file mode 100644
index 0000000..a2929ae
--- /dev/null
+++ b/src/main/java/edu/allegro/exercise/util/ServerUtil.java
@@ -0,0 +1,28 @@
+package edu.allegro.exercise.util;
+
+import ratpack.server.ServerConfig;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+
+import static edu.allegro.exercise.Constant.APP_CONFIG;
+import static edu.allegro.exercise.Constant.CONFIG_YAML;
+
+public class ServerUtil {
+
+ @SuppressWarnings("unchecked")
+ public static Map getConfiguration(ServerConfig config) {
+ return config.get(APP_CONFIG.getName(), Map.class);
+ }
+
+ public static URL getConfigurationFile() throws MalformedURLException {
+ File file = new File(CONFIG_YAML.getName());
+ if (file.exists()) {
+ return file.toURI().toURL();
+ } else {
+ return Thread.currentThread().getContextClassLoader().getResource(CONFIG_YAML.getName());
+ }
+ }
+}
diff --git a/src/main/java/edu/allegro/exercisie/Main.java b/src/main/java/edu/allegro/exercisie/Main.java
deleted file mode 100644
index d3dd01f..0000000
--- a/src/main/java/edu/allegro/exercisie/Main.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package edu.allegro.exercisie;
-
-public class Main {
-
- public static void main(String[] args) {
-
- }
-
-}
diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml
new file mode 100644
index 0000000..98965a1
--- /dev/null
+++ b/src/main/resources/config.yaml
@@ -0,0 +1,13 @@
+server:
+ port: 8080
+ threads: 1
+ development: false
+
+app:
+ clientPoolSize: 20
+ clientTimeout: 10
+ githubResourceV3: https://api.github.com/repos/
+ githubUserAgent: Allegro-exercise
+ githubAcept: application/vnd.github.v3+json
+ githubUser: sinu0
+ githubPassword: myk700i
\ No newline at end of file
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..71ab639
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/edu/allegro/exercise/handlers/GetUserRepoInfoTest.java b/src/test/java/edu/allegro/exercise/handlers/GetUserRepoInfoTest.java
new file mode 100644
index 0000000..48b35fa
--- /dev/null
+++ b/src/test/java/edu/allegro/exercise/handlers/GetUserRepoInfoTest.java
@@ -0,0 +1,20 @@
+package edu.allegro.exercise.handlers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GetUserRepoInfoTest {
+
+ private static final Map CONFIGURATION = new HashMap() {{
+ put("githubResourceV3", "https://api.github.com/repos/%s/%s");
+ put("githubUserAgent", "Allegro-exercise");
+ put("githubAcept", "application/vnd.github.v3+json");
+ }};
+
+
+ public static GetUserRepoInfoService userRepotTest() throws Exception {
+ return new GetUserRepoInfoService(CONFIGURATION);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/edu/allegro/exercise/handlers/IntegrationTest.java b/src/test/java/edu/allegro/exercise/handlers/IntegrationTest.java
new file mode 100644
index 0000000..5e97a84
--- /dev/null
+++ b/src/test/java/edu/allegro/exercise/handlers/IntegrationTest.java
@@ -0,0 +1,71 @@
+package edu.allegro.exercise.handlers;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.junit.*;
+import ratpack.http.client.ReceivedResponse;
+import ratpack.server.RatpackServer;
+import ratpack.server.ServerConfigBuilder;
+import ratpack.test.embed.EmbeddedApp;
+import ratpack.test.http.TestHttpClient;
+
+public class IntegrationTest {
+
+ private static final String GITHUB_OWNER = "sinu0";
+ private static final String GIHTUB_REPO = "SiSPD";
+ private static final String GIHTUB_REPO_NOK = "SiSPDs";
+ @Rule
+ public WireMockRule wireMockRule = new WireMockRule(8089);
+ private EmbeddedApp app;
+
+ @Before
+ public void setUp() throws Exception {
+ GetUserRepoInfoService getUserRepoInfoService = GetUserRepoInfoTest.userRepotTest();
+ app = EmbeddedApp.fromServer(RatpackServer.of(ratpackServerSpec ->
+ ratpackServerSpec
+ .serverConfig(ServerConfigBuilder::build)
+ .handlers(getUserRepoInfoService.create())));
+
+ }
+
+ @After
+ public void endTests() throws Exception {
+ app.getServer().stop();
+ app = null;
+ }
+
+ @Test
+ public void shouldReturnOKInfoAboutOwnerRepo() throws Exception {
+ app.test(this::shouldReturnOKInfoAboutOwnerRepo);
+ }
+
+ @Test
+ public void shouldReturnNOKInfoAboutOwnerRepo() throws Exception {
+ app.test(this::shouldReturnNOKInfoAboutOwnerRepo);
+ }
+
+ private void shouldReturnOKInfoAboutOwnerRepo(TestHttpClient testHttpClient) {
+ ReceivedResponse receivedResponse = testHttpClient.get(String.format("/repositories/%s/%s", GITHUB_OWNER, GIHTUB_REPO));
+ responseShouldBeValid(receivedResponse);
+ }
+
+ private void shouldReturnNOKInfoAboutOwnerRepo(TestHttpClient testHttpClient) {
+ ReceivedResponse receivedResponse = testHttpClient.get(String.format("/repositories/%s/%s", GITHUB_OWNER, GIHTUB_REPO_NOK));
+ responseShouldBeNotValid(receivedResponse);
+ }
+
+ private void responseShouldBeValid(ReceivedResponse receivedResponse) {
+ System.out.println(receivedResponse.getHeaders().asMultiValueMap().toString());
+ System.out.println(receivedResponse.getBody().getText());
+ Assert.assertTrue("Response should be ok for owner:" + GITHUB_OWNER + " and for repo: " + GIHTUB_REPO, receivedResponse.getStatusCode() == HttpResponseStatus.OK.code());
+ }
+
+ private void responseShouldBeNotValid(ReceivedResponse receivedResponse) {
+ System.out.println(receivedResponse.getHeaders().asMultiValueMap().toString());
+ System.out.println(receivedResponse.getBody().getText());
+ Assert.assertTrue("Response should be nok for owner:" + GITHUB_OWNER + " and for repo: " + GIHTUB_REPO_NOK, receivedResponse.getStatusCode() != HttpResponseStatus.OK.code());
+ }
+
+ private void performanceTest() {
+
+ }
+}