spring-projects / spring-projects/spring-data-rest

Wrong self link with linked resources [DATAREST-1352]

Open
#1,715 8 comments 0 reactions 1 assignee View on GitHub

@odrotbohm is already working on this.

Since Dec 31, 2020.

type: bug
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:

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.