Spring Boot - update response of every API using ResponseBodyAdvice

Not sure why we need it but I've seen several codebase where developers were wrapping the response body in some structure like below - where the actual content is put under an element eg: body and the root object has several other values.

{
"body": { //the actual body
"greeting": "Hello World!"
},

//additional elements
"timestamp": 1741653577468,
"traceId": "67cf8649f3c848a71d8171473d3b4955",
"spanId": "1d8171473d3b4955",
"durationMs": 0,
"status": 200
}

The corresponding controller method would look something like below where they would explicitly create the ResponseObject and map other parameters in every single endpoint definition:

@GetMapping("/object-bad-way/hi")
public ResponseObject<Hi> oldWayHi() {
log.info("Got request - object - old way - don't do this, use ResponseBodyAdvice");
var resp = new ResponseObject<Hi>();
var span = tracer.currentSpan();
if (span != null) {
resp.setTraceId(span.context().traceId());
resp.setSpanId(span.context().spanId());
}

resp.setTimestamp(System.currentTimeMillis());
try {
resp.setBody(myService.getHi()); //service call
resp.setStatus(HttpStatus.OK.value());
} catch (Exception e) {
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}

return resp;
}

The ResponseObject:

@Getter
@Setter
class ResponseObject<T> {
T body;
long timestamp;

String traceId;
String spanId;

long durationMs;
int status;
}


But wait, there's a better way to do this if you really want this. The idea is to use Spring's @ControllerAdvice that implements ResponseBodyAdvice to map the additional params so that you don't need to create the new ResponseObject instance on all endpoints.

After the change, your controller method would look simple as below: wouldn't that be nice?

@GetMapping("/better-way/hi")
public Hi objectHi() {
log.info("Got request - object");
return service.getHi();
}

 

ResponseBodyAdvice

From the docs:

ResponseBodyAdvice allows customizing the response after the execution of an @ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.
Implementations may be registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with @ControllerAdvice in which case they will be auto-detected by both.

 

A simple (empty) usage would look like this. On the supports method, we can decide if the response body needs to be modified. The beforeBodyWrite is where we can modify the request body or create new.

 

@ControllerAdvice
class ObjectResponseAdvice implements ResponseBodyAdvice<Object> {

//Whether this component supports the given controller method return type and the selected HttpMessageConverter type.
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

//Invoked after an HttpMessageConverter is selected and just before its write method is invoked.
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//modify body
return body;
}
}

 

A simple implementation of beforeBodyWrite that would work with both String response type and any other response type. Here we are handling the String response specially by checking if the original body(response body) is String. Otherwise, we are creating a ObjectNode and mapping timestamp and body elements under it.

 

supports() method: 
    return true;


beforeBodyWrit() method:
  ObjectNode root = objectMapper.createObjectNode();
root.put("timestamp", System.currentTimeMillis());

if (body instanceof String) {
root.set("body", objectMapper.convertValue(body, JsonNode.class));
return objectMapper.writeValueAsString(root);
} else {
root.put("body", String.valueOf(body));
return root;
}

 

Handle more stuff and map more stuff:

Here we are mapping the traceId/spanId from Micrometer Tracing , mapping 'status' from ServletServerHttpResponse. Also for backward compatibility with existing RestController methods that already return ResponseObject (with all elements we want on the response), we are skipping the additional mapping and simply returning body.

if (response instanceof ServletServerHttpResponse resp
//if body is already ResponseObject - do not convert
&& !(body instanceof ResponseObject)) {

ObjectNode root = objectMapper.createObjectNode();
var span = tracer.currentSpan();
if (span != null) {
root.put("traceId", span.context().traceId());
root.put("spanId", span.context().spanId());
}
root.put("status", resp.getServletResponse().getStatus());
if (BeanUtils.isSimpleValueType(body.getClass())) {
root.put("body", String.valueOf(body));
} else {
root.set("body", objectMapper.convertValue(body, JsonNode.class));
}
root.put("timestamp", System.currentTimeMillis());

if (body instanceof String) {
// String are handled special case with StringHttpMessageConverter
// we need to return String from this method
return objectMapper.writeValueAsString(root);
} else {
return root;
}
}

 

 

Full code in action: Note that it supports, string type, int type, map, and any object response.

 

Test it out with following endpoints:

###
GET http://localhost:8080/string

###
GET http://localhost:8080/map

###
GET http://localhost:8080/object/hi

###
GET http://localhost:8080/object/hello

###
GET http://localhost:8080/object-bad-way/hi

###
GET http://localhost:8080/object-bad-way/hello

 

 Full Code:

package responsebody;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.micrometer.tracing.Tracer;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.MethodParameter;
import org.springframework.http.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.net.*;
import java.util.Map;

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

@RestController
@Slf4j
@RequiredArgsConstructor
class GreetingController {

final Tracer tracer;

@GetMapping("/string")
public String string() {
log.info("Got request - string");
return "Hello World!";
}

@GetMapping("/int")
public int intVal() {
log.info("Got request - int");
return 1;
}

@GetMapping("/url")
public URL url() throws MalformedURLException {
log.info("Got request - url");
return URI.create("https://localhost:8080/").toURL();
}

@GetMapping("/map")
public Map<String, String> map() {
log.info("Got request - map");
return Map.of("greeting", "Hello World!");
}

@GetMapping("/object/hello")
public Hello objectHello() {
log.info("Got request - object");
return new Hello("Hello World!");
}

@GetMapping("/better-way/hi")
public Hi objectHi() {
log.info("Got request - object");
return new Hi("Hi World!");
}

@GetMapping("/object-bad-way/hello")
public ResponseObject<Hello> oldWay() {
log.info("Got request - object - old way - don't do this, use ResponseBodyAdvice");
var resp = new ResponseObject<Hello>();
var span = tracer.currentSpan();
if (span != null) {
resp.setTraceId(span.context().traceId());
resp.setSpanId(span.context().spanId());
}

resp.setTimestamp(System.currentTimeMillis());
try {
resp.setBody(new Hello("Hello World!")); //service call
resp.setStatus(HttpStatus.OK.value());
} catch (Exception e) {
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}

return resp;
}

@GetMapping("/object-bad-way/hi")
public ResponseObject<Hi> oldWayHi() {
log.info("Got request - object - old way - don't do this, use ResponseBodyAdvice");
var resp = new ResponseObject<Hi>();
var span = tracer.currentSpan();
if (span != null) {
resp.setTraceId(span.context().traceId());
resp.setSpanId(span.context().spanId());
}

resp.setTimestamp(System.currentTimeMillis());
try {
resp.setBody(new Hi("Hello World!")); //service call
resp.setStatus(HttpStatus.OK.value());
} catch (Exception e) {
resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}

return resp;
}


}


@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
class ObjectResponseAdvice implements ResponseBodyAdvice<Object> {

final Tracer tracer;
final ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {


if (response instanceof ServletServerHttpResponse resp
//if body is already ResponseObject - do not convert
&& !(body instanceof ResponseObject)) {

ObjectNode root = objectMapper.createObjectNode();
var span = tracer.currentSpan();
if (span != null) {
root.put("traceId", span.context().traceId());
root.put("spanId", span.context().spanId());
}
root.put("status", resp.getServletResponse().getStatus());
if (BeanUtils.isSimpleValueType(body.getClass())) {
root.put("body", String.valueOf(body));
} else {
root.set("body", objectMapper.convertValue(body, JsonNode.class));
}
root.put("timestamp", System.currentTimeMillis());

if (body instanceof String) {
// String are handled special case with StringHttpMessageConverter
// we need to return String from this method
return objectMapper.writeValueAsString(root);
} else {
return root;
}
}

return body;
}
}


@Getter
@NoArgsConstructor
@AllArgsConstructor
class Hello {
String greeting;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
class Hi {
String hi;
}

@Getter
@Setter
class ResponseObject<T> {
T body;
long timestamp;

String traceId;
String spanId;

long durationMs;
int status;
}

 

 


Run a task in a different interval during weekend using Spring Scheduler and Custom Trigger

Run a task in a different interval during weekend.

Instead of defining fixed interval to run a task every 1 minute (as shown in example below) using @Scheduled, we can customize Spring Task Scheduler to schedule job with a custom trigger to run the tasks in different schedule depending on business logic. 

@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
void job1() {
System.out.println("Running job1 - this won't run on weekend");
}

 

For example, we can do the following to run the task every 15 minute instead of 1 minute during weekends. Here we are registering a task job1 with TaskScheduler with a CustomTrigger.

The CustomTrigger implements Spring's Trigger interface and overrides nextExecution() method to do business logic to find the interval on which the task should run. For the example purpose we are running tasks less often (every 15 minute) during weekend.


DayOfWeek day = LocalDate.now(ZoneId.of(ZoneId.SHORT_IDS.get("CST"))).getDayOfWeek();
Duration nextSchedulePeriod = WORKDAY_INTERVAL;

/*
* Run the task every 15 minute often during weekend
*/

if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
nextSchedulePeriod = WEEKEND_INTERVAL;
}
return new PeriodicTrigger(nextSchedulePeriod).nextExecution(ctx);



