Skip to content

Commit 519dec0

Browse files
Lexert19Lexert19
andauthored
Add TimeZone header support to REST API (#17344) (#17387)
* Add X-TimeZone header support to REST API (#17344) * Support Time-Zone header and add examples for REST API * fix test resource leaks * copilot review * null checks, table v1 integration tests --------- Co-authored-by: Lexert19 <admin@DESKTOP-BN0D3J5>
1 parent 9c6b90a commit 519dec0

File tree

11 files changed

+513
-16
lines changed

11 files changed

+513
-16
lines changed

example/rest-java-example/src/main/java/org/apache/iotdb/HttpExample.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static void main(String[] args) {
5353
httpExample.ping();
5454
httpExample.insertTablet();
5555
httpExample.query();
56+
httpExample.queryWithTimeZone();
5657
}
5758

5859
public void ping() {
@@ -138,4 +139,30 @@ public void query() {
138139
}
139140
}
140141
}
142+
143+
public void queryWithTimeZone() {
144+
CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
145+
CloseableHttpResponse response = null;
146+
try {
147+
HttpPost httpPost = getHttpPost("http://127.0.0.1:18080/rest/v1/query");
148+
httpPost.setHeader("Time-Zone", "Asia/Shanghai");
149+
String sql = "{\"sql\":\"select * from root.sg25 where time <= 2026-03-28T00:00:00\"}";
150+
httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
151+
response = httpClient.execute(httpPost);
152+
HttpEntity responseEntity = response.getEntity();
153+
String message = EntityUtils.toString(responseEntity, UTF8);
154+
ObjectMapper mapper = new ObjectMapper();
155+
LOGGER.info("message with time zone = {}", mapper.readValue(message, Map.class));
156+
} catch (IOException e) {
157+
LOGGER.error("The query with time zone rest api failed", e);
158+
} finally {
159+
try {
160+
if (response != null) {
161+
response.close();
162+
}
163+
} catch (IOException e) {
164+
LOGGER.error("Response close error", e);
165+
}
166+
}
167+
}
141168
}

example/rest-java-example/src/main/java/org/apache/iotdb/HttpsExample.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public static void main(String[] args) {
5252
httpsExample.pingHttps();
5353
httpsExample.insertTablet();
5454
httpsExample.query();
55+
httpsExample.queryWithTimeZone();
5556
}
5657

5758
public void pingHttps() {
@@ -138,4 +139,30 @@ public void query() {
138139
}
139140
}
140141
}
142+
143+
public void queryWithTimeZone() {
144+
CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
145+
CloseableHttpResponse response = null;
146+
try {
147+
HttpPost httpPost = getHttpPost("https://127.0.0.1:18080/rest/v1/query");
148+
httpPost.addHeader("Time-Zone", "+05:00");
149+
String sql = "{\"sql\":\"select * from root.sg25 where time <= 2026-03-28T00:00:00\"}";
150+
httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
151+
response = httpClient.execute(httpPost);
152+
HttpEntity responseEntity = response.getEntity();
153+
String message = EntityUtils.toString(responseEntity, UTF8);
154+
ObjectMapper mapper = new ObjectMapper();
155+
LOGGER.info("message with time zone = {}", mapper.readValue(message, Map.class));
156+
} catch (IOException e) {
157+
LOGGER.error("Https query with time zone rest api failed", e);
158+
} finally {
159+
try {
160+
if (response != null) {
161+
response.close();
162+
}
163+
} catch (IOException e) {
164+
LOGGER.error("Response close error", e);
165+
}
166+
}
167+
}
141168
}

example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpExample.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@
2727
import org.apache.http.entity.StringEntity;
2828
import org.apache.http.impl.client.CloseableHttpClient;
2929
import org.apache.http.util.EntityUtils;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
3032

3133
import java.io.IOException;
3234
import java.nio.charset.Charset;
3335
import java.nio.charset.StandardCharsets;
3436
import java.util.Base64;
3537

