Skip to content

Commit

Permalink
shorturl-s3-lambda: first full implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
agarciadom committed Dec 5, 2024
1 parent 9b7e281 commit 157e76f
Show file tree
Hide file tree
Showing 16 changed files with 389 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public class ShortURLController implements IShortURLController {

@Override
@Post
public ShortURLMessage shorten(@Body ShortURLMessage request) {
var response = new ShortURLMessage();
public ShortURLResponse shorten(@Body ShortURLRequest request) {
var response = new ShortURLResponse();
try {
Storage storage = StorageOptions.newBuilder().setProjectId(projectId)
.setCredentials(GoogleCredentials.fromStream(new FileInputStream(credentialsPath))).build()
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ include 'ep-tool-server'
include 'backend-server'
include 'shorturl-api'
include 'shorturl-s3'
include 'shorturl-s3-lambda'
include 'gcp-function'
include 'standalone-server'
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
*/
public interface IShortURLController {
@Post
ShortURLMessage shorten(@Valid @Body ShortURLMessage request);
ShortURLResponse shorten(@Valid @Body ShortURLRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import jakarta.validation.constraints.Size;

@Serdeable
public class ShortURLMessage {
public class ShortURLRequest {

public static final int MAX_CONTENT_LENGTH = 100_000;
public static final String SHORTENED_REGEX = "[a-f0-9]{8}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.eclipse.epsilon.labs.playground.fn.shorturl;

import io.micronaut.serde.annotation.Serdeable;

@Serdeable
public class ShortURLResponse extends ShortURLRequest {
private String error;

public String getError() {
return error;
}

public void setError(String error) {
this.error = error;
}
}
39 changes: 39 additions & 0 deletions shorturl-s3-lambda/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id 'io.micronaut.application'
id 'com.gradleup.shadow'
id 'io.micronaut.openapi'
id 'backend.java-conventions'
}

dependencies {
implementation(project(':shorturl-api'))

annotationProcessor(mn.micronaut.serde.processor)
annotationProcessor(mn.micronaut.validation.processor)
implementation("com.amazonaws:aws-lambda-java-events")
implementation(mn.micronaut.http.client.jdk)
implementation(mn.micronaut.aws.lambda.events.serde)
implementation(mn.micronaut.aws.sdk.v2)
implementation(mn.micronaut.function.aws)
implementation(mn.micronaut.function.aws.custom.runtime)
implementation(mn.micronaut.object.storage.aws)
implementation(mn.micronaut.serde.jackson)
implementation(mn.micronaut.validation)
implementation("jakarta.validation:jakarta.validation-api")
runtimeOnly("ch.qos.logback:logback-classic")

testImplementation(mn.micronaut.object.storage.local)
}

application {
mainClass = "org.eclipse.epsilon.labs.playground.FunctionLambdaRuntime"
}

micronaut {
runtime("lambda_provided")
testRuntime("junit5")
processing {
incremental(true)
annotations("org.eclipse.epsilon.labs.playground.*")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.eclipse.epsilon.labs.playground;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.function.aws.runtime.AbstractMicronautLambdaRuntime;
import java.net.MalformedURLException;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import io.micronaut.core.annotation.Nullable;
public class FunctionLambdaRuntime extends AbstractMicronautLambdaRuntime<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent, APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>
{
public static void main(String[] args) {
try {
new FunctionLambdaRuntime().run(args);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}

@Override
@Nullable
protected RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> createRequestHandler(String... args) {
return new FunctionRequestHandler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.eclipse.epsilon.labs.playground;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.function.aws.MicronautRequestHandler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import io.micronaut.http.HttpHeaders;
import io.micronaut.http.MediaType;
import io.micronaut.json.JsonMapper;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.objectstorage.ObjectStorageEntry;
import io.micronaut.objectstorage.ObjectStorageOperations;
import io.micronaut.objectstorage.request.UploadRequest;
import io.micronaut.objectstorage.response.UploadResponse;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.eclipse.epsilon.labs.playground.fn.shorturl.ShortURLRequest;
import org.eclipse.epsilon.labs.playground.fn.shorturl.ShortURLResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

public class FunctionRequestHandler extends MicronautRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(FunctionRequestHandler.class);

@Inject
JsonMapper objectMapper;

@Inject
Validator validator;

@Inject
ObjectStorageOperations<?, ?, ?> objectStorage;

@Override
public APIGatewayProxyResponseEvent execute(APIGatewayProxyRequestEvent input) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
if (!"POST".equals(input.getHttpMethod())) {
setResponseText(response, "Invalid HTTP method. Valid HTTP methods are: [POST]");
response.setStatusCode(400);
return response;
}

try {
var request = objectMapper.readValue(input.getBody(), ShortURLRequest.class);
Set<ConstraintViolation<ShortURLRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
processInvalidRequest(violations, response);
} else {
processValidRequest(request, response);
}
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
response.setStatusCode(500);
}
return response;
}

private void processInvalidRequest(Set<ConstraintViolation<ShortURLRequest>> violations, APIGatewayProxyResponseEvent response) throws IOException {
var responseBody = new ShortURLResponse();
responseBody.setError("Invalid request: " + String.join(",",
violations.stream().map(e -> e.getPropertyPath() + " " + e.getMessage()
).collect(Collectors.toList())));
response.setStatusCode(400);
setResponseJSON(response, responseBody);
}

protected static void setResponseText(APIGatewayProxyResponseEvent response, String body) {
response.setBody(body);
response.setHeaders(Collections.singletonMap(
HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN));
}

protected void setResponseJSON(APIGatewayProxyResponseEvent response, ShortURLResponse responseBody) throws IOException {
response.setBody(objectMapper.writeValueAsString(responseBody));
response.setHeaders(Collections.singletonMap(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON));
}

protected void processValidRequest(ShortURLRequest request, APIGatewayProxyResponseEvent response) throws IOException {
var responseMsg = new ShortURLResponse();
if (request.getShortened() != null) {
processDownloadRequest(request, response, responseMsg);
} else if (request.getContent() != null) {
processUploadRequest(request, response, responseMsg);
} else {
setResponseText(response, "Exactly one of shortened or content must be specified");
response.setStatusCode(400);
}
}

protected void processUploadRequest(ShortURLRequest request, APIGatewayProxyResponseEvent response, ShortURLResponse responseMsg) throws IOException {
String key = getShortened(request.getContent());
UploadRequest uploadRequest = UploadRequest.fromBytes(
request.getContent().getBytes(StandardCharsets.UTF_8), key);

@NonNull UploadResponse<?> uploadResponse = objectStorage.upload(uploadRequest);
responseMsg.setShortened(uploadResponse.getKey());
setResponseJSON(response, responseMsg);
response.setStatusCode(200);
}

protected void processDownloadRequest(ShortURLRequest request, APIGatewayProxyResponseEvent response, ShortURLResponse responseMsg) throws IOException {
Optional<ObjectStorageEntry<?>> content = objectStorage.retrieve(request.getShortened());
if (content.isPresent()) {
try (
InputStream is = content.get().getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
) {
byte[] buffer = new byte[1024];
for (int length; (length = is.read(buffer)) != -1; ) {
baos.write(buffer, 0, length);
}
responseMsg.setContent(baos.toString(StandardCharsets.UTF_8));
setResponseJSON(response, responseMsg);
response.setStatusCode(200);
}
} else {
response.setStatusCode(404);
}
}

protected String getShortened(String content) {
return UUID.nameUUIDFromBytes(content.getBytes()).toString().substring(0, 8);
}

}
2 changes: 2 additions & 0 deletions shorturl-s3-lambda/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#Thu Dec 05 11:57:54 UTC 2024
micronaut.application.name=shorturl-s3-lambda
14 changes: 14 additions & 0 deletions shorturl-s3-lambda/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Loading

0 comments on commit 157e76f

Please sign in to comment.