import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.support.PeriodicTrigger;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

@Configuration
class CustomTaskScheduler implements InitializingBean {

static final Duration WORKDAY_INTERVAL = Duration.ofMinutes(1);
static final Duration WEEKEND_INTERVAL = Duration.ofMinutes(15);

@Autowired
TaskScheduler taskScheduler;


@Override
public void afterPropertiesSet() {
taskScheduler.schedule(this::job1, new CustomTrigger());
// schedule more tasks
}

void job1() {
System.out.println("Running job1 - this won't run on weekend");
}


static class CustomTrigger implements Trigger {
/**
* Determine the next execution time according to the given trigger context.
*/
@Override
public Instant nextExecution(TriggerContext ctx) {
DayOfWeek day = LocalDate.now(ZoneId.of(ZoneId.SHORT_IDS.get("CST"))).getDayOfWeek();
Duration nextSchedulePeriod = WORKDAY_INTERVAL;

/*
* Run the task every 15 minute often during weekend
*/

if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
nextSchedulePeriod = WEEKEND_INTERVAL;
}
return new PeriodicTrigger(nextSchedulePeriod).nextExecution(ctx);
}


}

}

Append a file into existing ZIP using Java

 

The following code reads a input file 'in.zip' appends 'abc.txt' with some String content into it and creates output zip file 'out.zip' using Java.


import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.zip.ZipOutputStream;

public class ZipTests {

public static void main(String[] args) throws Exception {
var zipFile = new java.util.zip.ZipFile("in.zip");
final var zos = new ZipOutputStream(new FileOutputStream("out.zip"));
for (var e = zipFile.entries(); e.hasMoreElements(); ) {
var entryIn = e.nextElement();
System.out.println("Processing entry " + entryIn.getName());

//need to set next entry before modifying /adding content
zos.putNextEntry(entryIn);

if (entryIn.getName().equalsIgnoreCase("abc.txt")) {
//modify content and save to output zip stream

String content = isToString(zipFile.getInputStream(entryIn));
content = content.replaceAll("key1=value1", "key1=val2");
content += LocalDateTime.now() + "\r\n";

byte[] buf = content.getBytes();
zos.write(buf, 0, buf.length);
} else {
//copy the content
var inputStream = zipFile.getInputStream(entryIn);
byte[] buf = new byte[9096];
int len;
while ((len = inputStream.read(buf)) > 0) {
zos.write(buf, 0, len);
}

}
zos.closeEntry();
}
zos.close();
}

static String isToString(InputStream stringStream) throws Exception {
var result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int length; (length = stringStream.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
return result.toString(StandardCharsets.UTF_8);
}

}

Spring Boot - get Trace and Span ID programmatically

 Autowire Tracer tracer

and get traceId and spanId from current Span


Span
span = tracer.currentSpan();
span.context().traceId() //trace ID span.context().spanId() //span ID

8 puzzle solver using A* algorithm

 8-puzzle is a very interesting and a well known problem in the field of Artificial Intelligence. It always has been an important subject in articles, books and become a part of course material in many universities. The 8-puzzle is also known as the sliding-block puzzle or tile-puzzle and is meant for a single user. 8-puzzle consists of 8 square tiles numbered 1 through 8 and one blank space on a 3  by 3 board. Moves of the puzzle are made by sliding an adjacent tile into the position occupied by the blank space, which has the effect of exchanging the positions of the tile and blank space. Only tiles that are horizontally or vertically adjacent (not diagonally adjacent) may be moved into the blank space. The objective of the game is to start from an  initial configuration and end up in a configuration which the tiles are placed in ascending number order as shown in: figure 1 below.