3638
public class TableHttpExample {
39+
private static final Logger LOGGER = LoggerFactory.getLogger(TableHttpExample.class);
3740

3841
private static final String UTF8 = "utf-8";
3942

@@ -50,6 +53,7 @@ public static void main(String[] args) {
5053
httpExample.nonQuery();
5154
httpExample.insertTablet();
5255
httpExample.query();
56+
httpExample.queryWithTimeZone();
5357
}
5458

5559
public void ping() {
@@ -220,4 +224,30 @@ public void query() {
220224
}
221225
}
222226
}
227+
228+
public void queryWithTimeZone() {
229+
CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
230+
CloseableHttpResponse response = null;
231+
try {
232+
HttpPost httpPost = getHttpPost("http://127.0.0.1:18080/rest/table/v1/query");
233+
httpPost.addHeader("Time-Zone", "+05:00");
234+
String sql =
235+
"{\"database\":\"test\",\"sql\":\"select * from sg211 where time <= 2026-03-28T00:00:00\"}";
236+
httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
237+
response = httpClient.execute(httpPost);
238+
HttpEntity responseEntity = response.getEntity();
239+
String message = EntityUtils.toString(responseEntity, UTF8);
240+
LOGGER.info("message with time zone = {}", JsonParser.parseString(message).getAsJsonObject());
241+
} catch (IOException e) {
242+
LOGGER.error("The query with time zone rest api failed", e);
243+
} finally {
244+
try {
245+
if (response != null) {
246+
response.close();
247+
}
248+
} catch (IOException e) {
249+
LOGGER.error("Response close error", e);
250+
}
251+
}
252+
}
223253
}

example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpsExample.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@
2727
import org.apache.http.entity.StringEntity;
2828
import org.apache.http.impl.client.CloseableHttpClient;
2929
import org.apache.http.util.EntityUtils;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
3032

3133
import java.io.IOException;
3234
import java.nio.charset.Charset;
3335
import java.nio.charset.StandardCharsets;
3436
import java.util.Base64;
3537

3638
public class TableHttpsExample {
39+
private static final Logger LOGGER = LoggerFactory.getLogger(TableHttpsExample.class);
3740

3841
private static final String UTF8 = "utf-8";
3942

@@ -50,6 +53,7 @@ public static void main(String[] args) {
5053
httpExample.nonQuery();
5154
httpExample.insertTablet();
5255
httpExample.query();
56+
httpExample.queryWithTimeZone();
5357
}
5458

5559
public void ping() {
@@ -220,4 +224,30 @@ public void query() {
220224
}
221225
}
222226
}
227+
228+
public void queryWithTimeZone() {
229+
CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
230+
CloseableHttpResponse response = null;
231+
try {
232+
HttpPost httpPost = getHttpPost("https://127.0.0.1:18080/rest/table/v1/query");
233+
httpPost.addHeader("Time-Zone", "+05:00");
234+
String sql =
235+
"{\"database\":\"test\",\"sql\":\"select * from sg211 where time <= 2026-03-28T00:00:00\"}";
236+
httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
237+
response = httpClient.execute(httpPost);
238+
HttpEntity responseEntity = response.getEntity();
239+
String message = EntityUtils.toString(responseEntity, UTF8);
240+
LOGGER.info("message with time zone = {}", JsonParser.parseString(message).getAsJsonObject());
241+
} catch (IOException e) {
242+
LOGGER.error("The query with time zone rest api failed", e);
243+
} finally {
244+
try {
245+
if (response != null) {
246+
response.close();
247+
}
248+
} catch (IOException e) {
249+
LOGGER.error("Response close error", e);
250+
}
251+
}
252+
}
223253
}

external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/filter/AuthorizationFilter.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import javax.ws.rs.ext.Provider;
4040

4141
import java.io.IOException;
42+
import java.time.DateTimeException;
4243
import java.time.ZoneId;
4344
import java.util.Base64;
4445
import java.util.UUID;
@@ -88,6 +89,12 @@ public void filter(ContainerRequestContext containerRequestContext) throws IOExc
8889
if (user == null) {
8990
return;
9091
}
92+
93+
ZoneId zoneId = resolveTimeZone(containerRequestContext);
94+
if (zoneId == null) {
95+
return;
96+
}
97+
9198
String sessionid = UUID.randomUUID().toString();
9299
if (SESSION_MANAGER.getCurrSession() == null) {
93100
RestClientSession restClientSession = new RestClientSession(sessionid);
@@ -97,7 +104,7 @@ public void filter(ContainerRequestContext containerRequestContext) throws IOExc
97104
SESSION_MANAGER.getCurrSession(),
98105
user.getUserId(),
99106
user.getUsername(),
100-
ZoneId.systemDefault(),
107+
zoneId,
101108
IoTDBConstant.ClientVersion.V_1_0);
102109
}
103110
BasicSecurityContext basicSecurityContext =
@@ -147,6 +154,37 @@ private User checkLogin(
147154
return user;
148155
}
149156

