How to extend existing links to resources with custom Spring Data Rest Controller - spring-data-rest

I'm using Spring boot 1.5.10 with spring-boot-starter-data-rest.
I created domain entities:
#Entity
public class User {
#Column
private long id;
#Column
private String firstName;
#Column
private String secondName;
#Column
private int age;
#Fetch(FetchMode.SELECT)
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Address address;
...
}
#Entity
public class Address {
#Column
private long id;
#Column
private String city;
#Column
private String street;
#Column
private String build;
...
}
And I've created repositories for this domain entities:
interface UserRepository extends JpaRepository<User, String> {
User getUserByFirstNameAndSecondName(String firstName, String secondName);
#RestResource(exported = false)
User getUserByAddress(Address address);
}
interface AddressRepository extends JpaRepository<Address, String> {
Address getAddressByStreet(String street);
}
After that I give next HATEOAS root endpoints:
{
"_links" : {
"users" : {
"href" : "http://localhost:8080/api/v1/users{?page,size,sort}",
"templated" : true
},
"address" : {
"href" : "http://localhost:8080/api/v1/address{?page,size,sort}",
"templated" : true
}
}
And if I navigate to http://localhost:8080/api/v1/users I'll get something like that:
{
"_links" : {
"getUserByFirstNameAndSecondName" : {
"href" : "http://localhost:8080/api/v1/users/search/getUserByFirstNameAndSecondName{?firstName, secondName ,projection}",
"templated" : true
},
"self" : {
"href" : "http://localhost:8081/api/v1/users/search"
}
}
}
But I want to add a new endpoint to "http://localhost:8080/api/v1/users/search" use a custom controller, with #RepositoryRestController, for example:
#RepositoryRestController
public class UserRestController {
private UserRepository userRepository;
#Autowired
public UserRestController(UserRepository userRepository) {
this.userRepository = userRepository;
}
#RequestMapping("/users/search/getUserByStreetAddress")
public User getUserByStreetAddress(String street) {
Address address = new Address();
address.setStreet(street);
return userRepository.getUserByAddress(address);
}
}
But my controller doesn't add anything to existing endpoints.
I've read How to add links to root resource in Spring Data REST? and this decision works fine for root endpoints but I want add getUserByStreetAddress endpoint to users/search and eventually get the following:
{
"_links" : {
"getUserByFirstNameAndSecondName" : {
"href" : "http://localhost:8080/api/v1/users/search/getUserByFirstNameAndSecondName{?firstName, secondName ,projection}",
"templated" : true
},
//Method from my custom controller added to http://localhost:8080/api/v1/users/search" endpoint
"getUserByStreetAddress" : {
"href" : "http://localhost:8080/api/v1/users/search/getUserByStreetAddress{?street ,projection}",
"templated" : true
},
"self" : {
"href" : "http://localhost:8080/api/v1/users/search"
}
}
}
Is it possible to do this?

You need to implement a ResourceProcessor<RepositorySearchesResource> and add the link manually.
// other imports here
import static org.springframework.hateoas.TemplateVariable.VariableType.REQUEST_PARAM;
#Component
public class RepositorySearchesResourceProcessor implements ResourceProcessor<RepositorySearchesResource> {
#Autowired
private RepositoryRestConfiguration restConfiguration;
#Override
public RepositorySearchesResource process(RepositorySearchesResource resource) {
// early exit if we're not dealing with a User resource
if (!User.class.equals(resource.getDomainType())) {
return resource;
}
// add a custom link to /users/search
String search = resource.getId().getHref();
List<TemplateVariable> list = new ArrayList<>;
list.add(new TemplateVariable("street", REQUEST_PARAM);
boolean addProjection = restConfiguration.getProjectionConfiguration().hasProjectionFor(Parameter.class);
if (addProjection) {
// should be "projection" unless you configured it differently
list.add(restConfiguration.getProjectionConfiguration().getParameterName());
}
TemplateVariables tvs = new TemplateVariables(list);
Link link = new Link(new UriTemplate(search + "/getUserByStreetAddress", tvs"), "getUserByStreetAddress");
resource.add(link);
return resource;
}
I put in the RepositoryRestConfiguration and UriTemplate, so as to give a hint about adding other rest parameters (pagination, sorting...) later if necessary.

Related

Changing IdentityUser's Id column's type to int

I am using .net core identity. When I create a new user in db, I can see that its ID column is GUID and it is string. I want to make it int so that User IDs can be 1,2,3 ...
How can I do that?
Usually we don't do that, It seems like it will be more complicated than simply adding a new id property with type int in your derived user class. But there still one way you can do it. Here's what plugging in guids instead of strings should look for example:
public class GuidRole : IdentityRole<Guid, GuidUserRole> {
public GuidRole() {
Id = Guid.NewGuid();
}
public GuidRole(string name) : this() { Name = name; }
}
public class GuidUserRole : IdentityUserRole<Guid> { }
public class GuidUserClaim : IdentityUserClaim<Guid> { }
public class GuidUserLogin : IdentityUserLogin<Guid> { }
public class GuidUser : IdentityUser<Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> {
public GuidUser() {
Id = Guid.NewGuid();
}
public GuidUser(string name) : this() { UserName = name; }
}
private class GuidUserContext : IdentityDbContext<GuidUser, GuidRole, Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> { }
private class GuidUserStore : UserStore<GuidUser, GuidRole, Guid, GuidUserLogin, GuidUserRole, GuidUserClaim> {
public GuidUserStore(DbContext context)
: base(context) {
}
}
private class GuidRoleStore : RoleStore<GuidRole, Guid, GuidUserRole> {
public GuidRoleStore(DbContext context)
: base(context) {
}
}
[TestMethod]
public async Task CustomUserGuidKeyTest() {
var manager = new UserManager<GuidUser, Guid>(new GuidUserStore(new GuidUserContext()));
GuidUser[] users = {
new GuidUser() { UserName = "test" },
new GuidUser() { UserName = "test1" },
new GuidUser() { UserName = "test2" },
new GuidUser() { UserName = "test3" }
};
foreach (var user in users) {
UnitTestHelper.IsSuccess(await manager.CreateAsync(user));
}
foreach (var user in users) {
var u = await manager.FindByIdAsync(user.Id);
Assert.IsNotNull(u);
Assert.AreEqual(u.UserName, user.UserName);
}
}

How to return ObjectNode from Controller in Micronaut?

I have following code
#Controller()
public class TestController {
#Get(value = "test", produces = MediaType.APPLICATION_JSON)
public MyDto fetch() throws Exception {
return new MyDto(
"test",
new ObjectMapper().readValue("{\"a\": 1}", ObjectNode.class)
);
}
#Serializable
#Data
public static class MyDto {
private final String name;
private final ObjectNode extraFields;
public MyDto(String name, ObjectNode extraFields) {
this.name = name;
this.extraFields = extraFields;
}
}
}
And I have an unexpected output on the client, extraFields object is empty
{
"name": "test",
"extraFields": [
[]
]
}
How to make Micronaut controller properly serialize com.fasterxml.jackson.databind.node.ObjectNode ?

Getting "no serializer found for class" Exception in restAssured post request

I have a Json Payload for a Post call as below:
{
"action" : "Closed",
"Id" : 30144,
"expireDate" : null,
"inputUser" : "abc",
"previousStatusId" : 1,
"statusId" : 4,
"Notes" : [ ]
}
My POJO classes for the above payload is as below
public class UpdateNoteStatus {
private String action;
private int Id;
private String expireDate;
private String inputUser;
private int previousStatusId;
private int statusId;
private List<Notes> Notes;
public void setAction(String action) {
this.action = action;
}
public void setId(int Id) {
this.Id = Id;
}
public void setExpireDate(String expireDate) {
this.expireDate = expireDate;
}
public void setinputUser(String inputUser) {
this.inputUser = inputUser;
}
public void setPreviousStatusId(int previousStatusId) {
this.previousStatusId = previousStatusId;
}
public void setStatusId(int statusId) {
this.statusId = statusId;
}
public void setNotes(List<Notes> Notes) {
this.Notes = Notes;
}
}
public class Notes{
}
Now I have assigned the values in the main class from where I am making the API call is as below:
ArrayList<Notes> Notes = new ArrayList<Notes>();
UpdateNoteStatus objUpdateNoteStatus = new UpdateNoteStatus();
objUpdateNoteStatus.setAction("Closed");
objUpdateNoteStatus.setId(Integer.parseInt("30144"));
objUpdateNoteStatus.setinputUser("abc");
objUpdateNoteStatus.setPreviousStatusId(1);
objUpdateNoteStatus.setStatusId(4);
objUpdateNoteStatus.setNotes(Notes);
But when I am making the API POST call it is throwing exception - "no serializer found for class and no properties discovered to create beanserializer". Could you please help. The Step is hightlighted in Bold.
RequestSpecification rs = given().contentType("application/json");
**rs = rs.body(objUpdateNoteStatus);** //In This Step I am getting the above mentioned Exception
Response res = rs.when().post("/UpdateStatus");
as you are initializing an empty object , you need to use below Annotation supported in below library
com.jayway.restassured.RestAssured;
#JsonIgnoreProperties(ignoreUnknown=true)
class UpdateNoteStatus

How to exclude Weld metadata from JSON object serialization

Assume the following REST resource:
#Path("/ActiveLeadTask")
//Also possible MediaType.APPLICATION_XML
#Produces(MediaType.APPLICATION_JSON)
public class ActiveLeadTask {
private #Inject ActiveLeadTaskBo activeLeadBo;
#GET
#Path("/getBo")
public ActiveLeadTaskBo getBo() {
return activeLeadBo;
}
}
////////////////////////////////////////////////////
#XmlRootElement
#XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
public class ActiveLeadTaskBo implements Serializable {
private static final long serialVersionUID = 1L;
private String firstName;
private String lastName;
private String phoneNumber;
private String phoneCountryCode;
private AtomicInteger accessCounterField = new AtomicInteger(0);
public ActiveLeadTaskBo() {
firstName = "test";
lastName = "test";
}
public int getAccessCounter() {
return accessCounterField.incrementAndGet();
}
public void setAccessCounter(int seed) {
accessCounterField.set(seed);
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
The REST response for getBo() JSON (but not for XML) returns the following:
{
"accessCounter": 1,
"firstName": "test",
"lastName": "test",
"metadata": {
"contextualInstance": {
"accessCounter": 2,
"firstName": "test",
"lastName": "test"
},
"instance": {
"accessCounter": 3,
"firstName": "test",
"lastName": "test"
}
}
}
The JSON response contains an additional "metadata" field - how can I configure the project to not generate it, or avoid generating it? The CDI container is Weld and the JSON serializer is provided by Yasson.
Two solutions are possible:
create a wrapper class, for example, ActiveLeadTaskBoInjectWrapper:
#Inject
ActiveLeadTaskBoInjectWrapper activeLeadBo;
activeLeadBo.getInstance();
workaround Weld specifics:
#Inject
Foo foo;
public void doSomething() {
if (foo instanceof WeldClientProxy) {
// foo is a proxy
((WeldClientProxy)foo).getMetadata().getContextualInstance()
} else {
// not a proxy
}
}
Depends on what JSON processing framework is used in your REST endpoint. For jsonb-api (jsr367) the only possible solution is to implement javax.json.bind.config.PropertyVisibilityStrategy to exclude generated properties from processing.

Tweak jackson polymorphic deserialization

I have a simple polymorphic model like this
public class Foo {
private Bar bar1;
private Bar bar2;
public Bar getBar1() {
return bar1;
}
public Bar getBar2() {
return bar2;
}
public void setBar1(Bar bar1) {
this.bar1 = bar1;
}
public void setBar2(Bar bar2) {
this.bar2 = bar2;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#type")
public class Bar {
}
public class BarExpression extends Bar {
private String expression;
public String getExpression() {
return expression;
}
#JsonIgnore
public Object getValue() {
return null;
}
public void setExpression(String expression) {
this.expression = expression;
}
}
public class BarLiteral extends Bar {
private String value;
private String type;
public String getType() {
return type;
}
public Object getValue() {
return value;
}
public void setType(String type) {
this.type = type;
}
public void setValue(String value) {
this.value = value;
}
}
Serializing a simple example like this
public void run() throws Exception {
Foo foo;
BarLiteral bar1;
BarExpression bar2;
//
foo = new Foo();
bar1 = new BarLiteral();
bar1.setType("java.lang.String");
bar1.setValue("gnu");
foo.setBar1(bar1);
bar2 = new BarExpression();
bar2.setExpression("bean.property * 2");
foo.setBar2(bar2);
//
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
StringWriter w = new StringWriter();
mapper.writeValue(w, foo);
System.out.println(w.toString());
}
gives the expected result:
{
"bar1" : {
"#type" : "de.mit.jackson.BarLiteral",
"value" : "gnu",
"type" : "java.lang.String"
},
"bar2" : {
"#type" : "de.mit.jackson.BarExpression",
"expression" : "bean.property * 2"
}
}
The question is now: I want to improve user experience when handwriting this "DSL" by supporting "primitive shortcuts" for the typed "Bar..." classes like this
{
"bar1" : "gnu",
"bar2" : "#{bean.property * 2}"
}
The solution that came closest was using a converter on Foo#bar1 and Foo#bar2, checking for either String or "Bar" input, but this solution requires decoration of every attribute definition.
Creating a deserializer with a comparable behavior did not work, as the #JsonTypeInfo is not compatible in the sense that i can have a #JsonDeserialize implementation that will check for a String event and then delegate to the standard #JsonTypeInfo process. The #JsonTypeInfo standard will check only for the #type and then delegate back to the (subtype) deserializer which is again my wrapper implementation.
The required process is
if input event is string {
parse and return string input
} else {
activate #type parsing delegate;
after #type parsing activate standard BeanDeserializer
(**not** my implementation)
}
Is there another hook i am missing?