Skip to content

Commit db157ff

Browse files
committed
Add database support to WebService.
1 parent 44ced17 commit db157ff

File tree

6 files changed

+167
-111
lines changed

6 files changed

+167
-111
lines changed

README.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,27 @@ If a service method returns `null`, an HTTP 404 (not found) response will be ret
219219

220220
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.
221221

222+
### Exceptions
223+
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:
224+
225+
* `IllegalArgumentException` or `UnsupportedOperationException` - HTTP 403 (forbidden)
226+
* `NoSuchElementException` - HTTP 404 (not found)
227+
* `IllegalStateException` - HTTP 409 (conflict)
228+
* Any other exception - HTTP 500 (internal server error)
229+
230+
Subclasses can override the `reportError()` method to perform custom error handling.
231+
232+
### Database Connectivity
233+
For services that require database connectivity, the following method can be used to obtain a JDBC connection object associated with the current invocation:
234+
235+
```java
236+
protected static Connection getConnection() { ... }
237+
```
238+
239+
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.
240+
241+
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.
242+
222243
### Request and Repsonse Properties
223244
The following methods provide access to the request and response objects associated with the current invocation:
224245

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

232253
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.
233254

234-
### Exceptions
235-
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:
236-
237-
* `IllegalArgumentException` or `UnsupportedOperationException` - HTTP 403 (forbidden)
238-
* `NoSuchElementException` - HTTP 404 (not found)
239-
* `IllegalStateException` - HTTP 409 (conflict)
240-
* Any other exception - HTTP 500 (internal server error)
241-
242-
Subclasses can override the `reportError()` method to perform custom error handling.
243-
244255
### Inter-Service Communication
245256
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.
246257

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
subprojects {
1616
group = 'org.httprpc'
17-
version = '4.8.1'
17+
version = '4.8.2'
1818

1919
apply plugin: 'java-library'
2020

kilo-server/src/main/java/org/httprpc/kilo/WebService.java

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import org.httprpc.kilo.io.JSONEncoder;
2626
import org.httprpc.kilo.io.TemplateEncoder;
2727

28+
import javax.naming.InitialContext;
29+
import javax.naming.NamingException;
30+
import javax.sql.DataSource;
2831
import java.io.IOException;
2932
import java.lang.reflect.Array;
3033
import java.lang.reflect.Field;
@@ -36,6 +39,8 @@
3639
import java.net.URI;
3740
import java.nio.charset.StandardCharsets;
3841
import java.nio.file.Path;
42+
import java.sql.Connection;
43+
import java.sql.SQLException;
3944
import java.time.Duration;
4045
import java.time.Instant;
4146
import java.time.LocalDate;
@@ -670,6 +675,8 @@ private static class Resource {
670675
private static final Comparator<Method> methodNameComparator = Comparator.comparing(Method::getName);
671676
private static final Comparator<Method> methodParameterCountComparator = Comparator.comparing(Method::getParameterCount);
672677

678+
private static final ThreadLocal<Connection> connection = new ThreadLocal<>();
679+
673680
private static final ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
674681
private static final ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();
675682

@@ -829,8 +836,92 @@ private static void sort(Resource root) {
829836
* {@inheritDoc}
830837
*/
831838
@Override
832-
@SuppressWarnings("unchecked")
833839
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
840+
try (var connection = openConnection()) {
841+
if (connection != null) {
842+
connection.setAutoCommit(false);
843+
}
844+
845+
setConnection(connection);
846+
847+
try {
848+
invoke(request, response);
849+
850+
if (connection != null) {
851+
if (response.getStatus() / 100 == 2) {
852+
connection.commit();
853+
} else {
854+
connection.rollback();
855+
}
856+
}
857+
} catch (Exception exception) {
858+
if (connection != null) {
859+
connection.rollback();
860+
}
861+
862+
log(exception.getMessage(), exception);
863+
864+
throw exception;
865+
} finally {
866+
if (connection != null) {
867+
connection.setAutoCommit(true);
868+
}
869+
870+
setConnection(null);
871+
}
872+
} catch (SQLException exception) {
873+
throw new ServletException(exception);
874+
}
875+
}
876+
877+
/**
878+
* Opens a database connection.
879+
*
880+
* @return
881+
* A database connection, or {@code null} if the service does not require a
882+
* database connection.
883+
*/
884+
protected Connection openConnection() throws SQLException {
885+
var dataSourceName = getDataSourceName();
886+
887+
if (dataSourceName != null) {
888+
DataSource dataSource;
889+
try {
890+
var initialContext = new InitialContext();
891+
892+
dataSource = (DataSource)initialContext.lookup(dataSourceName);
893+
} catch (NamingException exception) {
894+
throw new IllegalStateException(exception);
895+
}
896+
897+
return dataSource.getConnection();
898+
} else {
899+
return null;
900+
}
901+
}
902+
903+
/**
904+
* Returns the data source name.
905+
*
906+
* @return
907+
* The data source name, or {@code null} if the service does not require a
908+
* data source.
909+
*/
910+
protected String getDataSourceName() {
911+
return null;
912+
}
913+
914+
/**
915+
* Invokes a service method.
916+
*
917+
* @param request
918+
* The HTTP servlet request.
919+
*
920+
* @param response
921+
* The HTTP servlet response.
922+
*/
923+
@SuppressWarnings("unchecked")
924+
protected void invoke(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
834925
var method = request.getMethod().toUpperCase();
835926
var pathInfo = request.getPathInfo();
836927

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

885976
if (child == null) {
886-
super.service(request, response);
977+
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
887978
return;
888979
}
889980

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

899990
if (handlerList == null) {
900-
super.service(request, response);
991+
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
901992
return;
902993
}
903994

@@ -1121,7 +1212,7 @@ private Object[] getArguments(Parameter[] parameters, List<String> keys, Map<Str
11211212
throw new UnsupportedOperationException("Unsupported collection type.");
11221213
}
11231214
} else {
1124-
throw new UnsupportedOperationException("Invalid element type.");
1215+
throw new UnsupportedOperationException("Unsupported element type.");
11251216
}
11261217
} else {
11271218
Object value;
@@ -1162,6 +1253,30 @@ private Object[] getArguments(Parameter[] parameters, List<String> keys, Map<Str
11621253
return arguments;
11631254
}
11641255

1256+
/**
1257+
* Returns the database connection.
1258+
*
1259+
* @return
1260+
* The database connection.
1261+
*/
1262+
protected static Connection getConnection() {
1263+
return connection.get();
1264+
}
1265+
1266+
/**
1267+
* Sets the database connection.
1268+
*
1269+
* @param connection
1270+
* The database connection.
1271+
*/
1272+
protected static void setConnection(Connection connection) {
1273+
if (connection != null) {
1274+
WebService.connection.set(connection);
1275+
} else {
1276+
WebService.connection.remove();
1277+
}
1278+
}
1279+
11651280
/**
11661281
* Returns the servlet request.
11671282
*
@@ -1221,11 +1336,20 @@ protected Object decodeBody(HttpServletRequest request, Type type) throws IOExce
12211336
protected void encodeResult(HttpServletRequest request, HttpServletResponse response, Object result) throws IOException {
12221337
response.setContentType(String.format(CONTENT_TYPE_FORMAT, APPLICATION_JSON, StandardCharsets.UTF_8));
12231338

1224-
var jsonEncoder = new JSONEncoder();
1339+
var jsonEncoder = new JSONEncoder(isCompact());
12251340

12261341
jsonEncoder.write(result, response.getOutputStream());
12271342
}
12281343

1344+
/**
1345+
* Enables compact output.
1346+
*
1347+
* {@code true} if compact output is enabled; {@code false}, otherwise.
1348+
*/
1349+
protected boolean isCompact() {
1350+
return false;
1351+
}
1352+
12291353
/**
12301354
* Reports an error.
12311355
*
@@ -1324,7 +1448,7 @@ private void describeResource(String path, Resource resource) {
13241448
}
13251449

13261450
private TypeDescriptor describeGenericType(Type type) {
1327-
if (type instanceof Class) {
1451+
if (type instanceof Class<?>) {
13281452
return describeRawType((Class<?>)type);
13291453
} else if (type instanceof ParameterizedType parameterizedType) {
13301454
var rawType = (Class<?>)parameterizedType.getRawType();
@@ -1335,10 +1459,10 @@ private TypeDescriptor describeGenericType(Type type) {
13351459
} else if (Map.class.isAssignableFrom(rawType)) {
13361460
return new MapTypeDescriptor(describeGenericType(actualTypeArguments[0]), describeGenericType(actualTypeArguments[1]));
13371461
} else {
1338-
throw new IllegalArgumentException();
1462+
throw new IllegalArgumentException("Unsupported parameterized type.");
13391463
}
13401464
} else {
1341-
throw new IllegalArgumentException();
1465+
throw new IllegalArgumentException("Unsupported type.");
13421466
}
13431467
}
13441468

kilo-test/src/main/java/org/httprpc/kilo/test/AbstractDatabaseService.java

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,66 +14,11 @@
1414

1515
package org.httprpc.kilo.test;
1616

17-
import jakarta.servlet.ServletException;
18-
import jakarta.servlet.http.HttpServletRequest;
19-
import jakarta.servlet.http.HttpServletResponse;
2017
import org.httprpc.kilo.WebService;
2118

22-
import javax.naming.InitialContext;
23-
import javax.naming.NamingException;
24-
import javax.sql.DataSource;
25-
import java.io.IOException;
26-
import java.sql.Connection;
27-
import java.sql.SQLException;
28-
2919
public abstract class AbstractDatabaseService extends WebService {
30-
private static final ThreadLocal<Connection> connection = new ThreadLocal<>();
31-
3220
@Override
33-
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
34-
try (var connection = openConnection()) {
35-
connection.setAutoCommit(false);
36-
37-
AbstractDatabaseService.connection.set(connection);
38-
39-
try {
40-
super.service(request, response);
41-
42-
if (response.getStatus() / 100 == 2) {
43-
connection.commit();
44-
} else {
45-
connection.rollback();
46-
}
47-
} catch (Exception exception) {
48-
connection.rollback();
49-
50-
log(exception.getMessage(), exception);
51-
52-
throw exception;
53-
} finally {
54-
connection.setAutoCommit(true);
55-
56-
AbstractDatabaseService.connection.remove();
57-
}
58-
} catch (SQLException exception) {
59-
throw new ServletException(exception);
60-
}
61-
}
62-
63-
protected Connection openConnection() throws SQLException {
64-
DataSource dataSource;
65-
try {
66-
var initialContext = new InitialContext();
67-
68-
dataSource = (DataSource)initialContext.lookup("java:comp/env/jdbc/DemoDB");
69-
} catch (NamingException exception) {
70-
throw new IllegalStateException(exception);
71-
}
72-
73-
return dataSource.getConnection();
74-
}
75-
76-
protected static Connection getConnection() {
77-
return connection.get();
21+
protected String getDataSourceName() {
22+
return "java:comp/env/jdbc/DemoDB";
7823
}
7924
}

kilo-test/src/main/java/org/httprpc/kilo/test/EmployeeService.java

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,25 @@
1919
import org.hibernate.cfg.Configuration;
2020
import org.httprpc.kilo.RequestMethod;
2121
import org.httprpc.kilo.ResourcePath;
22+
import org.httprpc.kilo.WebService;
2223
import org.httprpc.kilo.beans.BeanAdapter;
2324
import org.httprpc.kilo.sql.QueryBuilder;
2425
import org.httprpc.kilo.util.concurrent.Pipe;
2526

26-
import javax.naming.InitialContext;
27-
import javax.naming.NamingException;
28-
import javax.sql.DataSource;
29-
import java.sql.Connection;
3027
import java.sql.SQLException;
3128
import java.util.List;
3229
import java.util.concurrent.ExecutorService;
3330
import java.util.concurrent.Executors;
3431

3532
@WebServlet(urlPatterns = {"/employees/*"}, loadOnStartup = 1)
36-
public class EmployeeService extends AbstractDatabaseService {
33+
public class EmployeeService extends WebService {
3734
private static ExecutorService executorService = null;
3835

36+
@Override
37+
protected String getDataSourceName() {
38+
return "java:comp/env/jdbc/EmployeeDB";
39+
}
40+
3941
@Override
4042
public void init() throws ServletException {
4143
super.init();
@@ -50,20 +52,6 @@ public void destroy() {
5052
super.destroy();
5153
}
5254

53-
@Override
54-
protected Connection openConnection() throws SQLException {
55-
DataSource dataSource;
56-
try {
57-
var initialContext = new InitialContext();
58-
59-
dataSource = (DataSource)initialContext.lookup("java:comp/env/jdbc/EmployeeDB");
60-
} catch (NamingException exception) {
61-
throw new IllegalStateException(exception);
62-
}
63-
64-
return dataSource.getConnection();
65-
}
66-
6755
@RequestMethod("GET")
6856
public List<Employee> getEmployees() throws SQLException {
6957
var queryBuilder = QueryBuilder.select(Employee.class);

0 commit comments

Comments
 (0)