How Can we use Policy-enforcer dynamically in Java Springboot? - spring-boot

Saurav Chaurasia
Fri, May 7, 5:00 PM (20 hours ago)
to me
Like I am able to use the static Policy enforcer by providing the path and method and resource into the application.properties. But in realtime application we will be having N number of roles and we will be having N number of API which we will be provide in the Resource, Policies and Permissions in KeyCloak. In Future if we want some more roles to be added into the keycloak and new resources will be added with necessary permission. We wont be coming back to our springboot to change the code for resource and roles. All the permission checking should be dynamic to the spring boot how can we do that please help me out.
The static one which I am using currently is working fine for few roles. We are adding more roles into the keyCloak.
Below is the Static code.
application.properties
server.port = 8090
keycloak.realm=university
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.ssl-required=external
keycloak.resource=course-management
keycloak.bearer-only=true
keycloak.credentials.secret=a5df9621-73c9-4e0e-9d7a-97e9c692a930
keycloak.securityConstraints[0].authRoles[0]=teacher
keycloak.securityConstraints[0].authRoles[1]=ta
keycloak.securityConstraints[0].authRoles[2]=student
keycloak.securityConstraints[0].authRoles[3]=parent
keycloak.securityConstraints[0].securityCollections[0].name=course managment
keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /courses/get/*
#keycloak.policy-enforcer-config.lazy-load-paths=true
keycloak.policy-enforcer-config.paths[0].path=/courses/get/*
keycloak.policy-enforcer-config.paths[0].methods[0].method=GET
keycloak.policy-enforcer-config.paths[0].methods[0].scopes[0]=view
keycloak.policy-enforcer-config.paths[0].methods[1].method=DELETE
keycloak.policy-enforcer-config.paths[0].methods[1].scopes[0]=delete
Configuration.class
package com.lantana.school.course.coursemanagment.security;
`import java.util.List;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.idm.authorization.Permission;
public class Identity {
private final KeycloakSecurityContext securityContext;
public Identity(KeycloakSecurityContext securityContext) {
this.securityContext = securityContext;
}
/**
* An example on how you can use the {#link org.keycloak.AuthorizationContext} to check for permissions granted by Keycloak for a particular user.
*
* #param name the name of the resource
* #return true if user has was granted with a permission for the given resource. Otherwise, false.
*/
public boolean hasResourcePermission(String name) {
System.out.println("Permission: "+getAuthorizationContext().hasResourcePermission(name));
return getAuthorizationContext().hasResourcePermission(name);
}
/**
* An example on how you can use {#link KeycloakSecurityContext} to obtain information about user's identity.
*
* #return the user name
*/
public String getName() {
System.out.println("UserName: "+securityContext.getIdToken().getPreferredUsername());
return securityContext.getIdToken().getPreferredUsername();
}
/**
* An example on how you can use the {#link org.keycloak.AuthorizationContext} to obtain all permissions granted for a particular user.
*
* #return
*/
public List<Permission> getPermissions() {
System.out.println("Permission 2: "+getAuthorizationContext().getPermissions());
return getAuthorizationContext().getPermissions();
}
/**
* Returns a {#link AuthorizationContext} instance holding all permissions granted for an user. The instance is build based on
* the permissions returned by Keycloak. For this particular application, we use the Entitlement API to obtain permissions for every single
* resource on the server.
*
* #return
*/
private AuthorizationContext getAuthorizationContext() {
System.out.println("getAuthorizationContext: "+ securityContext.getAuthorizationContext());
return securityContext.getAuthorizationContext();
}
}`
Controller.class
package com.lantana.school.course.coursemanagment.services;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
//import org.springframework.hateoas.Link;
//import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
//import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.lantana.school.course.coursemanagment.security.Identity;
#RestController
public class CourseController{
#Autowired
private HttpServletRequest request;
#Autowired
private CourseService couseService;
#Autowired
private hateo hatoeslink;
List<String> rol=new ArrayList<String>();
//
// #GetMapping(value = "/courses/api")
// public String generateApi(#RequestHeader("Authorization") String token){
////// rol.clear();
////// List headers = token.;
// System.out.println("Token: "+token);
////// System.out.println("Role Controller: "+rol);
////// rol=couseService.getRole(token);
////// List<?> link=new ArrayList<>();
//////
// return new String("Role Fetched");
// }
#GetMapping(value = "/courses/get/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public EntityModel<Course> getCourse(#PathVariable("id") long id, Model model,#RequestHeader("Authorization") String token) throws JsonProcessingException {
configCommonAttributes(model);
rol=couseService.getRole(token);
System.out.println("GetRole: "+rol);
Course course = couseService.getCourse(id);
hatoeslink.hateoLink(rol, id, model, token);
return EntityModel.of(course);
}
#DeleteMapping("/courses/delete/{id}")
public EntityModel<Course> deleteStudent(#PathVariable long id) {
System.out.println("calling delete operation");
//
Course course = couseService.getCourse(id);
couseService.deleteById(id);
return EntityModel.of(course);
}
#PostMapping("/courses")
public ResponseEntity<Course> createCourse(#RequestBody Course course) {
Course savedCourse = couseService.addCourse(course);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedCourse.getCode()).toUri();
return ResponseEntity.created(location).build();
}
private void configCommonAttributes(Model model) {
model.addAttribute("identity", new Identity(getKeycloakSecurityContext()));
}
private KeycloakSecurityContext getKeycloakSecurityContext() {
return (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
}
}
Service.class
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
//import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
#Component
public class CourseService {
public static final Map<Long, Course> courseMap = new LinkedHashMap<Long, Course>();
static {
Course cs2001 = new Course("CS2001", "Mathematical Foundations of Computing", "introduction", "term1");
Course cs2002 = new Course("CS2002", "Computer Organization and Systems", "introduction", "term1");
Course cs2003 = new Course("CS2003", "Data Management and Data Systems", "introduction", "term2");
Course cs2004 = new Course("CS2004", "Introduction to Computer Graphics and Imaging", "introduction", "term3");
Course cs2005 = new Course("CS2005", "Design and Analysis of Algorithms", "introduction", "term4");
Course cs2006 = new Course("CS2006", "Analysis of Networks", "introduction", "term4");
courseMap.put(cs2001.getId(), cs2001);
courseMap.put(cs2002.getId(), cs2002);
courseMap.put(cs2003.getId(), cs2003);
courseMap.put(cs2004.getId(), cs2004);
courseMap.put(cs2005.getId(), cs2005);
courseMap.put(cs2006.getId(), cs2006);
}
public Course getCourse(Long id) {
return courseMap.get(id);
}
public Course addCourse(Course course) {
courseMap.put(course.getId(), course);
return course;
}
public void deleteById(long id) {
courseMap.remove(id);
// return id+"Deleted Successfully";
}
public List<String> getRole(String token) {
String[] payload=token.split("\\.");
byte[] bytes = Base64.decodeBase64(payload[1]);
String decodedString = new String(bytes, StandardCharsets.UTF_8);
// System.out.println("Decoded: " + decodedString);
JSONObject jo=new JSONObject(decodedString);
JSONObject obj=jo.getJSONObject("realm_access");
JSONArray jArray=obj.getJSONArray("roles");
System.out.println(obj);
System.out.println(jArray);
List<String> role=new ArrayList();
for(int i=0;i<jArray.length();i++)
{ String ro=(String) jArray.get(i);
System.out.println(jArray.get(i));
role.add(ro);
}
// List action=new ArrayList();
// Map roles=((Map)jo.get("realm_access"));
// Iterator<Map.Entry> itr1=roles.entrySet().iterator();
// while(itr1.hasNext())
// {
// Map.Entry pair=itr1.next();
// System.out.println(pair);
// }
return role;
}
}
Model.class
package com.lantana.school.course.coursemanagment.services;
import org.springframework.hateoas.RepresentationModel;
public class Course extends RepresentationModel {
private static long nextID = 1000;
public Course(String code, String name, String modules, String enrollmentTerm) {
super();
this.id = nextID++;
this.code = code;
this.name = name;
this.modules = modules;
this.enrollmentTerm = enrollmentTerm;
}
Long id;
String code;
String name;
String modules;
String enrollmentTerm;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getModules() {
return modules;
}
public void setModules(String modules) {
this.modules = modules;
}
public String getEnrollmentTerm() {
return enrollmentTerm;
}
public void setEnrollmentTerm(String enrollmentTerm) {
this.enrollmentTerm = enrollmentTerm;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
hateos Custom class
package com.lantana.school.course.coursemanagment.services;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import com.fasterxml.jackson.core.JsonProcessingException;
#Component
public class hateo {
#Autowired
private CourseService couseService;
public EntityModel<Course> hateoLink(List<String> role,long id,Model model,String token)
{ Course course = couseService.getCourse(id);
course.removeLinks();
role.stream().forEach(action ->{
if(action.equalsIgnoreCase("teacher"))
{
// course.removeLinks();
course.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CourseController.class).createCourse(course)).withRel("add"));
}
if(action.equalsIgnoreCase("student"))
{
try {
course.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CourseController.class).getCourse(id, model,token)).withRel("view"));
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(action.equalsIgnoreCase("parent"))
{
course.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CourseController.class).deleteStudent(id)).withRel("delete"));
}
});
return EntityModel.of(course);
}
}

If you are using Keycloak's Authorization Services, (which you are if you're using PEP), you shouldn't have to define roles in your spring boot keycloak-configuration. Notice how the roles aren't part of the policy-enforcer-config. If you just remove keycloak.securityConstraints[0].authRoles, and check for roles in your Policies over at the Keycloak server instead, you should be good.
As for the resource paths, I see that you have commented out keycloak.policy-enforcer-config.lazy-load-paths. With this together with http-method-as-scope, you shouldn't have to provide any additional configuration regarding your resources since the Keycloak-adapter will automatically grab that from annotations like #PostMapping("/courses"). (You would have to name your scopes in Keycloak after HTTP-methods for this to work). In this case, you're really using the default PEP-configuration and shouldn't have to specify anything else than enabling policy enforcing for your application.

Related

How to get osgi configuration values to a servlet

This is my OSGI configuration file which is having three names. I want to read these values in a servlet and sort them Alphabetically and send that response to a ajax to display in a custom component AEM.
package com.demo.training.core.services.impl;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import com.demo.training.core.services.MyProjectServ;
#Component(service=MyProjectServ.class,immediate = true)
#Designate(ocd= MyProject.ServiceConfig.class)
public class MyProject implements MyProjectServ {
#ObjectClassDefinition(name="My-Project OSGI",
description="Demo OSGI configuration")
public #interface ServiceConfig {
#AttributeDefinition(
name="Name1",
description="Add First name",
type = AttributeType.STRING
)
public String Name1() default "Abhinay";
#AttributeDefinition(
name="Name2",
description="Add second name ",
type = AttributeType.STRING
)
public String Name2() default "Pavan";
#AttributeDefinition(
name="Name3",
description="Add third name ",
type = AttributeType.STRING )
public String Name3() default "Ram";
}
private String Name1;
private String Name2;
private String Name3;
#Activate
protected void activate(ServiceConfig myconfig) {
Name1=myconfig.Name1();
Name2=myconfig.Name2();
Name3=myconfig.Name3();
}
#Override
public String getNAME1() {
return Name1; }
#Override
public String getNAME2() {
return Name2; }
#Override
public String getNAME3() {
return Name3;
} }
'''This is my Servlet code , I have wrote multiple resp.getwriter() to see upto which line it is working. It is working upto response named a1(i.e below dictionary command). Could anyone please help to get values from osgi configuration to this servlet ?
package com.demo.training.core.servlets;
import java.io.IOException;
import java.util.Arrays;
import java.util.Dictionary;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
#Component(service=Servlet.class,
property={
Constants.SERVICE_DESCRIPTION + "=Practice Servlet",
"sling.servlet.methods=" + HttpConstants.METHOD_GET,
"sling.servlet.methods=" + HttpConstants.METHOD_POST,
"sling.servlet.paths=/bin/myproject",
"sling.servlet.extensions=" + "txt"
})
public class MyProjectServlet extends SlingAllMethodsServlet {/**
*
*/
private static final long serialVersionUID = 1L;
#Reference
private ConfigurationAdmin MYPROJECT_CONFIG;
private static final String MY_PROJECT="com.demo.training.core.services.impl.MyProject";
#Override
protected void doGet(final SlingHttpServletRequest req,
final SlingHttpServletResponse resp) throws ServletException, IOException {
Configuration My_Servlet=MYPROJECT_CONFIG.getConfiguration(MY_PROJECT);
Dictionary<String,Object> property =My_Servlet.getProperties();
resp.getWriter().write("a1");
String first=property.get("Name1").toString();
String second=property.get("Name2").toString();
String third=property.get("Name3").toString();
resp.getWriter().write("a2");
resp.getWriter().write(first);
resp.getWriter().write("a3");
String[] myArray = new String[]{first,second,third};
Arrays.sort(myArray);
String js=myArray.toString();
resp.getWriter().write(js);
}
}
You try to use the #reference annotation for your service. If this object null you can use the ResourceResolverFactoy. This object does always exists, else you have your instance has a serious problem:
Map<String, Object> serviceParameter = new HashMap<>();
serviceParameter.put(ResourceResolverFactory.SUBSERVICE, Put the name name of your service here);
return resolverFactory.getServiceResourceResolver(serviceParameter);
In Servlet use annotation #reference to inject the ResourceResolverFactoy:
#Reference
private ResourceResolverFactory ...;
By the way, have an eye to java code convetions. Method names starts always with smal letters even in service configs.

How can I capture a value that comes through POST to execute a procedure stored in Spring Boot

I am working with Spring Boot, in which I am relatively new, and in this case I am doing a database validation through a Stored Procedue, which I could already solve, the reality is that until now I had done the tests sent the parameter of entry (a mobile number) by GET, but it is required for project reasons, send the parameter through POST, that is to say in a Body with the method:
Method Get
With a Body using the POST Method:
Request
{
"movil":"04242374781";
}
Reponse:
{
"result": "Cliente no encontrado",
"code": "NA22003"
}
mobile is an attribute of the database where the Stored Procedure is executed, for this case it is only necessary to pass that parameter to execute the SP, which returns a response that is not the same object of the database in which it is mobile, then you will see it in the code.
I understand that you can send the parameter for consultation with POST, but in my case try to guide me according to what I got on the internet, but I got an error:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]
My Code
Main class
package com.app.validacion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My controller
package com.app.validacion.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.app.validacion.dao.DriverBonificadosRepository;
import com.app.validacion.entity.RespuestaVo;
#RestController
public class DriverBonificadosController {
#Autowired // Inyeccion de Dependecia, en este caso del Respository
private DriverBonificadosRepository dao;
#GetMapping("/service/{movil}")
public RespuestaVo ConsultarMovil(#PathVariable("movil") String movil) {
System.out.println(movil);
return dao.validarClienteBonifiado(movil);
}
/*
the code I was trying to use to send a request in JSON and try to get the mobile parameter,but
I got an error:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type
'text/plain' not supported]
/*
* #PostMapping(value = "/service",consumes = "application/json", produces="application/json")
* public RespuestaVo ValidateClient(#RequestBody DriverBonificados driver) {
* System.out.println(driver.getMovil());
* return dao.validarClienteBonifiado(driver.getMovil());
} */
}
My Repository
package com.app.validacion.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.app.validacion.entity.DriverBonificados;
import com.app.validacion.entity.RespuestaVo;
#Repository
public interface DriverBonificadosRepository extends JpaRepository<DriverBonificados, Integer> {
#Query(nativeQuery = true,value = "call ValidacionClienteBonificado(:movil)")
RespuestaVo validarClienteBonifiado(#Param("movil") String pMovil);
}
My Entity
package com.app.validacion.entity;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table(name="DriveBonificados")
public class DriverBonificados {
#Id
private int id;
private String movil;
private String contador;
private Date fecha_driver;
private Date fecha_alta;
private Date fecha_fin;
private Date codigo_transaccion;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMovil() {
return movil;
}
public void setMovil(String movil) {
this.movil = movil;
}
public String getContador() {
return contador;
}
public void setContador(String contador) {
this.contador = contador;
}
public Date getFecha_driver() {
return fecha_driver;
}
public void setFecha_driver(Date fecha_driver) {
this.fecha_driver = fecha_driver;
}
public Date getFecha_alta() {
return fecha_alta;
}
public void setFecha_alta(Date fecha_alta) {
this.fecha_alta = fecha_alta;
}
public Date getFecha_fin() {
return fecha_fin;
}
public void setFecha_fin(Date fecha_fin) {
this.fecha_fin = fecha_fin;
}
public Date getCodigo_transaccion() {
return codigo_transaccion;
}
public void setCodigo_transaccion(Date codigo_transaccion) {
this.codigo_transaccion = codigo_transaccion;
}
}
My Model Response
package com.app.validacion.entity;
public interface RespuestaVo {
String getCode();
String getResult();
}
Nice post, but the (first encountered problem) solution is trivial:
With:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]
and with this postman request, You need to:
postman specific: switch the (request>body) content type from "Text" to "JSON (application/json)"
generally: add a (http) header to Your request like
Content-Type: application/json;...

Get _links with spring RepositoryRestController

I have defined custom controllers for my repositories. For example, one looks like this
#RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
MyEntity myEntity= myEntityRepository.findById(1);
return ResponseEntity.ok(new Resource<>(myEntity));
}
This returns a JSON format data which includes a _links section, where I can get the href to the entity.
Now if I want to return an array of entities which are all resources, I get stuck.
What I have tried so far:
1.
#RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
List<MyEntity> myEntityList= myEntityRepository.findAll(1);
return ResponseEntity.ok(new Resources<>(myEntityList));
}
2.
#RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
List<MyEntity> myEntityList= myEntityRepository.findAll();
List<Resource<MyEntity>> resources = new ArrayList<>();
myEntityList.forEach(me -> {
resources.add(new Resource<>(me));
})
return ResponseEntity.ok(resources);
}
Option 1. and 2. don't add _links to the result and I don't understand why. I have googled it a lot and you can add links manually but this seems to be a much clearer way. Can anybody understand, what I'm doing wrong?
There are answers at "adding association links to spring data rest custom exposed method" and "Enable HAL serialization in Spring Boot for custom controller method" for similar questions. I tried something a little different from those answers, for solving a similar situation, and it worked fine. I wish you get your problem solved.
It follows how I did solve my specific situation:
#PostMapping(value = "/myEntities/searchWithParams"
, consumes = MediaType.APPLICATION_JSON_VALUE
, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> searchWithParams(#RequestBody SearchParamsDtoClass params, PersistentEntityResourceAssembler assembler)
{
List<MyEntity> entities = myEntityService.searchWithParams(params);
List<PersistentEntityResource> resourcesList = new ArrayList<PersistentEntityResource>();
for (MyEntity entity: entities) {
PersistentEntityResource resource = assembler.toResource(entity);
resourcesList.add(resource);
}
Resources<PersistentEntityResource> resources = new Resources(resourcesList);
return ResponseEntity.ok(resources);
}
The Resources constructor accepts a collection of embedded content, which is not the same as a collection of links. You have to add the links manually.
Resources resources = new Resources();
resources.add(myEntityRepository
.findAll()
.stream()
.map(entry -> convertToLink(entry)
.collect(Collectors.toList()));
return ResponseEntity.ok(resources);
Here is an example that includes embedded content and pagination in a HAL compliant format.
import static java.util.Objects.*;
import static java.util.Optional.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.hateoas.MediaTypes.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.hateoas.Link;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.codahale.metrics.annotation.Timed;
#RestController
#RequestMapping("/catalog/users")
public class UserResource {
private final #NotNull UserLocator locator;
private final #NotNull ElasticsearchOperations elasticsearchTemplate;
#Inject
public UserResource(
final #NotNull UserLocator locator,
final #NotNull ElasticsearchOperations elasticsearchTemplate
) {
this.locator = requireNonNull(locator, "locator cannot be null");
this.elasticsearchTemplate = requireNonNull(elasticsearchTemplate, "elasticsearchTemplate cannot be null");
}
/**
* GET /users : get all the users.
*
* #return the ResponseEntity with status 200 (OK) and the list of users in body
*/
#Timed
#RequestMapping(method = GET, produces = { HAL_JSON_VALUE, APPLICATION_JSON_VALUE })
public ResponseEntity<Representations<User>> allUsers(
#RequestParam(name = "page", required = false, defaultValue = "0") #Min(0) int page,
#RequestParam(name = "size", required = false, defaultValue = "25") #Min(1) int size,
#RequestParam(name = "like", required = false) String like
) {
final PageRequest pageRequest = new PageRequest(page, size, Sort.Direction.ASC, "surname.raw", "givenName.raw");
final Page<User> entries = elasticsearchTemplate.queryForPage(
new NativeSearchQueryBuilder()
.withQuery(startingWith(like))
.withPageable(pageRequest)
.build(),
User.class
);
final ArrayList<Link> links = new ArrayList<>();
links.add(linkTo(UserResource.class).withSelfRel());
if (!entries.isFirst()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(0, size, like)).withRel("first"));
}
if (!entries.isLast()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.getTotalPages() - 1, size, like)).withRel("last"));
}
if (entries.hasNext()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.nextPageable().getPageNumber(), size, like)).withRel("next"));
}
if (entries.hasPrevious()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.previousPageable().getPageNumber(), size, like)).withRel("prev"));
}
final Representations<User> resourceList = new Representations<>(entries, Representations.extractMetadata(entries), links);
return ResponseEntity.ok(resourceList);
}
private QueryBuilder startingWith(String like) {
return isNull(like) ? null : matchPhrasePrefixQuery("_all", like);
}
/**
* GET /users/:identifier : get the "identifier" user.
*
* #param identifier the identifier of the role to retrieve
* #return the ResponseEntity with status 200 (OK) and with body the role, or with status 404 (Not Found) or with 410 (Gone)
*/
#Timed
#RequestMapping(value = "/{identifier}", method = GET, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<UserRepresentation> aUser(
#PathVariable("identifier") #NotNull String identifier
) {
return ofNullable(this.locator.findOne(identifier))
.map(user -> toRepresentation(user))
.map(ResponseEntity::ok)
.orElse(notFound())
;
}
private #NotNull UserRepresentation toRepresentation(final #NotNull User role) {
final String id = role.getIdentifier();
final Link self = linkTo(methodOn(UserResource.class).aUser(id)).withSelfRel().expand(id);
final Link collection = linkTo(UserResource.class).withRel("collection");
return new UserRepresentation(role, self, collection);
}
protected final #NotNull <U> ResponseEntity<U> notFound() {
return new ResponseEntity<>(NOT_FOUND);
}
}
import java.util.Optional;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.core.Relation;
#Relation(value = "item", collectionRelation = "items")
public class UserRepresentation extends Representation<User> {
public UserRepresentation(User content, Iterable<Optional<Link>> links) {
super(content, links);
}
public UserRepresentation(User content, Link... links) {
super(content, links);
}
#SafeVarargs
public UserRepresentation(User content, Optional<Link>... links) {
super(content, links);
}
public UserRepresentation(User content) {
super(content);
}
}
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static java.util.Objects.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Representation<T> extends Resource<T> {
private final Map<String, ResourceSupport> embedded = new HashMap<>();
public Representation(final #NotNull T content) {
super(content, emptyList());
}
public Representation(final #NotNull T content, final #NotNull Link... links) {
super(content, links);
}
#SafeVarargs
public Representation(final #NotNull T content, final #NotNull Optional<Link>... links) {
this(content, (Iterable<Optional<Link>>) asList(links));
}
public Representation(final #NotNull T content, final #NotNull Iterable<Optional<Link>> links) {
super(content, emptyList());
asStream(links).forEach(this::add);
}
public void add(final #NotNull Optional<Link> link) {
if (null != link && link.isPresent()) super.add(link.get());
}
#JsonProperty("_embedded")
#JsonInclude(Include.NON_EMPTY)
public final #NotNull Map<String, ResourceSupport> getEmbedded() {
return this.embedded;
}
/**
* #param rel must not be {#literal null} or empty.
* #param resource the resource to embed
*/
public final void embed(final #NotNull #Size(min=1) String rel, final ResourceSupport resource) {
requireNonNull(rel, "rel cannot be null");
if (rel.trim().isEmpty()) {
throw new IllegalArgumentException("rel cannot be empty");
}
if (null != resource) {
this.embedded.put(rel, resource);
}
}
}
import javax.validation.constraints.NotNull;
import org.springframework.data.domain.Page;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.PagedResources.PageMetadata;
import org.springframework.hateoas.core.Relation;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
#Relation(collectionRelation = "items")
public class Representations<T> extends Resources<T> {
#JsonUnwrapped
#JsonInclude(JsonInclude.Include.NON_NULL)
private final PageMetadata metadata;
public Representations(Iterable<T> content) {
super(content);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata) {
super(content);
this.metadata = metadata;
}
public Representations(Iterable<T> content, Iterable<Link> links) {
super(content, links);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata, Iterable<Link> links) {
super(content, links);
this.metadata = metadata;
}
public Representations(Iterable<T> content, Link... links) {
super(content, links);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata, Link... links) {
super(content, links);
this.metadata = metadata;
}
/**
* Returns the pagination metadata.
*
* #return the metadata
*/
#JsonProperty("page")
public PageMetadata getMetadata() {
return metadata;
}
public static <U> PageMetadata extractMetadata(final #NotNull Page<U> page) {
return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages());
}
}

serializing annotations as well as fields to JSON

I have a spring boot app, and I want to send DTO validation constraints as well as field value to the client.
Having DTO
class PetDTO {
#Length(min=5, max=15)
String name;
}
where name happens to be 'Leviathan', should result in this JSON being sent to client:
{
name: 'Leviathan'
name_constraint: { type: 'length', min:5, max: 15},
}
Reasoning is to have single source of truth for validations. Can this be done with reasonable amount of work?
To extend Frederik's answer I'll show a little sample code that convers an object to map and serializes it.
So here is the User pojo:
import org.hibernate.validator.constraints.Length;
public class User {
private String name;
public User(String name) {
this.name = name;
}
#Length(min = 5, max = 15)
public String getName() {
return name;
}
}
Then the actual serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
public class UserSerializer extends StdSerializer<User> {
public UserSerializer(){
this(User.class);
}
private UserSerializer(Class t) {
super(t);
}
#Override
public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> properties = beanProperties(bean);
gen.writeStartObject();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gen.writeObjectField(entry.getKey(), entry.getValue());
}
gen.writeEndObject();
}
private static Map<String, Object> beanProperties(Object bean) {
try {
return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
.filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
.flatMap(descriptor -> {
String name = descriptor.getName();
Method getter = descriptor.getReadMethod();
Object value = ReflectionUtils.invokeMethod(getter, bean);
Property originalProperty = new Property(name, value);
Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
.map(anno -> new Property(name + "_constraint", annotationProperties(anno)));
return Stream.concat(Stream.of(originalProperty), constraintProperties);
})
.collect(toMap(Property::getName, Property::getValue));
} catch (Exception e) {
return Collections.emptyMap();
}
}
// Methods from Annotation.class
private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");
private static Map<String, Object> annotationProperties(Annotation anno) {
try {
Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
.filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
.map(descriptor -> {
String name = descriptor.getName();
Method method = descriptor.getMethod();
Object value = ReflectionUtils.invokeMethod(method, anno);
return new Property(name, value);
});
Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
} catch (IntrospectionException e) {
return Collections.emptyMap();
}
}
private static class Property {
private String name;
private Object value;
public Property(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
}
And finally we need to register this serializer to be used by Jackson:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {
#Bean
public Jackson2ObjectMapperBuilder mapperBuilder(){
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.serializers(new UserSerializer());
return jackson2ObjectMapperBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SerializationApp.class, args);
}
}
#RestController
class SerializationController {
#GetMapping("/user")
public User user() {
return new User("sample");
}
}
The Json that will be emitted:
{
"name_constraint":{
"min":5,
"max":15,
"payload":[],
"groups":[],
"message":"{org.hibernate.validator.constraints.Length.message}",
"type":"org.hibernate.validator.constraints.Length"
},
"name":"sample"
}
Hope this helps. Good luck.
You can always use a custom Jackson Serializer for this. Plenty of docs to do this can be found on the internet, might look something like this:
public void serialize(PetDTO value, JsonGenerator jgen, ...) {
jgen.writeStartObject();
jgen.writeNumberField("name", value.name);
jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}
public ConstaintDTO getConstraintValue(PetDTO value) {
// Use reflection to check if the name field on the PetDTO is annotated
// and extract the min, max and type values from the annotation
return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}
You may want to create a base-DTO class for which the converter kicks in so you don't have to create a custom converter for all your domain objects that need to expose the constraints.
By combining reflection and smart use of writing fields, you can get close. Downside is you can't take advantage of the #JsonXXX annotations on your domain objects, since you're writing the JSON yourself.
More ideal solution whould be to have Jackson convert, but have some kind of post-conversion-call to add additional XX_condtion properties to the object. Maybe start by overriding the default object-serializer (if possible)?

Problem about controller and dao functions

i made some small project
there are just add, delete, showlist functions
add and delete functions good work but get list functions are didn't work.
(in my project memoservice.getAll() function didn't work well)
so i try to get lists like this,
List <Memo> mem = getAll();
but there are no return values;
what's the problem in my project. help me plz!
some codes are here.
MemoController.java
package springbook.sug.web;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.support.SessionStatus;
import springbook.sug.domain.Memo;
import springbook.sug.service.MemoService;
import springbook.sug.web.security.LoginInfo;
#Controller
#RequestMapping("/memo")
public class MemoController {
#Autowired
private MemoService memoService;
private #Inject Provider<LoginInfo> loginInfoProvider;
#RequestMapping("/list")
public String list(ModelMap model) {
model.addAttribute(this.memoService.getAll());
return "memo/list";
}
#RequestMapping("/list/{userId}")
public String list(#PathVariable int userId, ModelMap model) {
model.addAttribute(this.memoService.get(userId));
return "memo/list";
}
#RequestMapping("/delete/{memoId}")
public String delete(#PathVariable int memoId) {
this.memoService.delete(memoId);
return "memo/deleted";
}
#RequestMapping("/add/")
public String showform(ModelMap model) {
Memo memo = new Memo();
model.addAttribute(memo);
return "memo/add";
}
#RequestMapping(method=RequestMethod.POST)
public String add(#ModelAttribute #Valid Memo memo, BindingResult result, SessionStatus status) {
if (result.hasErrors()) {
return "list";
}
else {
this.memoService.add(memo);
status.setComplete();
return "redirect:list";
}
}
}
MemoService.java
package springbook.sug.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import springbook.sug.dao.MemoDao;
import springbook.sug.domain.Memo;
#Service
#Transactional
public class MemoServiceImpl implements MemoService{
private MemoDao memoDao;
#Autowired
public void setMemoDao(MemoDao memoDao) {
this.memoDao = memoDao;
}
public Memo add(Memo memo) {
memo.initDates();
return this.memoDao.add(memo);
}
public void delete(int memoId) {
this.memoDao.delete(memoId);
}
public Memo get(int userId) {
return this.memoDao.get(userId);
}
public Memo update(Memo memo) {
return this.memoDao.update(memo);
}
public List<Memo> getAll() {
return this.memoDao.getAll();
}
}
MemoDao.java
package springbook.sug.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Repository;
import springbook.sug.domain.Memo;
#Repository
public class MemoDaoJdbc implements MemoDao {
private SimpleJdbcTemplate jdbcTemplate;
private SimpleJdbcInsert memoInsert;
private RowMapper<Memo> rowMapper =
new RowMapper<Memo>() {
public Memo mapRow(ResultSet rs, int rowNum) throws SQLException {
Memo memo = new Memo();
memo.setMemoId(rs.getInt("memoId"));
memo.setMemoContents(rs.getString("memoContents"));
memo.setMemoRegister(rs.getString("memoRegister"));
memo.setCreated(rs.getDate("created"));
return memo;
}
};
#Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
this.memoInsert = new SimpleJdbcInsert(dataSource)
.withTableName("memos")
.usingGeneratedKeyColumns("memoId");
}
public Memo add(Memo memo) {
int generatedId = this.memoInsert.executeAndReturnKey(
new BeanPropertySqlParameterSource(memo)).intValue();
memo.setMemoId(generatedId);
return memo;
}
public Memo update(Memo memo) {
int affected = jdbcTemplate.update(
"update memos set " +
"memoContents = :memoContents, " +
"memoRegister = :memoRegister, " +
"created = :created, " +
"where memoId = :memoId",
new BeanPropertySqlParameterSource(memo));
return memo;
}
public void delete(int memoId) {
this.jdbcTemplate.update("delete from memos where memoId = ?", memoId);
}
public int deleteAll() {
return this.jdbcTemplate.update("delete from memos");
}
public Memo get(int memoId) {
try {
return this.jdbcTemplate.queryForObject("select * from memos where memoId = ?",
this.rowMapper, memoId);
}
catch(EmptyResultDataAccessException e) {
return null;
}
}
public List<Memo> search(String memoRegister) {
return this.jdbcTemplate.query("select * from memos where memoRegister = ?",
this.rowMapper, "%" + memoRegister + "%");
}
public List<Memo> getAll() {
return this.jdbcTemplate.query("select * from memos order by memoId desc",
this.rowMapper);
}
public long count() {
return this.jdbcTemplate.queryForLong("select count(0) from memos");
}
}
Are you getting an exception when running the code? If so, stack trace is needed. If not, do you have data in your table to return? How are you injecting datasource into your dao implementation? Your application context might help.

Resources