1

I have a simple ReactJS/SpringBoot application which generates XML files used for software licenses.

This has been working fine, but now I'm trying to add an "attributes" table which has a many-to-one relationship with the license table. It will keep track of attributes specified in the front end that will be set to true on the license.

I've used these URLs as a guide for the backend (video and related code):

https://www.youtube.com/watch?v=8qhaDBCJh6I
https://github.com/Java-Techie-jt/spring-data-jpa-one2many-join-example

However, I'm getting a 400 error both on the update and the addition of a license when I try to use the updated code.

The front end seems to be working correctly.

Edit: looks like this is the culprit; although I haven't figured out why, yet.

Could not resolve parameter [0] in org.springframework.http.ResponseEntity<com.license.gen.app.model.License> com.license.gen.app.web.LicenseController.updateLicense(com.license.gen.app.model.License): JSON parse error: Cannot construct instance of `com.license.gen.app.model.Attribute` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('CONFIG_MSC_PARAMETERS'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.license.gen.app.model.Attribute` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('CONFIG_MSC_PARAMETERS')

/endEdit

It's producing a JSON object with the attributes added as an array (e.g. at the end of the following object):

{
  "id": 861,
  "fullName": "johnsmith@abc.com",
  "contact": "",
  "requester": "johnsmith@abc.com",
  "tag": "",
  "company": "ACME",
  "companyId": "ABC",
  "template": "AN_4_2",
  "product": "Analytics",
  "expiration": "2022-04-15",
  "macs": "11-11-11-11-11-11",
  "typeId": "555",
  "family": "FACILITY",
  "systems": "2",
  "licenseFilename": "license_johnsmith@abc.com.xml",
  "url": "https://test-licenses.s3.amazonaws.com/license_johnsmith%40abc.com.xml",
  "dateCreated": "2021-04-09T02:43:39.000+0000",
  "dateUpdated": "2021-04-09T02:43:39.000+0000",
  "user": {
    "id": "00u560lmjop5poy624x6",
    "name": "myname",
    "email": "myname@gmail.com"
  },
  "attributes": [
    "CONFIG_MSC_PARAMETERS",
    "REPORTING"
  ]
}

Here is the updated License entity, with attributes added as a one-to-many List:

@EqualsAndHashCode
@Data
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
@Table(name = "licenses")
public class License {

    @Id
    @GeneratedValue
    private Long id;
    @NonNull
    private String fullName;
    private String contact;
    private String requester;
    private String tag;
    private String company;
    private String companyId;
    private String template;
    private String product;
    private String expiration;
    private String macs;
    private String typeId;
    private String family;
    private String systems;


    @ManyToOne(cascade = CascadeType.PERSIST)
    private User user;

    @OneToMany(targetEntity = Attribute.class,cascade = CascadeType.ALL)
    @JoinColumn(name ="license_fk",referencedColumnName = "id")
    private List<Attribute> attributes;

    // getters, setters
    ...    
 
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public List<Attribute> getAttributes() {
        return attributes;
    }

    public void setAttributes(List<Attribute> attributes) {
        this.attributes = attributes;
    }
}

License Repository (no change):

package com.license.gen.app.model;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;


public interface LicenseRepository extends JpaRepository<License, Long>
{
    License findByFullName(String fullName);
    List<License> findAllByUserId(String id);
    Page<License> findAll(Pageable pageable);


    @Query("SELECT l FROM License l " +
            "WHERE l.company LIKE %:company% " +
            "OR l.macs LIKE %:macs% " +
            "OR l.requester LIKE %:requester% " +
            "OR l.tag LIKE %:tag% " +
            "OR l.fullName LIKE %:fullName% " +
            "OR l.template LIKE %:template% " +
            "OR l.expiration LIKE %:expiration% " +
            "OR l.family LIKE %:family% " +
            "OR l.licenseFilename LIKE %:filename% " +
            "OR l.product LIKE %:product%"
    )
    List<License> findBySearchString(
            @Param("company") String company,
            @Param("macs") String macs,
            @Param("requester") String requester,
            @Param("tag") String tag,
            @Param("fullName") String fullName,
            @Param("template") String template,
            @Param("expiration") String expiration,
            @Param("family") String family,
            @Param("filename") String filename,
            @Param("product") String product);


    @Query("SELECT l FROM License l " +
            "WHERE l.macs LIKE %:macs%"
    )
    List<License> findByMacs(
            @Param("macs") String macs);


    @Query("SELECT l FROM License l " +
            "WHERE l.fullName LIKE %:fullName%"
    )
    List<License> findMatchesByFullName(
            @Param("fullName") String fullName);
}

Attribute Entity (new):

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Attribute {

    @Id
    private Long id;

    @NonNull
    private String attribute;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCreated = new Date();

    @ManyToOne(cascade = CascadeType.PERSIST)
    private License license;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAttribute() {
        return attribute;
    }

    public void setAttribute(String attribute) {
        this.attribute = attribute;
    }

    public Date getDateCreated() {
        return dateCreated;
    }

    public void setDateCreated(Date dateCreated) {
        this.dateCreated = dateCreated;
    }
}

Attribute Repository (new):

public interface AttributeRepository extends JpaRepository<User, Long> {

}

And the License Controller:

    @RestController
    @RequestMapping("/api")
    class LicenseController {
    
        private final Logger log = LoggerFactory.getLogger(LicenseController.class);
    
        private LicenseRepository licenseRepository;
        private UserRepository userRepository;
        private AttributeRepository attributeRepository;
    
        public static String bucket;
    
        public LicenseController(LicenseRepository licenseRepository,
                                 UserRepository userRepository,
                                 AttributeRepository attributeRepository) {
    
            this.licenseRepository = licenseRepository;
            this.userRepository = userRepository;
            this.attributeRepository = attributeRepository;
        }
    
    
.....    
    
    
        @PostMapping("/license")
        ResponseEntity<LicensePojo> createLicense(@Valid @RequestBody License license,
                                                  @AuthenticationPrincipal OAuth2User principal) throws URISyntaxException {
    
            log.info("Request to create license: {}", license);
            Map<String, Object> details = principal.getAttributes();
            String userId = details.get("sub").toString();
    
            // check to see if user already exists
            Optional<User> user = userRepository.findById(userId);
    
            license.setUser(user.orElse(new User(userId,
                    details.get("name").toString(), details.get("email").toString())));
    
    
            if(license.getLicenseFilename() == null){
                license.setLicenseFilename("");
            }
    
            License result = licenseRepository.save(license);
    
    
            User myUser = license.getUser();
    
            // Generate the license
            LicensePojo licensePojo = new LicensePojo(result);
    
    
            String fileName = GenLicense.genLicense(licensePojo);
    
            AmazonS3Utils.putObject(fileName);
            AmazonS3Utils.setToFileDownload(fileName);
            AmazonS3Utils.setObjectPublic(fileName);
    
    
            result.setLicenseFilename(fileName);
    
            String url = AmazonS3Utils.getUrl(fileName).toString();
            result.setUrl(url);
    
    
            String origTypeId = String.valueOf(result.getTypeId());
            String origId = String.valueOf(result.getId());
    
            if ((origTypeId == null) || origTypeId.equalsIgnoreCase("")){
                result.setTypeId(origId);
            }
    
            result = licenseRepository.save(result);
    
            return ResponseEntity.created(new URI("/api/license/" + result.getId()))
                    .body(licensePojo);
    
        }
    
        @PutMapping("/license/{id}")
        ResponseEntity<License> updateLicense(@Valid @RequestBody License license) {
    
            List<Attribute> attributes = license.getAttributes();
    
            License result = licenseRepository.save(license);
    
            LicensePojo licensePojo = new LicensePojo(result);
    
            String fileName = GenLicense.genLicense(licensePojo);
    
            AmazonS3Utils.putObject(fileName);
    
            AmazonS3Utils.setToFileDownload(fileName);
    
            AmazonS3Utils.setObjectPublic(fileName);
    
            String url = AmazonS3Utils.getUrl(fileName).toString();
    
            result.setUrl(url);
            result.setLicenseFilename(fileName);
        
            return ResponseEntity.ok().body(result);
        }
    
...
    }

As far as I can see, there are no error messages being generated. The IDE is showing the AttributeRepository isn't being used in the controller, but they may be because it's part of the underlying SpringData JPA code to implement it.

Any ideas what the problem might be?

Jack BeNimble
  • 35,733
  • 41
  • 130
  • 213
  • 1
    The json is not consistent with your data model - attributes is a list of string and not a list of "attributes". – xerx593 Jun 01 '21 at 23:39
  • Yes, that's definitely the problem. But how do I model it? The attributes are just a list of strings, but they need to be a separate table. What's the solution? Create a DTO with all the fields and a list of strings, then create the attributes separately and add them to the license? Or create a constructor for the License that accepts a list of Strings and creates attributes like that? – Jack BeNimble Jun 02 '21 at 14:40
  • But how do I model it? - it depends on your requirements (table structure) & restrictions (interface,..). Simplest/best, as i see, would be to model the Attributes as a 1-column-string-PK-table ("LOV",then its just fine with your json), but if you *need* the "technical fields" (id, created), then *you*'d have to fill/supply them from the incoming json in some "transformation step". (constructor sounds tempting, but rather an extra component/"mapper"/builder/factory) – xerx593 Jun 02 '21 at 16:03
  • That sounds good, but doesn't the table need an ID at least? Maybe not? – Jack BeNimble Jun 02 '21 at 16:05
  • the id must be "serializable" (due to jpa) so, a Sting id is totally ok/usual (think of UUID)! And i don't know any db-system, that make any restrictions on the data type of the PK colzumn(s)...^^ – xerx593 Jun 02 '21 at 16:09
  • 1
    Also nice (Q/A): https://stackoverflow.com/q/45401734/592355 !! – xerx593 Jun 02 '21 at 16:20

0 Answers0