- Introduction
- Technologies Used
- Setup Instructions
- Custom Expression Language
- API Endpoints
- Extending the Custom Expression Language
The Dynamic Email Generator is a service powered by the Spring Boot framework, offering a RESTful API to create personalized email addresses. Through a custom expression language, users can define rules to generate email addresses based on specific criteria and custom methods.
The service has a reverse Nginx proxy at the front, with the whole setup configured inside a docker-compose file for easy setup.
- Java 21
- Spring Boot 3
- Nginx
- Docker
- Postman
Note: The following instructions were written and tested on a Windows 10 system
To create the self-signed certificates needed for the Nginx reverse proxy configuration, you must
have OpenSSL installed on your system.
You most likely already have it in you git installation folder ("C:\Program Files\Git\usr\bin\openssl.exe")
just
add it to PATH.
After installing OpenSSL (and placing it on your PATH) you have 2 options:
- Navigate to \nginx
- Open a CMD window in that location
- Run the helper script create_certs.bat
Or manually:
- Open a CMD window
- Navigate to the \nginx folder in project root directory
- Create a \certs directory if not present: run
mkdir certs
- To generate a self-signed certificate
run
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certs/nginx.key -out certs/nginx.crt -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.localhost.com"
In both cases you will end up with a .crt and .key file in the \nginx\certs directory.
Note that they are referenced in docker-compose.yml and nginx.conf and must be updated in both places if a name/location change occurs.
NOTE: The generated self-signed certificate is for development/testing purposes.
To create self-signed certificates and a keystore for the service, you can use keytool
command that comes with the
JDK. To create a default certificate that is used in the default environment setup described in this documentation - you
can use the provided create_default_keystore.bat script inside the /tls
folder. YOU MUST HAVE keytool
command present in PATH or edit the script to launch keytool from its'
location C:\Users\<user>\.jdks\corretto-21.0.5\bin\keytool ...
To use the helper script:
- Navigate to /tls directory
- Open a CMD window at that location
- Run the helper script create_default_keystore.bat
- You should end up with a new directory inside /tls and a keystore file inside
NOTE: The generated self-signed certificate is for development/testing purposes.
All the nginx-related configurations can be found under \nginx. This includes certificates, custom error pages, helper scripts and most importantly - nginx.conf.
The important aspects of the proxy configuration are:
- SSL certificate locations (referenced in docker-compose)
ssl_certificate /etc/nginx/certs/nginx.crt; ssl_certificate_key /etc/nginx/certs/nginx.key;
- Error page config (referenced in docker-compose)
# Capture unexpected errors gracefully error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; internal; }
location
configs - the configuration is strict, only exposing the endpoints that are relevant for operation, preventing external users from malicious attemptslocation /app
- api prefixlocation /swagger-ui
- needed for swagger uilocation /v3
- needed for swagger ui
Note: Nginx is exposed on port 9443
, but listening on 443
internally, and if this has to be
changed, docker-compose has to be
updated to reflect the change, also nginx.conf location objects must be updated with the new port,
namely proxy_set_header X-Forwarded-Port 9443;
To run the configuration described inside the docker.compose.yml, you must have Docker installed on your system.
Note: there is a .dev-env environment file that holds a default keystore path and password needed for the
Spring service SSL config. It must be passed to the docker compose
command as described below.
Build the project before creating a Docker image:
- Run a
mvn clean verify
on the project - After all tests have passed, you should see a success build
After installing Docker:
- Open a CMD window
- Navigate to the project root directory
- Run the compose file, recreating any containers and forcing a rebuild
docker-compose --env-file ./.dev-env up --force-recreate --no-deps --build
- Clean up dangling containers (if any) produced by the previous command
docker image prune -f
This will build the new service Docker image described in the Dockerfile, run it, and pull and run the Nginx image serving as a reverse proxy.
Verify that you can hit the API swagger page at https://localhost:9443/swagger-ui/index.html
Assume inputs:
str1=Ivan
str2=Petar
str3=Falcon
Operation/Expression | Description |
---|---|
first(str1,N) | evaluates to the first N chars of the input parameter |
last(str1,N) | evaluates to the last N chars of the input parameter |
substr(str1, N start, N end) | evaluates to the substring between start(inclusive) and end(exclusive) indexes |
lit(str1) | literal expression, simply evaluates to the value of str1 key |
raw(.com) | evaluates to a raw string: ".com" in the example |
lit(str1) ; raw(.com) | concatenation is done using a ";" semicolon |
longer(lit(str1),lit(str2),lit(str3)) | if first expression parameter is longer than second expression parameter -> execute third expression, if not - empty string |
eq(lit(str1),lit(str2),lit(str3)) | if first expression parameter is equal to second expression parameter -> execute third expression , if not - empty string |
A postman collection that covers basic API usage can be found in /postman folder in the project root directory.
To import the collection:
- Open Postman
- Click on "import" on the left
- Drag and drop the collection from /postman directory
- Explore the stored requests
Endpoint: GET /app/v1/email/generate
This endpoint dynamically generates email addresses with the use of a Custom Expression Language and a set of input parameters. It's designed to process complex rules defined in the 'expression' parameter and apply them to the inputs provided.
- expression (required): A string parameter defining the rules for generating emails. It must be a single, non-empty string. The rules are defined here.
- strN: Additional parameters with keys starting with
str
followed by any number ( e.g.,str1=test
,str2=string
). These parameters are used as inputs for theexpression
defined.
-
200 OK:
- Content-Type:
application/json
- Body: An
ApiResponse
object containing a list of generated emails. - Example:
{ "data": [ "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]" ], "message": null }
- Content-Type:
-
400 Bad Request:
- Occurs when:
- The
expression
parameter is missing, empty, or multiple expressions are provided. - Any parameter key does not start with
str
. - Invalid expression format according to the Custom Expression Language definition
- The
- Content-Type:
application/json
- Body: An
ApiResponse
object containing an error message. - Example:
{ "data": null, "message": "An 'expression' string is required." }
- Occurs when:
- The endpoint strictly requires at least two query parameters: one
expression
and at least one input parameter prefixed withstr
. This ensures that there is at least one rule and one input to apply the rule to. - The endpoint is designed to be robust against invalid input formats and will provide descriptive error messages to assist in correcting request formats.
curl -X 'GET' \
'http://localhost:8080/app/v1/email/generate?str1=Ivan&str1=Petar&str1=Rado&str2=gmail&str2=yahoo&expression=first(str1, 3);raw(@);last(str2,4);raw(.com)' \
-H 'accept: application/json'
curl -X 'GET' \
'http://localhost:8080/app/v1/email/generate?expression=longer(lit(str1),lit(str2),lit(str3));first(str1, 3);raw(@);last(str2,4);raw(.com);eq(lit(str1),lit(str2),raw(.bg))&str1=Ivan&str1=Petar&str1=Radooo&str2=gmail&str2=yahoo&str3=test&str3=domain' \
-H 'accept: application/json'
The Custom Expression Language can be further extended with new expression methods very easily. The reason for that is the use of the Interpreter Design Pattern at the core of the project.
The simplest form of a new terminal expression:
@RequiredArgsConstructor
class SomeNewExpression implements Expression {
private final String inputKey;
@Override
public String interpret(final Context ctx) {
final String input = ctx.getValue(inputKey);
return doSomeNewStringManipulation(input);
}
}
The simplest form of a new non-terminal expression
@RequiredArgsConstructor
class SomeComplexConditionalExpression implements Expression {
private final Expression left;
private final Expression right;
private final Expression actionWhenTrue;
private final Expression actionWhenFalse;
@Override
public String interpret(final Context ctx) {
if (left.interpret(ctx).equals(right.interpret(ctx))) {
return actionWhenTrue.interpret(ctx);
}
return actionWhenFalse;
}
}
As you can see new expressions can be defined incredibly easy, you just have to stick to these steps:
- Define a new expression method keyword, for example
len()
- Create a new enum entry for the new expression keyword
in Operations enum
LEN("len")
- Create a new Expression class in the interpreter package
- Add a new case in the operation switch inside Interpreter class instantiating a new Expression from the newly created Expression class
- Add proper tests, update documentation, update postman collection
That's it! In these short few steps you can add new expressions to the interpreter.