                                                        Figure 1   Goal state of the 8-puzzle

HOW TO PLAY

The game can be played using the arrow keys. Up arrow slides a tile up into the empty space, left arrow slides a tile left into the empty space and so on. You can choose to solve it automatically by program. It will display the animation sliding the tiles and solve the 8 puzzle in least possible moves using AI.

METHODOLOGY

According to the authors  Russel  and  Norvig  in their book titled  Artificial Intelligence: A Modern Approach, the average solution for a randomly generated 8-puzzle instance is about 22 steps. This figure  is obtained by estimating the branching factor which is about 3 (when the empty tile is in the middle, there are four possible moves; when it is in a corner there are two; and when it is along an edge there are three). This means that an exhaustive search to depth 22 would look at about 322 ~ 3.1 x 1010 states. By keeping track of repeated states, it is possible to cut this down by a factor of about 170,000, because there are only 9!/2 = 181,440 distinct states that are reachable.


ALGORITHM

After a preliminary investigation of the heuristic search strategies we were able to figure out that A* algorithm is the best for the 8-puzzle problem. A* is the most widely used form of best first search algorithm which is an instance of Tree-Search or Graph-Search where a best node is expanded on the basis of an evaluation function f(n). Here, the evaluation function of each node is calculated as a sum of two functions g(n) and h(n) where, g(n) refers to the cost to reach the node n while h(n) is the cost to get from node n to the goal. A* algorithm is both optimal and complete. It is optimal in that sense h(n) is admissible and is never overestimated
to reach the goal and it is complete in that sense it guarantees to reach a solution whenever there is one.

The following pseudo code describes the A* algorithm:


function A*(start,goal)
     closedset := the empty set                 // The set of nodes already  evaluated.     
     openset := set containing the initial node // The set of tentative nodes to be evaluated.
     g_score[start] := 0                        // Distance from start along optimal path.
     h_score[start] := heuristic_estimate_of_distance(start, goal)
     f_score[start] := h_score[start]           // Estimated total distance from start to goal through y.

while openset is not empty
         x := the node in openset having the lowest f_score[] value
         if x = goal
             return reconstruct_path(came_from[goal])
         remove x from openset
         add x to closedset
         foreach y in neighbor_nodes(x)
             if y in closedset
                 continue


 tentative_g_score := g_score[x] + dist_between(x,y)
 
             if y not in openset
                 add y to openset
                 tentative_is_better := true
             elseif tentative_g_score < g_score[y]
                 tentative_is_better := true
             else
                 tentative_is_better := false
             if tentative_is_better = true
                 came_from[y] := x
                 g_score[y] := tentative_g_score
                 h_score[y] := heuristic_estimate_of_distance(y, goal)
                 f_score[y] := g_score[y] + h_score[y]
     return failure
 
 function reconstruct_path(current_node)
     if came_from[current_node] is set
         p = reconstruct_path(came_from[current_node])
         return (p + current_node)
     else
         return current_node


REFERENCE

1.  Rich and Knight, Artificial Intelligence.
2.  Russell and Norvig, Artificial Intelligence: A Modern approach,
3.  A* search algorithm - Wikipedia, the free encyclopedia
4.  Description and a solution strategy for the 8-puzzle from http://www.8puzzle.com/
5.  G. Novak, CS 381K: Heuristic Search: 8 Puzzle, 26 Sep 07
6.  An appealing Java applet from www.permadi.com/java/puzzle8/

JSP basics - Java Server Pages

 The JSP - Java Server Pages

  • JSP extension of the servlet technology which offers both scripting ( as in HTML) as well as programming (as in Servlets) 
  • When a JSP page is requested, the JSP container ( also called page compiler) compiles the jsp page into a servlet class. 
  • The JSP API that handles all the JSP technology has been bundled under the following two packages: 
    • javax.servlet.jsp 
    • javax.servlet.jsp.tagext 

JSP Syntax

A JSP page is all about JSP tags and some template data ( such as HTML tags). That's pretty much of it. It always feels good learning about JSP as we get to see the very well-know HTML tags (I presumed that you already know HTML at this point).

JSP consists of three types of elements.
  • Directive elements 
  • Scripting elements 
  • Action elements 

DIRECTIVES

<%@ directiveName (attributeName="atrributeValue") %>

These are the messages/information to the JSP container on how it should translate the JSP page into the corresponding servlet.

Again, there are three types of directives:

  • Page directives
  • Include directives
  • Tag library directives 

1. Page Directive
<%@ page (attributeName="atrributeValue") %>

You might have encountered some "import" statements at the beginning of some JSP pages. Those import statements falls under this category. E.g.

<%@ page import="java.util.Date, java.util.Enumeration" language="java" %>
where,
          page => represents directiveName
          import/language => represents attributeName
          and the values enclosed by quotes represents atrributeValue

The following is the list of available Page directive attributes:
Attribute
Value Type
Default Value
language
Scripting language name
"java"
info
String
Depends on the JSP container
contentType
MIME type, character set
"text/html;charset=ISO-8859-1"
extends
Class name
None
import
Fully qualified class name or package name
None
buffer
Buffer size or false
8Kb
autoFlush
Boolean
"true"
session
Boolean
"true"
isThreadSafe
Boolean
"true"
errorPage
URL
None
isErrorPage
Boolean
"false"

NOTE:
  • Except for the import attribute, JSP does not allow to repeat the same attribute within a page directive or in multiple directives on the same page. 

Page Attributes a little in-depth:
  • language 
    • <%@ page language="java" %>
    • The only supported language by Tomcat JSP Container
    • Other JSP containers might accept different languages
  • info 
    • <%@ page info="any string here" %>
    • Can be retrieved by calling the method getServletInfo() from anywhere withing the page
  • contentType 
    • <%@ page contentType="application/json; charset=UTF-8" %> 
    • Defines MIME type of the HTTP response sent to the client 
  • extends 
    • <%@ page extends="yourClass" %>
    • Defines the parent class that will be inherited by the generated servlet of the current page
    • This attribute is less common
  • import 
    • <%@ page import="java.io.*" %>
    • One of the most common attributes used
    • Same as we do in java classes/interfaces
    • By default, tomcat imports the following packages:
      • javax.servlet.* 
      • javax.servlet.http.* 
      • javax.servlet.jsp.* 
      • javax.servlet.jsp.tagext.*
      • org.apache.jasper.runtime.*
  • buffer 
    • <%@ page buffer="none" %>
    • <%@ page buffer="20kb" %>
    • <%@ page buffer="20" %>
    • buffer="20" is the same as "20kb"
    • But it's up to the JSP container to use its discretion to increase the buffer in order for the improved performance
  • autoFlush 
    • <%@ page autoFlush="false" %>
    • If true, the JSP container automatically flushes the buffer when it's full.
    • If false, the buffer can be flushed manually by the following statement: 
      • out.flush()
  • session 
    • <%@ page session="true" %>
    • By default, the session is set "true", meaning the JSP page takes part in session management
    • It is wise and strongly advised not to set it true unless you specifically require it because this consumes resources.
  • isThreadSafe 
    • <%@ page isThreadSafe="true" %>
    • True indicates that the simultaneous access to this page is safe
    • False indicates that the simultaneous access to this page is not safe. In that case, the JSP container puts multiple requests in a queue and serves them only one at a time.
  • errorPage 
    • <%@ page errorPage="MyErrorPage.jsp" %>
    • Indicates the error page which is displayed if an uncaught exception occurs in the current page
    • The error page must have the page attribute isErrorPage="true"
  • isErrorPage 
    • <%@ page isErrorPage="true" %>
    • If true, then only be set as an error page in other JSP pages
    • If true, this page has an access to the exception implicit object

2. Include directives
<%@ include file="includes/Footer.html" %>
  • As the name suggest, Include Directive enables us to include the contents of other files in the current JSP page.
  • The included page can be static HTML page as well as dynamic JSP page.
  • Very commonly used to include Header page, Footer page or a page containing import statements and so on.

2. Taglib directives
  • The taglib, or tag library, directive can be used to extend JSP functionality. It's a broad topic so it's not covered here at this moment. It will be covered under it's own topic later soon.

SCRIPTING ELEMENTS

Scripting elements allow you to insert Java code in your JSP pages. There are three types of scripting elements:
  • Scriptlets
  • Declarations
  • Expressions 

1. Scriptlets
<% ... %>
  • Syntax looks similar to Directives. But don't get confused, Directives starts with <%@ i.e. <%@ ... %>

<%
  try {
    Class.forName("com.mysql.jdbc.Driver");
    System.out.println("JDBC driver loaded");
  }
  catch (ClassNotFoundException e) {
    System.out.println(e.toString());
  }
%>

...
...

<%
out.println("You can do anything here");

String str = "some string";
str = str + " string concatenation";
out.println( str );

%>


2. Declarations
<%! ... %>
  • Used to declare methods and variables that can be used from any point in the JSP page.
  • Can appear anywhere throughout the page. For instance, a method declaration can appear above a page directive that imports a class, even though the class is used in the method.
<%!
  String getSystemTime() {
    return Calendar.getInstance().getTime().toString();
  }
%>

<%@ page import="java.util.Calendar" %>

<%
  out.println("Current Time: " + getSystemTime());
%>

3. Expressions 
<%= ... %>
  • Expressions get evaluated when the JSP page is requested and their results are converted into a String and fed to the print method of the out implicit object. 
  • If the result cannot be converted into a String, an error will be raised at translation time. If this is not detected at translation time, at request-processing time, a ClassCastException will be raised.
  • Don't need to add a semicolon at the end of an expression

Below is the two equivalent statements:


<!-- Expression -->

<%= java.util.Calendar.getInstance().getTime() %>

<!-- Equivalent Scriptlet -->

<% out.print(java.util.Calendar.getInstance().getTime()); %>