Skip to content

Commit

Permalink
Add database support to WebService.
Browse files Browse the repository at this point in the history
  • Loading branch information
gk-brown committed Nov 2, 2024
1 parent 44ced17 commit db157ff
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 111 deletions.
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,27 @@ If a service method returns `null`, an HTTP 404 (not found) response will be ret

Although return values are encoded as JSON by default, subclasses can override the `encodeResult()` method of the `WebService` class to support alternative representations. See the method documentation for more information.

### Exceptions
If an exception is thrown by a service method and the response has not yet been committed, the exception message (if any) will be returned as plain text in the response body. Error status is determined as follows:

* `IllegalArgumentException` or `UnsupportedOperationException` - HTTP 403 (forbidden)
* `NoSuchElementException` - HTTP 404 (not found)
* `IllegalStateException` - HTTP 409 (conflict)
* Any other exception - HTTP 500 (internal server error)

Subclasses can override the `reportError()` method to perform custom error handling.

### Database Connectivity
For services that require database connectivity, the following method can be used to obtain a JDBC connection object associated with the current invocation:

```java
protected static Connection getConnection() { ... }
```

The connection is opened via a data source identified by `getDataSourceName()`, which returns `null` by default. Service classes must override this method to provide the name of a valid data source.

Auto-commit is disabled so an entire request will be processed within a single transaction. If the request completes successfully, the transaction is committed. Otherwise, it is rolled back.

### Request and Repsonse Properties
The following methods provide access to the request and response objects associated with the current invocation:

Expand All @@ -231,16 +252,6 @@ For example, a service might use the request to read directly from the input str

The response object can also be used to produce a custom result. If a service method commits the response by writing to the output stream, the method's return value (if any) will be ignored by `WebService`. This allows a service to return content that cannot be easily represented as JSON, such as image data.

### Exceptions
If an exception is thrown by a service method and the response has not yet been committed, the exception message (if any) will be returned as plain text in the response body. Error status is determined as follows:

* `IllegalArgumentException` or `UnsupportedOperationException` - HTTP 403 (forbidden)
* `NoSuchElementException` - HTTP 404 (not found)
* `IllegalStateException` - HTTP 409 (conflict)
* Any other exception - HTTP 500 (internal server error)

Subclasses can override the `reportError()` method to perform custom error handling.

### Inter-Service Communication
A reference to any active service can be obtained via the `getInstance()` method of the `WebService` class. This can be useful when the implementation of one service depends on functionality provided by another service, for example.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

