Java Bean Validierung und React

Eine der Aufgaben einer professionellen Webanwendung ist die Validierung der Formular-Eingabefelder. Zum einen um der Business-Logik zu entsprechen, um Datenbank-Limits einzuhalten, und nicht zuletzt auch aus Security-Gründen.

Wenigstens aus dem letztgenannten Grund muss diese Validierung daher zwingend (auch) auf dem Server durchgeführt werden. Außerdem ist es natürlich auch sinnvoll, alle Constraints an einer Stelle zu konfigurieren; die Datenbank-Spaltendefinition und die Validierungs-Constraints an dem selben Property.

Spannend ist nun, diese Validierung auch in einer Java Single Page Applikation mit React zu verwenden.

Definition der Beans

Zur Definition von Persistenz verwende ich Jakarta Persistence API (JPA), und zur Angabe der Validierung die Jakarta Validation. Damit sieht meine Bean folgendermaßen aus:

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@Entity
@Table(name = "kunde")
@Data
public class KundeDto {

    @Column(nullable = false)
    @NotBlank
    private String name;

    @Column(nullable = false)
    @Pattern(regexp = "\\S+@\\S+", message = "ist keine gültige E-Mail-Adresse")
    private String email;

    @Column(nullable = false)
    @Pattern(regexp = "0[0-9]+", message = "ist keine gültige Telefonnummer")
    private String telefonnummer;
}

Als nächstens kommen wir zur Definition der Rest-Schnittstelle, an der insbesondere beim Schreibenden Zugriff die Validierung durchgeführt werden muss:

@Path("/customer")
public class KundenService {
    
    @PersistenceContext
    private EntityManager entityManager;

    @GET
    @RolesAllowed("user")
    @Produces(MediaType.APPLICATION_JSON)
    public KundeDto get(@QueryParam("id") int id) {
        return entityManager.find(KundeDto.class, id);
    }

    @POST
    @Transactional
    @RolesAllowed("user")
    @Produces(MediaType.APPLICATION_JSON)
    public void save(KundeDto kundeDto) {
        validate(kundeDto);
        entityManager.merge(kundeDto);
    }

    private void validate(Object object) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<Object>> result = validator.validate(object);
        if (!result.isEmpty()) {
            Map<String, String> validationErrors = new HashMap<>();
            result.forEach(objectConstraintViolation -> validationErrors
                    .put(objectConstraintViolation.getPropertyPath().toString(),
                         objectConstraintViolation
.getMessage()));
            throw new BadRequestException(Response.ok(validationErrors).status(Response.Status.BAD_REQUEST).build());
        }
    }
}

UI

Auf React-Seite erweitern wir die bereits vorgestellte LabeledInput-Komponente:

class LabeledInput extends Component {
  constructor(props) {
    super(props);
    
    this.state = { value: props.value, validationError: props.validationError };
    this.id = props.id;
    if (!this.state.value[this.id]) {
      this.state.value[this.id] = "";
    }
    if (!this.state.validationError || !this.state.validationError[this.id]) {
      this.state.validationError = { };
      this.state.validationError[this.id] = false;
    }
    this.handleChange = this.handleChange.bind(this);
    this.isInvalid = this.isInvalid.bind(this);
    this.label = props.label;
    this.size = props.size ?? 40;
    this.styleClass = props.styleClass;
  }
  handleChange(e) {
    this.state.value[this.id] = e.target.value;
    this.state.validationError[this.id] = false;
    this.setState({});
  }
  static getDerivedStateFromProps(props, state) {
    if (props.value !== state.value || props.validationError != state.validationError) {
      var validationError = props.validationError;
      if (!validationError || !validationError[props.id]) {
        validationError = { };
        validationError[props.id] = false;
      }
      return {value: props.value, validationError: validationError};
    }
    return null;
  }
  isInvalid(className) {
    if (this.state.validationError[this.id]) {
      return className;
    }
    return "";
  }
  getFormErrorMessage() {
    if (this.state.validationError[this.id]) {
      return <small className="p-error">{this.state.validationError[this.id]}</small>;
    } else {
      return;
    }
  }
  render() {
    return (
        <div className={this.styleClass + " p-field"}>
          <label htmlFor={this.state.value.id + this.id} className={"p-d-block " + this.isInvalid('p-error')}>{this.label}</label>
          <input type="text" id={this.state.value.id + this.id} className={"p-d-block " + this.isInvalid('p-invalid')} size={this.size}
              value={this.state.value[this.id]} onChange={this.handleChange} />
          {this.getFormErrorMessage()}
        </div>);
  }
}

Die Verwendung gestaltet sich dann folgendermaßen:

export class CustomerPage extends Component {
  constructor(props) {
    super(props);
    this.state = { model: { }, validationError: { } };
    this.persist = this.persist.bind(this);
  }
  componentDidMount() {
    axios.get('/customer')
    .then(response => response.data)
    .then(model=> this.setState({ model}));
  }
  handleValidationError(error) {
    if (error.response && error.response.status == 400 && error.response.data) {
      this.setState({ validationError: error.response.data});
    }
  }
  persist(e) {
    axios.post('/customer', this.state.model)
    .catch(this.handleValidationError);
  }
  render(){
    return (
      <div>
        <LabeledInput id="name" label="Name" value={this.state.model} validationError={this.state.validationError} />
        <LabeledInput id="email" label="E-Mail-Adresse" value={this.state.model} validationError={this.state.validationError} />
        <LabeledInput id="telefon" label="Telefonnummer" value={this.state.model} validationError={this.state.validationError} />
        <button onClick={this.persist}>Speichern</button>
      </div>);
  }
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert