Merge branch 'i18n-support'

This commit is contained in:
zhou-hao
2021-07-02 11:20:48 +08:00
48 changed files with 1279 additions and 175 deletions

View File

@@ -55,5 +55,5 @@ public @interface TwoFactor {
* @return 错误提示
* @since 3.0.6
*/
String message() default "需要进行双因子验证";
String message() default "validation.verify_code_error";
}

View File

@@ -1,9 +1,12 @@
package org.hswebframework.web.authorization.exception;
import lombok.Getter;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.Set;
/**
* 权限验证异常
*
@@ -11,7 +14,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
* @since 3.0
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
public class AccessDenyException extends RuntimeException {
public class AccessDenyException extends I18nSupportException {
private static final long serialVersionUID = -5135300127303801430L;
@@ -19,16 +22,21 @@ public class AccessDenyException extends RuntimeException {
private String code;
public AccessDenyException() {
this("权限不足,拒绝访问!");
this("error.access_denied");
}
public AccessDenyException(String message) {
super(message);
}
public AccessDenyException(String permission, Set<String> actions) {
super("error.permission_denied", permission, actions);
}
public AccessDenyException(String message, String code) {
this(message, code, null);
}
public AccessDenyException(String message, Throwable cause) {
this(message, "access_denied", cause);
}

View File

@@ -1,9 +1,10 @@
package org.hswebframework.web.authorization.exception;
import lombok.Getter;
import org.hswebframework.web.exception.I18nSupportException;
@Getter
public class AuthenticationException extends RuntimeException {
public class AuthenticationException extends I18nSupportException {
public static String ILLEGAL_PASSWORD = "illegal_password";
@@ -13,6 +14,10 @@ public class AuthenticationException extends RuntimeException {
private final String code;
public AuthenticationException(String code) {
this(code, "error." + code);
}
public AuthenticationException(String code, String message) {
super(message);
this.code = code;

View File

@@ -19,6 +19,7 @@
package org.hswebframework.web.authorization.exception;
import org.hswebframework.web.authorization.token.TokenState;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -29,7 +30,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
* @since 3.0
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class UnAuthorizedException extends RuntimeException {
public class UnAuthorizedException extends I18nSupportException {
private static final long serialVersionUID = 2422918455013900645L;
private final TokenState state;

View File

@@ -253,7 +253,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
.flatMap(this::checkTimeout)
.filterWhen(t -> {
if (t.isNormal()) {
return Mono.error(new AccessDenyException("该用户已在其他地方登陆"));
return Mono.error(new AccessDenyException("error.logged_in_elsewhere"));
}
return Mono.empty();
})

View File

@@ -13,30 +13,30 @@ public enum TokenState implements EnumDict<String> {
/**
* 正常,有效
*/
normal("normal","正常"),
normal("normal","message.token_state_normal"),
/**
* 已被禁止访问
*/
deny("deny", "已被禁止访问"),
deny("deny", "message.token_state_deny"),
/**
* 已过期
*/
expired("expired", "用户未登录"),
expired("expired", "message.token_state_expired"),
/**
* 已被踢下线
* @see AllopatricLoginMode#offlineOther
*/
offline("offline", "用户已在其他地方登录"),
offline("offline", "message.token_state_offline"),
/**
* 锁定
*/
lock("lock", "登录状态已被锁定");
lock("lock", "message.token_state_lock");
private String value;
private final String value;
private String text;
private final String text;
}

View File

@@ -244,7 +244,7 @@ public class RedisUserTokenManager implements UserTokenManager {
return userIsLoggedIn(userId)
.flatMap(r -> {
if (r) {
return Mono.error(new AccessDenyException("已在其他地方登录", TokenState.deny.getValue(), null));
return Mono.error(new AccessDenyException("error.logged_in_elsewhere", TokenState.deny.getValue(), null));
}
return doSign;
});

View File

@@ -0,0 +1,16 @@
error.access_denied=Access Denied
error.permission_denied=Permission Denied [{0}]:{1}
error.logged_in_elsewhere=User logged in elsewhere
error.illegal_password=Bad username or password
error.user_disabled=User is disabled
#
message.token_state_normal=Normal
message.token_state_deny=Login has denied
message.token_state_expired=Login has expired
message.token_state_offline=User logged in elsewhere
message.token_state_lock=User Locked
#
validation.need_two_factor_verify=Two factor verification required
validation.username_must_not_be_empty=Username must not be empty
validation.password_must_not_be_empty=Password must not be empty
validation.verify_code_error=Verification code error

View File

@@ -0,0 +1,16 @@
error.access_denied=权限不足,拒绝访问!
error.permission_denied=当前用户无权限[{0}]:{1}
error.logged_in_elsewhere=该用户已在其他地方登陆
error.illegal_password=用户名或密码错误
error.user_disabled=用户已被禁用
#
message.token_state_normal=正常
message.token_state_deny=已被禁止访问
message.token_state_expired=用户未登录
message.token_state_offline=用户已在其他地方登录
message.token_state_lock=登录状态已被锁定
#
validation.need_two_factor_verify=需要双因子验证
validation.username_must_not_be_empty=用户名不能为空
validation.password_must_not_be_empty=密码不能为空
validation.verify_code_error=验证码错误

View File

@@ -36,7 +36,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
private ResourcesDefinition resources = new ResourcesDefinition();
private DimensionsDefinition dimensions = new DimensionsDefinition();
private String message = "权限不足,拒绝访问";
private String message = "error.access_denied";
private Phased phased;

View File

@@ -7,7 +7,6 @@ import org.hswebframework.web.authorization.annotation.TwoFactor;
import org.hswebframework.web.authorization.exception.NeedTwoFactorException;
import org.hswebframework.web.authorization.twofactor.TwoFactorValidator;
import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -22,7 +21,7 @@ import javax.servlet.http.HttpServletResponse;
@AllArgsConstructor
public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapter {
private TwoFactorValidatorManager validatorManager;
private final TwoFactorValidatorManager validatorManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -45,9 +44,9 @@ public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapte
code = request.getHeader(factor.parameter());
}
if (StringUtils.isEmpty(code)) {
throw new NeedTwoFactorException(factor.message(), factor.provider());
throw new NeedTwoFactorException("validation.need_two_factor_verify", factor.provider());
} else if (!validator.verify(code, factor.timeout())) {
throw new NeedTwoFactorException("验证码错误", factor.provider());
throw new NeedTwoFactorException(factor.message(), factor.provider());
}
}
return super.preHandle(request, response, handler);

View File

