Saturday, 24 June 2023

Exception Handling and Validation in Spring Boot

Handling exceptions is an important part of building a robust application. This tutorial I will illustrate how to implement Exception Handling and Validation with Spring for a REST API
please follow the steps and download the code below. 



-------------------------------- UserController -------------------------------------------

package com.ramsis.restApi.controller;

import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ramsis.restApi.payloads.ApiResponse;
import com.ramsis.restApi.payloads.UserDto;
import com.ramsis.restApi.services.UserService;

@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;

@PostMapping("/")
public ResponseEntity<UserDto>addUser(@Valid @RequestBody UserDto userDto)
{
UserDto createUser=this.userService.createuser(userDto);
return new ResponseEntity<>(createUser,HttpStatus.CREATED);
}

@PutMapping("/{userId}")
public ResponseEntity<UserDto> updateUser(@Valid @RequestBody UserDto userDto , @PathVariable("userId")  Integer userId){

UserDto updateUser=this.userService.updateUser(userDto, userId);
return ResponseEntity.ok(updateUser);
}

@DeleteMapping("/{userId}")
public ResponseEntity<ApiResponse>deletedUser(@PathVariable("userId") Integer userId)
{
this.userService.deleteUser(userId);
return new ResponseEntity<ApiResponse>(new ApiResponse("user delete succsfully",String.valueOf(HttpStatus.OK.value()),true),HttpStatus.OK);

}
@GetMapping("/")
public ResponseEntity<List<UserDto>> getAllUsers()
{
return ResponseEntity.ok(this.userService.getAllUsers());
}

@GetMapping("/{userId}")
public ResponseEntity<UserDto> getAllUsers(@PathVariable("userId") Integer userId)
{
return ResponseEntity.ok(this.userService.getByuserId(userId));
}

}







--------------------------------- GlobelExceptionHandler --------------------------------

package com.ramsis.restApi.exception;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.ramsis.restApi.payloads.ApiResponse;

@RestControllerAdvice
public class GlobelExceptionHandler {
@ExceptionHandler(ResourcesNotoundExcpetion.class)
public ResponseEntity<ApiResponse> resourceNotfoiunexceptionHandler(ResourcesNotoundExcpetion ex)
{
String message=ex.getMessage();
ApiResponse apiResponse=new ApiResponse(message,String.valueOf(HttpStatus.NOT_FOUND.value()),false);
return new ResponseEntity<ApiResponse>(apiResponse,HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleMethodArgsNotValidateExpection(MethodArgumentNotValidException ex)
{
Map<String, String> resp=new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error->{
String filedName=((FieldError)error).getField();
String messsage=error.getDefaultMessage();
resp.put(filedName, messsage);
});
return new ResponseEntity<Map<String,String>>(resp,HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ApiException.class)
public ResponseEntity<ApiResponse> handleApiException(ApiException ex)
{
String message=ex.getMessage();
ApiResponse apiResponse=new ApiResponse(message,String.valueOf(HttpStatus.BAD_REQUEST.value()),true);
return new ResponseEntity<ApiResponse>(apiResponse,HttpStatus.BAD_REQUEST);
}

}

----------------------------------- ResourcesNotoundExcpetion -----------------------------------

package com.ramsis.restApi.exception;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ResourcesNotoundExcpetion  extends RuntimeException{

String resourceName;
String feildName;
long feildValue;
public ResourcesNotoundExcpetion(String resourceName, String feildName, long feildValue) {
super(String.format("%s not found with his name %s :%s", resourceName,feildName,feildValue));
this.resourceName = resourceName;
this.feildName = feildName;
this.feildValue = feildValue;
}
}

------------------------ ApiResponse--------------------------

package com.ramsis.restApi.payloads;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
private String  meaassge;
private String  status;
private boolean success;

}

--------------------------- userDto --------------------------------

package com.ramsis.restApi.payloads;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Setter
@Getter
public class UserDto {
private int userId;
@NotEmpty
@Size(min = 4,message ="username must be minimum 4 character")
private String name;
@Email(message = "your email id wrong")
private String email;
@NotEmpty
//@Size(min = 3,max = 15, message = "password must be minimum 3 and max 12 character")
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#&()–[{}]:;',?/*~$^+=<>]).{5,15}$", message = ""
+ "Password must contain at least one digit [0-9]."
+ "Password must contain at least one lowercase Latin character [a-z]."
+ "Password must contain at least one uppercase Latin character [A-Z]."
+ "Password must contain at least one special character like ! @ # & ( )."
+ "Password must contain a length of at least 5 characters and a maximum of 15 characters.")
private String password;
private String region;
@Size(min = 2,message ="department must be minimum 2 character")
private String department;


}

