[Spring MVC learning notes 3] deeply practice Spring MVC controller

In the previous Blog, we implemented the first framework program of Spring MVC based on configuration and annotation respectively. Next, this Blog makes an in-depth study and Discussion on our Controller. Since it is a Controller, it must contain two topics: receiving requests and returning responses. We will make an in-depth study on these two major directions, For example, when receiving a request, what are the parameter types, how to solve the problem of garbled code, how to use RestFul style to transfer parameters, how to obtain additional information of the request, etc; When returning the response, we need to understand the return form of the data and the return form of the page. Of course, with additional understanding, the access problem of static resources. For the next test environment, we use the previous Blog [Spring MVC learning notes 2] build the first Spring MVC framework program For the base annotation module in, Spring MVC still uses more annotations. Why? Because we can expand many methods with one annotation, and only one handleRequest can be used based on configuration

Static resource access problem

When the URL pattern of intercepting resources is set to / for the front-end controller, the static resources cannot be accessed

Problem phenomenon

For example, we create a folder image under webapp, and then add a picture:

Then, the following exception occurs when requesting image address access:

Cause of problem

Why? Because the path mapped by the servlet (default) that handles static resources in Tomcat is /. When starting the project, the web.xml in Tomcat is loaded first and the web.xml of the project is loaded later. If the same mapping path is configured, the web.xml in the project will overwrite the same configuration of web.xml in Tomcat.

    <!--Configure front-end controller-->
    <servlet>
        <!--1.register DispatcherServlet-->
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--Specified by initialization parameters SpringMVC The location of the configuration file is associated-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!--Associate a springMVC Configuration file for:[servlet-name]-servlet.xml-->
            <param-value>classpath:springmvc-servlet.xml</param-value>
        </init-param>
        <!--Startup level-1,stay Tomcat Initialize at startup Spring container-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

In other words, the mapping path of DispatcherServlet in spring MVC overrides the path of Tomcat's default processing of static resources. At this time, spring MVC will look for and access static resources as controllers. Of course, it will not be found

Solution

Configuring < MVC: default Servlet handler / > in springmvc-servlet.xml can solve this problem. With this configuration, the request to the dispatcher Servlet will be judged first. If it is a static resource, it will not be processed and handed over to the default Servlet of Tomcat:

After configuration, the server can request the static resource again:

Static resources can be pictures, files, html, txt, etc.

Controller request processing

Before testing the Controller request, we first create two models under the dto package for testing:

User

package com.example.base_annotation.controller.dto;

import lombok.Data;

import java.util.List;

@Data
public class User {
    private Long id;
    private String username;
    private Integer age;
    private List<String> accountIds;
}

Account

package com.example.base_annotation.controller.dto;

import lombok.Data;

@Data
public class Account {
    private Long id;
    private String name;
}

1. Common input parameter types

Next, we discuss six common input parameter types. All codes are under UserController, and their address is mapped to: / user. After opening the annotation, scan. The springmvc-servlet.xml configuration is as follows

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- Automatically scan the package to make the annotations under the specified package effective,from IOC Unified container management -->
    <context:component-scan base-package="com.example.base_annotation.controller"/>
    <!-- Give Way Spring MVC Do not process static resources -->
    <mvc:default-servlet-handler />
    <!--
    support mvc Annotation driven
        stay spring Generally used in@RequestMapping Annotation to complete the mapping relationship
        To make@RequestMapping Note effective
        You must register with the context DefaultAnnotationHandlerMapping
        And one AnnotationMethodHandlerAdapter example
        These two instances are handled at the class level and method level, respectively.
        and annotation-driven Configuration helps us automatically complete the injection of the above two instances.
     -->
    <mvc:annotation-driven />

    <!--view resolver -->
    <!--Parse the logical view name returned by the processor through the view parser and pass it to the DispatcherServlet-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="InternalResourceViewResolver">
        <!--prefix-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--suffix-->
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

1. Servlet API parameters