@@ -34,7 +34,6 @@ import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuth
import org.hswebframework.web.logging.AccessLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
@@ -85,8 +84,8 @@ public class AuthorizationController {
String username_ = (String) parameters.get("username");
String password_ = (String) parameters.get("password");
Assert.hasLength(username_, "用户名不能为空");
Assert.hasLength(password_, "密码不能为空");
Assert.hasLength(username_, "validation.username_must_not_be_empty");
Assert.hasLength(password_, "validation.password_must_not_be_empty");
Function<String, Object> parameterGetter = parameters::get;
return Mono.defer(() -> {
@@ -101,7 +100,7 @@ public class AuthorizationController {
.publish(eventPublisher)
.then(authenticationManager
.authenticate(Mono.just(new PlainTextUsernamePasswordAuthenticationRequest(username, password)))
.switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD,"密码错误")))
.switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD)))
.flatMap(auth -> {
//触发授权成功事件
AuthorizationSuccessEvent event = new AuthorizationSuccessEvent(auth, parameterGetter);

View File

@@ -8,7 +8,7 @@ public class OAuth2Exception extends BusinessException {
private final ErrorType type;
public OAuth2Exception(ErrorType type) {
super(type.message(), type.name(), type.code());
super(type.message(), type.name(), type.code(), (Object[]) null);
this.type = type;
}

View File

@@ -124,7 +124,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
if (realType == null) {
if (!Modifier.isInterface(beanClass.getModifiers()) && !Modifier.isAbstract(beanClass.getModifiers())) {
realType = beanClass;
}else {
} else {
mapper = defaultMapperFactory.apply(beanClass);
}
}
@@ -172,7 +172,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
return (T) new HashSet<>();
}
throw new NotFoundException("无法初始化实体类:"+beanClass);
throw new NotFoundException("error.cant_create_instance", beanClass);
}
@Override

View File

@@ -5,11 +5,14 @@ import org.hswebframework.ezorm.rdb.events.EventListener;
import org.hswebframework.ezorm.rdb.events.EventType;
import org.hswebframework.ezorm.rdb.mapping.events.MappingContextKeys;
import org.hswebframework.ezorm.rdb.mapping.events.MappingEventTypes;
import org.hswebframework.ezorm.rdb.mapping.events.ReactiveResultHolder;
import org.hswebframework.web.api.crud.entity.Entity;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.validator.CreateGroup;
import org.hswebframework.web.validator.UpdateGroup;
import java.util.List;
import java.util.Optional;
public class ValidateEventListener implements EventListener {
@@ -24,33 +27,49 @@ public class ValidateEventListener implements EventListener {
}
@Override
@SuppressWarnings("all")
public void onEvent(EventType type, EventContext context) {
Optional<ReactiveResultHolder> resultHolder = context.get(MappingContextKeys.reactiveResultHolder);
if (resultHolder.isPresent()) {
resultHolder
.ifPresent(holder -> holder
.before(LocaleUtils
.currentReactive()
.doOnNext(locale -> LocaleUtils.doWith(locale, (l) -> tryValidate(type, context)))
.then()
));
} else {
tryValidate(type, context);
}
}
@SuppressWarnings("all")
public void tryValidate(EventType type, EventContext context) {
if (type == MappingEventTypes.insert_before || type == MappingEventTypes.save_before) {
boolean single = context.get(MappingContextKeys.type).map("single"::equals).orElse(false);
if (single) {
context.get(MappingContextKeys.instance)
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.ifPresent(entity -> entity.tryValidate(CreateGroup.class));
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.ifPresent(entity -> entity.tryValidate(CreateGroup.class));
} else {
context.get(MappingContextKeys.instance)
.filter(List.class::isInstance)
.map(List.class::cast)
.ifPresent(lst -> lst.stream()
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.forEach(e -> ((Entity) e).tryValidate(CreateGroup.class))
);
.filter(List.class::isInstance)
.map(List.class::cast)
.ifPresent(lst -> lst.stream()
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.forEach(e -> ((Entity) e).tryValidate(CreateGroup.class))
);
}
} else if (type == MappingEventTypes.update_before) {
context.get(MappingContextKeys.instance)
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.ifPresent(entity -> entity.tryValidate(UpdateGroup.class));
.filter(Entity.class::isInstance)
.map(Entity.class::cast)
.ifPresent(entity -> entity.tryValidate(UpdateGroup.class));
}
}
}

View File

@@ -1,7 +1,6 @@
package org.hswebframework.web.crud.web;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import io.r2dbc.spi.R2dbcNonTransientException;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.authorization.exception.AccessDenyException;
import org.hswebframework.web.authorization.exception.AuthenticationException;
@@ -10,11 +9,11 @@ import org.hswebframework.web.authorization.token.TokenState;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.NotFoundException;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.logger.ReactiveLogger;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.Ordered;
import org.springframework.context.MessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.core.codec.DecodingException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
@@ -23,10 +22,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MediaTypeNotSupportedStatusException;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.*;
import reactor.core.publisher.Mono;
import javax.validation.ConstraintViolationException;
@@ -40,48 +36,66 @@ import java.util.stream.Collectors;
@Order
public class CommonErrorControllerAdvice {
private final MessageSource messageSource;
public CommonErrorControllerAdvice(MessageSource messageSource) {
this.messageSource = messageSource;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<Object>> handleException(BusinessException e) {
return Mono.just(ResponseMessage.error(e.getCode(), e.getMessage()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
return LocaleUtils
.resolveThrowable(messageSource,
e,
(err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), msg));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<ResponseMessage<?>> handleException(UnsupportedOperationException e) {
return Mono.just(ResponseMessage.error("unsupported", e.getMessage()));
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unsupported", msg)));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Mono<ResponseMessage<TokenState>> handleException(UnAuthorizedException e) {
return Mono.just(ResponseMessage.<TokenState>error(401, "unauthorized", e.getMessage()).result(e.getState()));
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unauthorized", msg)
.result(e.getState())));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.FORBIDDEN)
public Mono<ResponseMessage<?>> handleException(AccessDenyException e) {
return Mono.just(ResponseMessage.error(403, e.getCode(), e.getMessage()));
public Mono<ResponseMessage<Object>> handleException(AccessDenyException e) {
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(403, e.getCode(), e.getMessage()))
;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<ResponseMessage<?>> handleException(NotFoundException e) {
return Mono.just(ResponseMessage.error(404, "not_found", e.getMessage()));
public Mono<ResponseMessage<Object>> handleException(NotFoundException e) {
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(404, "not_found", msg))
;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ResponseMessage<List<ValidationException.Detail>>> handleException(ValidationException e) {
return Mono.just(ResponseMessage.<List<ValidationException.Detail>>error(400, "illegal_argument", e.getMessage())
.result(e.getDetails()));
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage
.<List<ValidationException.Detail>>error(400, "illegal_argument",msg)
.result(e.getDetails()))
;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ResponseMessage<List<ValidationException.Detail>>> handleException(ConstraintViolationException e) {
return handleException(new ValidationException(e.getMessage(), e.getConstraintViolations()));
return handleException(new ValidationException(e.getConstraintViolations()));
}
@ExceptionHandler
@@ -130,6 +144,7 @@ public class CommonErrorControllerAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
public Mono<ResponseMessage<Object>> handleException(TimeoutException e) {
return Mono.just(ResponseMessage.error(504, "timeout", e.getMessage()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
@@ -155,51 +170,70 @@ public class CommonErrorControllerAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ResponseMessage<Object>> handleException(IllegalArgumentException e) {
return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, "illegal_argument", msg))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)))
;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ResponseMessage<Object>> handleException(AuthenticationException e) {
return Mono.just(ResponseMessage.error(400, e.getCode(), e.getMessage()));
return LocaleUtils
.resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, e.getCode(), msg))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e)))
;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Mono<ResponseMessage<Object>> handleException(MediaTypeNotSupportedStatusException e) {
return Mono.just(ResponseMessage
.error(415, "unsupported_media_type", "不支持的请求类型")
.result(e.getSupportedMediaTypes()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
public Mono<ResponseMessage<Object>> handleException(UnsupportedMediaTypeStatusException e) {
return LocaleUtils
.resolveMessageReactive(messageSource, "error.unsupported_media_type")
.map(msg -> ResponseMessage
.error(415, "unsupported_media_type", msg)
.result(e.getSupportedMediaTypes()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e)));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
public Mono<ResponseMessage<Object>> handleException(NotAcceptableStatusException e) {
return Mono.just(ResponseMessage
.error(406, "not_acceptable_media_type", "不支持的响应类型")
.result(e.getSupportedMediaTypes()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
return LocaleUtils
.resolveMessageReactive(messageSource, "error.not_acceptable_media_type")
.map(msg -> ResponseMessage
.error(406, "not_acceptable_media_type", msg)
.result(e.getSupportedMediaTypes()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
public Mono<ResponseMessage<Object>> handleException(MethodNotAllowedException e) {
return Mono.just(ResponseMessage
.error(405, "method_not_allowed", "不支持的请求方法:" + e.getHttpMethod())
.result(e.getSupportedMethods()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
return LocaleUtils
.resolveMessageReactive(messageSource, "error.method_not_allowed")
.map(msg -> ResponseMessage
.error(406, "method_not_allowed", msg)
.result(e.getSupportedMethods()))
.doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException exception) {
if (exception.getMessage().contains("Duplicate")) {
return Mono.just(ResponseMessage.error("存在重复的数据"));
public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException e) {
String code;
if (e.getMessage().contains("Duplicate")) {
code = "error.duplicate_data";
} else {
code = "error.data_error";
log.warn(e.getMessage(), e);
}
log.warn(exception.getMessage(), exception);
return Mono.just(ResponseMessage.error("数据错误"));
return LocaleUtils
.resolveMessageReactive(messageSource, code)
.map(msg -> ResponseMessage.error(400, code, msg));
}
@@ -215,7 +249,10 @@ public class CommonErrorControllerAdvice {
} while (exception != null && exception != e);
return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()));
return LocaleUtils
.resolveThrowable(messageSource,
exception,
(err, msg) -> ResponseMessage.error(400, "illegal_argument", msg));
}
}

View File

@@ -1,14 +1,17 @@
package org.hswebframework.web.crud.web;
import org.hswebframework.web.i18n.WebFluxLocaleFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.WebFilter;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@@ -16,17 +19,24 @@ public class CommonWebFluxConfiguration {
@Bean
@ConditionalOnMissingBean
public CommonErrorControllerAdvice commonErrorControllerAdvice(){
return new CommonErrorControllerAdvice();
public CommonErrorControllerAdvice commonErrorControllerAdvice(MessageSource messageSource) {
return new CommonErrorControllerAdvice(messageSource);
}
@Bean
@ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper",name = "enabled",havingValue = "true",matchIfMissing = true)
@ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConfigurationProperties(prefix = "hsweb.webflux.response-wrapper")
public ResponseMessageWrapper responseMessageWrapper(ServerCodecConfigurer codecConfigurer,
RequestedContentTypeResolver resolver,
ReactiveAdapterRegistry registry){
return new ResponseMessageWrapper(codecConfigurer.getWriters(),resolver,registry);
ReactiveAdapterRegistry registry) {
return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
}
@Bean
public WebFilter localeWebFilter() {
return new WebFluxLocaleFilter();
}
}

View File

@@ -0,0 +1,5 @@
error.unsupported_media_type=Unsupported media type
error.not_acceptable_media_type=Not acceptable media type
error.method_not_allowed=Method not allowed
error.duplicate_data=Duplicate data
error.data_error=Data error

View File

@@ -0,0 +1,5 @@
error.unsupported_media_type=不支持的请求类型
error.not_acceptable_media_type=不支持的媒体类型
error.method_not_allowed=不支持的请求方法
error.duplicate_data=重复的数据
error.data_error=数据错误

View File

@@ -35,7 +35,6 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
@@ -97,5 +96,10 @@
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -8,7 +8,6 @@ import com.alibaba.fastjson.parser.JSONToken;
import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
import com.alibaba.fastjson.serializer.JSONSerializable;
import com.alibaba.fastjson.serializer.JSONSerializer;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -21,6 +20,7 @@ import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.StringUtils;
@@ -118,7 +118,7 @@ public interface EnumDict<V> extends JSONSerializable {
}
/**
* 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link this#getText()}
* 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link EnumDict#getText()}
*
* @return 描述
*/
@@ -127,7 +127,6 @@ public interface EnumDict<V> extends JSONSerializable {
}
/**
* 从指定的枚举类中查找想要的枚举,并返回一个{@link Optional},如果未找到,则返回一个{@link Optional#empty()}
*
@@ -150,8 +149,8 @@ public interface EnumDict<V> extends JSONSerializable {
static <T extends Enum & EnumDict> List<T> findList(Class<T> type, Predicate<T> predicate) {
if (type.isEnum()) {
return Arrays.stream(type.getEnumConstants())
.filter(predicate)
.collect(Collectors.toList());
.filter(predicate)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
@@ -159,16 +158,18 @@ public interface EnumDict<V> extends JSONSerializable {
/**
* 根据枚举的{@link EnumDict#getValue()}来查找.
*
* @see this#find(Class, Predicate)
* @see EnumDict#find(Class, Predicate)
*/
static <T extends Enum & EnumDict<?>> Optional<T> findByValue(Class<T> type, Object value) {
return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value)));
return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String
.valueOf(e.getValue())
.equalsIgnoreCase(String.valueOf(value)));
}
/**
* 根据枚举的{@link EnumDict#getText()} 来查找.
*
* @see this#find(Class, Predicate)
* @see EnumDict#find(Class, Predicate)
*/
static <T extends Enum & EnumDict> Optional<T> findByText(Class<T> type, String text) {
return find(type, e -> e.getText().equalsIgnoreCase(text));
@@ -177,7 +178,7 @@ public interface EnumDict<V> extends JSONSerializable {
/**
* 根据枚举的{@link EnumDict#getValue()},{@link EnumDict#getText()}来查找.
*
* @see this#find(Class, Predicate)
* @see EnumDict#find(Class, Predicate)
*/
static <T extends Enum & EnumDict> Optional<T> find(Class<T> type, Object target) {
return find(type, v -> v.eq(target));
@@ -203,8 +204,8 @@ public interface EnumDict<V> extends JSONSerializable {
if (all.length >= 64) {
List<T> list = Arrays.asList(t);
return Arrays.stream(all)
.map(EnumDict.class::cast)
.anyMatch(list::contains);
.map(EnumDict.class::cast)
.anyMatch(list::contains);
}
return maskIn(toMask(t), target);
}
@@ -253,24 +254,32 @@ public interface EnumDict<V> extends JSONSerializable {
/**
* @return 是否在序列化为json的时候, 将枚举以对象方式序列化
* @see this#DEFAULT_WRITE_JSON_OBJECT
* @see EnumDict#DEFAULT_WRITE_JSON_OBJECT
*/
default boolean isWriteJSONObjectEnabled() {
return DEFAULT_WRITE_JSON_OBJECT;
}
default String getI18nCode() {
return getText();
}
default String getI18nMessage(Locale locale) {
return LocaleUtils.resolveMessage(getI18nCode(), locale, getText());
}
/**
* 当{@link this#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象
* 当{@link EnumDict#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象
*
* @return 最终序列化的值
* @see this#isWriteJSONObjectEnabled()
* @see EnumDict#isWriteJSONObjectEnabled()
*/
@JsonValue
default Object getWriteJSONObject() {
if (isWriteJSONObjectEnabled()) {
Map<String, Object> jsonObject = new HashMap<>();
jsonObject.put("value", getValue());
jsonObject.put("text", getText());
jsonObject.put("text", getI18nMessage(LocaleUtils.current()));
// jsonObject.put("index", index());
// jsonObject.put("mask", getMask());
return jsonObject;
@@ -280,7 +289,7 @@ public interface EnumDict<V> extends JSONSerializable {
}
@Override
default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) throws IOException {
default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) {
if (isWriteJSONObjectEnabled()) {
jsonSerializer.write(getWriteJSONObject());
} else {
@@ -295,7 +304,7 @@ public interface EnumDict<V> extends JSONSerializable {
@AllArgsConstructor
@NoArgsConstructor
class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer {
private Function<Object,Object> mapper;
private Function<Object, Object> mapper;
@Override
@SuppressWarnings("all")
@@ -324,8 +333,10 @@ public interface EnumDict<V> extends JSONSerializable {
value = parser.parse();
if (value instanceof Map) {
return (T) EnumDict.find(((Class) type), ((Map) value).get("value"))
.orElseGet(() ->
EnumDict.find(((Class) type), ((Map) value).get("text")).orElse(null));
.orElseGet(() ->
EnumDict
.find(((Class) type), ((Map) value).get("text"))
.orElse(null));
}
}
@@ -347,11 +358,11 @@ public interface EnumDict<V> extends JSONSerializable {
@SneakyThrows
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
if(mapper!=null){
if(node.isTextual()){
if (mapper != null) {
if (node.isTextual()) {
return mapper.apply(node.asText());
}
if(node.isNumber()){
if (node.isNumber()) {
return mapper.apply(node.asLong());
}
}
@@ -364,19 +375,17 @@ public interface EnumDict<V> extends JSONSerializable {
findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
}
Supplier<ValidationException> exceptionSupplier = () -> {
List<Object> values= Stream.of(findPropertyType.getEnumConstants())
List<Object> values = Stream
.of(findPropertyType.getEnumConstants())
.map(Enum.class::cast)
.map(e->{
if(e instanceof EnumDict){
.map(e -> {
if (e instanceof EnumDict) {
return ((EnumDict) e).getValue();
}
return e.name();
}).collect(Collectors.toList());
return new ValidationException("参数[" + currentName + "]在选项中不存在",
Arrays.asList(
new ValidationException.Detail(currentName, "选项中不存在此值", values)
));
return new ValidationException(currentName,"validation.parameter_does_not_exist_in_enums", currentName);
};
if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) {
if (node.isObject()) {
@@ -394,12 +403,11 @@ public interface EnumDict<V> extends JSONSerializable {
.find(findPropertyType, node.textValue())
.orElseThrow(exceptionSupplier);
}
throw new ValidationException("参数[" + currentName + "]在选项中不存在", Arrays.asList(
new ValidationException.Detail(currentName, "选项中不存在此值", null)
));
return exceptionSupplier.get();
}
if (findPropertyType.isEnum()) {
return Stream.of(findPropertyType.getEnumConstants())
return Stream
.of(findPropertyType.getEnumConstants())
.filter(o -> {
if (node.isTextual()) {
return node.textValue().equalsIgnoreCase(((Enum) o).name());

View File

@@ -26,12 +26,11 @@ import lombok.Getter;
* @author zhouhao
* @since 2.0
*/
public class BusinessException extends RuntimeException {
public class BusinessException extends I18nSupportException {
private static final long serialVersionUID = 5441923856899380112L;
@Getter
private int status = 500;
@Getter
private String code;
@@ -39,20 +38,21 @@ public class BusinessException extends RuntimeException {
this(message, 500);
}
public BusinessException(String message, int status, Object... args) {
this(message, null, status, args);
}
public BusinessException(String message, String code) {
this(message, code, 500);
}
public BusinessException(String message, String code, int status) {
super(message);
public BusinessException(String message, String code, int status, Object... args) {
super(message, args);
this.code = code;
this.status = status;
}
public BusinessException(String message, int status) {
super(message);
this.status = status;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);

View File

@@ -0,0 +1,40 @@
package org.hswebframework.web.exception;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.i18n.LocaleUtils;
@Getter
@Setter(AccessLevel.PROTECTED)
public class I18nSupportException extends RuntimeException {
private String code;
private Object[] args;
protected I18nSupportException() {
}
public I18nSupportException(String code, Object... args) {
super(code);
this.code = code;
this.args = args;
}
public I18nSupportException(String code, Throwable cause, Object... args) {
super(code, cause);
this.args = args;
this.code = code;
}
@Override
public String getMessage() {
return super.getMessage() != null ? super.getMessage() : getLocalizedMessage();
}
@Override
public String getLocalizedMessage() {
return LocaleUtils.resolveMessage(code, args);
}
}

View File

@@ -24,11 +24,11 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(message, 404);
public NotFoundException(String message, Object... args) {
super(message, 404, args);
}
public NotFoundException() {
this("记录不存在");
this("error.not_found");
}
}

View File

@@ -4,19 +4,19 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.i18n.LocaleUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.ConstraintViolation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
@Getter
@Setter
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class ValidationException extends BusinessException {
public class ValidationException extends I18nSupportException {
private static final boolean propertyI18nEnabled = Boolean.getBoolean("i18n.validation.property.enabled");
private List<Detail> details;
@@ -24,25 +24,43 @@ public class ValidationException extends BusinessException {
super(message);
}
public ValidationException(String property, String message) {
this(message, Collections.singletonList(new Detail(property, message, null)));
public ValidationException(String property, String message, Object... args) {
this(message, Collections.singletonList(new Detail(property, message, null)), args);
}
public ValidationException(String message, List<Detail> details) {
super(message);
public ValidationException(String message, List<Detail> details, Object... args) {
super(message, 400, args);
this.details = details;
}
public ValidationException(String message, Set<? extends ConstraintViolation<?>> violations) {
super(message);
if (null != violations && !violations.isEmpty()) {
details = new ArrayList<>();
for (ConstraintViolation<?> violation : violations) {
details.add(new Detail(violation.getPropertyPath().toString(), violation.getMessage(), null));
}
for (Detail detail : this.details) {
detail.translateI18n(args);
}
}
public ValidationException(Set<? extends ConstraintViolation<?>> violations) {
ConstraintViolation<?> first = violations.iterator().next();
if (Objects.equals(first.getMessageTemplate(), first.getMessage())) {
//模版和消息相同,说明是自定义的message,而不是已经通过i18n获取的.
setCode(first.getMessage());
} else {
setCode("validation.property_validate_failed");
}
String property = first.getPropertyPath().toString();
//{0} 属性 {1} 验证消息
//property也支持国际化?
String resolveMessage = propertyI18nEnabled ?
LocaleUtils.resolveMessage(first.getRootBeanClass().getName() + "." + property, property)
: property;
setArgs(new Object[]{resolveMessage, first.getMessage()});
details = new ArrayList<>(violations.size());
for (ConstraintViolation<?> violation : violations) {
details.add(new Detail(violation.getPropertyPath().toString(), violation.getMessage(), null));
}
}
@Getter
@Setter
@AllArgsConstructor
@@ -55,5 +73,11 @@ public class ValidationException extends BusinessException {
@Schema(description = "详情")
Object detail;
public void translateI18n(Object... args) {
if (message.contains(".")) {
message = LocaleUtils.resolveMessage(message, message, args);
}
}
}
}

View File

@@ -0,0 +1,13 @@
package org.hswebframework.web.i18n;
import org.hibernate.validator.spi.messageinterpolation.LocaleResolver;
import org.hibernate.validator.spi.messageinterpolation.LocaleResolverContext;
import java.util.Locale;
public class ContextLocaleResolver implements LocaleResolver {
@Override
public Locale resolve(LocaleResolverContext context) {
return LocaleUtils.current();
}
}

View File

@@ -0,0 +1,182 @@
package org.hswebframework.web.i18n;
import org.hswebframework.web.exception.I18nSupportException;
import org.springframework.context.MessageSource;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Signal;
import reactor.core.publisher.SignalType;
import reactor.util.context.Context;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 用于进行国际化消息转换
*
* @author zhouhao
* @since 4.0.11
*/
public class LocaleUtils {
public static final Locale DEFAULT_LOCALE = Locale.getDefault();
private static final ThreadLocal<Locale> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
static MessageSource messageSource = UnsupportedMessageSource.instance();
/**
* 获取当前的语言地区,如果没有设置则返回系统默认语言
*
* @return Locale
*/
public static Locale current() {
Locale locale = CONTEXT_THREAD_LOCAL.get();
if (locale == null) {
locale = DEFAULT_LOCALE;
}
return locale;
}
/**
* 在指定的语言环境中执行函数,<b>只能</b>在非响应式同步操作时使用,如:转换实体类中某些属性的国际化消息。
* <p>
* 在函数的逻辑中可以通过{@link LocaleUtils#current()}来获取当前语言.
*
* @param data 参数
* @param locale 语言地区
* @param mapper 函数
* @param <T> 参数类型
* @param <R> 函数返回类型
* @return 返回值
*/
public static <T, R> R doWith(T data, Locale locale, BiFunction<T, Locale, R> mapper) {
try {
CONTEXT_THREAD_LOCAL.set(locale);
return mapper.apply(data, locale);
} finally {
CONTEXT_THREAD_LOCAL.remove();
}
}
public static Function<Context, Context> useLocale(Locale locale) {
return ctx -> ctx.put(Locale.class, locale);
}
public static void doWith(Locale locale, Consumer<Locale> consumer) {
try {
CONTEXT_THREAD_LOCAL.set(locale);
consumer.accept(locale);
} finally {
CONTEXT_THREAD_LOCAL.remove();
}
}
/**
* 响应式方式获取当前语言地区
*
* @return 语言地区
*/
@SuppressWarnings("all")
public static Mono<Locale> currentReactive() {
return Mono
.subscriberContext()
.map(ctx -> ctx.getOrDefault(Locale.class, DEFAULT_LOCALE));
}
public static <T> void onNext(Signal<T> signal, BiConsumer<T, Locale> consumer) {
if (signal.getType() != SignalType.ON_NEXT) {
return;
}
Locale locale = signal.getContext().getOrDefault(Locale.class, DEFAULT_LOCALE);
doWith(locale, l -> consumer.accept(signal.get(), l));
}
public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(S source,
BiFunction<S, String, R> mapper) {
return resolveThrowable(messageSource, source, mapper);
}
public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(MessageSource messageSource,
S source,
BiFunction<S, String, R> mapper) {
return doWithReactive(messageSource, source, I18nSupportException::getCode, mapper, source.getArgs());
}
public static <S extends Throwable, R> Mono<R> resolveThrowable(S source,
BiFunction<S, String, R> mapper,
Object... args) {
return resolveThrowable(messageSource, source, mapper, args);
}
public static <S extends Throwable, R> Mono<R> resolveThrowable(MessageSource messageSource,
S source,
BiFunction<S, String, R> mapper,
Object... args) {
return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args);
}
public static <S, R> Mono<R> doWithReactive(S source,
Function<S, String> message,
BiFunction<S, String, R> mapper,
Object... args) {
return doWithReactive(messageSource, source, message, mapper, args);
}
public static <S, R> Mono<R> doWithReactive(MessageSource messageSource,
S source,
Function<S, String> message,
BiFunction<S, String, R> mapper,
Object... args) {
return currentReactive()
.map(locale -> {
String msg = message.apply(source);
String newMsg = resolveMessage(messageSource, locale, msg, msg, args);
return mapper.apply(source, newMsg);
});
}
public static Mono<String> resolveMessageReactive(MessageSource messageSource,
String code,
Object... args) {
return currentReactive()
.map(locale -> resolveMessage(messageSource, locale, code, code, args));
}
public static String resolveMessage(String code,
Locale locale,
String defaultMessage,
Object... args) {
return resolveMessage(messageSource, locale, code, defaultMessage, args);
}
public static String resolveMessage(MessageSource messageSource,
Locale locale,
String code,
String defaultMessage,
Object... args) {
return messageSource.getMessage(code, args, defaultMessage, locale);
}
public static String resolveMessage(String code, Object... args) {
return resolveMessage(messageSource, current(), code, code, args);
}
public static String resolveMessage(String code,
String defaultMessage,
Object... args) {
return resolveMessage(messageSource, current(), code, defaultMessage, args);
}
public static String resolveMessage(MessageSource messageSource,
String code,
String defaultMessage,
Object... args) {
return resolveMessage(messageSource, current(), code, defaultMessage, args);
}
}

View File

@@ -0,0 +1,12 @@
package org.hswebframework.web.i18n;
import org.springframework.context.MessageSource;
public class MessageSourceInitializer {
public static void init(MessageSource messageSource) {
if (LocaleUtils.messageSource == null || LocaleUtils.messageSource instanceof UnsupportedMessageSource) {
LocaleUtils.messageSource = messageSource;
}
}
}

View File

@@ -0,0 +1,31 @@
package org.hswebframework.web.i18n;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import java.util.Locale;
public class UnsupportedMessageSource implements MessageSource {
private static final UnsupportedMessageSource INSTANCE = new UnsupportedMessageSource();
public static MessageSource instance() {
return INSTANCE;
}
@Override
public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
return defaultMessage;
}
@Override
public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
return code;
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
return resolvable.getDefaultMessage();
}
}

View File

@@ -0,0 +1,34 @@
package org.hswebframework.web.i18n;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Locale;
public class WebFluxLocaleFilter implements WebFilter {
@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) {
return chain
.filter(exchange)
.subscriberContext(LocaleUtils.useLocale(getLocaleContext(exchange)));
}
public Locale getLocaleContext(ServerWebExchange exchange) {
String lang = exchange.getRequest()
.getQueryParams()
.getFirst(":lang");
if (StringUtils.hasText(lang)) {
return Locale.forLanguageTag(lang);
}
Locale locale = exchange.getLocaleContext().getLocale();
if (locale == null) {
return Locale.getDefault();
}
return locale;
}
}

View File

@@ -1,12 +1,10 @@
package org.hswebframework.web.validator;
import org.hibernate.validator.BaseHibernateValidatorConfiguration;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.ContextLocaleResolver;
import javax.el.ExpressionFactory;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.*;
import java.util.Set;
public final class ValidatorUtils {
@@ -19,17 +17,26 @@ public final class ValidatorUtils {
public static Validator getValidator() {
if (validator == null) {
synchronized (ValidatorUtils.class) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Configuration<?> configuration = Validation
.byDefaultProvider()
.configure();
configuration.addProperty(BaseHibernateValidatorConfiguration.LOCALE_RESOLVER_CLASSNAME,
ContextLocaleResolver.class.getName());
configuration.messageInterpolator(configuration.getDefaultMessageInterpolator());
ValidatorFactory factory = configuration.buildValidatorFactory();
return validator = factory.getValidator();
}
}
return validator;
}
@SuppressWarnings("all")
public static <T> T tryValidate(T bean, Class... group) {
Set<ConstraintViolation<T>> violations = getValidator().validate(bean, group);
if (!violations.isEmpty()) {
throw new ValidationException(violations.iterator().next().getMessage(), violations);
throw new ValidationException(violations);
}
return bean;

View File

@@ -0,0 +1,4 @@
error.not_found=The data does not exist
error.cant_create_instance=Unable to create instance:{0}
validation.parameter_does_not_exist_in_enums=Parameter {0} does not exist in option
validation.property_validate_failed={0} {1}

View File

@@ -0,0 +1,5 @@
error.not_found=数据不存在
error.cant_create_instance=无法创建实例:{0}
validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在
validation.property_validate_failed={0}{1}

View File

@@ -0,0 +1,42 @@
package org.hswebframework.web.validator;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.junit.Test;
import javax.validation.constraints.NotBlank;
import java.util.Locale;
import static org.junit.Assert.*;
public class ValidatorUtilsTest {
@Test
public void test(){
test(Locale.CHINA,"不能为空");
test(Locale.ENGLISH,"must not be blank");
}
public void test(Locale locale,String msg){
try {
LocaleUtils.doWith(locale,en->{
ValidatorUtils.tryValidate(new TestEntity());
});
throw new IllegalStateException();
}catch (ValidationException e){
assertEquals(msg,e.getDetails().get(0).getMessage());
}
}
@Getter
@Setter
public static class TestEntity{
@NotBlank
private String notBlank;
}
}

View File

@@ -0,0 +1,71 @@
package org.hswebframework.web.starter.i18n;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
public class CompositeMessageSource implements MessageSource {
private final List<MessageSource> messageSources = new CopyOnWriteArrayList<>();
public void addMessageSources(Collection<MessageSource> source) {
messageSources.addAll(source);
}
public void addMessageSource(MessageSource source) {
messageSources.add(source);
}
@Override
public String getMessage(@Nonnull String code, Object[] args, String defaultMessage, @Nonnull Locale locale) {
for (MessageSource messageSource : messageSources) {
String result = messageSource.getMessage(code, args, null, locale);
if (StringUtils.hasText(result)) {
return result;
}
}
return defaultMessage;
}
@Override
@Nonnull
public String getMessage(@Nonnull String code, Object[] args, @Nonnull Locale locale) throws NoSuchMessageException {
for (MessageSource messageSource : messageSources) {
try {
String result = messageSource.getMessage(code, args, locale);
if (StringUtils.hasText(result)) {
return result;
}
} catch (NoSuchMessageException ignore) {
}
}
throw new NoSuchMessageException(code, locale);
}
@Override
@Nonnull
public String getMessage(@Nonnull MessageSourceResolvable resolvable, @Nonnull Locale locale) throws NoSuchMessageException {
for (MessageSource messageSource : messageSources) {
try {
String result = messageSource.getMessage(resolvable, locale);
if (StringUtils.hasText(result)) {
return result;
}
} catch (NoSuchMessageException ignore) {
}
}
String[] codes = resolvable.getCodes();
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
}
}

View File

@@ -0,0 +1,56 @@
package org.hswebframework.web.starter.i18n;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.i18n.MessageSourceInitializer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.StringUtils;
import java.util.stream.Collectors;
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class I18nConfiguration {
@Bean
@SneakyThrows
public MessageSource autoResolveI18nMessageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:i18n/**");
for (Resource resource : resources) {
String path = resource.getURL().getPath();
if (StringUtils.hasText(path) && (path.endsWith(".properties") || path.endsWith(".xml"))) {
String name = path.substring(path.lastIndexOf("i18n"),path.indexOf("_"));
log.info("register i18n message resource {} -> {}", path,name);
messageSource.addBasenames(name);
}
}
return messageSource;
}
@Bean
@Primary
public MessageSource compositeMessageSource(ObjectProvider<MessageSource> objectProvider) {
CompositeMessageSource messageSource = new CompositeMessageSource();
messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList()));
MessageSourceInitializer.init(messageSource);
return messageSource;
}
}

View File

@@ -51,6 +51,7 @@ public class CustomCodecsAutoConfiguration {
return (configurer) -> {
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
defaults.jackson2JsonDecoder(new CustomJackson2JsonDecoder(entityFactory, objectMapper));
defaults.jackson2JsonEncoder(new CustomJackson2jsonEncoder(objectMapper));
};
}

View File

@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import org.hswebframework.web.api.crud.entity.EntityFactory;
import org.hswebframework.web.i18n.LocaleUtils;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
@@ -67,17 +68,23 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
ObjectReader reader = getObjectReader(elementType, hints);
return tokens.handle((tokenBuffer, sink) -> {
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
logValue(value, hints);
if (value != null) {
sink.next(value);
}
} catch (IOException ex) {
sink.error(processException(ex));
}
});
return LocaleUtils
.currentReactive()
.flatMapMany(locale -> tokens
.handle((tokenBuffer, sink) -> {
LocaleUtils.doWith(locale, l -> {
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
logValue(value, hints);
if (value != null) {
sink.next(value);
}
} catch (IOException ex) {
sink.error(processException(ex));
}
});
}));
}
@Override
@@ -85,8 +92,15 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
public Mono<Object> decodeToMono(@NonNull Publisher<DataBuffer> input, @NonNull ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return DataBufferUtils.join(input)
.map(dataBuffer -> decode(dataBuffer, elementType, mimeType, hints));
return LocaleUtils
.currentReactive()
.flatMap(locale -> DataBufferUtils
.join(input)
.map(dataBuffer -> LocaleUtils
.doWith(dataBuffer,
locale,
(buf, l) -> decode(buf, elementType, mimeType, hints)))
);
}
@Override

View File

@@ -0,0 +1,340 @@
package org.hswebframework.web.starter.jackson;
import org.hswebframework.web.i18n.LocaleUtils;
import org.springframework.http.codec.json.Jackson2CodecSupport;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.util.*;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SequenceWriter;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.EncodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Base class providing support methods for Jackson 2.9 encoding. For non-streaming use
* cases, {@link Flux} elements are collected into a {@link List} before serialization for
* performance reason.
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @since 5.0
*/
public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements HttpMessageEncoder<Object> {
private static final byte[] NEWLINE_SEPARATOR = {'\n'};
private static final Map<MediaType, byte[]> STREAM_SEPARATORS;
private static final Map<String, JsonEncoding> ENCODINGS;
static {
STREAM_SEPARATORS = new HashMap<>(4);
STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1);
for (JsonEncoding encoding : JsonEncoding.values()) {
ENCODINGS.put(encoding.getJavaName(), encoding);
}
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
}
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
/**
* Constructor with a Jackson {@link ObjectMapper} to use.
*/
protected CustomJackson2jsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
/**
* Configure "streaming" media types for which flushing should be performed
* automatically vs at the end of the stream.
* <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
*
* @param mediaTypes one or more media types to add to the list
* @see HttpMessageEncoder#getStreamingMediaTypes()
*/
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
this.streamingMediaTypes.clear();
this.streamingMediaTypes.addAll(mediaTypes);
}
@Override
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
Class<?> clazz = elementType.toClass();
if (!supportsMimeType(mimeType)) {
return false;
}
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
if (!ENCODINGS.containsKey(charset.name())) {
return false;
}
}
return (Object.class == clazz ||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
return LocaleUtils
.currentReactive()
.flatMapMany(locale -> {
if (inputStream instanceof Mono) {
return Mono.from(inputStream)
.map(value -> LocaleUtils
.doWith(value, locale,
((val, loc) ->
encodeValue(val, bufferFactory, elementType, mimeType, hints)
)
))
.flux();
} else {
byte[] separator = streamSeparator(mimeType);
if (separator != null) { // streaming
try {
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer
.getFactory()
._getBufferRecycler());
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = getObjectMapper()
.getFactory()
.createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
return Flux
.from(inputStream)
.map(value -> LocaleUtils
.doWith(value,
locale,
((val, loc) -> this
.encodeStreamingValue(val,
bufferFactory,
hints,
sequenceWriter,
byteBuilder,
separator)
)
))
.doAfterTerminate(() -> {
try {
byteBuilder.release();
generator.close();
} catch (IOException ex) {
logger.error("Could not close Encoder resources", ex);
}
});
} catch (IOException ex) {
return Flux.error(ex);
}
} else { // non-streaming
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream)
.collectList()
.map(value -> LocaleUtils
.doWith(value, locale,
((val, loc) ->
encodeValue(val, bufferFactory, listType, mimeType, hints)
)
))
.flux();
}
}
});
}
@Override
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
logValue(hints, value);
try (JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding)) {
writer.writeValue(generator, value);
generator.flush();
} catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
} catch (JsonProcessingException ex) {
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
} catch (IOException ex) {
throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
}
byte[] bytes = byteBuilder.toByteArray();
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
} finally {
byteBuilder.release();
}
}
private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,
SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder, byte[] separator) {
logValue(hints, value);
try {
sequenceWriter.write(value);
sequenceWriter.flush();
} catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
} catch (JsonProcessingException ex) {
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
} catch (IOException ex) {
throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
}
byte[] bytes = byteArrayBuilder.toByteArray();
byteArrayBuilder.reset();
int offset;
int length;
if (bytes.length > 0 && bytes[0] == ' ') {
// SequenceWriter writes an unnecessary space in between values
offset = 1;
length = bytes.length - 1;
} else {
offset = 0;
length = bytes.length;
}
DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length);
buffer.write(bytes, offset, length);
buffer.write(separator);
return buffer;
}
private void logValue(@Nullable Map<String, Object> hints, Object value) {
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(value, !traceOn);
return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
});
}
}
private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
JavaType javaType = getJavaType(valueType.getType(), null);
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
ObjectWriter writer = (jsonView != null ?
getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
if (javaType.isContainerType()) {
writer = writer.forType(javaType);
}
return customizeWriter(writer, mimeType, valueType, hints);
}
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
ResolvableType elementType, @Nullable Map<String, Object> hints) {
return writer;
}
@Nullable
private byte[] streamSeparator(@Nullable MimeType mimeType) {
for (MediaType streamingMediaType : this.streamingMediaTypes) {
if (streamingMediaType.isCompatibleWith(mimeType)) {
return STREAM_SEPARATORS.getOrDefault(streamingMediaType, NEWLINE_SEPARATOR);
}
}
return null;
}
/**
* Determine the JSON encoding to use for the given mime type.
*
* @param mimeType the mime type as requested by the caller
* @return the JSON encoding to use (never {@code null})
* @since 5.0.5
*/
protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
JsonEncoding result = ENCODINGS.get(charset.name());
if (result != null) {
return result;
}
}
return JsonEncoding.UTF8;
}
// HttpMessageEncoder
@Override
public List<MimeType> getEncodableMimeTypes() {
return getMimeTypes();
}
@Override
public List<MediaType> getStreamingMediaTypes() {
return Collections.unmodifiableList(this.streamingMediaTypes);
}
@Override
public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType,
@Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
return (actualType != null ? getHints(actualType) : Hints.none());
}
// Jackson2CodecSupport
@Override
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
return parameter.getMethodAnnotation(annotType);
}
}

View File

@@ -2,4 +2,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration,\
org.hswebframework.web.starter.HswebAutoConfiguration,\
org.hswebframework.web.starter.CorsAutoConfiguration
org.hswebframework.web.starter.CorsAutoConfiguration,\
org.hswebframework.web.starter.i18n.I18nConfiguration

View File

@@ -0,0 +1,88 @@
package org.hswebframework.web.starter.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.i18n.MessageSourceInitializer;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Locale;
import java.util.function.Predicate;
public class CustomJackson2jsonEncoderTest {
@Before
public void init(){
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding("utf-8");
messageSource.setBasenames("i18n.messages");
MessageSourceInitializer.init(messageSource);
}
@Test
public void testI18n() {
doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("en-US"),s->s.contains("Option1"));
doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("zh-CN"),s->s.contains("选项1"));
}
public void doTest(TestEntity entity, Locale locale, Predicate<String> verify){
CustomJackson2jsonEncoder encoder = new CustomJackson2jsonEncoder(new ObjectMapper());
encoder.encode(Mono.just(entity),
new DefaultDataBufferFactory(),
ResolvableType.forType(TestEntity.class),
MediaType.APPLICATION_JSON,
Collections.emptyMap())
.as(DataBufferUtils::join)
.map(buf -> buf.toString(StandardCharsets.UTF_8))
.doOnNext(System.out::println)
.subscriberContext(ctx->ctx.put(LocaleContext.class,new SimpleLocaleContext(locale)))
.as(StepVerifier::create)
.expectNextMatches(verify)
.verifyComplete();
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class TestEntity {
private TestEnum testEnum;
}
@Getter
@AllArgsConstructor
public enum TestEnum implements EnumDict<String> {
e1("enum.e1"),
e2("enum.e2");
private final String text;
@Override
public String getValue() {
return name();
}
}
}

View File

@@ -0,0 +1,2 @@
enum.e1=Option1
enum.e2=Option2

View File

@@ -0,0 +1,2 @@
enum.e1=选项1
enum.e2=选项2

View File

@@ -75,7 +75,7 @@ public class PermissionProperties {
.map(Permission::getActions)
.orElseGet(Collections::emptySet));
throw new AccessDenyException("当前用户无权限:" + setting.getPermission() + "" +actions);
throw new AccessDenyException(setting.getPermission(), actions);
}
};

View File

@@ -59,7 +59,7 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
return this
.publishEvent(entityPublisher, DimensionBindEvent::new)
.as(super::insert)
.onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("重复的绑定请求"));
.onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("error.duplicate_key"));
}
@Override

View File

@@ -82,13 +82,12 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
.where(userEntity::getUsername)
.fetch()
.doOnNext(u -> {
throw new org.hswebframework.web.exception.ValidationException("用户已存在");
throw new org.hswebframework.web.exception.ValidationException("error.user_already_exists");
})
.then(Mono.just(userEntity))
.doOnNext(e -> e.tryValidate(CreateGroup.class))
.as(getRepository()::insert)
.onErrorMap(DuplicateKeyException.class, e -> {
throw new org.hswebframework.web.exception.ValidationException("用户已存在");
throw new org.hswebframework.web.exception.ValidationException("error.user_already_exists");
})
.thenReturn(userEntity)
.flatMap(user -> new UserCreatedEvent(user).publish(eventPublisher))

View File

@@ -0,0 +1,2 @@
error.duplicate_key=Duplicate Data
error.user_already_exists=User already exists

View File

@@ -0,0 +1,2 @@
error.duplicate_key=重复的请求
error.user_already_exists=用户已存在