--------------------------------- userRepository ------------------------------------

package com.ramsis.restApi.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.ramsis.restApi.entities.User;

public interface UserRepository  extends JpaRepository<User, Integer>{
  Optional<User> findByEmail(String email);
}

---------------------------- userService --------------------------

package com.ramsis.restApi.services;

import java.util.List;

import com.ramsis.restApi.payloads.UserDto;

public interface UserService {
UserDto registerNewUser(UserDto user);
UserDto createuser(UserDto user);
UserDto updateUser(UserDto user, Integer userId);
UserDto getByuserId(Integer userId);
List<UserDto> getAllUsers();
void deleteUser(Integer userId);

}

--------------------------- userServiceImpl --------------------

package com.ramsis.restApi.services;

import java.util.List;
import java.util.stream.Collectors;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ramsis.restApi.entities.User;
import com.ramsis.restApi.exception.ResourcesNotoundExcpetion;
import com.ramsis.restApi.payloads.UserDto;
import com.ramsis.restApi.repository.UserRepository;

@Service
public class UserServiceImpl  implements UserService{
@Autowired
private ModelMapper mapper;
@Autowired
private UserRepository userRepository;

@Override
public UserDto createuser(UserDto userDto) {
User user=this.dtoToUser(userDto);
User saveUsers=this.userRepository.save(user);
return this.userToDto(saveUsers);
}

@Override
public UserDto updateUser(UserDto userDto, Integer userId) {
User user=this.userRepository.findById(userId)
.orElseThrow(()-> new ResourcesNotoundExcpetion("User","Id",userId));
user.setName(userDto.getName());
user.setEmail(userDto.getEmail());
user.setPassword(userDto.getPassword());
user.setRegion(userDto.getRegion());
user.setDepartment(userDto.getDepartment());
User updateUser=this.userRepository.save(user);
UserDto userDto1= this.userToDto(updateUser);
return userDto1;
}

@Override
public UserDto getByuserId(Integer userId) {
User user=this.userRepository.findById(userId)
.orElseThrow(()-> new ResourcesNotoundExcpetion("User","Id",userId));
return this.userToDto(user);
}

@Override
public List<UserDto> getAllUsers() {

List<User> users=this.userRepository.findAll();
List<UserDto> userDtos=users.stream().map(user->this.userToDto(user)).collect(Collectors.toList());
return userDtos;
}

@Override
public void deleteUser(Integer userId) {
User user =this.userRepository.findById(userId).orElseThrow(()-> new ResourcesNotoundExcpetion("User","Id",userId));
this.userRepository.delete(user);
}
@Override
public UserDto registerNewUser(UserDto userDto) {
User user = this.mapper.map(userDto, User.class);
//endoed the password
user.setPassword(user.getPassword());
User newUser=this.userRepository.save(user);
return this.mapper.map(newUser, UserDto.class);
}

private User dtoToUser(UserDto userDto)
{
User user=this.mapper.map(userDto, User.class);
// user.setUserId(userDto.getUserId());
// user.setName(userDto.getName());
// user.setEmail(userDto.getEmail());
// user.setDepartment(userDto.getDepartment());
// user.setPassword(userDto.getPassword());
// user.setRegion(userDto.getRegion());
return user;
}
public UserDto userToDto (User user)
{
UserDto userDto=this.mapper.map(user, UserDto.class);
// userDto.setUserId(user.getUserId());
// userDto.setName(user.getName());
// userDto.setEmail(user.getEmail());
// userDto.setDepartment(user.getDepartment());
// userDto.setPassword(user.getPassword());
// userDto.setRegion(user.getRegion());
return userDto;
}

}

------------------------- pom.xml ----------------------------

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ramsis</groupId>
<artifactId>restApiWithValidationErrorHandling</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>restApiWithValidationErrorHandling</name>
<description>Demo project for Spring boot Exception Handling and validation</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>


------------------------------ application.properties -------------------------

spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=cctns@123

spring.jpa.show-sql = true
spring.jpa.properties.hibernate.format_sql=true

