Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Add ConstraintViolation API #1128

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,34 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.api.exceptions;

import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@NonnullByDefault
public class ConstraintViolation {
private final String message;
private final String errorCode;
private final String path;
private final Object validatedObject;
private final Object invalidValue;
@Builder.Default private final Map<String, Object> additionalAttributes = Map.of();
}
@@ -0,0 +1,47 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.api.exceptions;

import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

/**
* Holder for additional {@link ConstraintViolation} metadata when performing constraint validation.
* A request-scoped bean of this class should be registered for use with an exception handler.
*/
@NonnullByDefault
public interface ConstraintViolationContext {
void addViolation(ConstraintViolation violation);

default Optional<ConstraintViolation> removeMatchingViolation(
Predicate<ConstraintViolation> predicate) {
Iterator<ConstraintViolation> iterator = getConstraintViolations().iterator();
while (iterator.hasNext()) {
ConstraintViolation violation = iterator.next();
if (predicate.test(violation)) {
iterator.remove();
return Optional.of(violation);
}
}
return Optional.empty();
}

List<ConstraintViolation> getConstraintViolations();
}
@@ -0,0 +1,37 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.api.exceptions;

import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;

/**
* Default implementation of {@link ConstraintViolationContext}. This should be registered as a
* request-scoped bean so that a request exception handler can use it.
*/
@Getter
@NonnullByDefault
public class DefaultConstraintViolationContext implements ConstraintViolationContext {
private final List<ConstraintViolation> constraintViolations = new ArrayList<>();

@Override
public void addViolation(ConstraintViolation violation) {
constraintViolations.add(violation);
}
}
1 change: 1 addition & 0 deletions kork-web/kork-web.gradle
Expand Up @@ -17,6 +17,7 @@ dependencies {
api project(":kork-security")
api project(":kork-exceptions")
api "org.codehaus.groovy:groovy"
api "org.springframework.boot:spring-boot-starter-validation"
api "org.springframework.boot:spring-boot-starter-web"
api "org.springframework.boot:spring-boot-starter-webflux"
api "org.springframework.boot:spring-boot-starter-security"
Expand Down
Expand Up @@ -16,8 +16,11 @@

package com.netflix.spinnaker.config

import com.netflix.spinnaker.kork.api.exceptions.ConstraintViolationContext
import com.netflix.spinnaker.kork.api.exceptions.DefaultConstraintViolationContext
import com.netflix.spinnaker.kork.api.exceptions.ExceptionMessage
import com.netflix.spinnaker.kork.web.controllers.GenericErrorController
import com.netflix.spinnaker.kork.web.exceptions.ConstraintViolationAdvice
import com.netflix.spinnaker.kork.web.exceptions.ExceptionMessageDecorator
import com.netflix.spinnaker.kork.web.exceptions.ExceptionSummaryService
import com.netflix.spinnaker.kork.web.exceptions.GenericExceptionHandlers
Expand All @@ -27,10 +30,22 @@ import org.springframework.boot.web.servlet.error.DefaultErrorAttributes
import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.context.annotation.RequestScope
import org.springframework.web.context.request.WebRequest

@Configuration
class ErrorConfiguration {
@Bean
@RequestScope
ConstraintViolationContext constraintViolationContext() {
new DefaultConstraintViolationContext()
}

@Bean
ConstraintViolationAdvice constraintViolationAdvice(ConstraintViolationContext contextProvider) {
new ConstraintViolationAdvice(contextProvider)
}

@Bean
ErrorAttributes errorAttributes() {
final DefaultErrorAttributes defaultErrorAttributes = new DefaultErrorAttributes()
Expand Down
@@ -0,0 +1,94 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.web.exceptions;

import com.netflix.spinnaker.kork.api.exceptions.ConstraintViolationContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
@RequiredArgsConstructor
public class ConstraintViolationAdvice {
private final ConstraintViolationContext context;

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ConstraintViolationResponse> handleConstraintViolationException(
ConstraintViolationException e) {
ConstraintViolationResponse response = new ConstraintViolationResponse();
List<ConstraintViolationError> errors = combine(e.getConstraintViolations(), context);
response.setErrors(errors);
return ResponseEntity.badRequest().body(response);
}

private static List<ConstraintViolationError> combine(
Set<ConstraintViolation<?>> constraintViolations, ConstraintViolationContext context) {
var errors =
constraintViolations.stream()
.map(
constraintViolation ->
context
.removeMatchingViolation(
violation -> violationsMatch(constraintViolation, violation))
.map(
violation ->
ConstraintViolationError.builder()
.message(violation.getMessage())
.errorCode(violation.getErrorCode())
.path(constraintViolation.getPropertyPath().toString())
.additionalAttributes(violation.getAdditionalAttributes())
.build())
.orElseGet(() -> translate(constraintViolation)))
.collect(Collectors.toCollection(ArrayList::new));
context.getConstraintViolations().stream()
.map(ConstraintViolationAdvice::translate)
.forEach(errors::add);
return errors;
}

private static boolean violationsMatch(
ConstraintViolation<?> constraintViolation,
com.netflix.spinnaker.kork.api.exceptions.ConstraintViolation violation) {
return constraintViolation.getLeafBean() == violation.getValidatedObject()
&& constraintViolation.getMessageTemplate().equals(violation.getErrorCode());
}

private static ConstraintViolationError translate(ConstraintViolation<?> violation) {
return ConstraintViolationError.builder()
.message(violation.getMessage())
.errorCode(violation.getMessageTemplate())
.path(violation.getPropertyPath().toString())
.build();
}

private static ConstraintViolationError translate(
com.netflix.spinnaker.kork.api.exceptions.ConstraintViolation violation) {
return ConstraintViolationError.builder()
.message(violation.getMessage())
.errorCode(violation.getErrorCode())
.path(violation.getPath())
.additionalAttributes(violation.getAdditionalAttributes())
.build();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.web.exceptions;

import java.util.Map;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

/** Summarizes a {@link javax.validation.ConstraintViolation}. */
@Getter
@ToString
@Builder
public class ConstraintViolationError {
private final String message;
private final String errorCode;
private final String path;
@Builder.Default private final Map<String, Object> additionalAttributes = Map.of();
}
@@ -0,0 +1,30 @@
/*
* Copyright 2023 Apple Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.kork.web.exceptions;

import java.time.Instant;
import java.util.List;
import lombok.Getter;
import lombok.Setter;

/** Summarizes a {@link javax.validation.ConstraintViolationException}. */
@Getter
@Setter
public class ConstraintViolationResponse {
private Instant timestamp = Instant.now();
private List<ConstraintViolationError> errors;
}