spring-projects / spring-projects/spring-data-rest
Wrong self link with linked resources [DATAREST-1352]
@odrotbohm is already working on this.
Since Dec 31, 2020.
- Dominant language
- Java
- Stars
- 958
- Forks
- 568
- PR merge metrics
- No merged PRs in 30d
Description
Daniele Renda opened DATAREST-1352 and commented
I'm using Spring Boot 2.0.3, Spring Data REST, Spring HATEOAS. My domain model is quite structured but lately I found a strange behaviour in self links.
I'm going to show part of the model to point out the problem, removing useless part:
EyeExam:
@EntityListeners(value = EyeExamListener.class)
public class EyeExam extends AbstractEntity {
@NotNull
@JoinColumn(name = "contact_id", updatable = false)
@JsonDeserialize(using = ContactUriDeserializer.class)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Contact contact;
@NotNull
@Column(nullable = false, columnDefinition = "DATE")
private Instant date;
Contact:
@EntityListeners({ContactListener.class})
public class Contact extends AbstractEntity {
@NotNull
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'CUSTOMER'")
private ContactType type = ContactType.CUSTOMER;
@NotNull
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'NATURAL_PERSON'")
private PersonType personType = PersonType.NATURAL_PERSON;
private String firstName;
private String lastName;
private String companyName;
This is ContactRepository:
@Transactional
@PreAuthorize("isAuthenticated()")
public interface ContactRepository extends JpaRepository<Contact, Long> {
....
....
}
When I retrieve a specific EyeExam resource (https://myserver.com/api/v1/eyeExams/13) Spring returns:
{
"sid" : "f16d6e45-477f-11e9-898e-9d6f4f2f5990",
"createdBy" : "system",
"createdDate" : "2017-05-31T17:38:00Z",
"lastModifiedDate" : null,
"lastModifiedBy" : null,
"createdByName" : "System",
"lastModifiedByName" : null,
"date" : "2017-05-31T00:00:00Z",
"_links" : {
"self" : {
"href" : "https://myserver.com/api/v1/eyeExams/13"
},
"eyeExam" : {
"href" : "https://myserver.com/api/v1/eyeExams/13{?projection}",
"templated" : true
},
"supplyTypes" : {
"href" : "https://myserver.com/api/v1/eyeExams/13/supplyTypes"
},
"changeStatus" : {
"href" : "https://myserver.com/api/v1/eyeExams/13/changeStatus?status=%7Bstatus%7D"
},
"contact" : {
"href" : "https://myserver.com/api/v1/eyeExams/13/contact{?projection}",
"templated" : true
},
"store" : {
"href" : "https://myserver.com/api/v1/eyeExams/13/store{?projection}",
"templated" : true
}
}
}
As you can see, I get a link to linked resource Contact. That's ok. Now I get the resource https://myserver.com/api/v1/eyeExams/13/contact and Spring replies:
{
"sid" : "4c2ba300-477e-11e9-898e-9d6f4f2f5990",
"createdBy" : "system",
"createdDate" : "2018-11-01T09:00:00Z",
"lastModifiedDate" : null,
"lastModifiedBy" : null,
"createdByName" : "System",
"lastModifiedByName" : null,
"type" : "CUSTOMER",
"personType" : "NATURAL_PERSON",
"firstName" : "John",
"lastName" : "Smith",
"companyName" : null,
"_links" : {
"self" : {
"href" : "https://myserver.com/api/v1/contact/22352"
},
"contact" : {
"href" : "https://myserver.com/api/v1/contact/22352{?projection}",
"templated" : true
},
"notes" : {
"href" : "https://myserver.com/api/v1/contacts/22352/notes"
},
"auditLogs" : {
"href" : "https://myserver.com/api/v1/contacts/22352/auditLogs"
},
"media" : {
"href" : "https://myserver.com/api/v1/contacts/22352/media"
},
"privacyAgreements" : {
"href" : "https://myserver.com/api/v1/contacts/22352/privacyAgreements"
},
"eyeExams" : {
"href" : "https://myserver.com/api/v1/contacts/22352/eyeExams"
},
"eyeExamsCount" : {
"href" : "https://myserver.com/api/v1/contacts/22352/eyeExams/count"
},
"documents" : {
"href" : "https://myserver.com/api/v1/contacts/22352/documents"
},
"pendingSalesOrders" : {
"href" : "https://myserver.com/api/v1/contacts/22352/pendingSalesOrders"
},
"lastPurchasedFrames" : {
"href" : "https://myserver.com/api/v1/contacts/22352/lastPurchasedFrames"
},
"store" : {
"href" : "https://myserver.com/api/v1/contact/22352/store{?projection}",
"templated" : true
}
}
}
I have a couple of ResourceAssembler working on Contact and EyeExam:
@Component
public class ContactProcessor implements ResourceProcessor<Resource<Contact>> {
private Logger log = LogManager.getLogger();
Locale locale = LocaleContextHolder.getLocale();
@Autowired
private BasePathAwareLinks service;
@Override
public Resource<Contact> process(Resource<Contact> resource) {
Long id = resource.getContent().getId();
if (id == null)
return resource;
// notes
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(ControllerLinkBuilder
.methodOn(ContactController.class).getNotes(id, null, null, AppUtils.getDefaultPageable(), null, null)));
resource.add(linkBuilder.withRel("notes"));
} catch (Exception e) {
log.error("", e);
}
// auditLogs
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getAuditLogs(id, AppUtils.getDefaultPageable(), null, null)));
resource.add(linkBuilder.withRel("auditLogs"));
} catch (Exception e) {
log.error("", e);
}
// media
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder
.linkTo(ControllerLinkBuilder.methodOn(ContactController.class).getMedia(id, AppUtils.getDefaultPageable(), null, null)));
resource.add(linkBuilder.withRel("media"));
} catch (Exception e) {
log.error("", e);
}
// privacy agreements
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(ContactController.class)
.getPrivacyAgreements(id, AppUtils.getDefaultPageable(), null, null, null)));
resource.add(linkBuilder.withRel("privacyAgreements"));
} catch (Exception e) {
log.error("", e);
}
// eye exams
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getEyeExams(id, AppUtils.getDefaultPageable(), null, null, null)));
resource.add(linkBuilder.withRel("eyeExams"));
} catch (Exception e) {
log.error("", e);
}
// eye exams count
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getEyeExamsCount(id)));
resource.add(linkBuilder.withRel("eyeExamsCount"));
} catch (Exception e) {
log.error("", e);
}
// documents
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getDocuments(id, null, AppUtils.getDefaultPageable(), null, null)));
resource.add(linkBuilder.withRel("documents"));
} catch (Exception e) {
log.error("", e);
}
// pending sales orders
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getPendingSalesOrders(id, null, AppUtils.getDefaultPageable(), null, null)));
resource.add(linkBuilder.withRel("pendingSalesOrders"));
} catch (Exception e) {
log.error("", e);
}
// last purchased frames
try {
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(ContactController.class).getLastPurchasedFrames(id, AppUtils.getDefaultPageable(), null, null, null)));
resource.add(linkBuilder.withRel("lastPurchasedFrames"));
} catch (Exception e) {
log.error("", e);
}
return resource;
}
}
and:
@Component
public class EyeExamProcessor implements ResourceProcessor<Resource<EyeExam>> {
private Logger log = LogManager.getLogger();
Locale locale = LocaleContextHolder.getLocale();
@Autowired
private BasePathAwareLinks service;
@Override
public Resource<EyeExam> process(Resource<EyeExam> resource) {
Long id = resource.getContent().getId();
//SUPPLY TYPES
LinkBuilder linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(ControllerLinkBuilder
.methodOn(EyeExamController.class).getSupplyTypes(id)));
resource.add(linkBuilder.withRel("supplyTypes"));
//CHANGE STATUS
linkBuilder = service.underBasePath(ControllerLinkBuilder.linkTo(ControllerLinkBuilder
.methodOn(EyeExamController.class).changeStatusWithNote(id, null, null, null, null)));
resource.add(linkBuilder.withRel("changeStatus"));
return resource;
}
}
I want point out the self link. It is wrong, in fact it should be https://myserver.com/api/v1/contacts/22352 with the end -s.
I was using some custom ResourceProcessor, but even without them I've the same problem.
Right now I created a workaround in ContactResourceProcessor, rewriting the self link with the right one but I'd like to understand if I'm doing something wrong, if it's a bug or I just missed something.
I opened a question on stackoverflow, I report it for sake of completeness: https://stackoverflow.com/questions/55195882/spring-hateoas-wrong-self-link-with-linked-resources
Attachments:
- Senzanome.png (213.08 kB)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.