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() { + + } +}