
com.github.mcollovati.vertx.vaadin.connect.BaseVaadinConnectEndpointService Maven / Gradle / Ivy
/*
* The MIT License
* Copyright © 2016-2020 Marco Collovati ([email protected])
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.mcollovati.vertx.vaadin.connect;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.googlecode.gentyref.GenericTypeReflector;
import com.vaadin.flow.internal.CurrentInstance;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.hilla.EndpointRegistry;
import com.vaadin.hilla.ExplicitNullableTypeChecker;
import com.vaadin.hilla.exception.EndpointException;
import com.vaadin.hilla.exception.EndpointValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNullApi;
import com.github.mcollovati.vertx.vaadin.connect.auth.VaadinConnectAccessChecker;
/**
* Basic implementation if {@link VaadinConnectEndpointService}.
*
* Subclasses will provide support for technology specific information
* by providing a {@link EndpointServiceContext} implementation
* and by adapting incoming request to the corret {@link VaadinRequest} type.
*
*
* Source code adapted from Vaadin Flow (https://github.com/vaadin/flow)
*/
public abstract class BaseVaadinConnectEndpointService<
REQUEST, RESPONSE, CTX extends EndpointServiceContext>
implements VaadinConnectEndpointService {
private static final Logger LOGGER = LoggerFactory.getLogger(VertxVaadinConnectEndpointService.class);
private static Logger getLogger() {
return LOGGER;
}
private final Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
private final ObjectMapper vaadinEndpointMapper;
private final VaadinConnectAccessChecker accessChecker;
private final ExplicitNullableTypeChecker explicitNullableTypeChecker;
final VaadinEndpointRegistry endpointRegistry;
protected BaseVaadinConnectEndpointService(
ObjectMapper vaadinEndpointMapper,
VaadinEndpointRegistry endpointRegistry,
VaadinConnectAccessChecker accessChecker,
ExplicitNullableTypeChecker explicitNullableTypeChecker) {
this.vaadinEndpointMapper =
vaadinEndpointMapper != null ? vaadinEndpointMapper : createVaadinConnectObjectMapper();
this.accessChecker = accessChecker;
this.explicitNullableTypeChecker = explicitNullableTypeChecker;
this.endpointRegistry = endpointRegistry;
}
protected abstract ObjectMapper createVaadinConnectObjectMapper();
@Override
public RESPONSE serveEndpoint(String endpointName, String methodName, CTX context) {
getLogger()
.debug("Endpoint: {}, method: {}, request body: {}", endpointName, methodName, context.requestBody());
EndpointRegistry.VaadinEndpointData vaadinEndpointData = endpointRegistry.get(endpointName);
if (vaadinEndpointData == null) {
getLogger().debug("Endpoint '{}' not found", endpointName);
return context.failWithNotFound();
}
Method methodToInvoke = vaadinEndpointData.getMethod(methodName).orElse(null);
if (methodToInvoke == null) {
getLogger().debug("Method '{}' not found in endpoint '{}'", methodName, endpointName);
return context.failWithNotFound();
}
try {
// Put a VaadinRequest in the instances object so as the request is
// available in the end-point method
VaadinService service = VaadinService.getCurrent();
CurrentInstance.set(VaadinRequest.class, createVaadinRequest(context.request(), service));
return invokeVaadinEndpointMethod(endpointName, methodName, methodToInvoke, vaadinEndpointData, context);
} catch (JsonProcessingException e) {
/*
String errorMessage = String.format(
"Failed to serialize endpoint '%s' method '%s' response. "
+ "Double check method's return type or specify a custom mapper bean with qualifier '%s'",
endpointName, methodName,
VAADIN_ENDPOINT_MAPPER_BEAN_QUALIFIER);*/
String errorMessage = String.format(
"Failed to serialize endpoint '%s' method '%s' response. Double check method's return type",
endpointName, methodName);
getLogger().error(errorMessage, e);
try {
return context.failWith(createResponseErrorObject(errorMessage));
} catch (JsonProcessingException unexpected) {
throw new IllegalStateException(
String.format(
"Unexpected: Failed to serialize a plain Java string '%s' into a JSON. "
+ "Double check the provided mapper's configuration.",
errorMessage),
unexpected);
}
} finally {
CurrentInstance.set(VaadinRequest.class, null);
}
}
protected abstract VaadinRequest createVaadinRequest(REQUEST request, VaadinService service);
private String createResponseErrorObject(String errorMessage) throws JsonProcessingException {
return vaadinEndpointMapper.writeValueAsString(
Collections.singletonMap(EndpointException.ERROR_MESSAGE_FIELD, errorMessage));
}
private RESPONSE invokeVaadinEndpointMethod(
String endpointName,
String methodName,
Method methodToInvoke,
EndpointRegistry.VaadinEndpointData vaadinEndpointData,
CTX requestContext)
throws JsonProcessingException {
String checkError = accessChecker.check(methodToInvoke, requestContext.request());
if (checkError != null) {
return requestContext.failWithUnauthorized(createResponseErrorObject(String.format(
"Endpoint '%s' method '%s' request cannot be accessed, reason: '%s'",
endpointName, methodName, checkError)));
}
Map requestParameters = getRequestParameters(requestContext.requestBody());
Type[] javaParameters =
getJavaParameters(methodToInvoke, resolveUserClass(vaadinEndpointData.getEndpointObject()));
if (javaParameters.length != requestParameters.size()) {
return requestContext.failWithBadRequest(createResponseErrorObject(String.format(
"Incorrect number of parameters for endpoint '%s' method '%s', " + "expected: %s, got: %s",
endpointName, methodName, javaParameters.length, requestParameters.size())));
}
Object[] vaadinEndpointParameters;
try {
vaadinEndpointParameters =
getVaadinEndpointParameters(requestParameters, javaParameters, methodName, endpointName);
} catch (EndpointValidationException e) {
getLogger().debug("Endpoint '{}' method '{}' received invalid response", endpointName, methodName, e);
return requestContext.failWithBadRequest(vaadinEndpointMapper.writeValueAsString(e.getSerializationData()));
}
Set> methodParameterConstraintViolations = validator
.forExecutables()
.validateParameters(vaadinEndpointData.getEndpointObject(), methodToInvoke, vaadinEndpointParameters);
if (!methodParameterConstraintViolations.isEmpty()) {
return requestContext.failWithBadRequest(
vaadinEndpointMapper.writeValueAsString(new EndpointValidationException(
String.format(
"Validation error in endpoint '%s' method '%s'", endpointName, methodName),
createMethodValidationErrors(methodParameterConstraintViolations))
.getSerializationData()));
}
Object returnValue;
try {
returnValue = methodToInvoke.invoke(vaadinEndpointData.getEndpointObject(), vaadinEndpointParameters);
} catch (IllegalArgumentException e) {
String errorMessage = String.format(
"Received incorrect arguments for endpoint '%s' method '%s'. "
+ "Expected parameter types (and their order) are: '[%s]'",
endpointName, methodName, listMethodParameterTypes(javaParameters));
getLogger().debug(errorMessage, e);
return requestContext.failWithBadRequest(createResponseErrorObject(errorMessage));
} catch (IllegalAccessException e) {
String errorMessage = String.format("Endpoint '%s' method '%s' access failure", endpointName, methodName);
getLogger().error(errorMessage, e);
return requestContext.failWith(createResponseErrorObject(errorMessage));
} catch (InvocationTargetException e) {
return handleMethodExecutionError(endpointName, methodName, requestContext, e);
}
String implicitNullError = this.explicitNullableTypeChecker.checkValueForAnnotatedElement(
returnValue,
methodToInvoke,
isNonNullApi(methodToInvoke.getDeclaringClass().getPackage()));
if (implicitNullError != null) {
EndpointException returnValueException = new EndpointException(String.format(
"Unexpected return value in endpoint '%s' method '%s'. %s",
endpointName, methodName, implicitNullError));
getLogger().error(returnValueException.getMessage());
return requestContext.failWith(
vaadinEndpointMapper.writeValueAsString(returnValueException.getSerializationData()));
}
Set> returnValueConstraintViolations = validator
.forExecutables()
.validateReturnValue(vaadinEndpointData.getEndpointObject(), methodToInvoke, returnValue);
if (!returnValueConstraintViolations.isEmpty()) {
getLogger()
.error(
"Endpoint '{}' method '{}' had returned a value that has validation errors: '{}', this might cause bugs on the client side. Fix the method implementation.",
endpointName,
methodName,
returnValueConstraintViolations);
}
return requestContext.respondWithOk(vaadinEndpointMapper.writeValueAsString(returnValue));
}
private String listMethodParameterTypes(Type[] javaParameters) {
return Stream.of(javaParameters).map(Type::getTypeName).collect(Collectors.joining(", "));
}
private RESPONSE handleMethodExecutionError(
String endpointName, String methodName, CTX requestContext, InvocationTargetException e)
throws JsonProcessingException {
if (EndpointException.class.isAssignableFrom(e.getCause().getClass())) {
EndpointException endpointException = ((EndpointException) e.getCause());
getLogger()
.debug(
"Endpoint '{}' method '{}' aborted the execution",
endpointName,
methodName,
endpointException);
return requestContext.failWithBadRequest(
vaadinEndpointMapper.writeValueAsString(endpointException.getSerializationData()));
} else {
String errorMessage =
String.format("Endpoint '%s' method '%s' execution failure", endpointName, methodName);
getLogger().error(errorMessage, e);
return requestContext.failWith(createResponseErrorObject(errorMessage));
}
}
private boolean isNonNullApi(Package pkg) {
return Stream.of(pkg.getAnnotations())
.anyMatch(ann -> ann.annotationType().getSimpleName().equals(NonNullApi.class.getSimpleName()));
}
private Object[] getVaadinEndpointParameters(
Map requestParameters, Type[] javaParameters, String methodName, String endpointName) {
Object[] endpointParameters = new Object[javaParameters.length];
String[] parameterNames = new String[requestParameters.size()];
requestParameters.keySet().toArray(parameterNames);
Map errorParams = new HashMap<>();
Set> constraintViolations = new LinkedHashSet<>();
for (int i = 0; i < javaParameters.length; i++) {
Type expectedType = javaParameters[i];
try {
Object parameter = vaadinEndpointMapper
.readerFor(vaadinEndpointMapper.getTypeFactory().constructType(expectedType))
.readValue(requestParameters.get(parameterNames[i]));
endpointParameters[i] = parameter;
if (parameter != null) {
constraintViolations.addAll(validator.validate(parameter));
}
} catch (IOException e) {
String typeName = expectedType.getTypeName();
getLogger()
.error(
"Unable to deserialize an endpoint '{}' method '{}' " + "parameter '{}' with type '{}'",
endpointName,
methodName,
parameterNames[i],
typeName,
e);
errorParams.put(parameterNames[i], typeName);
}
}
if (errorParams.isEmpty() && constraintViolations.isEmpty()) {
return endpointParameters;
}
throw getInvalidEndpointParametersException(methodName, endpointName, errorParams, constraintViolations);
}
private EndpointValidationException getInvalidEndpointParametersException(
String methodName,
String endpointName,
Map deserializationErrors,
Set> constraintViolations) {
List validationErrorData =
new ArrayList<>(deserializationErrors.size() + constraintViolations.size());
for (Map.Entry deserializationError : deserializationErrors.entrySet()) {
String message = String.format(
"Unable to deserialize an endpoint method parameter into type '%s'",
deserializationError.getValue());
validationErrorData.add(
new EndpointValidationException.ValidationErrorData(message, deserializationError.getKey()));
}
validationErrorData.addAll(createBeanValidationErrors(constraintViolations));
String message = String.format("Validation error in endpoint '%s' method '%s'", endpointName, methodName);
return new EndpointValidationException(message, validationErrorData);
}
private List createBeanValidationErrors(
Collection> beanConstraintViolations) {
return beanConstraintViolations.stream()
.map(constraintViolation -> new EndpointValidationException.ValidationErrorData(
String.format(
"Object of type '%s' has invalid property '%s' with value '%s', validation error: '%s'",
constraintViolation.getRootBeanClass(),
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue(),
constraintViolation.getMessage()),
constraintViolation.getPropertyPath().toString()))
.collect(Collectors.toList());
}
private List createMethodValidationErrors(
Collection> methodConstraintViolations) {
return methodConstraintViolations.stream()
.map(constraintViolation -> {
String parameterPath = constraintViolation.getPropertyPath().toString();
return new EndpointValidationException.ValidationErrorData(
String.format(
"Method '%s' of the object '%s' received invalid parameter '%s' with value '%s', validation error: '%s'",
parameterPath.split("\\.")[0],
constraintViolation.getRootBeanClass(),
parameterPath,
constraintViolation.getInvalidValue(),
constraintViolation.getMessage()),
parameterPath);
})
.collect(Collectors.toList());
}
private Type[] getJavaParameters(Method methodToInvoke, Type classType) {
return Stream.of(GenericTypeReflector.getExactParameterTypes(methodToInvoke, classType))
.toArray(Type[]::new);
}
private Map getRequestParameters(ObjectNode body) {
Map parametersData = new LinkedHashMap<>();
if (body != null) {
body.fields().forEachRemaining(entry -> parametersData.put(entry.getKey(), entry.getValue()));
}
return parametersData;
}
protected Class> resolveUserClass(Object instance) {
// Check the bean type instead of the implementation type in
// case of e.g. proxies
// Class> beanType = ClassUtils.getUserClass(endpointBean.getClass());
return instance.getClass();
}
}