Let's try to use the native Servlet to make a request by operating the native Servlet API parameters:

  // You can manipulate the Servlet's api through parameters
    @RequestMapping("/servletParam")
    public void servletParam(HttpServletRequest request, HttpServletResponse responser, HttpSession session) {
        System.out.println(request.getParameter("username"));
        System.out.println(request);
        System.out.println(responser);
        System.out.println(session);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/servletParam?username=tml

The printed results are as follows:

2 simple type parameters

Obtain the request parameters and ensure that the request parameter name has the same name as the formal parameter (input parameter) of the Controller method, so as to obtain the parameter content of the request. If the name is different, the parameter cannot be obtained

    @RequestMapping("/simpleParamMatch")
    public void simpleParamMatch(String username, int age) {
        System.out.println("username:" + username);
        System.out.println("age:" + age);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/simpleParamMatch?username=tml&age=30

The printed results are as follows:

@Set alias for RequestParam annotation

If the request parameter name is different from the formal parameter name, use the RequestParam annotation. After using the @ RequestMapping annotation, the content of the request parameter can also be obtained if the name is different

    @RequestMapping("/simpleParamDiff")
    public void simpleParamDiff(@RequestParam("name") String username, @RequestParam(value = "age") Integer age) {
        System.out.println("username:" + username);
        System.out.println("age:" + age);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/simpleParamDiff?name=tml&age=30

The printed results are as follows:

3 array and List type parameters

When multiple values of a parameter are accepted, the array can be used to directly receive multiple parameters passed:

   @RequestMapping("/arrayParam")
    public void arrayParam(Long[] accountIds) {
        System.out.println(Arrays.asList(accountIds));
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/arrayParam?accountIds=10&accountIds=20&accountIds=30&accountIds=40

However, the use of collection List cannot be accepted directly. In this case, a collection needs to exist in the object and be passed in the form of JavaBean

    @RequestMapping("/listParam")
    public void listParam(User user) {
        System.out.println(user.getAccountIds());
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/listParam?accountIds=10&accountIds=20&accountIds=30&accountIds=40

The returned results are the same:

4 date type processing

Parameter transfer from foreground to background needs to be changed from String type to date type:

  @RequestMapping("/dateParam")
    public void dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
        System.out.println(date);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/dateParam?date=2021-08-31

The returned results are as follows:

5 JavaBean type parameters

Spring MVC can automatically encapsulate parameters into JavaBean s, provided that the attribute names must be the same

  //Encapsulate data directly into JavaBean objects
    @RequestMapping("/javaBeanParam")
    public void javaBeanParam(User user) {
        System.out.println(user);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/javaBeanParam?id=1&username=tml&age=30&accountIds=50&accountIds=80

The returned results are as follows:

@ModelAttribute annotation setting shared object

Of course, we can also use ModelAttribute to directly set the passed parameters as page shared data for display.

   //Encapsulate data directly into JavaBean objects
    @RequestMapping("/javaBeanModelAttributeParam")
    public String javaBeanModelAttributeParam(@ModelAttribute("userAllies") User user) {
        System.out.println(user);
        return "userInfo";
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/javaBeanModelAttributeParam?id=1&username=tml&age=30&accountIds=50&accountIds=80

The returned results are as follows:

The userInfo.jsp code is as follows:

<%--
  Created by IntelliJ IDEA.
  User: 13304
  Date: 2021/8/31
  Time: 11:47
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
${userInfo}
userAllies: ${userAllies}
</body>
</html>

The printing results are as follows:

6 multi object encapsulation transfer parameters

If we encounter that the parameters are multiple JavaBean s and they have the same attribute name, what should we do to make the data messy? We can use the data binding mechanism. Spring MVC analyzes the signature of the target processing method through the reflection mechanism and binds the request information to the formal parameters of the processing method. The core component of data binding is the DataBinder class. Data binding process:

  1. The framework passes the ServletRequest object and request parameters to DataBinder;
  2. DataBinder first calls the ConversionService component in the Spring Web environment for data type conversion, formatting and other operations, and fills the information in ServletRequest into the formal parameter object
  3. DataBinder calls the Validator component to verify the data validity of the formal parameter object that has bound the request message data
  4. DataBinder finally outputs the data binding result object BindingResult

Let's practice it. When the request arrives, first initialize the binding, let the parameters find the corresponding JavaBean and assign values, and then handle it by the Controller:

    //Encapsulate the parameters starting with User. Into the User object
    @InitBinder("user") // Custom data binding registration is used to convert request parameters into the properties of the corresponding object
    public void initBinderUserType(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("user.");
    }

    @InitBinder("account")
    public void initBinderCatType(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("account.");
    }

    @RequestMapping("/multiModelParam")
    public void multiModelParam(User user, Account account) {
        System.out.println(user);
        System.out.println(account);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/multiModelParam?user.id=1&user.username=tml&user.age=30&accountIds=100&accountIds=200&account.id=12&account.name=chinaBank

The returned results are as follows:

2 RestFul style reference

What is REST? REST (English: Representational State Transfer, referred to as REST, meaning: representational state transition, which describes an architecture style network system, such as web application). It is a software architecture style and design style, not a standard. It only provides a set of design principles and constraints. It is mainly used for interactive software between client and server. The software designed based on this style can be more brief, more hierarchical, and easier to implement caching and other mechanisms.

What is RESTFUL

What is RESTFUL? An application or design that satisfies REST constraints and principles is RESTFUL.

  • Resources: all things on the Internet can be abstracted as resources
  • Resource operation: use POST, DELETE, PUT and GET to operate resources using different methods. Add, DELETE, modify and query respectively.

For a simpler understanding, it can be considered that the url path is not written dead, but can be passed as a parameter

@PathVariable annotation

For example, if our request path contains a specific resource id, we will request the resource. Of course, we can also add some operations, such as adding, deleting, modifying and querying

   @RequestMapping("/restfulParam/{id}")
    public void restfulParam(@PathVariable("id") Long id) {
        System.out.println("restfulParam Parameter is" + id);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/restfulParam/10

The returned results are as follows:

3 request for additional information

If we want to obtain some additional information of the request, such as user agent and Cookie information in the request header, we can obtain it in this way:

    @RequestMapping("/headerInfo")
    public void headerInfo(@RequestHeader("User-Agent") String userAgent, @CookieValue("JSESSIONID") String cookieName) {
        System.out.println("User-Agent:" + userAgent);
        System.out.println("cookieName" + cookieName);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/headerInfo

The returned results are as follows:

4. The method attribute specifies the request type

Generally, the default request method is Get, but there can also be other request methods. For example, we use POST to request the interface

    @RequestMapping(value="/javaBeanParamPost",method = {RequestMethod.POST})
    public void javaBeanParamPost(User user) {
        System.out.println(user);
    }

The requested url is as follows:

http://localhost:8081/base_annotation/user/javaBeanParamPost?id=1&username=tml&age=30&accountIds=50&accountIds=80

The returned results are as follows:

Of course, an error will be reported, because our method requires a POST request and the browser requests a Get, which is not supported. In addition to the above methods, the following annotations can also be used to directly indicate the properties of the method:

@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping

5. Handling of garbled request data

Before, we encountered the problem of garbled code, which needs to be set manually:

request.setCharacterEncoding("UTF-8");

Now we have a framework to intercept as a whole. The GET request framework has helped us do a good job. For POST requests, we only need to add the following configuration in web.xml to solve the problem of garbled Code:

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <!-- If there is a coding format,Setting forces the use of the encoding format we set above-->
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Controller response processing

After talking about the request, let's talk about the response. We know that part of the response is the page and part is the data, so let's talk about the return format of the data and the jump method of the page.

1 return data model format

First, we reset the userInfo.jsp file for the experiment. There are only a few methods of Model, which are only suitable for storing data, simplifying the novice's operation and understanding of Model objects; ModelMap inherits LinkedMap. In addition to implementing some of its own methods, ModelMap also inherits the methods and features of LinkedMap; ModelAndView can store data, set the returned logical view, and control the jump of the display layer.

<%--
  Created by IntelliJ IDEA.
  User: 13304
  Date: 2021/8/31
  Time: 11:47
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
modelMap: ${modelMap} <br>
model: ${model}<br>
modelAndView: ${modelAndView}<br>
</body>
</html>

1-1 return to ModelAndView

The first is the classic way we use ModelAndView:

  @RequestMapping("/getUserInfoByModelAndView")
    public ModelAndView getUserInfoByModelAndView(@RequestParam("name") String username,@RequestParam("age") String age) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("modelAndView", "username: "+username + " age: "+age+" "+LocalDate.now());
        modelAndView.setViewName("userInfo");
        return modelAndView;
    }

The requested url is:

http://localhost:8081/base_annotation/user/getUserInfoByModelAndView?name=tml&age=30

The returned results are:

1-2 return String type - through Model

Second, we use the Model interface, and the view does not need to be set:

 @RequestMapping("/getUserInfoByStringModel")
    public String getUserInfo(@RequestParam("name") String username,@RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username"+username + "age"+age+LocalDate.now());
        return "userInfo";
    }

The requested url is:

http://localhost:8081/base_annotation/user/getUserInfoByStringModel?name=tml&age=30

The returned results are:

1-3 return String type - through ModelMap

Finally, we use ModelMap:

http://localhost:8081/base_annotation/user/getUserInfoByStringModelMap?name=tml&age=30

The requested url is:

@RequestMapping("/getUserInfoByStringModelMap")
    public String getUserInfoByStringModelMap(@RequestParam("name") String username,@RequestParam("age") String age, Model model) {
        model.addAttribute("model", "username: "+username + " age: "+age+" "+LocalDate.now());
        return "userInfo";
    }

The returned results are:

2 return to page Jump mode

When learning Servlet, we know that there are two types of page Jump: request forwarding and redirection. The difference between the two will not be repeated. It is described in detail in another blog: [Java Web programming 8] deeply understand the common objects of servlets , how was it implemented before?

  @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        //Writing of request forwarding
        RequestDispatcher requestDispatcher=request.getRequestDispatcher("/first.jsp");
        requestDispatcher.forward(request,response);
     
        //How to write redirection
        response.sendRedirect("/first.jsp");
    }

So how is Spring MVC implemented?

2-1 return to the specified page

First, let's look at a standard return to the specified page:

@RequestMapping("/getUserInfoByStringModel")
    public String getUserInfo(@RequestParam("name") String username, @RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: " + username + " age: " + age + " " + LocalDate.now());
        return "userInfo";
    }

2-2 forward request to new page

Then let's look at how request forwarding is implemented:

 @RequestMapping("requestDispatch")
    public String requestDispatch(@RequestParam("name") String username,@RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: "+username + " age: "+age+" "+LocalDate.now());
        return "forward:/index.jsp";
    }

The requested url is:

http://localhost:8081/base_annotation/user/requestDispatch?name=tml&age=30

The returned result is:

You can see that the shared object is kept within the scope of the request.

2-3 redirect URL to new page

Let's look at how redirection is implemented:

 @RequestMapping("requestRedirect")
    public String requestRedirect(@RequestParam("name") String username,@RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: "+username + " age: "+age+" "+LocalDate.now());
        return "redirect:/index.jsp";
    }

The requested url is:

http://localhost:8081/base_annotation/user/requestRedirect?name=tml&age=30

The returned result is:

You can see that the shared object was lost due to two requests.

Project structure and code list

The project structure after the completion of the overall practice is shown in the figure below:

The core practice code is as follows: UserController:

package com.example.base_annotation.controller;

import com.example.base_annotation.controller.dto.Account;
import com.example.base_annotation.controller.dto.User;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Date;

@Controller
@RequestMapping("/user")
public class UserController {


    @RequestMapping("requestDispatch")
    public String requestDispatch(@RequestParam("name") String username, @RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: " + username + " age: " + age + " " + LocalDate.now());
        return "forward:/index.jsp";
    }

    @RequestMapping("requestRedirect")
    public String requestRedirect(@RequestParam("name") String username, @RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: " + username + " age: " + age + " " + LocalDate.now());
        return "redirect:/index.jsp";
    }

    @RequestMapping("/getUserInfoByStringModel")
    public String getUserInfo(@RequestParam("name") String username, @RequestParam("age") String age, ModelMap modelMap) {
        modelMap.addAttribute("modelMap", "username: " + username + " age: " + age + " " + LocalDate.now());
        return "userInfo";
    }

    @RequestMapping("/getUserInfoByStringModelMap")
    public String getUserInfoByStringModelMap(@RequestParam("name") String username, @RequestParam("age") String age, Model model) {
        model.addAttribute("model", "username: " + username + " age: " + age + " " + LocalDate.now());
        return "userInfo";
    }

    @RequestMapping("/getUserInfoByModelAndView")
    public ModelAndView getUserInfoByModelAndView(@RequestParam("name") String username, @RequestParam("age") String age) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("modelAndView", "username: " + username + " age: " + age + " " + LocalDate.now());
        modelAndView.setViewName("userInfo");
        return modelAndView;
    }


    @RequestMapping("/servletParam")
    public void servletParam(HttpServletRequest request, HttpServletResponse responser, HttpSession session) {
        System.out.println(request.getParameter("username"));
        System.out.println(request);
        System.out.println(responser);
        System.out.println(session);
    }


    @RequestMapping("/simpleParamMatch")
    public void simpleParamMatch(String username, int age) {
        System.out.println("username:" + username);
        System.out.println("age:" + age);

    }

    @RequestMapping("/simpleParamDiff")
    public void simpleParamDiff(@RequestParam("name") String username, @RequestParam(value = "age") Integer age) {
        System.out.println("username:" + username);
        System.out.println("age:" + age);
    }


    @RequestMapping("/restfulParam/{id}")
    public void restfulParam(@PathVariable("id") Long id) {
        System.out.println("restfulParam Parameter is" + id);
    }


    @RequestMapping("/arrayParam")
    public void arrayParam(Long[] accountIds) {
        System.out.println(Arrays.asList(accountIds));
    }

    @RequestMapping("/listParam")
    public void listParam(User user) {
        System.out.println(user.getAccountIds());
    }


    @RequestMapping("/dateParam")
    public void dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
        System.out.println(date);
    }


    @RequestMapping("/javaBeanParam")
    public void javaBeanParam(User user) {
        System.out.println(user);
    }


    @RequestMapping("/javaBeanModelAttributeParam")
    public String javaBeanModelAttributeParam(@ModelAttribute("userAllies") User user) {
        System.out.println(user);
        return "userInfo";
    }


    @RequestMapping("/headerInfo")
    public void headerInfo(@RequestHeader("User-Agent") String userAgent, @CookieValue("JSESSIONID") String cookieName) {
        System.out.println("User-Agent:" + userAgent);
        System.out.println("cookieName" + cookieName);
    }


    @InitBinder("user")
    public void initBinderUserType(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("user.");
    }

    @InitBinder("account")
    public void initBinderCatType(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("account.");
    }

    @RequestMapping("/multiModelParam")
    public void multiModelParam(User user, Account account) {
        System.out.println(user);
        System.out.println(account);
    }

    @RequestMapping(value = "/javaBeanParamPost", method = {RequestMethod.POST})
    public void javaBeanParamPost(User user) {
        System.out.println(user);
    }


}

To sum up

The overall in-depth practice of the Spring MVC Controller is actually an analog implementation compared with the previous implementation of Servlet. In this way, we can see the convenience of the framework. Why is Spring MVC compatible with Spring? Remember the Controller when learning Spring annotation development? It's a kind of component. Looking at the framework from a practical perspective, on the whole, a Controller is like a small module. The various methods of this small module are the implementation of the module, which is also in line with the reality. Each method is like doGet or doPost in each Servlet before. Now we don't need to write a pile of servlets for a module, just replace it with a mapping method. Then look at the input parameters. The input parameters can be processed by a unified filter to solve the problem of garbled code as a whole, and are well compatible with various input parameter types. Various annotations replace our various data conversion implementations, which saves us a lot of worry. It is also convenient to display and jump back to the result page. Using the preset Model, there is no need to put the object into the scope, Because the framework has done well for us, and the request forwarding and redirection only add a word to the returned string. This is the charm of the framework.

Keywords: Spring RESTful Spring MVC mvc

Added by HaLo2FrEeEk on Wed, 01 Sep 2021 21:45:32 +0300