logging.level.org.hibernate.type.descriptor.sql=trace

spring.jpa.hibernate.ddl-auto = update
#spring.jpa.hibernate.ddl-auto = none





Wednesday, 14 June 2023

SpringBoot Pagination With Bootstrap , jquery and Ajax

If we have a large dataset and we want to present it to the user in smaller chunks, pagination is helpful solution. So in the tutorial, I am showing how to create "SpringBoot Ajax Pagination Example" use JQuery Ajax and Bootstrap to build a table solution for pagination with SpringBoot RestAPIs examples.

The Spring Data JPA provides a useful Pageable interface that provides number of methods for building pagination in the UI side. Even you can sort the data returned by the JPA query using the methods provided in this interface API.





--------------------------------- pom.xml---------------------------------


<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.roytuts</groupId>
<artifactId>springboot-jpa-pageable</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

---------------------------------- ProductRestController -------------------------------

package com.ramsis.springboot.jpa.pageable.rest.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.ramsis.springboot.jpa.pageable.dto.ProductDto;
import com.ramsis.springboot.jpa.pageable.service.ProductService;

@RestController
@CrossOrigin(origins = "*")
public class ProductRestController {

@Autowired
private ProductService productService;

@GetMapping("/product")
public Page<ProductDto> products(Pageable pageable) {
return productService.products(pageable);
}

}


-------------------------------- Product Entity------------------------

package com.ramsis.springboot.jpa.pageable.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;

@Column
private String name;

@Column
private String prise;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPrise() {
return prise;
}

public void setPrise(String prise) {
this.prise = prise;
}

}

---------------------------------- ProductDto ----------------------------

package com.ramsis.springboot.jpa.pageable.dto;

public class ProductDto {

private Integer id;
private String name;
private String prise;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrise() {
return prise;
}
public void setPrise(String prise) {
this.prise = prise;
}

}

------------------------ EntityDtoConverter -------------------------

package com.ramsis.springboot.jpa.pageable.converter;


import com.ramsis.springboot.jpa.pageable.dto.ProductDto;

import com.ramsis.springboot.jpa.pageable.entity.Product;


public final class EntityDtoConverter {


private EntityDtoConverter() {

}


public static ProductDto entityToDto(Product product) {

ProductDto productDto = new ProductDto();


productDto.setId(product.getId());

productDto.setName(product.getName());

productDto.setPrise(product.getPrise());

return productDto;

}


public static Product dtoToEntity(ProductDto productDto) {

Product product = new Product();


product.setId(productDto.getId());

product.setName(productDto.getName());

product.setPrise(productDto.getPrise());

return product;

}


}

---------------------------- ProductService --------------------------

package com.ramsis.springboot.jpa.pageable.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.ramsis.springboot.jpa.pageable.converter.EntityDtoConverter;
import com.ramsis.springboot.jpa.pageable.dto.ProductDto;
import com.ramsis.springboot.jpa.pageable.entity.Product;
import com.ramsis.springboot.jpa.pageable.repository.ProductRepository;

@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

public Page<ProductDto> products(Pageable pageable) {
Page<Product> products = productRepository.findAll(pageable);

Page<ProductDto> pages = products.map(entity -> {
ProductDto dto = EntityDtoConverter.entityToDto(entity);
return dto;
});

return pages;
}

}


------------------------------- ProductRepository -----------------------------

package com.ramsis.springboot.jpa.pageable.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.ramsis.springboot.jpa.pageable.entity.Product;

public interface ProductRepository extends JpaRepository<Product, Integer> {

}

----------------------------------- table-pagination.js ---------------------------

$(document).ready(function() {

let totalPages = 1;

function fetchNotes(startPage) {

//console.log('startPage: ' +startPage);

/**

* get data from Backend's REST API

*/

$.ajax({

type : "GET",

url : "http://localhost:8080/product",

data: {

page: startPage,

size: 10

},

success: function(response){

$('#noteTable tbody').empty();

// add table rows

$.each(response.content, (i, product) => {

let noteRow = '<tr>' +

'<td>' + product.id + '</td>' +

'<td>' + product.name + '</td>' +

'<td>' + product.prise + '</td>' +

'</tr>';

$('#noteTable tbody').append(noteRow);

});

if ($('ul.pagination li').length - 2 != response.totalPages){

// build pagination list at the first time loading

$('ul.pagination').empty();

buildPagination(response);

}

},

error : function(e) {

alert("ERROR: ", e);

console.log("ERROR: ", e);

}

});

}

function buildPagination(response) {

totalPages = response.totalPages;

var pageNumber = response.pageable.pageNumber;

var numLinks = 10;

// print 'previous' link only if not on page one

var first = '';

var prev = '';

if (pageNumber > 0) {

if(pageNumber !== 0) {

first = '<li class="page-item"><a class="page-link">« First</a></li>';

}

prev = '<li class="page-item"><a class="page-link">‹ Prev</a></li>';

} else {

prev = ''; // on the page one, don't show 'previous' link

first = ''; // nor 'first page' link

}

// print 'next' link only if not on the last page

var next = '';

var last = '';

if (pageNumber < totalPages) {

if(pageNumber !== totalPages - 1) {

next = '<li class="page-item"><a class="page-link">Next ›</a></li>';

last = '<li class="page-item"><a class="page-link">Last »</a></li>';

}

} else {

next = ''; // on the last page, don't show 'next' link

last = ''; // nor 'last page' link

}

var start = pageNumber - (pageNumber % numLinks) + 1;

var end = start + numLinks - 1;

end = Math.min(totalPages, end);

var pagingLink = '';

for (var i = start; i <= end; i++) {

if (i == pageNumber + 1) {

pagingLink += '<li class="page-item active"><a class="page-link"> ' + i + ' </a></li>'; // no need to create a link to current page

} else {

pagingLink += '<li class="page-item"><a class="page-link"> ' + i + ' </a></li>';

}

}

// return the page navigation link

pagingLink = first + prev + pagingLink + next + last;

$("ul.pagination").append(pagingLink);

}

$(document).on("click", "ul.pagination li a", function() {

var data = $(this).attr('data');

let val = $(this).text();

console.log('val: ' + val);

// click on the NEXT tag

if(val.toUpperCase() === "« FIRST") {

let currentActive = $("li.active");

fetchNotes(0);

$("li.active").removeClass("active");

// add .active to next-pagination li

currentActive.next().addClass("active");

} else if(val.toUpperCase() === "LAST »") {

fetchNotes(totalPages - 1);

$("li.active").removeClass("active");

// add .active to next-pagination li

currentActive.next().addClass("active");

} else if(val.toUpperCase() === "NEXT ›") {

let activeValue = parseInt($("ul.pagination li.active").text());

if(activeValue < totalPages){

let currentActive = $("li.active");

startPage = activeValue;

fetchNotes(startPage);

// remove .active class for the old li tag

$("li.active").removeClass("active");

// add .active to next-pagination li

currentActive.next().addClass("active");

}

} else if(val.toUpperCase() === "‹ PREV") {

let activeValue = parseInt($("ul.pagination li.active").text());

if(activeValue > 1) {

// get the previous page

startPage = activeValue - 2;

fetchNotes(startPage);

let currentActive = $("li.active");

currentActive.removeClass("active");

// add .active to previous-pagination li

currentActive.prev().addClass("active");

}

} else {

startPage = parseInt(val - 1);

fetchNotes(startPage);

// add focus to the li tag

$("li.active").removeClass("active");

$(this).parent().addClass("active");

//$(this).addClass("active");

}

});

(function(){

// get first-page at initial time

fetchNotes(0);

})();

});


------------------------------------------------------ index.html -------------------------------------------

<!DOCTYPE html>

<html lang="en">

<head>

<title>Bootstrap Ajax SpringBoot Pagination</title>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">

</head>

<body>

<div class="container">

<div class="row">

<div class="col-sm-7" style="background-color: #f6e796; margin: 5px; padding: 5px; border-radius: 5px; margin: auto;">

<div class="alert alert-warning">

<h4>SpringBoot Pagination + Bootstrap + jquery + Ajax </h4>

</div>

<table id="noteTable" class="table table-hover table-sm">

<thead class="thead-dark">

<tr>

<th>Id</th>

<th>Name</th>

<th>Prise</th>

</tr>

</thead>

<tbody>

</tbody>

</table>

<ul class="pagination justify-content-center" style="margin:10px 0; cursor: pointer;">

</ul>

</div>

</div>

</div>

<script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.10.2/umd/popper.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>

<script src="table-pagination.js"></script>

</body>

</html>