157+
/**
158+
* Resolves the Time-Zone header from the request.
159+
*
160+
* @param requestContext the incoming HTTP request
161+
* @return the resolved ZoneId, or {@code null} if the header is invalid (the request is aborted)
162+
*/
163+
private ZoneId resolveTimeZone(ContainerRequestContext requestContext) {
164+
String timeZoneHeader = requestContext.getHeaderString("Time-Zone");
165+
if (timeZoneHeader == null) {
166+
return ZoneId.systemDefault();
167+
}
168+
timeZoneHeader = timeZoneHeader.trim();
169+
if (timeZoneHeader.isEmpty()) {
170+
return ZoneId.systemDefault();
171+
}
172+
try {
173+
return ZoneId.of(timeZoneHeader);
174+
} catch (DateTimeException e) {
175+
Response resp =
176+
Response.status(Status.BAD_REQUEST)
177+
.type(MediaType.APPLICATION_JSON)
178+
.entity(
179+
new ExecutionStatus()
180+
.code(TSStatusCode.ILLEGAL_PARAMETER.getStatusCode())
181+
.message("Invalid time zone: " + timeZoneHeader))
182+
.build();
183+
requestContext.abortWith(resp);
184+
return null;
185+
}
186+
}
187+
150188
@Override
151189
public void filter(
152190
ContainerRequestContext requestContext, ContainerResponseContext responseContext)

external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/table/v1/impl/RestApiServiceImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import javax.ws.rs.core.Response;
5252
import javax.ws.rs.core.SecurityContext;
5353

54-
import java.time.ZoneId;
5554
import java.util.List;
5655
import java.util.Optional;
5756

@@ -287,7 +286,8 @@ private Statement createStatement(
287286
}
288287

289288
clientSession.setSqlDialect(IClientSession.SqlDialect.TABLE);
290-
return relationSqlParser.createStatement(sql.getSql(), ZoneId.systemDefault(), clientSession);
289+
return relationSqlParser.createStatement(
290+
sql.getSql(), clientSession.getZoneId(), clientSession);
291291
}
292292

