Submit Spring Form using Ajax where Entity has For

2019-08-08 14:53发布

问题:

Here's my entity:

@Entity
@Table( name = "tbl_license" )
public class License implements Serializable
{
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue
  @Column( name = "id", nullable = false, columnDefinition = "serial" )
  private int id;

  @Column( name = "key", nullable = false, columnDefinition = "text" )
  private String key;

  @Column( name = "users", columnDefinition = "text" )
  private String users;

  @Transient
  private final SimpleDateFormat dateFormat = new SimpleDateFormat( "MM/dd/yyyy" );

  @Column( name = "valid_from" )
  @Temporal( TemporalType.DATE )
  private Date from;

  @Column( name = "valid_to" )
  @Temporal( TemporalType.DATE )
  private Date to;

  @Column( name = "volume" )
  private Integer volume;

  @Column( name = "SSL", columnDefinition = "bool default false" )
  private boolean SSL = false;

  @ManyToOne
  @JoinColumn( name = "software_id", columnDefinition = "int" )
  @ForeignKey( name = "tbl_software_fkey" )
  private Software software;

  @ManyToOne
  @JoinColumn( name = "license_type_id", columnDefinition = "int" )
  @ForeignKey( name = "tbl_license_type_fkey" )
  private LicenseType licenseType;

  @ManyToOne
  @JoinColumn( name = "license_class_id", columnDefinition = "int" )
  @ForeignKey( name = "tbl_license_class_fkey" )
  private LicenseClass licenseClass;

  @ManyToOne
  @JoinColumn( name = "license_metric_id", columnDefinition = "int" )
  @ForeignKey( name = "tbl_license_metric_fkey" )
  private LicenseMetric licenseMetric;

  @ManyToOne
  @JoinColumn( name = "location_id", columnDefinition = "int" )
  @ForeignKey( name = "tbl_location_fkey" )
  private Location location;

  @OneToMany( fetch = FetchType.LAZY, mappedBy = "id.license"/*, cascade = { CascadeType.REMOVE }, orphanRemoval = true*/ )
  private List<LicenseUserAlert> alerts;

  public int getId()
  {
    return id;
  }

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

  public String getKey()
  {
    return key;
  }

  public void setKey( String key )
  {
    this.key = key;
  }

  public String getUsers()
  {
    return users;
  }

  public void setUsers( String users )
  {
    this.users = users;
  }

  public String getFrom()
  {
    if ( from == null )
    {
      return null;
    }

    return dateFormat.format( from );
  }

  public Date getFromDate()
  {
    return from;
  }

  public void setFrom( String from )
  {
    try
    {
      this.from = dateFormat.parse( from );
    }
    catch ( ParseException e )
    {
      this.from = null;
    }
  }

  public String getTo()
  {
    if ( to == null )
    {
      return null;
    }

    return dateFormat.format( to );
  }

  public Date getToDate()
  {
    return to;
  }

  public void setTo( String to )
  {
    try
    {
      this.to = dateFormat.parse( to );
    }
    catch ( ParseException e )
    {
      this.to = null;
    }
  }

  @Transient
  public Long getRemaining()
  {
    if ( from == null || to == null )
    {
      return null;
    }

    Date now = new Date();
    Date fromDate = from;
    Date toDate = to;

    if ( fromDate.before( now ) )
    {
      fromDate = now;
    }

    return toDate.getTime() - fromDate.getTime();
  }

  public int getVolume()
  {
    return volume != null ? volume : 0;
  }

  public void setVolume( int volume )
  {
    this.volume = volume;
  }

  public boolean isSSL()
  {
    return SSL;
  }

  public void setSSL( boolean SSL )
  {
    this.SSL = SSL;
  }

  public Software getSoftware()
  {
    return software;
  }

  public void setSoftware( Software software )
  {
    this.software = software;
  }

  public LicenseType getLicenseType()
  {
    return licenseType;
  }

  public void setLicenseType( LicenseType licenseType )
  {
    this.licenseType = licenseType;
  }

  public LicenseClass getLicenseClass()
  {
    return licenseClass;
  }

  public void setLicenseClass( LicenseClass licenseClass )
  {
    this.licenseClass = licenseClass;
  }

  public LicenseMetric getLicenseMetric()
  {
    return licenseMetric;
  }

  public void setLicenseMetric( LicenseMetric licenseMetric )
  {
    this.licenseMetric = licenseMetric;
  }

  public Location getLocation()
  {
    return location;
  }

  public void setLocation( Location location )
  {
    this.location = location;
  }

  public List<LicenseUserAlert> getAlerts()
  {
    return alerts;
  }

  public void setAlerts( List<LicenseUserAlert> alerts )
  {
    this.alerts = alerts;
  }
}