subprojects {
group = 'org.httprpc'
version = '4.8.1'
version = '4.8.2'

apply plugin: 'java-library'

Expand Down
140 changes: 132 additions & 8 deletions kilo-server/src/main/java/org/httprpc/kilo/WebService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import org.httprpc.kilo.io.JSONEncoder;
import org.httprpc.kilo.io.TemplateEncoder;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
Expand All @@ -36,6 +39,8 @@
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
Expand Down Expand Up @@ -670,6 +675,8 @@ private static class Resource {
private static final Comparator<Method> methodNameComparator = Comparator.comparing(Method::getName);
private static final Comparator<Method> methodParameterCountComparator = Comparator.comparing(Method::getParameterCount);

private static final ThreadLocal<Connection> connection = new ThreadLocal<>();

private static final ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();

Expand Down Expand Up @@ -829,8 +836,92 @@ private static void sort(Resource root) {
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try (var connection = openConnection()) {
if (connection != null) {
connection.setAutoCommit(false);
}

setConnection(connection);

try {
invoke(request, response);

if (connection != null) {
if (response.getStatus() / 100 == 2) {
connection.commit();
} else {
connection.rollback();
}
}
} catch (Exception exception) {
if (connection != null) {
connection.rollback();
}

log(exception.getMessage(), exception);

throw exception;
} finally {
if (connection != null) {
connection.setAutoCommit(true);
}

setConnection(null);
}
} catch (SQLException exception) {
throw new ServletException(exception);
}
}

/**
* Opens a database connection.
*
* @return
* A database connection, or {@code null} if the service does not require a
* database connection.
*/
protected Connection openConnection() throws SQLException {
var dataSourceName = getDataSourceName();

if (dataSourceName != null) {
DataSource dataSource;
try {
var initialContext = new InitialContext();

dataSource = (DataSource)initialContext.lookup(dataSourceName);
} catch (NamingException exception) {
throw new IllegalStateException(exception);
}

return dataSource.getConnection();
} else {
return null;
}
}

/**
* Returns the data source name.
*
* @return
* The data source name, or {@code null} if the service does not require a
* data source.
*/
protected String getDataSourceName() {
return null;
}

/**
* Invokes a service method.
*
* @param request
* The HTTP servlet request.
*
* @param response
* The HTTP servlet response.
*/
@SuppressWarnings("unchecked")
protected void invoke(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
var method = request.getMethod().toUpperCase();
var pathInfo = request.getPathInfo();

Expand Down Expand Up @@ -883,7 +974,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
child = resource.resources.get("?");

if (child == null) {
super.service(request, response);
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}

Expand All @@ -897,7 +988,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
var handlerList = resource.handlerMap.get(method);

if (handlerList == null) {
super.service(request, response);
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}

Expand Down Expand Up @@ -1121,7 +1212,7 @@ private Object[] getArguments(Parameter[] parameters, List<String> keys, Map<Str
throw new UnsupportedOperationException("Unsupported collection type.");
}
} else {
throw new UnsupportedOperationException("Invalid element type.");
throw new UnsupportedOperationException("Unsupported element type.");
}
} else {
Object value;
Expand Down Expand Up @@ -1162,6 +1253,30 @@ private Object[] getArguments(Parameter[] parameters, List<String> keys, Map<Str
return arguments;
}

/**
* Returns the database connection.
*
* @return
* The database connection.
*/
protected static Connection getConnection() {
return connection.get();
}

/**
* Sets the database connection.
*
* @param connection
* The database connection.
*/
protected static void setConnection(Connection connection) {
if (connection != null) {
WebService.connection.set(connection);
} else {
WebService.connection.remove();
}
}

/**
* Returns the servlet request.
*
Expand Down Expand Up @@ -1221,11 +1336,20 @@ protected Object decodeBody(HttpServletRequest request, Type type) throws IOExce
protected void encodeResult(HttpServletRequest request, HttpServletResponse response, Object result) throws IOException {
response.setContentType(String.format(CONTENT_TYPE_FORMAT, APPLICATION_JSON, StandardCharsets.UTF_8));

var jsonEncoder = new JSONEncoder();
var jsonEncoder = new JSONEncoder(isCompact());

jsonEncoder.write(result, response.getOutputStream());
}

/**
* Enables compact output.
*
* {@code true} if compact output is enabled; {@code false}, otherwise.
*/
protected boolean isCompact() {
return false;
}

/**
* Reports an error.
*
Expand Down Expand Up @@ -1324,7 +1448,7 @@ private void describeResource(String path, Resource resource) {
}

private TypeDescriptor describeGenericType(Type type) {
if (type instanceof Class) {
if (type instanceof Class<?>) {
return describeRawType((Class<?>)type);
} else if (type instanceof ParameterizedType parameterizedType) {
var rawType = (Class<?>)parameterizedType.getRawType();
Expand All @@ -1335,10 +1459,10 @@ private TypeDescriptor describeGenericType(Type type) {
} else if (Map.class.isAssignableFrom(rawType)) {
return new MapTypeDescriptor(describeGenericType(actualTypeArguments[0]), describeGenericType(actualTypeArguments[1]));
} else {
throw new IllegalArgumentException();
throw new IllegalArgumentException("Unsupported parameterized type.");
}
} else {
throw new IllegalArgumentException();
throw new IllegalArgumentException("Unsupported type.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,66 +14,11 @@

package org.httprpc.kilo.test;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.httprpc.kilo.WebService;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

public abstract class AbstractDatabaseService extends WebService {
private static final ThreadLocal<Connection> connection = new ThreadLocal<>();

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try (var connection = openConnection()) {
connection.setAutoCommit(false);

AbstractDatabaseService.connection.set(connection);

try {
super.service(request, response);

if (response.getStatus() / 100 == 2) {
connection.commit();
} else {
connection.rollback();
}
} catch (Exception exception) {
connection.rollback();

log(exception.getMessage(), exception);

throw exception;
} finally {
connection.setAutoCommit(true);

AbstractDatabaseService.connection.remove();
}
} catch (SQLException exception) {
throw new ServletException(exception);
}
}

protected Connection openConnection() throws SQLException {
DataSource dataSource;
try {
var initialContext = new InitialContext();

dataSource = (DataSource)initialContext.lookup("java:comp/env/jdbc/DemoDB");
} catch (NamingException exception) {
throw new IllegalStateException(exception);
}

return dataSource.getConnection();
}

protected static Connection getConnection() {
return connection.get();
protected String getDataSourceName() {
return "java:comp/env/jdbc/DemoDB";
}
}
26 changes: 7 additions & 19 deletions kilo-test/src/main/java/org/httprpc/kilo/test/EmployeeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@
import org.hibernate.cfg.Configuration;
import org.httprpc.kilo.RequestMethod;
import org.httprpc.kilo.ResourcePath;
import org.httprpc.kilo.WebService;
import org.httprpc.kilo.beans.BeanAdapter;
import org.httprpc.kilo.sql.QueryBuilder;
import org.httprpc.kilo.util.concurrent.Pipe;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@WebServlet(urlPatterns = {"/employees/*"}, loadOnStartup = 1)
public class EmployeeService extends AbstractDatabaseService {
public class EmployeeService extends WebService {
private static ExecutorService executorService = null;

@Override
protected String getDataSourceName() {
return "java:comp/env/jdbc/EmployeeDB";
}

@Override
public void init() throws ServletException {
super.init();
Expand All @@ -50,20 +52,6 @@ public void destroy() {
super.destroy();
}

@Override
protected Connection openConnection() throws SQLException {
DataSource dataSource;
try {
var initialContext = new InitialContext();

dataSource = (DataSource)initialContext.lookup("java:comp/env/jdbc/EmployeeDB");
} catch (NamingException exception) {
throw new IllegalStateException(exception);
}

return dataSource.getConnection();
}

@RequestMethod("GET")
public List<Employee> getEmployees() throws SQLException {
var queryBuilder = QueryBuilder.select(Employee.class);
Expand Down
Loading

0 comments on commit db157ff

Please sign in to comment.