Reputation: 6244
I'm using Spring Boot 2, Spring Data REST, Spring HATEOAS.
Let's say I've a model:
@EntityListeners({ContactListener.class})
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
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;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id", updatable = false)
private Store store;
and Store:
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Store extends AbstractEntity {
@NotBlank
@Column(nullable = false)
private String name;
@Username
@NotBlank
@Length(max = 16)
@Column(nullable = false/*, unique = true*/)
@ColumnTransformer(write = "UPPER(?)")
private String code;
private String address;
private String zipCode;
private String city;
private String district;
When I get a contact the response looks like this:
{
"sid": "962732c2-68a8-413b-9762-f676d42046b4",
"createdBy": "1ccf2329-4aa3-4d55-8878-25517edf1522",
"createdDate": "2019-05-28T14:06:07.011Z",
"lastModifiedDate": "2019-06-04T08:46:02.591Z",
"lastModifiedBy": "system",
"createdByName": "Rossi Mario",
"lastModifiedByName": null,
"type": "CUSTOMER",
"personType": "NATURAL_PERSON",
"firstName": "Mario",
"lastName": "Rossi",
"companyName": null,
"fullName": "Rossi Mario",
"gender": "MALE",
"birthDate": "2019-05-21T00:00:00Z",
"birthCity": null,
"job": null,
"billingAddress": "Via 123",
"billingZipCode": "14018",
"billingCity": "Roatto",
"billingDistrict": "AT",
"billingCountry": "IT",
"shippingAddress": "Via 123",
"shippingZipCode": "14018",
"shippingCity": "Roatto",
"shippingDistrict": "AT",
"shippingCountry": "IT",
"taxCode": "XXXX",
"vatNumber": null,
"landlinePhone": null,
"mobilePhone": null,
"fax": null,
"email": "[email protected]",
"certifiedEmail": null,
"survey": null,
"iban": null,
"swift": null,
"publicAdministration": false,
"sdiAccountId": "0000000",
"preset": false,
"_links": {
"self": {
"href": "http://localhost:8082/api/v1/contacts/1"
},
"contact": {
"href": "http://localhost:8082/api/v1/contacts/1{?projection}",
"templated": true
},
"store": {
"href": "http://localhost:8082/api/v1/contacts/1/store{?projection}",
"templated": true
}
}
}
as you can see the link of store it's not the self link of the resource Store. I'd like to override that link setting the self resource. So I created this processor:
@Component
public class DocumentRowProcessor implements ResourceProcessor<Resource<Contact>> {
@Autowired
private BasePathAwareLinks service;
@Autowired
private EntityLinks entityLinks;
@Override
public Resource<Contact> process(Resource<Contact> resource) {
Store store = resource.getContent().getStore();
if(store != null){
resource.add(entityLinks.linkToSingleResource(store.getClass(), store.getId()).withRel("store"));
}
return resource;
}
}
Unfortunately, the link is now overriden but I find 2 links inside "store". Debugging I saw that inside the resource is present just the self link. My guess is that related links are added in following steps.
How can I accomplish my goal in a clean way?
Upvotes: 1
Views: 437
Reputation: 2143
As of Spring Data REST 3.6.0, you can implement a new LinkCollector
. The following has clear flaws, and I've been unable to test the Java version (my Kotlin version works fine), but it generates links using the canonical URLs:
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
// https://github.com/spring-projects/spring-data-rest/issues/2042#issuecomment-1913914052
public class AbsoluteLinkCollector implements LinkCollector {
private final PersistentEntities entities;
private final Associations associationLinks;
private final SelfLinkProvider links;
public AbsoluteLinkCollector(LinkCollector oldCollector) throws NoSuchFieldException, IllegalAccessException {
Field entitiesField = oldCollector.getClass().getDeclaredField("entities");
entitiesField.setAccessible(true);
this.entities = (PersistentEntities) entitiesField.get(oldCollector);
Field associationLinksField = oldCollector.getClass().getDeclaredField("associationLinks");
associationLinksField.setAccessible(true);
this.associationLinks = (Associations) associationLinksField.get(oldCollector);
Field linksField = oldCollector.getClass().getDeclaredField("links");
linksField.setAccessible(true);
this.links = (SelfLinkProvider) linksField.get(oldCollector);
}
@Override
public Links getLinksFor(Object obj) {
return this.getLinksFor(obj, Links.NONE);
}
@Override
public Links getLinksFor(Object obj, Links existing) {
Map<String, Link> linkMap = new HashMap<>();
for (Link l : existing) {
linkMap.put(l.getRel().value(), l);
}
linkMap.put(IanaLinkRelations.SELF.value(), links.createSelfLinkFor(obj).withSelfRel());
PersistentEntity<?, ? extends PersistentProperty<?>> entity = entities.getRequiredPersistentEntity(obj.getClass());
entity.doWithAssociations((Association<?> assoc) -> {
try {
if (!associationLinks.isLinkableAssociation(assoc)) {
return;
}
ResourceMetadata ownerMetadata = associationLinks.getMetadataFor(assoc.getInverse().getOwner().getType());
ResourceMapping propertyMapping = ownerMetadata.getMappingFor(assoc.getInverse());
LinkRelation rel = propertyMapping.getRel();
// if it is a collection, we can't get the value
if (assoc.getInverse().isCollectionLike()) {
return;
}
Field fieldValueField = obj.getClass().getDeclaredField(assoc.getInverse().getName());
fieldValueField.setAccessible(true);
Object fieldValue = fieldValueField.get(obj);
if (fieldValue == null) return;
linkMap.put(rel.value(), links.createSelfLinkFor(fieldValue).withRel(rel));
} catch (Exception e) {
// without this, we get a useless and confusing "Failed to write request"
// error response.
System.out.println("Error getting links for " + obj);
e.printStackTrace();
throw new RuntimeException(e);
}
});
return Links.of(linkMap.values());
}
@Override
public Links getLinksForNested(Object obj, Links existing) {
return this.getLinksFor(obj, existing);
}
}
It is enabled by overriding customizeLinkCollector
in your RepositoryRestConfigurer
:
@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
@Override
public LinkCollector customizeLinkCollector(LinkCollector collector) {
return new AbsoluteLinkCollector(collector);
}
}
Upvotes: 1
Reputation: 3423
http://localhost:8082/api/v1/contacts/1/store
is the endpoint where you can check which store is linked to this contant, or you can delete/modify the association between this two object.However in certain use-cases you need the self-link for further actions and you don't want to send an extra request from the client. Do the following: 1. Create a projection for the contant. 2. Include all the properties you need and also the store. 3. If you don't need any properties of the store here - only the self link - then create an 'empty projection' for the store entoty and include that projection as store property into the contact property.
When you get this projection of the contact then the result will contain the self-link of the store inside the store property. So the main _links
collection will be still a regular hateos link-collection but there will be a store._links.self.href
property which will contain the self link of the associated store.
Upvotes: 1