And here are the controller methods:

  @RequestMapping( value = "/save.html", params = "json", method = RequestMethod.POST )
  public @ResponseBody Map<String, ? extends Object> saveJSON( @RequestBody License license, HttpServletRequest request, HttpServletResponse response )
  {
    Map<String, String> validationMessages = new HashMap<String, String>();
    for ( FieldError error : getFieldErrors( license ) )
    {
      validationMessages.put( error.getField(), error.getDefaultMessage() );
    }

    Set<ConstraintViolation<License>> constraintViolations = validator.validate( license );
    if ( !validationMessages.isEmpty() || !constraintViolations.isEmpty() )
    {
      for ( ConstraintViolation<License> constraintViolation : constraintViolations )
      {
        String violationMessage = constraintViolation.getMessage();

        try
        {
          violationMessage = messageSource.getMessage( constraintViolation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName() + ".license." + constraintViolation.getPropertyPath().toString(), null, null );
        }
        catch ( Exception e )
        {
        }

        validationMessages.put( constraintViolation.getPropertyPath().toString(), violationMessage );
      }

      return Collections.singletonMap( "validationMessages", validationMessages );
    }

    service.save( license );
    return Collections.singletonMap( "redirect", request.getContextPath() + "/index.html" );
  }

  @RequestMapping( value = "/save.html", method = RequestMethod.POST )
  public String save( @ModelAttribute( "license" ) @Valid License license, BindingResult result, Map<String, Object> map )
  {
    for ( FieldError error : getFieldErrors( license ) )
    {
      result.addError( error );
    }

    if ( result.hasErrors() )
    {
      map.put( "softwareList", softwareService.list() );
      map.put( "licenseTypeList", licenseTypeService.list() );
      map.put( "licenseClassList", licenseClassService.list() );
      map.put( "licenseMetricList", licenseMetricService.list() );
      map.put( "locationList", locationService.list() );
      return "license.form";
    }

    service.save( license );
    return "redirect:/index.html";
  }

And for completeness, here's the form:

  <c:url value="/licenses/save.html" var="formAction" />
  <form:form modelAttribute="license" action="${formAction}" method="post">
    <form:errors path="*" cssClass="error" element="div" />
    <form:hidden path="id" />
    <div>
      <form:label path="software.id">Software*:</form:label>
      <form:select path="software.id" items="${softwareList}" itemValue="id" itemLabel="title" />
    </div>
    <div>
      <form:label path="key">License key*:</form:label>
      <form:input path="key" />
    </div>
    <div>
      <form:label path="users">Installied at:</form:label>
      <form:input path="users" />
    </div>
    <div>
      <form:label path="licenseType">Type*:</form:label>
      <form:select id="licenseType" path="licenseType.id" items="${licenseTypeList}" itemValue="id" itemLabel="title" />
    </div>
    <div>
      <span id="licenseType1" class="typeAttr">
        <form:label path="volume">Volume*:</form:label>
        <form:input type="number" path="volume" />
      </span>
      <span id="licenseType2" class="typeAttr">
        <form:label path="location">Location*:</form:label>
        <form:select path="location.id" items="${locationList}" itemValue="id" itemLabel="title" />
      </span>
      <span id="licenseType3" class="typeAttr">
        <form:label path="from">Valid from*:</form:label>
        <form:input type="date" path="from" />
        <form:label path="to">to*:</form:label>
        <form:input type="date" path="to" />
      </span>
    </div>
    <div>
      <form:label path="licenseClass">Class*:</form:label>
      <form:select path="licenseClass.id" items="${licenseClassList}" itemValue="id" itemLabel="title" />
    </div>
    <div>
      <form:label path="licenseMetric">Metric*:</form:label>
      <form:select path="licenseMetric.id" items="${licenseMetricList}" itemValue="id" itemLabel="title" />
    </div>
    <input type="submit" value="Submit" />
  </form:form>

First method is for ajax submitting, second one as fallback/for testing without ajax. Without ajax is works great, but using the first method results in a 400 Bad Request: The request sent by the client was syntactically incorrect ().

It seems it has something to do with the other entities (class, metric, location, ...) I already tried to add for all those entities a @RequestBody param to the saveJSON method - but that didn't worked (same result)

EDIT

And here's the post body (this is a well formed json object, so i dunno why it is syntactically incorrect)

{
  "id":"0",
  "software.id":"5",
  "key":"12345",
  "users":"a, b, c",
  "licenseType.id":"1",
  "volume":"2",
  "from":"",
  "to":"",
  "licenseClass.id":"1",
  "licenseMetric.id":"1"
}

回答1:

I think you should implement a Converter that is able to convert a String representing the id to an instance of LicenseMetric by loading it from the database. (And other converters for the other entities to).

Then you should replace the parameter "licenseMetric.id" by "licenseMetric".

sketch of an converter

import org.springframework.core.convert.converter.Converter;

public class StringIdToLicenseMetricConverter implements  Converter<String, LicenseMetric> {   
    /** The string that represents null. */
    private static final String NULL_REPRESENTATION = "null";

    /** Generic dao. */
    @Resource
    private LicenseMetricDao licenseMetricDao;

    @Override
    public LicenseMetric convert(final String idString) {
        if (idString.equals(NULL_REPRESENTATION)) {
                return null;
        }
        try {
          int id = Integer.parse(idString);
          return this.licenseMetricDao.getById(id, this.businessClass);
        }
        catch NumberFormatException(e) {
            throw new RuntimeException("could not convert `" + idString + "` to an valid id");
        }
    }
}

@See Spring Reference Chapter 6.5.1 Converter SPI