293293
private Response validateStatement(Statement statement, boolean userQuery) {

external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/GrafanaApiServiceImpl.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.iotdb.commons.path.PartialPath;
2222
import org.apache.iotdb.db.conf.IoTDBConfig;
2323
import org.apache.iotdb.db.conf.IoTDBDescriptor;
24+
import org.apache.iotdb.db.protocol.session.IClientSession;
2425
import org.apache.iotdb.db.protocol.session.SessionManager;
2526
import org.apache.iotdb.db.queryengine.plan.Coordinator;
2627
import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
@@ -91,8 +92,9 @@ public Response variables(SQL sql, SecurityContext securityContext) {
9192
try {
9293
RequestValidationHandler.validateSQL(sql);
9394

94-
Statement statement =
95-
StatementGenerator.createStatement(sql.getSql(), ZoneId.systemDefault());
95+
IClientSession session = SESSION_MANAGER.getCurrSession();
96+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
97+
Statement statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
9698
if (!(statement instanceof ShowStatement) && !(statement instanceof QueryStatement)) {
9799
return Response.ok()
98100
.entity(
@@ -168,7 +170,9 @@ public Response expression(ExpressionRequest expressionRequest, SecurityContext
168170
sql += " " + expressionRequest.getControl();
169171
}
170172

171-
Statement statement = StatementGenerator.createStatement(sql, ZoneId.systemDefault());
173+
IClientSession session = SESSION_MANAGER.getCurrSession();
174+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
175+
Statement statement = StatementGenerator.createStatement(sql, zoneId);
172176

173177
Response response = authorizationHandler.checkAuthority(securityContext, statement);
174178
if (response != null) {
@@ -232,7 +236,9 @@ public Response node(List<String> requestBody, SecurityContext securityContext)
232236
// TODO: necessary to create a partial path?
233237
PartialPath path = new PartialPath(Joiner.on(".").join(requestBody));
234238
String sql = "show child paths " + path;
235-
Statement statement = StatementGenerator.createStatement(sql, ZoneId.systemDefault());
239+
IClientSession session = SESSION_MANAGER.getCurrSession();
240+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
241+
Statement statement = StatementGenerator.createStatement(sql, zoneId);
236242

237243
Response response = authorizationHandler.checkAuthority(securityContext, statement);
238244
if (response != null) {

external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/RestApiServiceImpl.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.iotdb.db.conf.IoTDBConfig;
2121
import org.apache.iotdb.db.conf.IoTDBDescriptor;
2222
import org.apache.iotdb.db.conf.rest.IoTDBRestServiceDescriptor;
23+
import org.apache.iotdb.db.protocol.session.IClientSession;
2324
import org.apache.iotdb.db.protocol.session.SessionManager;
2425
import org.apache.iotdb.db.protocol.thrift.OperationType;
2526
import org.apache.iotdb.db.queryengine.plan.Coordinator;
@@ -87,7 +88,9 @@ public Response executeNonQueryStatement(SQL sql, SecurityContext securityContex
8788
Statement statement = null;
8889
try {
8990
RequestValidationHandler.validateSQL(sql);
90-
statement = StatementGenerator.createStatement(sql.getSql(), ZoneId.systemDefault());
91+
IClientSession session = SESSION_MANAGER.getCurrSession();
92+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
93+
statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
9194
if (statement == null) {
9295
return Response.ok()
9396
.entity(
@@ -177,7 +180,9 @@ public Response executeQueryStatement(SQL sql, SecurityContext securityContext)
177180
Statement statement = null;
178181
try {
179182
RequestValidationHandler.validateSQL(sql);
180-
statement = StatementGenerator.createStatement(sql.getSql(), ZoneId.systemDefault());
183+
IClientSession session = SESSION_MANAGER.getCurrSession();
184+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
185+
statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
181186
if (statement == null) {
182187
return Response.ok()
183188
.entity(

external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/GrafanaApiServiceImpl.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.iotdb.commons.path.PartialPath;
2222
import org.apache.iotdb.db.conf.IoTDBConfig;
2323
import org.apache.iotdb.db.conf.IoTDBDescriptor;
24+
import org.apache.iotdb.db.protocol.session.IClientSession;
2425
import org.apache.iotdb.db.protocol.session.SessionManager;
2526
import org.apache.iotdb.db.queryengine.plan.Coordinator;
2627
import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
@@ -91,8 +92,9 @@ public Response variables(SQL sql, SecurityContext securityContext) {
9192
try {
9293
RequestValidationHandler.validateSQL(sql);
9394

94-
Statement statement =
95-
StatementGenerator.createStatement(sql.getSql(), ZoneId.systemDefault());
95+
IClientSession session = SESSION_MANAGER.getCurrSession();
96+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
97+
Statement statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
9698
if (!(statement instanceof ShowStatement) && !(statement instanceof QueryStatement)) {
9799
return Response.ok()
98100
.entity(
@@ -168,7 +170,9 @@ public Response expression(ExpressionRequest expressionRequest, SecurityContext
168170
sql += " " + expressionRequest.getControl();
169171
}
170172

171-
Statement statement = StatementGenerator.createStatement(sql, ZoneId.systemDefault());
173+
IClientSession session = SESSION_MANAGER.getCurrSession();
174+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
175+
Statement statement = StatementGenerator.createStatement(sql, zoneId);
172176

173177
Response response = authorizationHandler.checkAuthority(securityContext, statement);
174178
if (response != null) {
@@ -232,7 +236,9 @@ public Response node(List<String> requestBody, SecurityContext securityContext)
232236
// TODO: necessary to create a PartialPath
233237
PartialPath path = new PartialPath(Joiner.on(".").join(requestBody));
234238
String sql = "show child paths " + path;
235-
Statement statement = StatementGenerator.createStatement(sql, ZoneId.systemDefault());
239+
IClientSession session = SESSION_MANAGER.getCurrSession();
240+
ZoneId zoneId = (session != null) ? session.getZoneId() : ZoneId.systemDefault();
241+
Statement statement = StatementGenerator.createStatement(sql, zoneId);
236242

237243
Response response = authorizationHandler.checkAuthority(securityContext, statement);
238244
if (response != null) {

0 commit comments

Comments
 (0)