diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..353c844 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Other ### +*.log +logs/ +.DS_Store +.flattened-pom.xml diff --git a/axzo-common-autoconfigure/pom.xml b/axzo-common-autoconfigure/pom.xml new file mode 100644 index 0000000..1649ad3 --- /dev/null +++ b/axzo-common-autoconfigure/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-autoconfigure + Axzo Common AutoConfigure + + + + + org.springframework.boot + spring-boot-autoconfigure + + + cn.axzo.framework + axzo-common-boot + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + cn.axzo.framework.jackson + jackson-starter + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + cn.axzo.framework + axzo-common-webmvc + true + + + org.springframework + spring-jdbc + true + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.cloud + spring-cloud-context + true + + + org.springframework.boot + spring-boot-starter-undertow + true + + + + io.springfox + springfox-swagger2 + + + io.springfox + springfox-bean-validators + + + + org.springframework.security.oauth + spring-security-oauth2 + true + + + + org.apache.tomcat.embed + tomcat-embed-core + true + + + org.springframework.security + spring-security-web + + + + \ No newline at end of file diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/data/IdAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/data/IdAutoConfiguration.java new file mode 100644 index 0000000..4fb8ee6 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/data/IdAutoConfiguration.java @@ -0,0 +1,37 @@ +package cn.axzo.framework.autoconfigure.data; + +import cn.axzo.framework.domain.data.IdHelper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/11/13 11:45 + **/ +@Configuration +@Order(Ordered.HIGHEST_PRECEDENCE) +@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class, IdHelper.class}) +public class IdAutoConfiguration { + + @Value("${id.generator.node-id:}") + private Integer nodeId; + + @Value("${id.generator.base-timestamp:}") + private Long baseTimestamp; + + @Value("${id.generator.sequence-bits:}") + private Integer sequenceBits; + + @PostConstruct + public void init() { + IdHelper.reload(nodeId, baseTimestamp, sequenceBits); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/ApplicationReadyInfoAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/ApplicationReadyInfoAutoConfiguration.java new file mode 100644 index 0000000..462c833 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/ApplicationReadyInfoAutoConfiguration.java @@ -0,0 +1,138 @@ +package cn.axzo.framework.autoconfigure.env; + +import cn.axzo.framework.boot.EnvironmentUtil; +import cn.axzo.framework.core.FetchException; +import cn.axzo.framework.core.io.Resources; +import cn.axzo.framework.core.net.Inets; +import cn.axzo.framework.context.Placeholders; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.jooq.lambda.Seq; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.web.server.Ssl; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Servlet; +import java.util.Arrays; +import java.util.Optional; + +import static cn.axzo.framework.boot.DefaultProfileUtil.getActiveProfiles; +import static cn.axzo.framework.core.util.ClassUtil.isPresent; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 19:27 + **/ +@Slf4j +@Configuration +@ConditionalOnProperty(value = "spring.application.log-ready-info", havingValue = "true", matchIfMissing = true) +public class ApplicationReadyInfoAutoConfiguration { + + @Configuration + @RequiredArgsConstructor + @ConditionalOnWebApplication + @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) + public static class WebInfoPrinter implements ApplicationListener { + + // the application name + @Value(Placeholders.APPLICATION_NAME) + private String appName; + + private final ServerProperties properties; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + Environment environment = event.getApplicationContext().getEnvironment(); + Ssl ssl = properties.getSsl(); + String protocol = (ssl == null || ssl.getKeyStore() == null) ? "http" : "https"; + Integer port = properties.getPort(); + if (port == null) { + port = 8080; + } + String ip; + int timeoutSeconds = EnvironmentUtil.fetchLocalIpTimeoutSeconds(environment); + try { + ip = Inets.fetchLocalIp(timeoutSeconds); + } catch (FetchException e) { + log.debug("Cannot fetch local ip in " + timeoutSeconds + " seconds"); + ip = null; + } + String[] activeProfiles = getActiveProfiles(environment); + + // application base info + StringBuilder s = new StringBuilder() + .append("\n----------------------------------------------------------------\n") + .append("\tApplication ").append(appName).append(" is running! Access URLs:\n") + .append("\tLocal:\t\t\t").append(protocol).append("://localhost:").append(port).append("\n"); + if (ip != null) { + s.append("\tExternal:\t\t").append(protocol).append("://").append(ip) + .append(":").append(port).append("\n"); + String swaggerPageLocation = "classpath:META-INF/resources/swagger-ui.html"; + if (Seq.of(activeProfiles).contains("swagger") && Resources.exists(swaggerPageLocation)) { + s.append("\tSwaggerUI:\t\t").append(protocol).append("://").append(ip) + .append(":").append(port).append("/swagger-ui.html\n"); + } + } + s.append("\tProfile(s):\t\t").append(Arrays.toString(activeProfiles)).append("\n"); + s.append("----------------------------------------------------------------"); + + // config server info + s.append(getConfigServerStatus(event)); + + log.info(s.toString()); + } + } + + @Configuration + @ConditionalOnNotWebApplication + public static class InfoPrinter implements ApplicationListener { + + // the application name + @Value(Placeholders.APPLICATION_NAME) + private String appName; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + String[] activeProfiles = getActiveProfiles(event.getApplicationContext().getEnvironment()); + // application base info + String info = "\n----------------------------------------------------------------\n" + + "\tApplication " + appName + " is running!\n" + + "\tProfile(s):\t" + Arrays.toString(activeProfiles) + "\n" + + "----------------------------------------------------------------"; + + // config server info + info += getConfigServerStatus(event); + + log.info(info); + } + } + + /** + * Log config server info on condition that dependency is present. + * + * @param event event when this application is ready. + */ + private static String getConfigServerStatus(ApplicationReadyEvent event) { + val environment = event.getApplicationContext().getEnvironment(); + val classLoader = event.getApplicationContext().getClassLoader(); + if (isPresent("org.springframework.cloud.config.client.ConfigClientProperties", classLoader)) { + val status = Optional.ofNullable(environment.getProperty("configserver.status")); + return "\n----------------------------------------------------------------\n" + + "\tConfig Server:\t" + status.orElse("Not found or not setup for this application!") + "\n" + + "----------------------------------------------------------------"; + } + return ""; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/BuildInConfigOverridePostProcessor.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/BuildInConfigOverridePostProcessor.java new file mode 100644 index 0000000..fabbcc5 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/env/BuildInConfigOverridePostProcessor.java @@ -0,0 +1,318 @@ +package cn.axzo.framework.autoconfigure.env; + +import cn.axzo.framework.boot.EnvironmentUtil; +import cn.axzo.framework.boot.logging.LoggingConfigFixer; +import cn.axzo.framework.core.util.MapUtil; +import org.jooq.lambda.Seq; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.LogFile; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static jodd.util.StringPool.COMMA; +import static org.springframework.util.ClassUtils.isPresent; + +/** + * 覆盖不合理的配置或添加默认配置 + * + * @author liyong.tian + * @since 2019/10/13 10:21 + */ +public class BuildInConfigOverridePostProcessor implements EnvironmentPostProcessor, Ordered { + + /** + * The default order for the processor. + */ + public static final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE; + + private static final String BUILD_IN_CONFIG_OVERRIDE_PS_NAME = "buildInConfigOverride"; + + private int order = DEFAULT_ORDER; + + private LoggingConfigFixer loggingConfigFixer = new LoggingConfigFixer(); + + private ConfigurableEnvironment environment; + + private ClassLoader classLoader; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + this.environment = environment; + this.classLoader = application.getClassLoader(); + boolean isSpringCloudContext = EnvironmentUtil.isSpringCloudContext(environment); + + Map map = new HashMap<>(); + + // 设置日志文件名 + overrideLoggingConfig(map, isSpringCloudContext); + + // don't listen to events in a spring cloud context + if (!isSpringCloudContext) { + + // 在ribbon环境中,当Apache HttpClient和OKHttp依赖都存在,则使用OKHttp + overrideRibbonOKHttp(map); + + // 在feign环境中,当Apache HttpClient和OKHttp依赖都存在,则使用OKHttp + overrideFeignOKHttp(map); + + // 修复thymeleaf默认的配置在thymeleaf3环境下的告警 + overrideThymeleafProperties(map); + + // 给management的context-path设置默认值 + overrideManagementServerProperties(map); + + // 自动配置CGLIB代理 + overrideCglibProxy(map); + + // 增加json格式的mime-type + overrideCompressionMimeTypes(map); + + // 设置EurekaServer的环境名 + overrideEurekaServerEnvironment(map); + + // 默认禁用favicon + overrideMvcFavicon(map); + + // 设置config server审计端口的应用名 + overrideConfigServerHealthRepositories(map); + + // 自动配置mvc中的trace支持 + overrideServerErrorTraceSupport(map); + + // 默认不开启metrics的过滤器 + disableMetricsFilterIfAbsent(map); + } + + map = MapUtil.removeNulls(map); + if (MapUtil.isNotEmpty(map)) { + MapPropertySource propertySource = new MapPropertySource(BUILD_IN_CONFIG_OVERRIDE_PS_NAME, map); + environment.getPropertySources().addFirst(propertySource); + } + } + + private void disableMetricsFilterIfAbsent(Map map) { + String key = "endpoints.metrics.filter.enabled"; + if (isEnvContainsNone(key)) { + map.put(key, false); + } + } + + private void overrideServerErrorTraceSupport(Map map) { + if (isWebApplication()) { + String key = "server.error.includeStacktrace"; + String relaxedKey = "server.error.include-stacktrace"; + if (isEnvContainsNone(key, relaxedKey)) { + map.put(key, ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM); + } + } + } + + private void overrideConfigServerHealthRepositories(Map map) { + if (isConfigServerHealthPresent()) { + String key = "spring.cloud.config.server.health.repositories.application.name"; + if (isEnvContainsNone(key)) { + map.put(key, "application"); + } + } + } + + private void overrideMvcFavicon(Map map) { + if (isWebApplication()) { + String key = "spring.mvc.favicon.enabled"; + if (isEnvContainsNone(key, key)) { + map.put(key, "false"); + } + } + } + + private void overrideEurekaServerEnvironment(Map map) { + if (isEurekaServerPresent()) { + String key = "eureka.environment"; + if (isEnvContainsNone(key, key)) { + String[] activeProfiles = environment.getActiveProfiles(); + if (activeProfiles == null || activeProfiles.length == 0) { + map.put(key, "default"); + } else { + map.put(key, Seq.of(activeProfiles).toString(COMMA)); + } + } + } + } + + private void overrideLoggingConfig(Map map, boolean isSpringCloudContext) { + String pathKey = LogFile.FILE_PATH_PROPERTY; + String fileKey = LogFile.FILE_NAME_PROPERTY; + if (isEnvContainsNone(pathKey, fileKey)) { + return; + } + + // path + String loggingPath; + if (isEnvContainsNone(pathKey, pathKey)) { + loggingPath = "logs"; + } else { + loggingPath = environment.getProperty(pathKey); + } + + // file + String loggingFile; + if (isEnvContainsNone(fileKey, fileKey)) { + String appName = environment.getProperty("spring.application.name"); + if (appName != null) { + loggingFile = appName + ".log"; + } else { + loggingFile = isSpringCloudContext ? "bootstrap.log" : "spring.log"; + } + } else { + loggingFile = environment.getProperty(fileKey); + } + + // fix file + loggingFile = loggingConfigFixer.fixLoggingFile(loggingPath, loggingFile).orElse(loggingFile); + + map.put(pathKey, loggingPath); + map.put(fileKey, loggingFile); + } + + private void overrideCompressionMimeTypes(Map map) { + String key = "server.compression.mimeTypes"; + String relaxedKey = "server.compression.mime-types"; + if (isEnvContainsNone(key, relaxedKey)) { + String[] mimeTypes = { + "text/html", "text/xml", "text/plain", "text/css", "text/javascript", + "application/javascript", "application/json" + }; + map.put(key, mimeTypes); + map.put(relaxedKey, mimeTypes); + } + } + + private void overrideCglibProxy(Map map) { + String key = "spring.aop.proxyTargetClass"; + String relaxedKey = "spring.aop.proxy-target-class"; + if (isEnvContainsNone(key, relaxedKey)) { + map.put(key, true); + map.put(relaxedKey, true); + } + } + + private void overrideManagementServerProperties(Map map) { + if (isActuatorPresent()) { + String key = "management.contextPath"; + String relaxedKey = "management.context-path"; + if (isEnvContainsNone(key, relaxedKey)) { + map.put(key, "/management"); + map.put(relaxedKey, "/management"); + } + } + } + + private void overrideThymeleafProperties(Map map) { + if (isThymeleaf3Present()) { + String key = "spring.thymeleaf.mode"; +// if (isEnvContainsNone(key, key)) { +// map.put(key, TemplateMode.HTML.name()); +// } + + key = "spring.thymeleaf.checkTemplateLocation"; + String relaxedKey = "spring.thymeleaf.check-template-location"; + if (isEnvContainsNone(key, relaxedKey)) { + map.put(key, false); + map.put(relaxedKey, false); + } + } + } + + private void overrideRibbonOKHttp(Map map) { + if (isRibbonOKHttpPresent()) { + String key = "ribbon.httpclient.enabled"; + if (isEnvContainsNone(key, key)) { + map.put(key, false); + } + + key = "ribbon.okhttp.enabled"; + if (isEnvContainsNone(key, key)) { + map.put(key, true); + } + } + } + + private void overrideFeignOKHttp(Map map) { + if (isFeignOKHttpPresent()) { + String key = "feign.httpclient.enabled"; + if (isEnvContainsNone(key, key)) { + map.put(key, false); + } + + key = "feign.okhttp.enabled"; + if (isEnvContainsNone(key, key)) { + map.put(key, true); + } + } + } + + private boolean isConfigServerHealthPresent() { + Boolean healthEnabled = environment.getProperty("spring.cloud.config.server.health.enabled", Boolean.class); + return isPresent("org.springframework.cloud.config.server.config.ConfigServerHealthIndicator", classLoader) + && (healthEnabled == null || healthEnabled); + } + + private boolean isWebApplication() { + return isPresent("org.springframework.web.context.WebApplicationContext", classLoader) + && isPresent("org.springframework.web.context.support.GenericWebApplicationContext", classLoader); + } + + private boolean isEurekaServerPresent() { + return isPresent("org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap", classLoader); + } + + private boolean isActuatorPresent() { + return isPresent("org.springframework.boot.actuate.autoconfigure.ManagementServerProperties", classLoader); + } + + private boolean isThymeleaf3Present() { + return isPresent("org.thymeleaf.templatemode.TemplateMode", classLoader); + } + + private boolean isRibbonOKHttpPresent() { + return isPresent("org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration", classLoader) + && isPresent("okhttp3.OkHttpClient", classLoader); + } + + private boolean isFeignOKHttpPresent() { + return isPresent("org.springframework.cloud.netflix.feign.ribbon.OkHttpFeignLoadBalancedConfiguration", classLoader) + && isPresent("feign.okhttp.OkHttpClient", classLoader) + && isPresent("okhttp3.OkHttpClient", classLoader); + } + + private boolean isEnvContainsNone(String... keys) { + if (keys.length == 1) { + return !environment.containsProperty(keys[0]); + } + if (keys.length == 2) { + if (Objects.equals(keys[0], keys[1])) { + return !environment.containsProperty(keys[0]); + } else { + return !environment.containsProperty(keys[0]) && !environment.containsProperty(keys[1]); + } + } + return Arrays.stream(keys).noneMatch(key -> environment.containsProperty(key)); + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonCustomer.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonCustomer.java new file mode 100644 index 0000000..b37f6b7 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonCustomer.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.autoconfigure.jackson; + +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.core.Ordered; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.util.TimeZone; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT; +import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER; +import static com.fasterxml.jackson.databind.DeserializationFeature.*; +import static com.fasterxml.jackson.databind.MapperFeature.PROPAGATE_TRANSIENT_MARKER; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 19:37 + **/ +public class JacksonCustomer implements Jackson2ObjectMapperBuilderCustomizer, Ordered { + + + @Override + public void customize(Jackson2ObjectMapperBuilder builder) { + builder.serializationInclusion(NON_ABSENT); + builder.timeZone(TimeZone.getDefault()); + + // disable + builder.featuresToDisable(READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + builder.featuresToDisable(WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + builder.featuresToDisable(ACCEPT_FLOAT_AS_INT); + + // enable + builder.featuresToEnable(ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER); + builder.featuresToEnable(ACCEPT_SINGLE_VALUE_AS_ARRAY); + builder.featuresToEnable(PROPAGATE_TRANSIENT_MARKER); + } + + @Override + public int getOrder() { + return -1; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonModuleAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonModuleAutoConfiguration.java new file mode 100644 index 0000000..4c5e353 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/jackson/JacksonModuleAutoConfiguration.java @@ -0,0 +1,71 @@ +package cn.axzo.framework.autoconfigure.jackson; + +import cn.axzo.framework.jackson.datatype.enumstd.EnumStdModule; +import cn.axzo.framework.jackson.datatype.fraction.FractionModule; +import cn.axzo.framework.jackson.datatype.period.PeriodModule; +import cn.axzo.framework.jackson.datatype.string.TrimModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.hppc.HppcModule; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 19:39 + **/ +@Configuration +@ConditionalOnClass({ObjectMapper.class, Jackson2ObjectMapperBuilder.class}) +public class JacksonModuleAutoConfiguration { + + @Bean + public JacksonCustomer jacksonCustomer() { + return new JacksonCustomer(); + } + + @ConditionalOnClass(name = "cn.axzo.framework.jackson.datatype.enumstd.cn.axzo.framework.jackson.datatype.enumstd.EnumStdModule") + @Bean + public EnumStdModule enumStdModule() { + return new EnumStdModule(); + } + + @ConditionalOnClass(name = "cn.axzo.framework.jackson.datatype.fraction.cn.axzo.framework.jackson.datatype.fraction.FractionModule") + @Bean + public FractionModule fractionModule() { + return new FractionModule(); + } + + @ConditionalOnClass(name = "cn.axzo.framework.jackson.datatype.period.cn.axzo.framework.jackson.datatype.period.PeriodModule") + @Bean + public PeriodModule periodModule() { + return new PeriodModule(); + } + + @ConditionalOnClass(name = "cn.axzo.framework.jackson.datatype.string.TrimModule") + @Bean + public TrimModule trimModule() { + return new TrimModule(); + } + + @ConditionalOnClass(name = "com.fasterxml.jackson.module.afterburner.AfterburnerModule") + @Bean + public AfterburnerModule afterburnerModule() { + return new AfterburnerModule(); + } + + @ConditionalOnClass(name = "com.fasterxml.jackson.datatype.hppc.HppcModule") + @Bean + public HppcModule hppcModule() { + return new HppcModule(); + } + + @ConditionalOnClass(name = "com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule") + @Bean + public JsonOrgModule jsonOrgModule() { + return new JsonOrgModule(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/package-info.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/package-info.java new file mode 100644 index 0000000..6de566f --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/package-info.java @@ -0,0 +1,4 @@ +/** + * Service layer beans. + */ +package cn.axzo.framework.autoconfigure; diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/MethodValidationAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/MethodValidationAutoConfiguration.java new file mode 100644 index 0000000..8259292 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/MethodValidationAutoConfiguration.java @@ -0,0 +1,35 @@ +package cn.axzo.framework.autoconfigure.validation; + +import lombok.val; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; + +/** + * @Description JSR-349规范 + * @Author liyong.tian + * @Date 2020/9/9 19:42 + **/ +@Configuration +@ConditionalOnClass(ExecutableValidator.class) +@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") +@AutoConfigureBefore(ValidationAutoConfiguration.class) +public class MethodValidationAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, Validator validator) { + val processor = ValidationAutoConfiguration.methodValidationPostProcessor(environment, validator, null); + processor.setBeforeExistingAdvisors(true); + return processor; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/SpringValidatorAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/SpringValidatorAutoConfiguration.java new file mode 100644 index 0000000..9e77f87 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/validation/SpringValidatorAutoConfiguration.java @@ -0,0 +1,39 @@ +package cn.axzo.framework.autoconfigure.validation; + +import cn.axzo.framework.context.validation.SpringValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.validation.SmartValidator; + +import java.util.Optional; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 19:44 + **/ +@Configuration +@ConditionalOnBean(SmartValidator.class) +@ConditionalOnClass(SpringValidator.class) +@RequiredArgsConstructor +public class SpringValidatorAutoConfiguration { + + private final SmartValidator validator; + + private final ObjectProvider conversionServiceProvider; + + @Bean + @ConditionalOnMissingBean + public SpringValidator springValidator() { + ConversionService conversionService = Optional.ofNullable(conversionServiceProvider.getIfAvailable()) + .orElseGet(DefaultFormattingConversionService::new); + return new SpringValidator(validator, conversionService); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/FilterAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/FilterAutoConfiguration.java new file mode 100644 index 0000000..7b0eda6 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/FilterAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.axzo.framework.autoconfigure.web; + +import cn.axzo.framework.web.servlet.filter.OrderedBadRequestFilter; +import cn.axzo.framework.web.servlet.filter.OrderedTimerFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Servlet; +/** + * @Description 自定义过滤器 + * @Author liyong.tian + * @Date 2020/9/9 19:49 + **/ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) +public class FilterAutoConfiguration { + + @Bean + @ConditionalOnClass(name = "cn.axzo.framework.web.servlet.filter.OrderedTimerFilter") + public OrderedTimerFilter orderedTimerFilter() { + return new OrderedTimerFilter(); + } + + @Bean + @ConditionalOnClass(name = "cn.axzo.framework.web.servlet.filter.OrderedBadRequestFilter") + public OrderedBadRequestFilter orderedBadRequestFilter() { + return new OrderedBadRequestFilter(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/PageWebAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/PageWebAutoConfiguration.java new file mode 100644 index 0000000..08f2f35 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/PageWebAutoConfiguration.java @@ -0,0 +1,64 @@ +package cn.axzo.framework.autoconfigure.web; + +import cn.axzo.framework.web.page.PageableArgumentResolver; +import cn.axzo.framework.web.page.RestPageProperties; +import cn.axzo.framework.web.page.SortArgumentResolver; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Servlet; +import java.util.List; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 19:51 + **/ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class, RestPageProperties.class}) +public class PageWebAutoConfiguration { + + @Bean + @Validated + @ConfigurationProperties("spring.rest.page") + @ConditionalOnMissingBean + public RestPageProperties restPageProperties() { + return new RestPageProperties(); + } + + @Configuration + @ConditionalOnClass({SortArgumentResolver.class, PageableArgumentResolver.class}) + static class PageAndSortConfiguration implements WebMvcConfigurer { + + private final RestPageProperties restPageProperties; + + public PageAndSortConfiguration(RestPageProperties restPageProperties) { + this.restPageProperties = restPageProperties; + } + + @Bean + public SortArgumentResolver sortArgumentResolver() { + return new SortArgumentResolver(restPageProperties); + } + + @Bean + public PageableArgumentResolver pageableArgumentResolver() { + return new PageableArgumentResolver(restPageProperties, sortArgumentResolver()); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(sortArgumentResolver()); + argumentResolvers.add(pageableArgumentResolver()); + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/ApiResultDynamicBodyExtractor.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/ApiResultDynamicBodyExtractor.java new file mode 100644 index 0000000..5ce27df --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/ApiResultDynamicBodyExtractor.java @@ -0,0 +1,18 @@ +package cn.axzo.framework.autoconfigure.web.advice; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author liyong.tian + * @since 2019/4/16 19:01 + */ +public class ApiResultDynamicBodyExtractor implements DynamicBodyExtractor{ + + @Override + public JsonNode extract(JsonNode body) { + if (body.has("data")) { + return body.get("data"); + } + return body; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/BodyAdviceAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/BodyAdviceAutoConfiguration.java new file mode 100644 index 0000000..d46667c --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/BodyAdviceAutoConfiguration.java @@ -0,0 +1,42 @@ +package cn.axzo.framework.autoconfigure.web.advice; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Servlet; + +/** + * @author liyong.tian + * @since 2019/4/17 10:17 + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) +public class BodyAdviceAutoConfiguration { + + @Bean + @ConditionalOnClass(name = "cn.axzo.framework.web.servlet.filter.OrderedTimerFilter") + @ConditionalOnProperty(value = "spring.mvc.response.verbose-enabled", havingValue = "true") + public VerboseResultAdvice verboseResultAdvice() { + return new VerboseResultAdvice(); + } + + @Bean + @ConditionalOnMissingBean + public DynamicBodyExtractor dynamicBodyExtractor() { + return new ApiResultDynamicBodyExtractor(); + } + + @Bean + @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") + @ConditionalOnBean(ObjectMapper.class) + @ConditionalOnProperty(value = "spring.mvc.response.dynamic-enabled", havingValue = "true", matchIfMissing = true) + public DynamicResponseBodyAdvice dynamicResponseBodyAdvice(ObjectMapper objectMapper, + DynamicBodyExtractor dynamicBodyExtractor) { + return new DynamicResponseBodyAdvice(objectMapper, dynamicBodyExtractor); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicBodyExtractor.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicBodyExtractor.java new file mode 100644 index 0000000..b03c6aa --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicBodyExtractor.java @@ -0,0 +1,12 @@ +package cn.axzo.framework.autoconfigure.web.advice; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author liyong.tian + * @since 2019/4/16 19:00 + */ +public interface DynamicBodyExtractor { + + JsonNode extract(JsonNode body); +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicResponseBodyAdvice.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicResponseBodyAdvice.java new file mode 100644 index 0000000..7bb3d89 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/DynamicResponseBodyAdvice.java @@ -0,0 +1,98 @@ +package cn.axzo.framework.autoconfigure.web.advice; + +import cn.axzo.framework.jackson.utility.DynamicJSON; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import javax.servlet.http.HttpServletRequest; + +/** + * 动态JSON响应 + * + * @author liyong.tian + * @since 2019/4/16 19:04 + */ +@SuppressWarnings("WeakerAccess") +@Slf4j +@RestControllerAdvice(annotations = RestController.class) +public class DynamicResponseBodyAdvice implements ResponseBodyAdvice, Ordered { + + private final static String PARAMETER_FIELDS = "fields"; + + private final ObjectMapper objectMapper; + + private final DynamicBodyExtractor extractor; + + public DynamicResponseBodyAdvice(ObjectMapper objectMapper, DynamicBodyExtractor extractor) { + log.debug("Add ResponseBodyAdvice: DynamicResponseBodyAdvice"); + this.objectMapper = objectMapper; + this.extractor = extractor; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response) { + if (!(request instanceof ServletServerHttpRequest)) { + return body; + } + + HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + String[] fields = servletRequest.getParameterValues(PARAMETER_FIELDS); + if (ArrayUtils.isEmpty(fields)) { + return body; + } + + fields = convertIfNecessary(fields); + if (ArrayUtils.isNotEmpty(fields)) { + JsonNode node = objectMapper.valueToTree(body); + DynamicJSON.filter(extractor.extract(node), fields); + return node; + } + + return body; + } + + private String[] convertIfNecessary(String[] origin) { + String[][] valuesArray = new String[origin.length][]; + int length = 0; + for (int i = 0; i < origin.length; i++) { + String[] values = StringUtils.commaDelimitedListToStringArray(origin[i]); + length += values.length; + valuesArray[i] = values; + } + + String[] result = new String[length]; + int index = 0; + for (String[] values : valuesArray) { + System.arraycopy(values, 0, result, index, values.length); + index += values.length; + } + return result; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/VerboseResultAdvice.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/VerboseResultAdvice.java new file mode 100644 index 0000000..54c48a5 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/advice/VerboseResultAdvice.java @@ -0,0 +1,98 @@ +package cn.axzo.framework.autoconfigure.web.advice; + +import cn.axzo.framework.domain.web.result.Result; +import cn.axzo.framework.web.servlet.filter.OrderedTimerFilter; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpEntity; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 返回接口中加上一些额外信息 + * + * @author liyong.tian + * @since 2019/4/16 19:19 + */ +@SuppressWarnings({"WeakerAccess"}) +@Slf4j +@RestControllerAdvice(annotations = RestController.class) +public class VerboseResultAdvice implements ResponseBodyAdvice, Ordered { + + private final static String PROPERTY_DURATION = "duration"; + + public VerboseResultAdvice() { + log.debug("Add ResponseBodyAdvice: ResultAdvice"); + } + + @Override + public boolean supports(MethodParameter methodParameter, Class> aClass) { + val returnType = methodParameter.getParameterType(); + return HttpEntity.class.isAssignableFrom(returnType) + || Result.class.isAssignableFrom(returnType) + || ObjectNode.class.isAssignableFrom(returnType); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter methodParameter, + MediaType mediaType, + Class> aClass, + ServerHttpRequest request, + ServerHttpResponse response) { + if (!(request instanceof ServletServerHttpRequest)) { + return body; + } + + if (body instanceof Result) { + Long duration = calcDuration(((ServletServerHttpRequest) request).getServletRequest()); + if (duration != null) { + Map map = ((Result) body).toMap(); + map.put(PROPERTY_DURATION, duration); + return map; + } + } else if (body instanceof ObjectNode) { + Long duration = calcDuration(((ServletServerHttpRequest) request).getServletRequest()); + if (duration != null) { + ((ObjectNode) body).put(PROPERTY_DURATION, duration); + } + } + + return body; + } + + @Nullable + private Long calcDuration(HttpServletRequest servletRequest) { + Long startTime = getStartTime(servletRequest); + if (startTime != null) { + return (System.nanoTime() - startTime) / 1000_000; + } + return null; + } + + @Nullable + private Long getStartTime(HttpServletRequest servletRequest) { + Object startTime = servletRequest.getAttribute(OrderedTimerFilter.ATTRIBUTE_START_TIME); + if (startTime instanceof Long) { + return (Long) startTime; + } + return null; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} \ No newline at end of file diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/context/WebMvcAwareAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/context/WebMvcAwareAutoConfiguration.java new file mode 100644 index 0000000..ffdcf7f --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/context/WebMvcAwareAutoConfiguration.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.autoconfigure.web.context; + +import cn.axzo.framework.web.servlet.context.RequestMappingHandlerAdapterLazyAwareProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Servlet; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/12/14 17:35 + **/ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) +public class WebMvcAwareAutoConfiguration { + + @Bean + public RequestMappingHandlerAdapterLazyAwareProcessor requestMappingHandlerAdapterLazyAwareProcessor() { + return new RequestMappingHandlerAdapterLazyAwareProcessor(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsAutoConfiguration.java new file mode 100644 index 0000000..7d1b031 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsAutoConfiguration.java @@ -0,0 +1,95 @@ +package cn.axzo.framework.autoconfigure.web.cors; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.CorsRegistration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +import javax.servlet.Servlet; + +import static java.util.Arrays.stream; + +/** + * CORS跨域配置 + * + * @author liyong.tian + * @since 2019/5/30 16:50 + */ +@Slf4j +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +@ConditionalOnProperty(prefix = "spring.mvc.cors", name = "enabled", havingValue = "true") +@EnableConfigurationProperties(CorsProperties.class) +@RequiredArgsConstructor +public class CorsAutoConfiguration extends WebMvcConfigurationSupport { + + private final CorsProperties properties; + + /** + * 注册CORS过滤器 + */ + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsStdRegistration(properties).getCorsConfiguration(); + if (!config.getAllowedOrigins().isEmpty()) { + log.debug("Registering CORS filter"); + stream(properties.getPatterns()).forEach(pattern -> source.registerCorsConfiguration(pattern, config)); + } + return new CorsFilter(source); + } + + /** + * 配置CorsInterceptor的CORS参数 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + log.debug("Registering CORS interceptor"); + stream(properties.getPatterns()).forEach(pattern -> + registry.addMapping(pattern) + .allowedOrigins(properties.getAllowedOrigins()) + .allowedMethods(properties.getAllowedMethods()) + .allowedHeaders(properties.getAllowedHeaders()) + .exposedHeaders(properties.getExposedHeaders()) + .allowCredentials(properties.getAllowCredentials()) + .maxAge(properties.getMaxAge()) + ); + } + + static class CorsStdRegistration extends CorsRegistration { + + /** + * Create a new {@link CorsRegistration} that allows all origins, headers, and + * credentials for {@code GET}, {@code HEAD}, and {@code POST} requests with + * max age set to 1800 seconds (30 minutes) for the specified path. + * + * @param properties cors配置属性 + */ + public CorsStdRegistration(CorsProperties properties) { + super(CorsConfiguration.ALL); + super.allowCredentials(properties.getAllowCredentials()); + super.allowedHeaders(properties.getAllowedHeaders()); + super.allowedMethods(properties.getAllowedMethods()); + super.allowedOrigins(properties.getAllowedOrigins()); + super.exposedHeaders(properties.getExposedHeaders()); + super.maxAge(properties.getMaxAge()); + } + + @Override + public CorsConfiguration getCorsConfiguration() { + return super.getCorsConfiguration(); + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsProperties.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsProperties.java new file mode 100644 index 0000000..b9878bb --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/cors/CorsProperties.java @@ -0,0 +1,94 @@ +package cn.axzo.framework.autoconfigure.web.cors; + +import jodd.util.StringUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.stream.Stream; + +import static org.springframework.http.HttpMethod.*; +import static org.springframework.web.cors.CorsConfiguration.ALL; + +/** + * CORS跨域配置 + * + * @author liyong.tian + * @since 2019/5/30 16:01 + */ +@Data +@Validated +@ConfigurationProperties("spring.mvc.cors") +public class CorsProperties { + + private boolean enabled; + + /** + * the path that the CORS configuration should apply to; + * exact path mapping URIs (such as {@code "/admin"}) are supported as well + * as Ant-style path patterns (such as {@code "/admin/**"}). When not set, + * defaults to '/**'. + */ + @NotEmpty + private String[] patterns = {"/**"}; + + /** + * Comma-separated list of origins to allow. '*' allows all origins. When not set, + * defaults to '*'. + */ + @NotNull + private String[] allowedOrigins = new String[]{ALL}; + + /** + * Comma-separated list of methods to allow. '*' allows all methods. When not set, + * defaults to GET, HEAD, POST. + */ + @NotNull + private String[] allowedMethods = {GET.name(), HEAD.name(), POST.name()}; + + /** + * Comma-separated list of headers to allow in a request. '*' allows all headers. When not set, + * defaults to '*'. + */ + @NotNull + private String[] allowedHeaders = {ALL}; + + /** + * Comma-separated list of headers to include in a response. + */ + @NotNull + private String[] exposedHeaders = {}; + + /** + * Set whether credentials are supported. When not set, defaults to 'true'. + */ + @NotNull + private Boolean allowCredentials = true; + + /** + * How long, in seconds, the response from a pre-flight request can be cached by clients. When not set, + * defaults to 1800. + */ + @NotNull + @Min(1) + private Long maxAge = 1800L; + + public String[] getAllowedOrigins() { + return Stream.of(allowedOrigins).filter(StringUtil::isNotBlank).toArray(String[]::new); + } + + public String[] getAllowedMethods() { + return Stream.of(allowedMethods).filter(StringUtil::isNotBlank).map(String::toUpperCase).toArray(String[]::new); + } + + public String[] getAllowedHeaders() { + return Stream.of(allowedHeaders).filter(StringUtil::isNotBlank).toArray(String[]::new); + } + + public String[] getExposedHeaders() { + return Stream.of(exposedHeaders).filter(StringUtil::isNotBlank).toArray(String[]::new); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/ExceptionHandlerAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/ExceptionHandlerAutoConfiguration.java new file mode 100644 index 0000000..fed8d6e --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/ExceptionHandlerAutoConfiguration.java @@ -0,0 +1,187 @@ +package cn.axzo.framework.autoconfigure.web.exception; + +import cn.axzo.framework.autoconfigure.web.exception.handler.ExceptionResultHandler; +import cn.axzo.framework.autoconfigure.web.exception.handler.internal.*; +import cn.axzo.framework.autoconfigure.web.exception.resolver.HttpStatusResolver; +import cn.axzo.framework.autoconfigure.web.exception.resolver.internal.FileTooLargeExceptionHttpStatusResolver; +import cn.axzo.framework.autoconfigure.web.exception.resolver.internal.RequestRejectedExceptionHttpStatusResolver; +import cn.axzo.framework.autoconfigure.web.exception.support.GlobalErrorController; +import cn.axzo.framework.autoconfigure.web.exception.support.GlobalExceptionHandler; +import cn.axzo.framework.domain.web.result.Result; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import javax.servlet.Servlet; +import java.util.List; + +import static org.springframework.boot.autoconfigure.condition.SearchStrategy.CURRENT; + +/** + * @author liyong.tian + * @since 2017/1/17 + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +@AutoConfigureBefore(ErrorMvcAutoConfiguration.class) +@EnableConfigurationProperties(RespErrorCodeMappingProperties.class) +public class ExceptionHandlerAutoConfiguration implements WebMvcConfigurer { + + @Configuration + public static class ExceptionResultHandlerConfiguration { + + private final RespErrorCodeMappingProperties properties; + + public ExceptionResultHandlerConfiguration(RespErrorCodeMappingProperties properties) { + this.properties = properties; + } + + /** + * 异常的标准处理方式 + */ + @Bean + public StandardExceptionResultHandler standardErrorControllerCustomer() { + return new StandardExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "org.springframework.dao.ConcurrencyFailureException") + @ConditionalOnMissingBean(name = "concurrencyFailureExceptionResultHandler") + public ConcurrencyFailureExceptionResultHandler concurrencyFailureExceptionResultHandler() { + return new ConcurrencyFailureExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "org.springframework.security.access.AccessDeniedException") + @ConditionalOnMissingBean(name = "accessDeniedExceptionResultHandler") + public AccessDeniedExceptionResultHandler accessDeniedExceptionResultHandler() { + return new AccessDeniedExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnMissingBean(name = "pageRequestExceptionResultHandler") + public PageRequestExceptionResultHandler pageRequestExceptionResultHandler() { + return new PageRequestExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "org.springframework.web.client.HttpClientErrorException") + @ConditionalOnMissingBean(name = "httpClientErrorExceptionResultHandler") + public HttpClientErrorExceptionResultHandler httpClientErrorExceptionResultHandler() { + return new HttpClientErrorExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "cn.axzo.framework.context.validation.SpringValidatorException") + @ConditionalOnMissingBean(name = "springValidatorExceptionResultHandler") + public SpringValidatorExceptionResultHandler springValidatorExceptionResultHandler() { + return new SpringValidatorExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "org.springframework.security.web.firewall.RequestRejectedException") + @ConditionalOnMissingBean(name = "requestRejectedExceptionResultHandler") + public RequestRejectedExceptionResultHandler requestRejectedExceptionResultHandler() { + return new RequestRejectedExceptionResultHandler(properties); + } + + @Bean + @ConditionalOnClass(name = "javax.validation.ValidationException") + @ConditionalOnMissingBean(name = "validationExceptionResultHandler") + public ValidationExceptionResultHandler validationExceptionResultHandler() { + return new ValidationExceptionResultHandler(properties); + } + } + + @Configuration + @ConditionalOnClass({OAuth2Exception.class, WebResponseExceptionTranslator.class}) + public static class OAuth2ExceptionResultHandlerConfiguration { + + private final RespErrorCodeMappingProperties properties; + + public OAuth2ExceptionResultHandlerConfiguration(RespErrorCodeMappingProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public WebResponseExceptionTranslator webResponseExceptionTranslator() { + return new DefaultWebResponseExceptionTranslator(); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2ExceptionResultHandler oAuth2ExceptionReturnHandler(WebResponseExceptionTranslator translator) { + return new OAuth2ExceptionResultHandler(properties, translator); + } + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationExceptionReturnHandler clientRegistrationExceptionReturnHandler( + WebResponseExceptionTranslator translator) { + return new ClientRegistrationExceptionReturnHandler(properties, translator); + } + } + + @Configuration + public static class ErrorControllerConfiguration { + + private final ServerProperties serverProperties; + + private final ErrorAttributes errorAttributes; + + private final List> handlers; + + public ErrorControllerConfiguration(ServerProperties serverProperties, ErrorAttributes errorAttributes, + List> handlers) { + this.serverProperties = serverProperties; + this.errorAttributes = errorAttributes; + this.handlers = handlers; + } + + /** + * 全局异常捕获 + */ + @Bean + @ConditionalOnMissingBean(value = {ErrorController.class, ResponseEntityExceptionHandler.class}, search = CURRENT) + public GlobalExceptionHandler globalExceptionHandler() { + return new GlobalExceptionHandler(handlers, errorAttributes, serverProperties); + } + + /** + * HTTP请求异常捕获 + */ + @Bean + @ConditionalOnMissingBean(value = ErrorController.class, search = CURRENT) + public GlobalErrorController globalErrorController(List resolvers) { + return new GlobalErrorController(errorAttributes, serverProperties, resolvers); + } + + @Bean + @ConditionalOnMissingBean(name = "fileTooLargeExceptionHttpStatusResolver") + public FileTooLargeExceptionHttpStatusResolver fileTooLargeExceptionHttpStatusResolver() { + return new FileTooLargeExceptionHttpStatusResolver(); + } + + @Bean + @ConditionalOnMissingBean(name = "requestRejectedExceptionHttpStatusResolver") + public RequestRejectedExceptionHttpStatusResolver requestRejectedExceptionHttpStatusResolver() { + return new RequestRejectedExceptionHttpStatusResolver(); + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/RespErrorCodeMappingProperties.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/RespErrorCodeMappingProperties.java new file mode 100644 index 0000000..9d53f28 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/RespErrorCodeMappingProperties.java @@ -0,0 +1,80 @@ +package cn.axzo.framework.autoconfigure.web.exception; + +import cn.axzo.framework.domain.web.code.BaseCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +import static com.google.common.collect.Maps.newHashMap; +import static org.springframework.http.HttpStatus.*; + +/** + * 配置错误响应码到HTTP状态码的映射关系 + *

+ * 默认情况下:基础错误响应码会映射到相同的HTTP状态码,非基础错误响应码都映射到451(UNAVAILABLE_FOR_LEGAL_REASONS) + * 可配置:可自定义映射关系用来满足自己的业务需求 + *

+ * 几种常见的配置需求: + *

+ *     // 所有的错误响应码都映射到HTTP状态码200,即HTTP永远返回成功,只需关注错误响应码
+ *     resp.error-code.base-mapping.*=ok
+ *     resp.error-code.non-base-mapping.*=ok
+ * 
+ *
+ *     // 所有的非基础错误响应码都映射到HTTP状态码200,而基础错误响应码仍然映射到相同的HTTP状态码
+ *     resp.error-code.non-base-mapping.*=ok
+ * 
+ * + * @author liyong.tian + * @since 2019/4/15 16:50 + */ +@ToString +@Validated +@ConfigurationProperties(prefix = "resp.error-code") +public class RespErrorCodeMappingProperties { + + private static final Map DEFAULT_BASE_MAPPING = newHashMap(); + + private static final Map DEFAULT_NON_BASE_MAPPING = newHashMap(); + + static { + DEFAULT_BASE_MAPPING.put(BaseCode.BAD_REQUEST.getRespCode(), BAD_REQUEST); + DEFAULT_BASE_MAPPING.put(BaseCode.FORBIDDEN.getRespCode(), FORBIDDEN); + DEFAULT_BASE_MAPPING.put(BaseCode.NOT_FOUND.getRespCode(), NOT_FOUND); + DEFAULT_BASE_MAPPING.put(BaseCode.SERVER_ERROR.getRespCode(), INTERNAL_SERVER_ERROR); + DEFAULT_BASE_MAPPING.put(BaseCode.SERVICE_UNAVAILABLE.getRespCode(), SERVICE_UNAVAILABLE); + DEFAULT_BASE_MAPPING.put(BaseCode.UNAUTHORIZED.getRespCode(), UNAUTHORIZED); + DEFAULT_BASE_MAPPING.put(BaseCode.NOT_ACCEPTABLE.getRespCode(), NOT_ACCEPTABLE); + DEFAULT_BASE_MAPPING.put(BaseCode.CONFLICT.getRespCode(), CONFLICT); + DEFAULT_BASE_MAPPING.put(BaseCode.PAYLOAD_TOO_LARGE.getRespCode(), PAYLOAD_TOO_LARGE); + DEFAULT_BASE_MAPPING.put(BaseCode.UNSUPPORTED_MEDIA_TYPE.getRespCode(), UNSUPPORTED_MEDIA_TYPE); + DEFAULT_BASE_MAPPING.put(BaseCode.UNAVAILABLE_FOR_LEGAL_REASONS.getRespCode(), UNAVAILABLE_FOR_LEGAL_REASONS); + DEFAULT_BASE_MAPPING.put(BaseCode.METHOD_NOT_ALLOWED.getRespCode(), METHOD_NOT_ALLOWED); + + DEFAULT_NON_BASE_MAPPING.put("*", UNAVAILABLE_FOR_LEGAL_REASONS); + } + + // 基础错误响应码 -> HTTP状态码 + @NotNull + @Setter + @Getter + private Map baseMapping = DEFAULT_BASE_MAPPING; + + // 非基础错误响应码 -> HTTP状态码 + @NotNull + @Setter + @Getter + private Map nonBaseMapping = DEFAULT_NON_BASE_MAPPING; + + // 错误码在mapping中无法匹配时映射的HTTP状态码 + @NotNull + @Setter + @Getter + private HttpStatus fallbackHttpStatus = INTERNAL_SERVER_ERROR; +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/AbstractExceptionApiResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/AbstractExceptionApiResultHandler.java new file mode 100644 index 0000000..1e490be --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/AbstractExceptionApiResultHandler.java @@ -0,0 +1,144 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.result.ApiResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static cn.axzo.framework.core.Constants.API_MARKER; +import static cn.axzo.framework.domain.web.code.BaseCode.SERVER_ERROR; +import static cn.axzo.framework.domain.web.result.ApiResult.err; +import static jodd.util.StringPool.ASTERISK; +import static org.springframework.http.HttpHeaders.CACHE_CONTROL; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 20:14 + **/ +@Slf4j +public abstract class AbstractExceptionApiResultHandler implements ExceptionApiResultHandler, Ordered { + + // 基础错误响应码 -> HTTP状态码 + private final Map baseMapping; + + // 非基础错误响应码 -> HTTP状态码 + private final Map nonBaseMapping; + + // 错误码在mapping中无法匹配时映射的HTTP状态码 + private final HttpStatus fallbackStatus; + + public AbstractExceptionApiResultHandler(RespErrorCodeMappingProperties properties) { + this.baseMapping = properties.getBaseMapping(); + this.nonBaseMapping = properties.getNonBaseMapping(); + this.fallbackStatus = properties.getFallbackHttpStatus(); + } + + @Override + public ApiResult handle(HttpServletRequest request, HttpServletResponse response, T ex, Map attributes) { + response.setHeader(CACHE_CONTROL, "no-store"); + + // 1.获取异常源 + T error = getRealCause(ex); + String errorMsg = error.getMessage(); + + // 2.获取code + IRespCode respCode = decode(error, getFallbackCode()); + final String code = respCode.getRespCode(); + + // 3.映射HTTP状态码 + HttpStatus status = mappingHttpStatus(code, ex); + response.setStatus(status.value()); + + // 4.获取message + final String message; + if (status.is5xxServerError()) { + message = getMessage(respCode, attributes, ex); + } else { + message = getMessage(respCode, ex); + } + + // 5.打印日志 + log(status, request, errorMsg, error); + + // 6.返回body + return err(code, message); + } + + protected T getRealCause(T ex) { + return ex; + } + + protected IRespCode getFallbackCode() { + return Stream.of(BaseCode.class.getEnumConstants()) + .filter(code -> Objects.equals(code.getStatus(), fallbackStatus.value())) + .findFirst() + .orElse(SERVER_ERROR); + } + + protected String getMessage(IRespCode respCode, Map attributes, T ex) { + Object trace = attributes.get("trace"); + if (trace != null) { + return trace.toString(); + } else { + return getMessage(respCode, ex); + } + } + + protected String getMessage(IRespCode respCode, T ex) { + return respCode.getMessage(); + } + + protected abstract IRespCode decode(T ex, IRespCode fallbackCode); + + protected HttpStatus mappingHttpStatus(String code, T ex) { + return Stream.of(BaseCode.class.getEnumConstants()) + .filter(baseCode -> Objects.equals(baseCode.getRespCode(), code)) + .findFirst() + .map(baseCode -> { + if (baseMapping.containsKey(ASTERISK)) { + return baseMapping.get(ASTERISK); + } + if(baseMapping.containsKey(code)) { + return baseMapping.get(code); + } + return fallbackStatus; + }).orElseGet(() -> { + if (nonBaseMapping.containsKey(ASTERISK)) { + return nonBaseMapping.get(ASTERISK); + } + if (nonBaseMapping.containsKey(code)) { + return nonBaseMapping.get(code); + } + return fallbackStatus; + }); + } + + protected void log(HttpStatus status, HttpServletRequest request, String errorMsg, Throwable error) { + if (status.is5xxServerError()) { + log.error(API_MARKER, "Exception occurred: " + errorMsg + ". [URL=" + request.getRequestURI() + "]", error); + } else { + log.warn(API_MARKER, errorMsg + ". [URL=" + request.getRequestURI() + "]"); + } + } + + + @Override + public int getOrder() { + return 0; + } + + @Override + public Class getSuperClass() { + return AbstractExceptionApiResultHandler.class; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionApiResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionApiResultHandler.java new file mode 100644 index 0000000..26c1c0b --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionApiResultHandler.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler; + +import cn.axzo.framework.domain.web.result.ApiResult; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 20:13 + **/ +public interface ExceptionApiResultHandler extends ExceptionResultHandler>{ + + @Override + default Class getSuperClass() { + return ExceptionApiResultHandler.class; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionResultHandler.java new file mode 100644 index 0000000..f075cd9 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/ExceptionResultHandler.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler; + +import cn.axzo.framework.core.util.ReflectUtil; +import cn.axzo.framework.domain.web.result.Result; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 20:12 + **/ +public interface ExceptionResultHandler { + + R handle(HttpServletRequest request, HttpServletResponse response, T e, Map attributes); + + default Class getExceptionClass() { + return ReflectUtil.getSuperGenericType(getClass(), getSuperClass()); + } + + default Class getSuperClass() { + return ExceptionResultHandler.class; + } + + default boolean isRecursive() { + return false; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/AccessDeniedExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/AccessDeniedExceptionResultHandler.java new file mode 100644 index 0000000..9cba8a0 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/AccessDeniedExceptionResultHandler.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import org.springframework.security.access.AccessDeniedException; + +/** + * @Description 拒绝访问异常结果 + * @Author liyong.tian + * @Date 2020/9/10 10:27 + **/ +public class AccessDeniedExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public AccessDeniedExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(AccessDeniedException e, IRespCode fallbackCode) { + return BaseCode.FORBIDDEN; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ClientRegistrationExceptionReturnHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ClientRegistrationExceptionReturnHandler.java new file mode 100644 index 0000000..72c67a9 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ClientRegistrationExceptionReturnHandler.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.web.servlet.security.oauth.OAuth2Util; +import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.ClientRegistrationException; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; + +/** + * @Description 客户端注册异常返回 + * @Author liyong.tian + * @Date 2020/9/10 10:29 + **/ +public class ClientRegistrationExceptionReturnHandler extends AbstractExceptionApiResultHandler { + + private final WebResponseExceptionTranslator exceptionTranslator; + + private BadClientCredentialsException exception = new BadClientCredentialsException(); + + public ClientRegistrationExceptionReturnHandler(RespErrorCodeMappingProperties properties, + WebResponseExceptionTranslator exceptionTranslator) { + super(properties); + this.exceptionTranslator = exceptionTranslator; + } + + @Override + protected IRespCode decode(ClientRegistrationException e, IRespCode fallbackCode) { + return BaseCode.parse(exception.getHttpErrorCode()); + } + + @Override + protected String getMessage(IRespCode respCode, ClientRegistrationException e) { + try { + return OAuth2Util.getMessage((OAuth2Exception) exceptionTranslator.translate(exception).getBody()); + } catch (Exception ex) { + throw new InternalException(ex); + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ConcurrencyFailureExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ConcurrencyFailureExceptionResultHandler.java new file mode 100644 index 0000000..284ff0c --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ConcurrencyFailureExceptionResultHandler.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * @Description 并发故障异常 + * @Author liyong.tian + * @Date 2020/9/10 10:31 + **/ +public class ConcurrencyFailureExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public ConcurrencyFailureExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(ConcurrencyFailureException e, IRespCode fallbackCode) { + return BaseCode.CONFLICT; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/HttpClientErrorExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/HttpClientErrorExceptionResultHandler.java new file mode 100644 index 0000000..62536cc --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/HttpClientErrorExceptionResultHandler.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import org.springframework.web.client.HttpClientErrorException; + +/** + * @Description HttpClient错误异常 + * @Author liyong.tian + * @Date 2020/9/10 10:33 + **/ +public class HttpClientErrorExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public HttpClientErrorExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(HttpClientErrorException e, IRespCode fallbackCode) { + return BaseCode.parse(e.getRawStatusCode()); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/OAuth2ExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/OAuth2ExceptionResultHandler.java new file mode 100644 index 0000000..a6b4c06 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/OAuth2ExceptionResultHandler.java @@ -0,0 +1,63 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.web.servlet.security.oauth.OAuth2Util; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; + +import static org.springframework.security.oauth2.common.exceptions.OAuth2Exception.INVALID_GRANT; +import static org.springframework.security.oauth2.common.exceptions.OAuth2Exception.INVALID_TOKEN; + +/** + * OAuth2异常 + * + * @author liyong.tian + * @since 2019/4/15 17:54 + */ +public class OAuth2ExceptionResultHandler extends AbstractExceptionApiResultHandler { + + private final WebResponseExceptionTranslator exceptionTranslator; + + public OAuth2ExceptionResultHandler(RespErrorCodeMappingProperties properties, + WebResponseExceptionTranslator exceptionTranslator) { + super(properties); + this.exceptionTranslator = exceptionTranslator; + } + + @Override + protected IRespCode decode(OAuth2Exception e, IRespCode fallbackCode) { + try { + e = (OAuth2Exception) exceptionTranslator.translate(e).getBody(); + return BaseCode.parse(e.getHttpErrorCode()); + } catch (Exception ex) { + throw new InternalException(ex); + } + } + + @Override + protected String getMessage(IRespCode respCode, OAuth2Exception e) { + try { + e = (OAuth2Exception) exceptionTranslator.translate(e).getBody(); + String errorCode = e.getOAuth2ErrorCode(); + switch (errorCode) { + case INVALID_GRANT: + return "用户名或密码错误"; + case INVALID_TOKEN: + return "不合法的Token"; + default: + return OAuth2Util.getMessage(e); + } + } catch (Exception ex) { + throw new InternalException(ex); + } + } + + @Override + public boolean isRecursive() { + return true; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/PageRequestExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/PageRequestExceptionResultHandler.java new file mode 100644 index 0000000..609dba4 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/PageRequestExceptionResultHandler.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.page.PageRequestException; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; + +/** + * @Description 分页请求异常 + * @Author liyong.tian + * @Date 2020/9/10 10:37 + **/ +public class PageRequestExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public PageRequestExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(PageRequestException e, IRespCode fallbackCode) { + return BaseCode.BAD_REQUEST; + } + + @Override + protected String getMessage(IRespCode respCode, PageRequestException ex) { + return ex.getMessage(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/RequestRejectedExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/RequestRejectedExceptionResultHandler.java new file mode 100644 index 0000000..dc4d911 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/RequestRejectedExceptionResultHandler.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import org.springframework.security.web.firewall.RequestRejectedException; + +/** + * @Description 请求拒绝异常 + * @Author liyong.tian + * @Date 2020/9/10 10:39 + **/ +public class RequestRejectedExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public RequestRejectedExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(RequestRejectedException ex, IRespCode fallbackCode) { + return BaseCode.BAD_REQUEST; + } + + @Override + protected String getMessage(IRespCode respCode, RequestRejectedException ex) { + return "非法的URL"; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/SpringValidatorExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/SpringValidatorExceptionResultHandler.java new file mode 100644 index 0000000..762d6f3 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/SpringValidatorExceptionResultHandler.java @@ -0,0 +1,38 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.context.validation.SpringValidatorException; +import org.springframework.validation.FieldError; + +import static java.lang.String.format; +import static jodd.util.StringUtil.isNotBlank; + +/** + * @Description Spring校验异常 + * @Author liyong.tian + * @Date 2020/9/10 10:41 + **/ +public class SpringValidatorExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public SpringValidatorExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(SpringValidatorException ex, IRespCode fallbackCode) { + return BaseCode.BAD_REQUEST; + } + + @Override + protected String getMessage(IRespCode respCode, SpringValidatorException e) { + FieldError error = e.getBindingResult().getFieldError(); + if (isNotBlank(error.getDefaultMessage())) { + return format("Validation refused [%s %s]", error.getField(), error.getDefaultMessage()); + } else { + return "Validation refused"; + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/StandardExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/StandardExceptionResultHandler.java new file mode 100644 index 0000000..2930588 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/StandardExceptionResultHandler.java @@ -0,0 +1,91 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.domain.ServiceException; +import cn.axzo.framework.domain.web.ApiException; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.code.RespCode; +import org.springframework.http.HttpStatus; + +import java.util.Map; + +import static jodd.util.StringUtil.isNotBlank; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * @Description 标准异常 + * @Author liyong.tian + * @Date 2020/9/10 10:51 + **/ +public class StandardExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public StandardExceptionResultHandler(RespErrorCodeMappingProperties properties){ + super(properties); + } + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE - 10; + } + + @Override + protected IRespCode decode(Throwable error, IRespCode fallbackCode) { + String errorMsg = error.getMessage(); + final String code; + final String message; + if (error instanceof ApiException || error instanceof ServiceException) { + if (error instanceof ApiException) { + code = ((ApiException) error).getCode(); + message = errorMsg; + } else { + String serviceCode = ((ServiceException) error).getCode(); + if (isNotBlank(serviceCode)) { + code = serviceCode; + message = errorMsg; + } else { + code = fallbackCode.getRespCode(); + message = fallbackCode.getMessage(); + } + } + } else { + code = fallbackCode.getRespCode(); + message = fallbackCode.getMessage(); + } + return new RespCode(code, message); + } + @Override + protected HttpStatus mappingHttpStatus(String code, Throwable ex) { + if (ex instanceof ApiException && ((ApiException) ex).isBadRequest()) { + return BAD_REQUEST; + } + return super.mappingHttpStatus(code, ex); + } + + @Override + protected String getMessage(IRespCode respCode, Map attributes, Throwable e) { + Object trace = attributes.get("trace"); + if (trace != null && e instanceof ServiceException) { + return e.getMessage(); + } + return super.getMessage(respCode, attributes, e); + } + + @Override + protected Throwable getRealCause(Throwable error) { + while (error.getCause() != null) { + if (error instanceof ServiceException && !(error.getCause() instanceof ServiceException)) { + break; + } + if (error instanceof ApiException && !(error.getCause() instanceof ApiException)) { + break; + } + if (error instanceof InternalException && !(error.getCause() instanceof InternalException)) { + break; + } + error = error.getCause(); + } + return error; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ValidationExceptionResultHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ValidationExceptionResultHandler.java new file mode 100644 index 0000000..916d035 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/handler/internal/ValidationExceptionResultHandler.java @@ -0,0 +1,31 @@ +package cn.axzo.framework.autoconfigure.web.exception.handler.internal; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; + +import javax.validation.ValidationException; +import java.util.Objects; + +/** + * @Description 验证异常 + * @Author liyong.tian + * @Date 2020/9/9 20:20 + **/ +public class ValidationExceptionResultHandler extends AbstractExceptionApiResultHandler { + + public ValidationExceptionResultHandler(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(ValidationException ex, IRespCode fallbackCode) { + return BaseCode.BAD_REQUEST; + } + + @Override + protected String getMessage(IRespCode respCode, ValidationException ex) { + return Objects.nonNull(ex.getMessage()) ? ex.getMessage() : respCode.getMessage(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/HttpStatusResolver.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/HttpStatusResolver.java new file mode 100644 index 0000000..6797b95 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/HttpStatusResolver.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.autoconfigure.web.exception.resolver; + +import org.springframework.http.HttpStatus; + +import java.util.Optional; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 10:56 + **/ +public interface HttpStatusResolver { + + Optional resolve(Throwable e); +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/FileTooLargeExceptionHttpStatusResolver.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/FileTooLargeExceptionHttpStatusResolver.java new file mode 100644 index 0000000..e89b6aa --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/FileTooLargeExceptionHttpStatusResolver.java @@ -0,0 +1,40 @@ +package cn.axzo.framework.autoconfigure.web.exception.resolver.internal; + +import cn.axzo.framework.autoconfigure.web.exception.resolver.HttpStatusResolver; +import cn.axzo.framework.core.util.ClassUtil; +import io.undertow.server.RequestTooBigException; +import io.undertow.server.handlers.form.MultiPartParserDefinition; +import org.apache.tomcat.util.http.fileupload.FileUploadBase; +import org.apache.tomcat.util.http.fileupload.impl.SizeException; +import org.springframework.http.HttpStatus; + +import java.util.Optional; + +import static cn.axzo.framework.core.util.ClassUtil.getDefaultClassLoader; +import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 10:57 + **/ +public class FileTooLargeExceptionHttpStatusResolver implements HttpStatusResolver { + + private boolean isTomcat = ClassUtil.isPresent("org.apache.catalina.startup.Tomcat", getDefaultClassLoader()); + + private static boolean isUndertow = ClassUtil.isPresent("io.undertow.Undertow", getDefaultClassLoader()); + + @Override + public Optional resolve(Throwable e) { + if (isUndertow && e instanceof RequestTooBigException) { + return Optional.of(PAYLOAD_TOO_LARGE); + } + if (isUndertow && e instanceof MultiPartParserDefinition.FileTooLargeException) { + return Optional.of(PAYLOAD_TOO_LARGE); + } + if (isTomcat && e instanceof SizeException) { + return Optional.of(PAYLOAD_TOO_LARGE); + } + return Optional.empty(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/RequestRejectedExceptionHttpStatusResolver.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/RequestRejectedExceptionHttpStatusResolver.java new file mode 100644 index 0000000..85128db --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/resolver/internal/RequestRejectedExceptionHttpStatusResolver.java @@ -0,0 +1,33 @@ +package cn.axzo.framework.autoconfigure.web.exception.resolver.internal; + +import cn.axzo.framework.autoconfigure.web.exception.resolver.HttpStatusResolver; +import cn.axzo.framework.core.util.ClassUtil; +import org.springframework.http.HttpStatus; +import org.springframework.security.web.firewall.RequestRejectedException; + +import java.util.Optional; + +import static cn.axzo.framework.core.util.ClassUtil.getDefaultClassLoader; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 10:59 + **/ +public class RequestRejectedExceptionHttpStatusResolver implements HttpStatusResolver { + + private static boolean isClassPresent; + + static { + String className = "org.springframework.security.web.firewall.RequestRejectedException"; + isClassPresent = ClassUtil.isPresent(className, getDefaultClassLoader()); + } + + @Override + public Optional resolve(Throwable e) { + if (isClassPresent && e instanceof RequestRejectedException) { + return Optional.of(HttpStatus.BAD_REQUEST); + } + return Optional.empty(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalErrorController.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalErrorController.java new file mode 100644 index 0000000..57a708c --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalErrorController.java @@ -0,0 +1,146 @@ +package cn.axzo.framework.autoconfigure.web.exception.support; + +import cn.axzo.framework.autoconfigure.web.exception.resolver.HttpStatusResolver; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.result.ApiResult; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 11:27 + **/ +@RestController +@RequestMapping(value = "${server.error.path:${error.path:/error}}", produces = APPLICATION_JSON_UTF8_VALUE) +public class GlobalErrorController extends AbstractErrorController { + + private final ErrorProperties errorProperties; + + private final ErrorAttributes errorAttributes; + + private final List httpStatusResolvers; + + public GlobalErrorController(ErrorAttributes errorAttributes, ServerProperties properties, + List httpStatusResolvers) { + super(errorAttributes); + this.errorProperties = properties.getError(); + this.errorAttributes = errorAttributes; + this.httpStatusResolvers = httpStatusResolvers; + } + + @RequestMapping + public ResponseEntity> error(HttpServletRequest request) { + // 1.5.X Throwable error = this.errorAttributes.getError(new ServletRequestAttributes(request)); + Throwable error = this.errorAttributes.getError(new ServletWebRequest(request)); + Cause cause = getRealCause(error); + + // 1.获取HTTP状态码 + HttpStatus status = getStatus(cause.getError(), request); + + // 2.获取code + BaseCode baseCode = BaseCode.parse(status.value()); + String code = baseCode.getRespCode(); + + // 3.获取message + String msg = getMessage(baseCode, status, cause, request); + + // 4.响应 + val body = ApiResult.err(code, msg); + return new ResponseEntity<>(body, status); + } + + @Override + public String getErrorPath() { + return errorProperties.getPath(); + } + + private HttpStatus getStatus(Throwable e, HttpServletRequest request) { + return httpStatusResolvers.stream() + .map(resolver -> resolver.resolve(e)) + .filter(Optional::isPresent) + .findFirst() + .map(Optional::get) + .orElseGet(() -> { + Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); + if (statusCode == null) { + return HttpStatus.OK; + } + try { + return HttpStatus.valueOf(statusCode); + } catch (Exception ex) { + return HttpStatus.INTERNAL_SERVER_ERROR; + } + }); + } + + private String getMessage(BaseCode baseCode, HttpStatus status, Cause cause, HttpServletRequest request) { + if (cause.getError() instanceof IllegalArgumentException) { + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, ""); + } + Map data = getErrorAttributes(request, isIncludeStackTrace(request)); + Object message = data.get("message"); + Object trace = data.get("trace"); + if (trace != null && status.is5xxServerError()) { + return trace.toString(); + } else if (!Objects.equals(message, "No errors") && !Objects.equals(message, "No message available")) { + if (cause.getDepth() > 0) { + return cause.getError().getMessage(); + } + return message.toString(); + } else { + return baseCode.getMessage(); + } + } + + /** + * Determine if the stacktrace attribute should be included. + * + * @param request the source request + * @return if the stacktrace attribute should be included + */ + private boolean isIncludeStackTrace(HttpServletRequest request) { + IncludeStacktrace include = errorProperties.getIncludeStacktrace(); + return include == IncludeStacktrace.ALWAYS + || include == IncludeStacktrace.ON_TRACE_PARAM + && getTraceParameter(request); + } + + private Cause getRealCause(Throwable error) { + if (error == null) { + return new Cause(0, null); + } + int depth = 0; + while (depth++ < 3 && error.getCause() != null) { + error = error.getCause(); + } + return new Cause(depth - 1, error); + } + + @RequiredArgsConstructor + @Getter + private class Cause { + private final int depth; + private final Throwable error; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalExceptionHandler.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalExceptionHandler.java new file mode 100644 index 0000000..20aba4b --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/GlobalExceptionHandler.java @@ -0,0 +1,288 @@ +package cn.axzo.framework.autoconfigure.web.exception.support; + +import cn.axzo.framework.autoconfigure.web.exception.handler.ExceptionResultHandler; +import cn.axzo.framework.autoconfigure.web.exception.handler.internal.StandardExceptionResultHandler; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.framework.domain.web.result.Result; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; +import static jodd.util.StringUtil.isNotBlank; + +/** + * @Description spring mvc全局异常捕获 + * @Author liyong.tian + * @Date 2020/9/10 11:41 + **/ +@RestControllerAdvice(annotations = {Controller.class, RestController.class}) +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private final List> handlers; + + private final ErrorProperties errorProperties; + + private final ErrorAttributes errorAttributes; + + public GlobalExceptionHandler(List> handlers, + ErrorAttributes errorAttributes, + ServerProperties properties) { + this.handlers = handlers; + this.errorAttributes = errorAttributes; + this.errorProperties = properties.getError(); + } + + @Override + protected ResponseEntity handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, + HttpStatus status, WebRequest request) { + if (body instanceof Result) { + return super.handleExceptionInternal(ex, body, headers, status, request); + } + + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + Map data = Requests.from(request).getErrorAttributes(errorAttributes, errorProperties); + Object trace = data.get("trace"); + if (trace != null && status.is5xxServerError()) { + message = trace.toString(); + } else { + message = ex.getMessage(); + } + + // 3.响应 + return ApiResult.err(code, message); + }); + + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + /** + * Get和表单请求参数错误 + */ + @Override + protected ResponseEntity handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, + WebRequest request) { + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + FieldError error = ex.getFieldError(); + if (isNotBlank(error.getDefaultMessage())) { + message = format("Invalid parameter [%s %s]", error.getField(), error.getDefaultMessage()); + } else { + message = "Invalid parameter"; + } + + // 3.响应 + return ApiResult.err(code, message); + }); + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + /** + * Get请求参数类型不匹配 + */ + @Override + protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + if (ex instanceof MethodArgumentTypeMismatchException) { + String field = ((MethodArgumentTypeMismatchException) ex).getName(); + message = format("Type mismatched [%s = %s]", field, ex.getValue()); + } else { + message = format("Type mismatched for value [%s]", ex.getValue()); + } + + // 3.响应 + return ApiResult.err(code, message); + }); + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + /** + * 请求字段校验不通过(Validation规范) + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, + WebRequest request) { + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + FieldError error = ex.getBindingResult().getFieldError(); + if (isNotBlank(error.getDefaultMessage())) { + message = format("%s %s", error.getField(), error.getDefaultMessage()); + } else { + message = "Validation refused"; + } + + // 3.响应 + return ApiResult.err(code, message); + }); + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + /** + * Get请求参数不存在 + */ + @Override + protected ResponseEntity handleMissingServletRequestParameter(MissingServletRequestParameterException ex, + HttpHeaders headers, HttpStatus status, + WebRequest request) { + + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + String field = ex.getParameterName(); + message = "Required parameter '" + field + "' is not present"; + + // 3.响应 + return ApiResult.err(code, message); + }); + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + /** + * 请求体格式错误 + */ + @Override + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, HttpStatus status, + WebRequest request) { + Result result = _handleException(request, ex).orElseGet(() -> { + // 1.获取code + String code = BaseCode.parse(status.value()).getRespCode(); + + // 2.获取message + String message; + Throwable e = ex.getCause(); + String fallbackMessage = "Request body contains invalid format"; + if (e == null) { + message = "Required request body is missing"; + } else if (e instanceof InvalidFormatException) { + // Json请求数据格式错误 + InvalidFormatException ife = ((InvalidFormatException) e); + message = ife.getPath().stream() + .map(JsonMappingException.Reference::getFieldName) + .reduce((field1, field2) -> field1 + "." + field2) + .map(field -> format("Invalid format [%s = %s]", field, ife.getValue())) + .orElse(fallbackMessage); + } else if (e instanceof JsonMappingException) { + // Json请求数据格式错误 + JsonMappingException jme = (JsonMappingException) e; + message = jme.getPath().stream() + .map(JsonMappingException.Reference::getFieldName) + .reduce((field1, field2) -> field1 + "." + field2) + .map(field -> format("Invalid format [%s]", field)) + .orElse(fallbackMessage); + } else if (e instanceof JsonParseException) { + message = "Json request body contains invalid format"; + } else { + message = fallbackMessage; + } + + // 3.响应 + return ApiResult.err(code, message); + }); + return super.handleExceptionInternal(ex, result, headers, status, request); + } + + @ExceptionHandler(MultipartException.class) + public void handleMultipartException(MultipartException e) throws MultipartException { + throw e; + } + + @ExceptionHandler(Throwable.class) + public Result handleAllException(HttpServletRequest request, HttpServletResponse response, Throwable e) { + Map attributes = Requests.from(request).getErrorAttributes(errorAttributes, errorProperties); + return _handleException(request, response, e, attributes) + .orElseGet(() -> handlers.stream() + .filter(handler -> handler.getExceptionClass() == Throwable.class) + .findFirst() + .map(handler -> handler.handle(request, response, ClassUtil.cast(e), attributes)) + .orElseThrow(() -> new InternalException("No exception handler with: " + e.getClass())) + ); + } + + private Optional _handleException(WebRequest webRequest, Throwable e) { + Map attributes = Requests.from(webRequest).getErrorAttributes(errorAttributes, errorProperties); + if (webRequest instanceof ServletWebRequest) { + HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); + HttpServletResponse response = ((ServletWebRequest) webRequest).getResponse(); + return _handleException(request, response, e, attributes); + } + return Optional.empty(); + } + + private Optional _handleException(HttpServletRequest request, HttpServletResponse response, Throwable e, + Map attributes) { + Optional resultOptional = handlers.stream() + .filter(handler -> shouldFilter(handler, e)) + .map(handler -> handler.handle(request, response, ClassUtil.cast(e), attributes)) + .findFirst(); + if (resultOptional.isPresent()) { + return resultOptional; + } else { + return handlers.stream() + .filter(handler -> handler.getClass() != StandardExceptionResultHandler.class) + .filter(handler -> handler.getExceptionClass() == Throwable.class) + .findFirst() + .map(handler -> handler.handle(request, response, ClassUtil.cast(e), attributes)); + } + } + + private boolean shouldFilter(ExceptionResultHandler handler, Throwable e) { + if (handler.isRecursive()) { + return handler.getExceptionClass().isAssignableFrom(e.getClass()); + } + return handler.getExceptionClass() == e.getClass(); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/Requests.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/Requests.java new file mode 100644 index 0000000..01b7ab5 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/exception/support/Requests.java @@ -0,0 +1,64 @@ +package cn.axzo.framework.autoconfigure.web.exception.support; + +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 11:08 + **/ +public class Requests { + + public static RequestHelper from(RequestAttributes attributes) { + return new RequestHelper(attributes); + } + + public static RequestHelper from(HttpServletRequest request) { + return new RequestHelper(request); + } + + public static class RequestHelper { + + private final RequestAttributes attributes; + + private final HttpServletRequest request; + + private RequestHelper(RequestAttributes attributes) { + this.attributes = attributes; + this.request = (HttpServletRequest) attributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); + } + + private RequestHelper(HttpServletRequest request) { + this.attributes = new ServletWebRequest(request); + this.request = request; + } + + public Map getErrorAttributes(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { + return errorAttributes.getErrorAttributes((WebRequest) attributes, isIncludeStackTrace(errorProperties)); + } + + /** + * Determine if the stacktrace attribute should be included. + * + * @return if the stacktrace attribute should be included + */ + private boolean isIncludeStackTrace(ErrorProperties errorProperties) { + ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace(); + return include == ErrorProperties.IncludeStacktrace.ALWAYS + || include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM + && getTraceParameter(); + } + + private boolean getTraceParameter() { + Object parameter = request.getParameter("trace"); + return parameter != null && !"false".equals(parameter.toString().toLowerCase()); + } + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/AxzoProperties.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/AxzoProperties.java new file mode 100644 index 0000000..52d9b66 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/AxzoProperties.java @@ -0,0 +1,66 @@ +package cn.axzo.framework.autoconfigure.web.swagger; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 11:53 + **/ +@Validated +@ConfigurationProperties("axzo") +@Data +public class AxzoProperties { + + private Swagger swagger; + + private YApi yApi; + + @Data + public static class Swagger { + private boolean enabled = false; + private String basePackage = "cn"; + private String title = "Application API"; + private String description = "API documentation"; + private String version = "1.0.0"; + private String termsOfServiceUrl = null; + private String contactName = null; + private String contactUrl = null; + private String contactEmail = null; + private String license = null; + private String licenseUrl = null; + private String defaultIncludePattern = "/api/.*"; + private String host = null; + private String[] protocols = new String[0]; + } + + @Data + public static class YApi { + /** + * 是否同步YApi + */ + public boolean enabled = false; + + /** + * 数据同步方式 normal"(普通模式) , "good"(智能合并), "merge"(完全覆盖) 三种模式 + */ + private String merge = "normal"; + + /** + * 对应文件夹的 token + */ + private String token; + + /** + * json 数据来源(代替 json 字符串)。 + */ + private String url; + + /** + * 组名称,一般 默认 default + */ + private String groupName = "default"; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/SwaggerAutoConfiguration.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/SwaggerAutoConfiguration.java new file mode 100644 index 0000000..054860c --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/SwaggerAutoConfiguration.java @@ -0,0 +1,216 @@ +package cn.axzo.framework.autoconfigure.web.swagger; + +import cn.axzo.framework.autoconfigure.web.swagger.customizer.BuildInSwaggerCustomizer; +import cn.axzo.framework.autoconfigure.web.swagger.customizer.SecuritySwaggerCustomizer; +import cn.axzo.framework.autoconfigure.web.swagger.customizer.SwaggerCustomizer; +import cn.axzo.framework.domain.web.result.ApiListResult; +import cn.axzo.framework.domain.web.result.ApiPageResult; +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.framework.context.Placeholders; +import cn.axzo.framework.web.http.ApiEntity; +import cn.axzo.framework.web.http.ApiListEntity; +import cn.axzo.framework.web.http.ApiPageEntity; +import com.fasterxml.classmate.TypeResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StopWatch; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.DispatcherServlet; +import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; +import springfox.documentation.schema.AlternateTypeRule; +import springfox.documentation.schema.AlternateTypeRules; +import springfox.documentation.schema.WildcardType; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger.web.UiConfiguration; +import springfox.documentation.swagger.web.UiConfigurationBuilder; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import javax.servlet.Servlet; +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.Callable; + +import static cn.axzo.framework.core.Constants.*; +import static springfox.documentation.builders.PathSelectors.regex; +import static springfox.documentation.schema.AlternateTypeRules.newRule; + +/** + * Springfox Swagger configuration. + *

+ * Warning! When having a lot of REST endpoints, Springfox can become a performance issue. In that case, you can use a + * specific Spring profile for this class, so that only front-end developers have access to the Swagger view. + */ +@Slf4j +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({ + ApiInfo.class, + BeanValidatorPluginsConfiguration.class, + Servlet.class, + DispatcherServlet.class, + AxzoProperties.class +}) +@ConditionalOnProperty(value = "axzo.swagger.enabled", havingValue = "true") +@EnableConfigurationProperties(AxzoProperties.class) +@Profile({ENV_LOCAL, ENV_DEV, ENV_TEST, ENV_UAT, ENV_FAT}) +@EnableSwagger2 +@Import(BeanValidatorPluginsConfiguration.class) +@RequiredArgsConstructor +public class SwaggerAutoConfiguration { + + public static final String STARTING_MESSAGE = "Starting Swagger"; + public static final String STARTED_MESSAGE = "Started Swagger in {} ms"; + public static final String MANAGEMENT_TITLE_SUFFIX = "Management API"; + public static final String MANAGEMENT_GROUP_NAME = "management"; + public static final String MANAGEMENT_DESCRIPTION = "Management endpoints documentation"; + + private final AxzoProperties properties; + + /** + * Springfox configuration for the API Swagger docs. + * + * @return the Swagger Springfox configuration + */ + @Bean + @ConditionalOnMissingBean(name = "swaggerApiDocket") + public Docket swaggerApiDocket(List swaggerCustomizers, + ObjectProvider alternateTypeRulesProviders) { + log.debug(STARTING_MESSAGE); + StopWatch watch = new StopWatch(); + watch.start(); + + Docket docket = createDocket(); + + // Apply all customizers + swaggerCustomizers.forEach(customizer -> customizer.customize(docket)); + + // Add AlternateTypeRules if available in spring bean factory. + // Also you can add them in your customizer bean. + Optional.ofNullable(alternateTypeRulesProviders.getIfAvailable()).ifPresent(docket::alternateTypeRules); + + watch.stop(); + log.debug(STARTED_MESSAGE, watch.getTotalTimeMillis()); + return docket; + } + + @Bean + public BuildInSwaggerCustomizer buildInSwaggerCustomizer() { + return new BuildInSwaggerCustomizer(properties.getSwagger()); + } + + @ConditionalOnClass(name = "org.springframework.security.core.annotation.AuthenticationPrincipal") + @Bean + public SecuritySwaggerCustomizer securitySwaggerCustomizer() { + return new SecuritySwaggerCustomizer(); + } + + @Bean + public AlternateTypeRule localDateListAlternateTypeRule(TypeResolver typeResolver) { + return newRule( + typeResolver.resolve(List.class, LocalDate.class), + typeResolver.resolve(List.class, String.class) + ); + } + + @ConditionalOnClass(name = "cn.axzo.framework.web.http.ApiEntity") + @Bean + public AlternateTypeRule apiEntityAlternateTypeRule(TypeResolver typeResolver) { + return AlternateTypeRules.newRule( + typeResolver.resolve(ApiEntity.class, WildcardType.class), + typeResolver.resolve(ApiResult.class, WildcardType.class) + ); + } + + @ConditionalOnClass(name = "cn.axzo.framework.web.http.ApiEntity") + @Bean + public AlternateTypeRule callableApiEntityAlternateTypeRule(TypeResolver typeResolver) { + return newRule( + typeResolver.resolve(Callable.class, typeResolver.resolve(ApiEntity.class, WildcardType.class)), + typeResolver.resolve(ApiResult.class, WildcardType.class) + ); + } + + @ConditionalOnClass(name = "cn.axzo.framework.web.http.ApiListEntity") + @Bean + public AlternateTypeRule apiListEntityAlternateTypeRule(TypeResolver typeResolver) { + return newRule(typeResolver.resolve(ApiListEntity.class, WildcardType.class), + typeResolver.resolve(ApiListResult.class, WildcardType.class)); + } + + @ConditionalOnClass(name = "cn.axzo.framework.web.http.ApiPageEntity") + @Bean + public AlternateTypeRule apiPageEntityAlternateTypeRule(TypeResolver typeResolver) { + return newRule(typeResolver.resolve(ApiPageEntity.class, WildcardType.class), + typeResolver.resolve(ApiPageResult.class, WildcardType.class)); + } + + @Bean + public AlternateTypeRule requestEntityAlternateTypeRule(TypeResolver typeResolver) { + return AlternateTypeRules.newRule( + typeResolver.resolve(RequestEntity.class, WildcardType.class), + typeResolver.resolve(WildcardType.class) + ); + } + + /** + * Springfox configuration for the management endpoints (actuator) Swagger docs. + * + * @return the Swagger Springfox configuration + */ + @Bean + @ConditionalOnClass(name = "org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties") + @ConditionalOnProperty("management.server.servlet.context-path") + @ConditionalOnExpression("'${management.server.servlet.context-path}'.length() > 0") + @ConditionalOnMissingBean(name = "swaggerManagementDocket") + public Docket swaggerManagementDocket(@Value(Placeholders.APPLICATION_NAME) String appName, + @Value("${management.server.servlet.context-path}") String managementContextPath) { + ApiInfo apiInfo = new ApiInfo( + StringUtils.capitalize(appName) + " " + MANAGEMENT_TITLE_SUFFIX, + MANAGEMENT_DESCRIPTION, + properties.getSwagger().getVersion(), + "", + ApiInfo.DEFAULT_CONTACT, + "", + "", + new ArrayList<>() + ); + + return createDocket() + .apiInfo(apiInfo) + .groupName(MANAGEMENT_GROUP_NAME) + .host(properties.getSwagger().getHost()) + .protocols(new HashSet<>(Arrays.asList(properties.getSwagger().getProtocols()))) + .forCodeGeneration(true) + .directModelSubstitute(java.nio.ByteBuffer.class, String.class) + .genericModelSubstitutes(ResponseEntity.class) + .select() + .paths(regex(managementContextPath + ".*")) + .build(); + } + + @Bean + @ConditionalOnMissingBean + public UiConfiguration uiConfiguration() { + return UiConfigurationBuilder.builder() + .defaultModelsExpandDepth(-1) + .displayRequestDuration(true) + .validatorUrl("") + .build(); + } + + protected Docket createDocket() { + return new Docket(DocumentationType.SWAGGER_2); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/BuildInSwaggerCustomizer.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/BuildInSwaggerCustomizer.java new file mode 100644 index 0000000..b7930df --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/BuildInSwaggerCustomizer.java @@ -0,0 +1,96 @@ +package cn.axzo.framework.autoconfigure.web.swagger.customizer; + +import cn.axzo.framework.autoconfigure.web.swagger.AxzoProperties; +import com.fasterxml.classmate.TypeResolver; +import org.apache.commons.math3.fraction.BigFraction; +import org.apache.commons.math3.fraction.Fraction; +import org.springframework.core.Ordered; +import org.springframework.http.RequestEntity; +import springfox.documentation.schema.AlternateTypeRule; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spring.web.plugins.Docket; + +import java.time.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static springfox.documentation.builders.PathSelectors.regex; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 12:00 + **/ +public class BuildInSwaggerCustomizer implements SwaggerCustomizer, Ordered { + + /** + * The default order for the customizer. + */ + public static final int DEFAULT_ORDER = 0; + + private int order = DEFAULT_ORDER; + + private final AlternateTypeRule[] rules; + + private final AxzoProperties.Swagger properties; + + public BuildInSwaggerCustomizer(AxzoProperties.Swagger properties, AlternateTypeRule... rules) { + this.properties = properties; + this.rules = rules; + } + + @Override + public void customize(Docket docket) { + Contact contact = new Contact( + properties.getContactName(), + properties.getContactUrl(), + properties.getContactEmail() + ); + + ApiInfo apiInfo = new ApiInfo( + properties.getTitle(), + properties.getDescription(), + properties.getVersion(), + properties.getTermsOfServiceUrl(), + contact, + properties.getLicense(), + properties.getLicenseUrl(), + new ArrayList<>() + ); + + docket.select() + .paths(regex(properties.getDefaultIncludePattern())) + .build() + + .host(properties.getHost()) + .protocols(new HashSet<>(Arrays.asList(properties.getProtocols()))) + .apiInfo(apiInfo) + .forCodeGeneration(true) + .ignoredParameterTypes(RequestEntity.class) + .additionalModels(new TypeResolver().resolve(List.class)) + .directModelSubstitute(ZonedDateTime.class, Long.class) + .directModelSubstitute(Instant.class, Long.class) + .directModelSubstitute(OffsetDateTime.class, Long.class) + .directModelSubstitute(LocalTime.class, String.class) + .directModelSubstitute(LocalDate.class, String.class) + .directModelSubstitute(LocalDateTime.class, String.class) + .directModelSubstitute(YearMonth.class, String.class) + .directModelSubstitute(BigFraction.class, String.class) + .directModelSubstitute(Fraction.class, String.class) + .directModelSubstitute(Period.class, String.class) + .directModelSubstitute(java.nio.ByteBuffer.class, String.class) + .alternateTypeRules(rules); + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SecuritySwaggerCustomizer.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SecuritySwaggerCustomizer.java new file mode 100644 index 0000000..ed00631 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SecuritySwaggerCustomizer.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.autoconfigure.web.swagger.customizer; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 12:00 + **/ +public class SecuritySwaggerCustomizer implements SwaggerCustomizer { + + @Override + public void customize(Docket docket) { + docket.ignoredParameterTypes(AuthenticationPrincipal.class); + } +} diff --git a/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SwaggerCustomizer.java b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SwaggerCustomizer.java new file mode 100644 index 0000000..74157d7 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/java/cn.axzo.framework.autoconfigure/web/swagger/customizer/SwaggerCustomizer.java @@ -0,0 +1,14 @@ +package cn.axzo.framework.autoconfigure.web.swagger.customizer; + +import springfox.documentation.spring.web.plugins.Docket; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/10 11:59 + **/ +@FunctionalInterface +public interface SwaggerCustomizer { + + void customize(Docket docket); +} diff --git a/axzo-common-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/axzo-common-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..f29fc45 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,62 @@ +{ + "groups": [ + ], + "properties": [ + { + "name": "id.generator.node-id", + "type": "java.lang.Integer" + }, + { + "name": "id.generator.base-timestamp", + "type": "java.lang.Long" + }, + { + "name": "id.generator.sequence-bits", + "type": "java.lang.Integer" + }, + { + "name": "spring.mvc.http-log.enabled", + "type": "java.lang.Boolean", + "defaultValue": false + }, + { + "name": "spring.application.log-ready-info", + "type": "java.lang.Boolean", + "defaultValue": true + }, + { + "name": "uaa.enabled", + "type": "java.lang.Boolean", + "defaultValue": true + }, + { + "name": "spring.mvc.enable-matrix-variables", + "type": "java.lang.Boolean", + "defaultValue": true + }, + { + "name": "spring.cloud.config.server.health.enabled", + "type": "java.lang.Boolean", + "defaultValue": true + }, + { + "name": "spring.cloud.config.server.health.token", + "type": "java.lang.String" + }, + { + "name": "spring.cloud.config.server.consul.watch.enabled", + "type": "java.lang.Boolean", + "defaultValue": false + }, + { + "name": "spring.mvc.response.verbose-enabled", + "type": "java.lang.Boolean", + "defaultValue": false + }, + { + "name": "spring.mvc.response.dynamic-enabled", + "type": "java.lang.Boolean", + "defaultValue": true + } + ] +} \ No newline at end of file diff --git a/axzo-common-autoconfigure/src/main/resources/META-INF/spring.factories b/axzo-common-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ba1b083 --- /dev/null +++ b/axzo-common-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,18 @@ +# Environment Post Processors +org.springframework.boot.env.EnvironmentPostProcessor=\ +cn.axzo.framework.autoconfigure.env.BuildInConfigOverridePostProcessor + +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.axzo.framework.autoconfigure.env.ApplicationReadyInfoAutoConfiguration,\ +cn.axzo.framework.autoconfigure.validation.MethodValidationAutoConfiguration,\ +cn.axzo.framework.autoconfigure.jackson.JacksonModuleAutoConfiguration,\ +cn.axzo.framework.autoconfigure.data.IdAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.cors.CorsAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.PageWebAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.exception.ExceptionHandlerAutoConfiguration,\ +# cn.axzo.framework.autoconfigure.web.swagger.SwaggerAutoConfiguration,\ +cn.axzo.framework.autoconfigure.validation.SpringValidatorAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.advice.BodyAdviceAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.FilterAutoConfiguration,\ +cn.axzo.framework.autoconfigure.web.context.WebMvcAwareAutoConfiguration diff --git a/axzo-common-boot/pom.xml b/axzo-common-boot/pom.xml new file mode 100644 index 0000000..e67695e --- /dev/null +++ b/axzo-common-boot/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-boot + Axzo Common Boot + + + cn/axzo/framework/boot + + + + + + cn.axzo.framework.framework + axzo-common-context + + + org.springframework.boot + spring-boot + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-devtools + true + + + org.apache.logging.log4j + log4j-api + true + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + + process-packageVersion + + replace + + generate-sources + + + + ${basedir}/src/main/java/${pkgVersion.dir}/PackageVersion.java.in + ${project.basedir}/src/main/java/${pkgVersion.dir}/PackageVersion.java + + + @package@ + ${project.groupId}.boot + + + @projectversion@ + ${project.version} + + + @spring_cloud_version@ + ${spring-cloud.version} + + + + + + + \ No newline at end of file diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileOverrideListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileOverrideListener.java new file mode 100644 index 0000000..4800bbe --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileOverrideListener.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.boot; + +import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.context.ApplicationListener; + +/** + * @Descriptiono + * @Author liyong.tian + * @Date 2020/9/8 19:07 + **/ +public class DefaultProfileOverrideListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationStartingEvent event) { + DefaultProfileUtil.setDefaultProfile(event.getSpringApplication()); + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileUtil.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileUtil.java new file mode 100644 index 0000000..62d3129 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DefaultProfileUtil.java @@ -0,0 +1,58 @@ +package cn.axzo.framework.boot; + +import cn.axzo.framework.core.Constants; +import jodd.util.ArraysUtil; +import org.springframework.boot.SpringApplication; +import org.springframework.core.env.Environment; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.collect.Sets.newHashSet; + +/** + * Utility class to load a Spring profile to be used as default + * when there is no spring.profiles.active set in the environment or as command line argument. + * If the value is not available in application.yml then local profile will be used as default. + */ +public final class DefaultProfileUtil { + + private static final String SPRING_PROFILE_DEFAULT = "spring.profiles.default"; + + private DefaultProfileUtil() { + } + + /** + * Set a default to use when no profile is configured. + * + * @param app the Spring application + */ + public static void setDefaultProfile(SpringApplication app) { + Map defProperties = new HashMap<>(); + /* + * The default profile to use when no other profiles are defined + * This cannot be set in the application.yml file. + * See https://github.com/spring-projects/spring-boot/issues/1219 + */ + defProperties.put(SPRING_PROFILE_DEFAULT, Constants.ENV_LOCAL); + app.setDefaultProperties(defProperties); + } + + /** + * Get the profiles that are applied else get default profiles. + * + * @param env spring environment + * @return profiles + */ + public static String[] getActiveProfiles(Environment env) { + String[] profiles = env.getActiveProfiles(); + if (profiles.length == 0) { + return env.getDefaultProfiles(); + } + if (Arrays.stream(profiles).noneMatch(profile -> ArraysUtil.contains(Constants.ENV_ALL, profile))) { + return newHashSet(ArraysUtil.join(profiles, env.getDefaultProfiles())).toArray(new String[0]); + } + return profiles; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DynamicBannerEnvironmentPostProcessor.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DynamicBannerEnvironmentPostProcessor.java new file mode 100644 index 0000000..f3df5a9 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/DynamicBannerEnvironmentPostProcessor.java @@ -0,0 +1,40 @@ +package cn.axzo.framework.boot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import java.util.HashMap; +import java.util.Map; + +import static cn.axzo.framework.boot.PackageVersion.VERSION; +import static cn.axzo.framework.boot.PackageVersion.SPRING_CLOUD_VERSION; +import static cn.axzo.framework.core.util.ClassUtil.isPresent; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:14 + **/ +public class DynamicBannerEnvironmentPostProcessor implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + ClassLoader classLoader = application.getClassLoader(); + boolean isSpringCloudPresent = isPresent("org.springframework.cloud.client.SpringCloudApplication", classLoader); + + Map versionsMap = new HashMap<>(); + versionsMap.put("axzo-framework.formatted-version", " (v" + VERSION + ")"); + + versionsMap.put("spring-cloud.version", isSpringCloudPresent ? SPRING_CLOUD_VERSION : null); + + StringBuilder runningDesc = new StringBuilder(); + if (isSpringCloudPresent) { + runningDesc.append("Spring Cloud ${spring-cloud.version} : "); + } + versionsMap.put("running.description", runningDesc.append("Spring Boot ${spring-boot.version}").toString()); + + environment.getPropertySources().addLast(new MapPropertySource("version", versionsMap)); + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/EnvironmentUtil.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/EnvironmentUtil.java new file mode 100644 index 0000000..6a48ee5 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/EnvironmentUtil.java @@ -0,0 +1,37 @@ +package cn.axzo.framework.boot; + +import cn.axzo.framework.core.FetchException; +import cn.axzo.framework.core.net.Inets; +import lombok.experimental.UtilityClass; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +import java.util.Optional; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:11 + **/ +@UtilityClass +public class EnvironmentUtil { + + private static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap"; + + public boolean isSpringCloudContext(ConfigurableEnvironment environment) { + return environment != null && environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME); + } + + public int fetchLocalIpTimeoutSeconds(Environment env) { + return env.getProperty("system.properties.fetch-local-ip-timeout-seconds", Integer.class, 2); + } + + public Optional fetchLocalIp(Environment env) { + int timeoutSeconds = fetchLocalIpTimeoutSeconds(env); + try { + return Optional.ofNullable(Inets.fetchLocalIp(timeoutSeconds)); + } catch (FetchException e) { + return Optional.empty(); + } + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java new file mode 100644 index 0000000..d5855e5 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java @@ -0,0 +1,13 @@ +package cn.axzo.framework.boot; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion { + + public final static String VERSION = "1.0.0-SNAPSHOT"; + + public final static String SPRING_CLOUD_VERSION = "2020.0.6"; +} \ No newline at end of file diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java.in b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java.in new file mode 100644 index 0000000..847ed45 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PackageVersion.java.in @@ -0,0 +1,13 @@ +package @package@; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion { + + public final static String VERSION = "@projectversion@"; + + public final static String SPRING_CLOUD_VERSION = "@spring_cloud_version@"; +} \ No newline at end of file diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PropertySourceUtils.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PropertySourceUtils.java new file mode 100644 index 0000000..b14ab11 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/PropertySourceUtils.java @@ -0,0 +1,68 @@ +package cn.axzo.framework.boot; + +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/12/15 11:37 + **/ +public abstract class PropertySourceUtils { + + /** + * Return a Map of all values from the specified {@link PropertySources} that start + * with a particular key. + * @param propertySources the property sources to scan + * @param keyPrefix the key prefixes to test + * @return a map of all sub properties starting with the specified key prefixes. + * @see PropertySourceUtils#getSubProperties(PropertySources, String, String) + */ + public static Map getSubProperties(PropertySources propertySources, String keyPrefix) { + return PropertySourceUtils.getSubProperties(propertySources, null, keyPrefix); + } + + /** + * Return a Map of all values from the specified {@link PropertySources} that start + * with a particular key. + * @param propertySources the property sources to scan + * @param rootPrefix a root prefix to be prepended to the keyPrefix (can be + * {@code null}) + * @param keyPrefix the key prefixes to test + * @return a map of all sub properties starting with the specified key prefixes. + * @see #getSubProperties(PropertySources, String, String) + */ + public static Map getSubProperties(PropertySources propertySources, String rootPrefix, + String keyPrefix) { + RelaxedNames keyPrefixes = new RelaxedNames(keyPrefix); + Map subProperties = new LinkedHashMap(); + for (PropertySource source : propertySources) { + if (source instanceof EnumerablePropertySource) { + for (String name : ((EnumerablePropertySource) source).getPropertyNames()) { + String key = PropertySourceUtils.getSubKey(name, rootPrefix, keyPrefixes); + if (key != null && !subProperties.containsKey(key)) { + subProperties.put(key, source.getProperty(name)); + } + } + } + } + return Collections.unmodifiableMap(subProperties); + } + + private static String getSubKey(String name, String rootPrefixes, RelaxedNames keyPrefix) { + rootPrefixes = (rootPrefixes != null) ? rootPrefixes : ""; + for (String rootPrefix : new RelaxedNames(rootPrefixes)) { + for (String candidateKeyPrefix : keyPrefix) { + if (name.startsWith(rootPrefix + candidateKeyPrefix)) { + return name.substring((rootPrefix + candidateKeyPrefix).length()); + } + } + } + return null; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/RelaxedNames.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/RelaxedNames.java new file mode 100644 index 0000000..0bd1b8d --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/RelaxedNames.java @@ -0,0 +1,236 @@ +package cn.axzo.framework.boot; + +import org.springframework.util.StringUtils; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/12/15 11:40 + **/ +public final class RelaxedNames implements Iterable { + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])"); + + private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern.compile("[_\\-.]"); + + private final String name; + + private final Set values = new LinkedHashSet(); + + /** + * Create a new {@link RelaxedNames} instance. + * @param name the source name. For the maximum number of variations specify the name + * using dashed notation (e.g. {@literal my-property-name} + */ + public RelaxedNames(String name) { + this.name = (name != null) ? name : ""; + initialize(RelaxedNames.this.name, this.values); + } + + @Override + public Iterator iterator() { + return this.values.iterator(); + } + + private void initialize(String name, Set values) { + if (values.contains(name)) { + return; + } + for (Variation variation : Variation.values()) { + for (Manipulation manipulation : Manipulation.values()) { + String result = name; + result = manipulation.apply(result); + result = variation.apply(result); + values.add(result); + initialize(result, values); + } + } + } + + /** + * Name variations. + */ + enum Variation { + + NONE { + + @Override + public String apply(String value) { + return value; + } + + }, + + LOWERCASE { + + @Override + public String apply(String value) { + return (value.isEmpty() ? value : value.toLowerCase(Locale.ENGLISH)); + } + + }, + + UPPERCASE { + + @Override + public String apply(String value) { + return (value.isEmpty() ? value : value.toUpperCase(Locale.ENGLISH)); + } + + }; + + public abstract String apply(String value); + + } + + /** + * Name manipulations. + */ + enum Manipulation { + + NONE { + + @Override + public String apply(String value) { + return value; + } + + }, + + HYPHEN_TO_UNDERSCORE { + + @Override + public String apply(String value) { + return (value.indexOf('-') != -1) ? value.replace('-', '_') : value; + } + + }, + + UNDERSCORE_TO_PERIOD { + + @Override + public String apply(String value) { + return (value.indexOf('_') != -1) ? value.replace('_', '.') : value; + } + + }, + + PERIOD_TO_UNDERSCORE { + + @Override + public String apply(String value) { + return (value.indexOf('.') != -1) ? value.replace('.', '_') : value; + } + + }, + + CAMELCASE_TO_UNDERSCORE { + + @Override + public String apply(String value) { + if (value.isEmpty()) { + return value; + } + Matcher matcher = CAMEL_CASE_PATTERN.matcher(value); + if (!matcher.find()) { + return value; + } + matcher = matcher.reset(); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, + matcher.group(1) + '_' + StringUtils.uncapitalize(matcher.group(2))); + } + matcher.appendTail(result); + return result.toString(); + } + + }, + + CAMELCASE_TO_HYPHEN { + + @Override + public String apply(String value) { + if (value.isEmpty()) { + return value; + } + Matcher matcher = CAMEL_CASE_PATTERN.matcher(value); + if (!matcher.find()) { + return value; + } + matcher = matcher.reset(); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, + matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2))); + } + matcher.appendTail(result); + return result.toString(); + } + + }, + + SEPARATED_TO_CAMELCASE { + + @Override + public String apply(String value) { + return separatedToCamelCase(value, false); + } + + }, + + CASE_INSENSITIVE_SEPARATED_TO_CAMELCASE { + + @Override + public String apply(String value) { + return separatedToCamelCase(value, true); + } + + }; + + private static final char[] SUFFIXES = new char[] { '_', '-', '.' }; + + public abstract String apply(String value); + + private static String separatedToCamelCase(String value, boolean caseInsensitive) { + if (value.isEmpty()) { + return value; + } + StringBuilder builder = new StringBuilder(); + for (String field : SEPARATED_TO_CAMEL_CASE_PATTERN.split(value)) { + field = (caseInsensitive ? field.toLowerCase(Locale.ENGLISH) : field); + builder.append((builder.length() != 0) ? StringUtils.capitalize(field) : field); + } + char lastChar = value.charAt(value.length() - 1); + for (char suffix : SUFFIXES) { + if (lastChar == suffix) { + builder.append(suffix); + break; + } + } + return builder.toString(); + } + + } + + /** + * Return a {@link RelaxedNames} for the given source camelCase source name. + * @param name the source name in camelCase + * @return the relaxed names + */ + public static RelaxedNames forCamelCase(String name) { + StringBuilder result = new StringBuilder(); + for (char c : name.toCharArray()) { + result.append((Character.isUpperCase(c) && result.length() > 0 && result.charAt(result.length() - 1) != '-') + ? "-" + Character.toLowerCase(c) : c); + } + return new RelaxedNames(result.toString()); + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/CheckActiveProfilesListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/CheckActiveProfilesListener.java new file mode 100644 index 0000000..a2e36d0 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/CheckActiveProfilesListener.java @@ -0,0 +1,48 @@ +package cn.axzo.framework.boot.env; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.Arrays; +import java.util.List; + +import static cn.axzo.framework.core.Constants.*; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:24 + **/ +@Slf4j +public class CheckActiveProfilesListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + boolean checkActiveProfiles = event.getApplicationContext().getEnvironment() + .getProperty("spring.profiles.check-active", Boolean.class, true); + if (!checkActiveProfiles) { + return; + } + ConfigurableEnvironment env = event.getApplicationContext().getEnvironment(); + List activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(ENV_LOCAL) && activeProfiles.contains(ENV_PROD)) { + log.error(errorMsg(ENV_LOCAL, ENV_PROD)); + } + if (activeProfiles.contains(ENV_DEV) && activeProfiles.contains(ENV_PROD)) { + log.error(errorMsg(ENV_DEV, ENV_PROD)); + } + if (activeProfiles.contains(ENV_TEST) && activeProfiles.contains(ENV_PROD)) { + log.error(errorMsg(ENV_TEST, ENV_PROD)); + } + if (activeProfiles.contains(ENV_STG) && activeProfiles.contains(ENV_PROD)) { + log.error(errorMsg(ENV_STG, ENV_PROD)); + } + } + + private String errorMsg(String env1, String env2) { + return "You have misconfigured your application! It should not run " + + "with both the '" + env1 + "' and '" + env2 + "' profiles at the same time."; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/UnicodePropertiesPropertySourceLoader.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/UnicodePropertiesPropertySourceLoader.java new file mode 100644 index 0000000..28f21eb --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/UnicodePropertiesPropertySourceLoader.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.boot.env; + +import com.google.common.collect.Lists; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.core.Ordered; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Properties; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @Description 使properties配置文件支持UTF_8编码 + * @Author liyong.tian + * @Date 2020/9/8 19:33 + **/ +public class UnicodePropertiesPropertySourceLoader implements PropertySourceLoader, Ordered { + + @Override + public String[] getFileExtensions() { + return new String[]{"properties"}; + } + + @Override + public List> load(String name, Resource resource) throws IOException { + Properties properties = new Properties(); + properties.load(new InputStreamReader(resource.getInputStream(), UTF_8)); + if (!properties.isEmpty()) { + List> list = Lists.newArrayList(); + list.add(new PropertiesPropertySource(name, properties)); + return list; + } + return null; + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE + LOWEST_PRECEDENCE; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideEnvironmentPostProcessor.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideEnvironmentPostProcessor.java new file mode 100644 index 0000000..5cf696d --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideEnvironmentPostProcessor.java @@ -0,0 +1,85 @@ +package cn.axzo.framework.boot.env.configoverride; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.io.Resources; +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.util.MapUtil; +import jodd.props.Props; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import static cn.axzo.framework.core.io.ResourceStrings.CLASSPATH_ALL_URL_PREFIX; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:43 + **/ +public class ConfigOverrideEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + /** + * The default order for the processor. + */ + public static final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 10; + + private int order = DEFAULT_ORDER; + + private static final String CONFIG_OVERRIDE_PROPERTY_SOURCE = "configOverride"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + ConfigOverrideProperties overrideProperties = Binder.get(environment) + .bind(ConfigOverrideProperties.PREFIX, ConfigOverrideProperties.class) + .orElse(null); + + if (overrideProperties != null) { + if (!overrideProperties.isEnabled()) { + return; + } + Props props = new Props(); + props.setSkipEmptyProps(false); + String locationPattern = CLASSPATH_ALL_URL_PREFIX + "**/" + overrideProperties.getPropsFilepath(); + Resource[] resources = Resources.findResources(locationPattern); + if (resources.length > 0) { + try (InputStream is = resources[0].getInputStream()) { + props.load(is); + } catch (IOException | ResourceException e) { + throw new IllegalArgumentException("cannot load config override file", e); + } + } else { + return; + } + + if (props.getActiveProfiles() == null || props.getActiveProfiles().length == 0) { + props.setActiveProfiles(environment.getActiveProfiles()); + } else { + environment.setActiveProfiles(props.getActiveProfiles()); + } + + Map map = props.innerMap(null); + if (MapUtil.isNotEmpty(map)) { + MapPropertySource propertySource = new MapPropertySource(CONFIG_OVERRIDE_PROPERTY_SOURCE, map); + environment.getPropertySources().addFirst(propertySource); + } + } else { + return; + } + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideProperties.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideProperties.java new file mode 100644 index 0000000..86d9f9d --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/configoverride/ConfigOverrideProperties.java @@ -0,0 +1,31 @@ +package cn.axzo.framework.boot.env.configoverride; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:37 + **/ +@Data +@ConfigurationProperties(prefix = ConfigOverrideProperties.PREFIX) +public class ConfigOverrideProperties { + + public final static String PREFIX = "spring.config.override"; + + public final static String PROPS = ".props"; + + // 是否允许配置重写 + private boolean enabled; + + // 在classpath下的配置文件,必须是.props格式 + private String propsFile = "override"; + + public String getPropsFilepath() { + if (getPropsFile().endsWith(PROPS)) { + return getPropsFile(); + } + return propsFile + PROPS; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/devtools/DevToolsPropertyPostProcessor.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/devtools/DevToolsPropertyPostProcessor.java new file mode 100644 index 0000000..e865593 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/env/devtools/DevToolsPropertyPostProcessor.java @@ -0,0 +1,103 @@ +package cn.axzo.framework.boot.env.devtools; + +import cn.axzo.framework.boot.EnvironmentUtil; +import cn.axzo.framework.boot.env.configoverride.ConfigOverrideEnvironmentPostProcessor; +import com.google.common.collect.ImmutableMap; +import lombok.val; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.devtools.restart.Restarter; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; + +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.util.ClassUtils.isPresent; + +/** + * @author liyong.tian + * @since 2017/11/6 下午3:13 + */ +@Order +public class DevToolsPropertyPostProcessor implements EnvironmentPostProcessor, Ordered { + + /** + * The default order for the processor. + */ + public static final int DEFAULT_ORDER = ConfigOverrideEnvironmentPostProcessor.DEFAULT_ORDER - 1; + + private int order = DEFAULT_ORDER; + + private static final String REFRESH_MORE_PROPERTY_SOURCE = "refreshMore"; + + private static final String REFRESH_MORE_PROPERTY_WITHOUT_BOOTSTRAP_SOURCE = "refreshMoreWithoutBootstrap"; + + private static final Map properties; + + private static final Map propertiesWithoutBootstrap; + + static { + Map devToolsProperties = new HashMap<>(); + devToolsProperties.put("spring.config.override.enabled", "true"); + properties = ImmutableMap.copyOf(devToolsProperties); + + devToolsProperties.clear(); + devToolsProperties.put("spring.mvc.http-log.enabled", "true"); + devToolsProperties.put("spring.output.ansi.enabled", AnsiOutput.Enabled.ALWAYS); + devToolsProperties.put("spring.messages.cache-seconds", 1); + propertiesWithoutBootstrap = ImmutableMap.copyOf(devToolsProperties); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + ClassLoader classLoader = application.getClassLoader(); + if (!isPresent("org.springframework.boot.devtools.env.DevToolsPropertyDefaultsPostProcessor", classLoader)) { + return; + } + if (this.isLocalApplication(environment) && this.canAddProperties(environment)) { + environment.getPropertySources().addLast(new MapPropertySource(REFRESH_MORE_PROPERTY_SOURCE, properties)); + + if (!EnvironmentUtil.isSpringCloudContext(environment)) { + val source = new MapPropertySource(REFRESH_MORE_PROPERTY_WITHOUT_BOOTSTRAP_SOURCE, propertiesWithoutBootstrap); + environment.getPropertySources().addLast(source); + } + } + } + + private boolean isLocalApplication(ConfigurableEnvironment environment) { + return environment.getPropertySources().get("remoteUrl") == null; + } + + private boolean canAddProperties(Environment environment) { + return this.isRestarterInitialized() || this.isRemoteRestartEnabled(environment); + } + + private boolean isRestarterInitialized() { + try { + Restarter restarter = Restarter.getInstance(); + return restarter != null && restarter.getInitialUrls() != null; + } catch (Exception ex) { + return false; + } + } + + private boolean isRemoteRestartEnabled(Environment environment) { + /*RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, "spring.devtools.remote."); + return resolver.containsProperty("secret");*/ + return environment.containsProperty("spring.devtools.remote.secret"); + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/LoggingConfigFixer.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/LoggingConfigFixer.java new file mode 100644 index 0000000..ecfbc2f --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/LoggingConfigFixer.java @@ -0,0 +1,31 @@ +package cn.axzo.framework.boot.logging; + +import org.springframework.util.StringUtils; + +import java.util.Optional; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 20:06 + **/ +public class LoggingConfigFixer { + + private static final String LOGGING_FILE_EXTENSION = ".log"; + + public Optional fixLoggingFile(String loggingPath, String loggingFile) { + if (loggingFile == null || loggingPath == null) { + return Optional.empty(); + } + if (!loggingFile.endsWith(LOGGING_FILE_EXTENSION)) { + loggingFile = loggingFile + LOGGING_FILE_EXTENSION; + } + if (!loggingFile.startsWith(loggingPath)) { + if (!loggingPath.endsWith("/")) { + loggingPath = loggingPath + "/"; + } + loggingFile = StringUtils.applyRelativePath(loggingPath, loggingFile); + } + return Optional.of(loggingFile); + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCHandler.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCHandler.java new file mode 100644 index 0000000..f233160 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCHandler.java @@ -0,0 +1,131 @@ +package cn.axzo.framework.boot.logging.log4j2; + +import cn.axzo.framework.boot.EnvironmentUtil; +import lombok.RequiredArgsConstructor; +import org.apache.logging.log4j.ThreadContext; +import org.springframework.boot.logging.LogFile; +import org.springframework.core.env.*; +import org.springframework.util.ClassUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static cn.axzo.framework.boot.PropertySourceUtils.getSubProperties; +import static cn.axzo.framework.core.net.Inets.IP_SYSTEM_KEY; +import static com.google.common.collect.Maps.newHashMap; + +/** + * @author liyong.tian + * @since 2017/9/18 下午8:45 + */ +@RequiredArgsConstructor +public class Log4j2MDCHandler { + + private final ConfigurableEnvironment environment; + + private final ClassLoader classLoader; + + private static final String LOG4J2_THREAD_CONTEXT = "org.apache.logging.log4j.ThreadContext"; + + private static final String LOGGING_PATH_KEY = LogFile.FILE_PATH_PROPERTY; + private static final String DEFAULT_LOGGING_PATH = "logs"; + + private static final String FILE_NAME_KEY = "spring.application.name"; + private static final String DEFAULT_FILE_NAME = "spring"; + + private static final String ROOT_LOGGER_LEVEL_KEY = "logging.level.root"; + private static final String DEFAULT_ROOT_LOGGER_LEVEL = "info"; + + private static final String LOGGING_FILE_ENABLED_KEY = "logging.file-enabled"; + private static final String DEFAULT_LOGGING_FILE_ENABLED = "true"; + + public Log4j2MDCHandler(ClassLoader classLoader) { + this.classLoader = classLoader; + this.environment = new StandardEnvironment(); + } + + public void overwrite() { + if (ClassUtils.isPresent(LOG4J2_THREAD_CONTEXT, classLoader)) { + Map properties = resolveEnvironment(environment); + Map systemProperties = newHashMap(environment.getSystemProperties()); + Map systemEnvironment = newHashMap(environment.getSystemEnvironment()); + + properties.forEach((k, v) -> { + if (v != null && systemProperties.remove(k) == null && systemEnvironment.remove(k) == null) { + ThreadContext.put(k, v.toString()); + } + }); + + if (!ThreadContext.containsKey(LOGGING_PATH_KEY)) { + ThreadContext.put(LOGGING_PATH_KEY, DEFAULT_LOGGING_PATH); + } + if (!ThreadContext.containsKey(FILE_NAME_KEY)) { + ThreadContext.put(FILE_NAME_KEY, DEFAULT_FILE_NAME); + } + if (!ThreadContext.containsKey(ROOT_LOGGER_LEVEL_KEY)) { + ThreadContext.put(ROOT_LOGGER_LEVEL_KEY, DEFAULT_ROOT_LOGGER_LEVEL); + } + if (!ThreadContext.containsKey(LOGGING_FILE_ENABLED_KEY)) { + ThreadContext.put(LOGGING_FILE_ENABLED_KEY, DEFAULT_LOGGING_FILE_ENABLED); + } + + // HOST_IP + EnvironmentUtil.fetchLocalIp(environment).ifPresent(ip -> ThreadContext.put(IP_SYSTEM_KEY, ip)); + } + } + + public void setup() { + if (ClassUtils.isPresent(LOG4J2_THREAD_CONTEXT, classLoader)) { + System.setProperty("isThreadContextMapInheritable", "true"); + } + } + + private Map resolveEnvironment(ConfigurableEnvironment environment) { + //spring-boot-2.0.X版本自定义getSubProperties方法 spring-boot-1.5.X版本引用PropertySourceUtils.getSubProperties()方法 + Map properties = getSubProperties(environment.getPropertySources(), null); + Map resolvedProperties = new HashMap<>(); + properties.forEach((k, v) -> { + if (v instanceof String) { + resolvedProperties.put(k, environment.resolvePlaceholders((String) v)); + } else { + resolvedProperties.put(k, v); + } + }); + //spring-boot-1.5.X 版本 + /*Map properties = getSubProperties(environment.getPropertySources(), null); + Map resolvedProperties = new HashMap<>(); + properties.forEach((k, v) -> { + if (v instanceof String) { + resolvedProperties.put(k, environment.resolvePlaceholders((String) v)); + } else { + resolvedProperties.put(k, v); + } + });*/ + return resolvedProperties; + } + + /*private Map getSubProperties(PropertySources propertySources) { + Iterator> sourceIterator = propertySources.iterator(); + Map subProperties = new HashMap<>(); + while (true) { + PropertySource source; + do { + if (sourceIterator.hasNext()) { + return Collections.unmodifiableMap(subProperties); + } + source = sourceIterator.next(); + } while (!(source instanceof EnumerablePropertySource)); + + String[] propertyNames = ((EnumerablePropertySource) source).getPropertyNames(); + int length = propertyNames.length; + for (int i = 0; i < length; ++i) { + String key = propertyNames[i]; + if (key != null && !subProperties.containsKey(key)) { + subProperties.put(key, source.getProperty(key)); + } + } + } + }*/ +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCListener.java new file mode 100644 index 0000000..4b5355b --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCListener.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.boot.logging.log4j2; + +import lombok.val; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; + +/** + * Lookup properties of Spring + * + * @author liyong.tian + * @since 2016/11/28 + */ +public class Log4j2MDCListener implements ApplicationListener, Ordered { + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + val handler = new Log4j2MDCHandler(event.getEnvironment(), event.getSpringApplication().getClassLoader()); + handler.overwrite(); + } + + @Override + public int getOrder() { + return LoggingApplicationListener.DEFAULT_ORDER - 1; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCSetupListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCSetupListener.java new file mode 100644 index 0000000..dd1075b --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/Log4j2MDCSetupListener.java @@ -0,0 +1,26 @@ +package cn.axzo.framework.boot.logging.log4j2; + +import lombok.val; +import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; + +import javax.annotation.Nonnull; + +/** + * @author liyong.tian + * @since 2017/11/20 下午7:21 + */ +public class Log4j2MDCSetupListener implements ApplicationListener, Ordered { + + @Override + public void onApplicationEvent(@Nonnull ApplicationStartingEvent event) { + val handler = new Log4j2MDCHandler(event.getSpringApplication().getClassLoader()); + handler.setup(); + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/log4j2-sample.xml b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/log4j2-sample.xml new file mode 100644 index 0000000..2444f97 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/logging/log4j2/log4j2-sample.xml @@ -0,0 +1,49 @@ + + + + ${ctx:logging.path} + ${ctx:spring.application.name} + ${ctx:logging.level.root} + ???? + %xwEx + %5p + %clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%10t]}{faint} %clr{%c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN} ${sys:PID} --- [%t] %c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/AbstractScript.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/AbstractScript.java new file mode 100644 index 0000000..317fc95 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/AbstractScript.java @@ -0,0 +1,37 @@ +package cn.axzo.framework.boot.script; + +import lombok.val; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:56 + **/ +public abstract class AbstractScript { + + private AtomicBoolean executed = new AtomicBoolean(false); + + void process() { + //确保只执行一次 + if (executed.compareAndSet(false, true)) { + val timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + execute(); + timer.cancel(); + } + }, getDelayMills()); + } + } + + abstract protected void execute(); + + protected long getDelayMills() { + return 15000; + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/ScriptListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/ScriptListener.java new file mode 100644 index 0000000..8f98dbe --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/script/ScriptListener.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.boot.script; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 19:58 + **/ +@Slf4j +public class ScriptListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + val scripts = event.getApplicationContext().getBeansOfType(AbstractScript.class).values(); + scripts.parallelStream().forEach(script -> { + try { + script.process(); + } catch (Exception e) { + log.error("script execute error", e); + } + }); + } +} diff --git a/axzo-common-boot/src/main/java/cn/axzo/framework/boot/system/SystemPropertiesListener.java b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/system/SystemPropertiesListener.java new file mode 100644 index 0000000..7f59b96 --- /dev/null +++ b/axzo-common-boot/src/main/java/cn/axzo/framework/boot/system/SystemPropertiesListener.java @@ -0,0 +1,68 @@ +package cn.axzo.framework.boot.system; + +import cn.axzo.framework.boot.EnvironmentUtil; +import cn.axzo.framework.core.FetchException; +import cn.axzo.framework.core.net.Inets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StopWatch; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 20:00 + **/ +@Slf4j +public class SystemPropertiesListener implements ApplicationListener, Ordered { + + private AtomicBoolean executed = new AtomicBoolean(false); + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + ConfigurableEnvironment env = event.getEnvironment(); + + // don't listen to events in a spring cloud context + if (EnvironmentUtil.isSpringCloudContext(env)) { + return; + } + + // Think about twice invoking this listener + if (executed.get()) { + return; + } + + StopWatch watch = new StopWatch("setup system properties"); + if (env.containsProperty("system.properties.fetch-local-ip")) { + watch.start("fetch local ip"); + fetchLocalIp(EnvironmentUtil.fetchLocalIpTimeoutSeconds(env)); + watch.stop(); + } + + if (watch.getTaskCount() > 0) { + log.info(watch.prettyPrint()); + } + + executed.set(true); + } + + private void fetchLocalIp(int timeoutSeconds) { + log.info("Fetching local ip...."); + try { + System.setProperty(Inets.IP_SYSTEM_KEY, Inets.fetchLocalIp(timeoutSeconds)); + log.info("Local ip: " + Inets.fetchLocalIp()); + } catch (FetchException e) { + log.info("Local ip: cannot fetch in " + timeoutSeconds + " seconds"); + } + } + + @Override + public int getOrder() { + return LoggingApplicationListener.DEFAULT_ORDER + 1; + } +} diff --git a/axzo-common-clients/pom.xml b/axzo-common-clients/pom.xml new file mode 100644 index 0000000..be6b81a --- /dev/null +++ b/axzo-common-clients/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + cn.axzo.framework.client + axzo-common-clients + pom + Axzo Common Client Parent + + + retrofit-starter + + \ No newline at end of file diff --git a/axzo-common-clients/retrofit-starter/pom.xml b/axzo-common-clients/retrofit-starter/pom.xml new file mode 100644 index 0000000..c84f001 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + cn.axzo.framework.client + axzo-common-clients + 1.0.0-SNAPSHOT + + + retrofit-starter + Axzo Common Client Retrofit Starter + + + + cn.axzo.framework + axzo-common-domain + + + cn.axzo.framework.jackson + jackson-starter + + + com.squareup.retrofit2 + retrofit + + + com.squareup.retrofit2 + converter-jackson + + + com.squareup.retrofit2 + adapter-rxjava2 + + + \ No newline at end of file diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/FastRetrofit.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/FastRetrofit.java new file mode 100644 index 0000000..6b0d3c3 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/FastRetrofit.java @@ -0,0 +1,33 @@ +package cn.axzo.framework.client.retrofit; + +import cn.axzo.framework.jackson.utility.JSON; +import lombok.experimental.UtilityClass; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; + +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:41 + **/ +@UtilityClass +public class FastRetrofit { + + public T target(String baseUrl, Class targetType) { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(5, SECONDS) + .readTimeout(5, SECONDS) + .writeTimeout(5, SECONDS) + .addInterceptor(new HttpLogInterceptor()) + .build(); + return new Retrofit.Builder() + .client(client) + .baseUrl(baseUrl) + .addConverterFactory(JacksonConverterFactory.create(JSON.objectMapper())) + .build() + .create(targetType); + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/HttpLogInterceptor.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/HttpLogInterceptor.java new file mode 100644 index 0000000..4de9a36 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/HttpLogInterceptor.java @@ -0,0 +1,132 @@ +package cn.axzo.framework.client.retrofit; + +import cn.axzo.framework.domain.http.*; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static okhttp3.Protocol.HTTP_1_1; + +/** + * @Description 日志拦截器 + * @Author liyong.tian + * @Date 2020/9/11 15:42 + **/ +@Slf4j +@SuppressWarnings({"WeakerAccess", "unused"}) +public class HttpLogInterceptor implements Interceptor { + + private final HttpLogFormatter formatter; + + private final String[] ignoreUrlPatterns; + + public HttpLogInterceptor() { + this(JsonHttpLogFormatter.INSTANCE); + } + + public HttpLogInterceptor(String... ignoreUrlPatterns) { + this(JsonHttpLogFormatter.INSTANCE, ignoreUrlPatterns); + } + + public HttpLogInterceptor(HttpLogFormatter formatter, String... ignoreUrlPatterns) { + if (formatter == null) { + this.formatter = JsonHttpLogFormatter.INSTANCE; + } else { + this.formatter = formatter; + } + this.ignoreUrlPatterns = ignoreUrlPatterns; + } + + @Override + @ParametersAreNonnullByDefault + public Response intercept(Chain chain) throws IOException { + return null; + } + + /** + * 输出正常响应日志 + */ + private HttpResponseLog _responseLog(@NonNull Response response, long tookMs) { + HttpResponseLog.HttpResponseLogBuilder logBuilder = HttpResponseLog.builder(); + // code + logBuilder.status(response.code()); + + // msg + logBuilder.msg(response.message()); + + // url + logBuilder.url(Objects.toString(response.request().url())); + + // tookMs + logBuilder.tookMs(tookMs); + + // header + Headers headers = response.headers(); + List responseHeaders = new ArrayList<>(); + for (int i = 0, count = headers.size(); i < count; i++) { + responseHeaders.add(headers.name(i) + ": " + headers.value(i)); + } + logBuilder.headers(responseHeaders); + + // body + if (!HttpHeaderUtil.isDownloadResponse(responseHeaders)) { + logBuilder.body(OkHttpUtil.getBody(response).orElse(null)); + } + + return logBuilder.build(); + } + + /** + * 输出异常响应日志 + */ + private HttpResponseLog _responseLog(@NonNull Request request, long tookMs, Exception e) { + HttpResponseLog.HttpResponseLogBuilder logBuilder = HttpResponseLog.builder(); + // url + logBuilder.url(Objects.toString(request.url())); + + // errorMsg + logBuilder.errorMsg(e.getMessage()); + + // tookMs + logBuilder.tookMs(tookMs); + + return logBuilder.build(); + } + + /** + * 输出请求日志 + */ + private HttpRequestLog _requestLog(Connection connection, @NonNull Request request) { + HttpRequestLog.HttpRequestLogBuilder logBuilder = HttpRequestLog.builder(); + // protocol + Protocol protocol = connection != null ? connection.protocol() : HTTP_1_1; + logBuilder.protocol(Objects.toString(protocol)); + + // method + logBuilder.method(request.method()); + + // url + logBuilder.url(Objects.toString(request.url())); + + // headers + Headers headers = request.headers(); + List requestHeaders = new ArrayList<>(); + for (int i = 0, count = headers.size(); i < count; i++) { + requestHeaders.add(headers.name(i) + ": " + headers.value(i)); + } + logBuilder.headers(requestHeaders); + + // body + if (!HttpHeaderUtil.isMultipartRequest(requestHeaders)) { + logBuilder.body(OkHttpUtil.getBody(request).orElse(null)); + } + + return logBuilder.build(); + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/MediaTypes.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/MediaTypes.java new file mode 100644 index 0000000..085bfa5 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/MediaTypes.java @@ -0,0 +1,13 @@ +package cn.axzo.framework.client.retrofit; + +import okhttp3.MediaType; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:41 + **/ +public interface MediaTypes { + + MediaType APPLICATION_JSON = MediaType.parse("application/json; charset=UTF-8"); +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/OkHttpUtil.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/OkHttpUtil.java new file mode 100644 index 0000000..c736e49 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/OkHttpUtil.java @@ -0,0 +1,126 @@ +package cn.axzo.framework.client.retrofit; + +import okhttp3.*; +import okhttp3.internal.http.HttpHeaders; +import okio.Buffer; +import okio.BufferedSource; + +import java.io.EOFException; +import java.nio.charset.Charset; +import java.util.Optional; + +import static java.lang.Character.isISOControl; +import static java.lang.Long.MAX_VALUE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.empty; +import static java.util.Optional.of; + +/** + * @Description OkHttp工具类 + * @Author liyong.tian + * @Date 2020/9/11 15:46 + **/ +public class OkHttpUtil { + + /** + * 获取请求体内容 + * + * @param request 请求对象 + * @apiNote 如果出现异常, 则忽略, 并返回empty + */ + public static Optional getBody(Request request) { + if (request == null || _bodyEncoded(request.headers())) { + return empty(); + } + RequestBody body = request.body(); + if (body == null) { + return empty(); + } + try { + // 1.写入buffer + Buffer buffer = new Buffer(); + body.writeTo(buffer); + // 2.指定字符集 + MediaType contentType = body.contentType(); + Charset charset = contentType == null ? UTF_8 : contentType.charset(UTF_8); + // 3.从buffer中读取字符串 + if (!_isPlaintext(buffer)) { + return empty(); + } + assert charset != null; + return of(buffer.clone().readString(charset)); + } catch (Exception e) { + // 忽略并返回empty + return empty(); + } + } + + /** + * 获取响应体内容 + * + * @param response 响应对象 + */ + public static Optional getBody(Response response) { + if (response == null || !HttpHeaders.hasBody(response) || _bodyEncoded(response.headers())) { + return empty(); + } + return getBody(response.body()); + } + + /** + * 获取响应体内容 + * + * @param responseBody 响应体对象 + */ + public static Optional getBody(ResponseBody responseBody) { + if (responseBody == null) { + return empty(); + } + try { + // 1.获取buffer + BufferedSource source = responseBody.source(); + source.request(MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.buffer(); + // 2.指定字符集 + MediaType contentType = responseBody.contentType(); + Charset charset = contentType == null ? UTF_8 : contentType.charset(UTF_8); + // 3.从buffer中读取字符串 + if (responseBody.contentLength() == 0) { + return empty(); + } + assert charset != null; + return of(buffer.clone().readString(charset)); + } catch (Exception e) { + return empty(); + } + } + + /** + * Returns true if the body in question probably contains human readable text. Uses a small sample + * of code points to detect unicode control characters commonly used in binary file signatures. + */ + private static boolean _isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < 16; i++) { + if (prefix.exhausted()) { + break; + } + if (isISOControl(prefix.readUtf8CodePoint())) { + return false; + } + } + return true; + } catch (EOFException e) { + // 忽略这个异常 + return false; // Truncated UTF-8 sequence. + } + } + + private static boolean _bodyEncoded(Headers headers) { + String contentEncoding = headers.get("Content-Encoding"); + return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity"); + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/Retrofits.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/Retrofits.java new file mode 100644 index 0000000..ce555ac --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/Retrofits.java @@ -0,0 +1,52 @@ +package cn.axzo.framework.client.retrofit; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import retrofit2.Call; +import retrofit2.Response; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.function.Function; + +import static cn.axzo.framework.core.Constants.CLIENT_MARKER; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:37 + **/ +@Slf4j +@UtilityClass +public class Retrofits { + + @Nonnull + public T execute(Call call, Function fallbackHandler) { + try { + return execute(call); + } catch (IOException e) { + return fallbackHandler.apply(e); + } + } + + @Nonnull + public T execute(Call call) throws IOException { + try { + Response response = call.execute(); + if (response.isSuccessful()) { + T t = response.body(); + if (t == null) { + log.error(CLIENT_MARKER, "response body is null"); + throw new IOException("response body is null"); + } + return t; + } else { + log.error(CLIENT_MARKER, "response error, status = {}, message = {}", response.code(), response.message()); + throw new IOException("response error"); + } + } catch (IOException e) { + log.error(CLIENT_MARKER, "network error", e); + throw new IOException("network error", e); + } + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonRequestBodyConverter.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonRequestBodyConverter.java new file mode 100644 index 0000000..87bb118 --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonRequestBodyConverter.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.client.retrofit.converter; + +import cn.axzo.framework.client.retrofit.MediaTypes; +import com.fasterxml.jackson.databind.ObjectWriter; +import okhttp3.RequestBody; +import retrofit2.Converter; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:49 + **/ +@ParametersAreNonnullByDefault +public class JacksonRequestBodyConverter implements Converter { + + private final ObjectWriter adapter; + + public JacksonRequestBodyConverter(ObjectWriter adapter) { + this.adapter = adapter; + } + + @Override + public RequestBody convert(T value) throws IOException { + byte[] bytes = adapter.writeValueAsBytes(value); + return RequestBody.create(MediaTypes.APPLICATION_JSON, bytes); + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonResponseBodyConverter.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonResponseBodyConverter.java new file mode 100644 index 0000000..a9a55fe --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/JacksonResponseBodyConverter.java @@ -0,0 +1,32 @@ +package cn.axzo.framework.client.retrofit.converter; + +import com.fasterxml.jackson.databind.ObjectReader; +import okhttp3.ResponseBody; +import retrofit2.Converter; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:51 + **/ +@ParametersAreNonnullByDefault +public class JacksonResponseBodyConverter implements Converter { + + private final ObjectReader adapter; + + public JacksonResponseBodyConverter(ObjectReader adapter) { + this.adapter = adapter; + } + + @Override + public T convert(ResponseBody value) throws IOException { + try { + return adapter.readValue(value.charStream()); + } finally { + value.close(); + } + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawRequestBodyConverter.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawRequestBodyConverter.java new file mode 100644 index 0000000..31175fc --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawRequestBodyConverter.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.client.retrofit.converter; + +import lombok.RequiredArgsConstructor; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import retrofit2.Converter; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.nio.charset.Charset; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:52 + **/ +@RequiredArgsConstructor +@ParametersAreNonnullByDefault +public class RawRequestBodyConverter implements Converter { + + private final MediaType contentType; + + @Override + public RequestBody convert(String s) { + byte[] bytes = s.getBytes(Charset.defaultCharset()); + return RequestBody.create(contentType, bytes); + } +} diff --git a/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawResponseBodyConverter.java b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawResponseBodyConverter.java new file mode 100644 index 0000000..53f784f --- /dev/null +++ b/axzo-common-clients/retrofit-starter/src/main/java/cn/axzo/framework/client/retrofit/converter/RawResponseBodyConverter.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.client.retrofit.converter; + +import okhttp3.ResponseBody; +import retrofit2.Converter; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/11 15:53 + **/ +@ParametersAreNonnullByDefault +public class RawResponseBodyConverter implements Converter { + + @Override + public String convert(ResponseBody value) throws IOException { + return value.string(); + } +} diff --git a/axzo-common-core/pom.xml b/axzo-common-core/pom.xml new file mode 100644 index 0000000..3956f45 --- /dev/null +++ b/axzo-common-core/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-core + Axzo Common Core + + + + + org.slf4j + slf4j-api + + + com.google.code.findbugs + jsr305 + + + + + com.github.ben-manes.caffeine + caffeine + + + org.jooq + jool + + + + org.jooq + joor + + + + org.jodd + jodd-bean + + + + org.javatuples + javatuples + + + + javax.validation + validation-api + + + + + org.springframework.boot + spring-boot-starter-log4j2 + test + + + diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/Constants.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/Constants.java new file mode 100644 index 0000000..aedb552 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/Constants.java @@ -0,0 +1,95 @@ +package cn.axzo.framework.core; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +import java.time.format.DateTimeFormatter; + +/** + * 通用常量 + * + * @author liyong.tian + * @since 2020/8/12 16:13 + */ +public interface Constants { + + /** + * 时间 + */ + String PATTERN_DATE = "yyyy-MM-dd"; + String PATTERN_DATE_COMPACT = "yyyyMMdd"; + String PATTERN_DATE_HOUR_COMPACT = "yyyyMMddHH"; + String PATTERN_TIME = "HH:mm:ss"; + String PATTERN_DATE_TIME = "yyyy-MM-dd HH:mm:ss"; + String PATTERN_DATE_TIME_COMPACT = "yyyyMMddHHmmss"; + String PATTERN_DATE_TIME_MILLS = "yyyy-MM-dd HH:mm:ss.SSS"; + String PATTERN_DATE_TIME_MILLS_COMPACT = "yyMMddHHmmssSSS"; + + DateTimeFormatter FORMATTER_DATE_COMPACT = DateTimeFormatter.ofPattern(PATTERN_DATE_COMPACT); + DateTimeFormatter FORMATTER_DATE_HOUR_COMPACT = DateTimeFormatter.ofPattern(PATTERN_DATE_HOUR_COMPACT); + DateTimeFormatter FORMATTER_DATE_TIME_COMPACT = DateTimeFormatter.ofPattern(PATTERN_DATE_TIME_COMPACT); + DateTimeFormatter FORMATTER_DATE_TIME_MILLS_COMPACT = DateTimeFormatter.ofPattern(PATTERN_DATE_TIME_MILLS_COMPACT); + + /** + * 线程池,除了IO密集型任务,2~4倍可获得最佳性能 + */ + Integer POOL_SIZE_SM = Runtime.getRuntime().availableProcessors(); + Integer POOL_SIZE_MD = POOL_SIZE_SM * 2; + Integer POOL_SIZE_LG = POOL_SIZE_SM * 4; + + /** + * 日志 + */ + String MARKER_API = "api"; + Marker API_MARKER = MarkerFactory.getMarker(MARKER_API); + + String MARKER_CLIENT = "client"; + Marker CLIENT_MARKER = MarkerFactory.getMarker(MARKER_CLIENT); + + String MARKER_JOB = "job"; + Marker JOB_MARKER = MarkerFactory.getMarker(MARKER_JOB); + + /** + * 环境 + */ + String ENV_LOCAL = "local"; + String ENV_DEV = "dev"; + String ENV_DEVMOBILE = "devmobile"; + String ENV_DAILY = "daily"; + String ENV_TEST = "test"; + String ENV_FAT = "fat"; + String ENV_UAT = "uat"; + String ENV_STG = "stg"; + String ENV_PRE = "pre"; + String ENV_PROD = "prod"; + String ENV_PRO = "pro"; + String ENV_GTAPRO = "gtapro"; + String ENV_PROMOBILE = "promobile"; + String[] ENV_ALL = {ENV_LOCAL, ENV_DEV, ENV_DEVMOBILE, ENV_DAILY, ENV_TEST, ENV_FAT, ENV_UAT, ENV_STG, ENV_PRE, ENV_PROD, ENV_PRO, ENV_GTAPRO, ENV_PROMOBILE}; + + /** + * MVC + */ + String URI_PATTERN_FILTER_ALL = "/*"; + String URI_PATTERN_INTERCEPTOR_ALL = "/**"; + + /** + * Secure + */ + String SYSTEM_ACCOUNT = "system"; + String ANONYMOUS_USER = "anonymoususer"; + + /** + * Locale + */ + String DEFAULT_LANGUAGE = "zh-cn"; + + /** + * String + */ + int DEFAULT_MIN_LENGTH = 1; + int DEFAULT_SHORT_LENGTH = 64; + int DEFAULT_MEDIUM_LENGTH = 256; + int DEFAULT_LONG_LENGTH = 1024; + int DEFAULT_LARGE_LENGTH = 4096; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/FetchException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/FetchException.java new file mode 100644 index 0000000..851bcdd --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/FetchException.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.core; + +/** + * 获取资源异常 + * @author liyong.tian + * @since 2020/8/12 16:16 + */ +public class FetchException extends Exception{ + + public FetchException(String message) { + super(message); + } + + public FetchException(String message, Throwable cause) { + super(message, cause); + } + + public FetchException(Throwable cause) { + super(cause); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/IName.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/IName.java new file mode 100644 index 0000000..b0b9d4f --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/IName.java @@ -0,0 +1,9 @@ +package cn.axzo.framework.core; + +/** + * @author liyong.tian + * @since 2020/8/12 16:17 + */ +public interface IName { + String getName(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/InternalException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/InternalException.java new file mode 100644 index 0000000..bef4f7a --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/InternalException.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.core; + +/** + * 内部异常 + * @author liyong.tian + * @since 2020/8/12 16:18 + */ +public class InternalException extends RuntimeException { + + public InternalException(String message) { + super(message); + } + + public InternalException(String message, Throwable cause) { + super(message, cause); + } + + public InternalException(Throwable cause) { + super(cause); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/NamingStrategy.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/NamingStrategy.java new file mode 100644 index 0000000..9a85068 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/NamingStrategy.java @@ -0,0 +1,51 @@ +package cn.axzo.framework.core; + +import javax.validation.constraints.NotNull; +import java.util.Objects; + +/** + * @author liyong.tian + * @since 2020/8/12 16:18 + */ +public class NamingStrategy { + + public static final NamingStrategy SAME_CASE = new NamingStrategy(); + + public static final NamingStrategy SNAKE_CASE = new SnakeCaseStrategy(); + + @NotNull + public String translate(String name) { + Objects.requireNonNull(name); + return name; + } + + private static class SnakeCaseStrategy extends NamingStrategy { + @Override + public String translate(String name) { + Objects.requireNonNull(name); + int length = name.length(); + StringBuilder result = new StringBuilder(length * 2); + int resultLength = 0; + boolean wasPrevTranslated = false; + for (int i = 0; i < length; i++) { + char c = name.charAt(i); + if (i > 0 || c != '_') // skip first starting underscore + { + if (Character.isUpperCase(c)) { + if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') { + result.append('_'); + resultLength++; + } + c = Character.toLowerCase(c); + wasPrevTranslated = true; + } else { + wasPrevTranslated = false; + } + result.append(c); + resultLength++; + } + } + return resultLength > 0 ? result.toString() : name; + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/RegexPool.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/RegexPool.java new file mode 100644 index 0000000..84dac34 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/RegexPool.java @@ -0,0 +1,14 @@ +package cn.axzo.framework.core; + +/** + * @author liyong.tian + * @since 2020/8/12 16:20 + */ +public interface RegexPool { + + String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$"; + + String ID_REGEX = "^[_'.@A-Za-z0-9-]*$"; + + String ASCII_PATTERN = "^[\\x00-\\xff]+$"; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/annotation/Description.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/annotation/Description.java new file mode 100644 index 0000000..692587c --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/annotation/Description.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.core.annotation; + +import java.lang.annotation.*; + +/** + * @author liyong.tian + * @since 2020/8/12 16:21 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Description { + + String value(); + + String scope() default "Default"; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/BatchHelper.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/BatchHelper.java new file mode 100644 index 0000000..91e3fd9 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/BatchHelper.java @@ -0,0 +1,195 @@ +package cn.axzo.framework.core.concurrent; + +import cn.axzo.framework.core.Constants; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collection; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.stream.Collectors.toList; + +/** + * 批处理任务工具 + * + * @author liyong.tian + * @since 2020/8/12 16:24 + */ +@Slf4j +public class BatchHelper { + + private static final ExecutorService DEFAULT_POOL = Executors.newFixedThreadPool(Constants.POOL_SIZE_MD); + + private static final Integer DEFAULT_ERROR_THRESHOLD_PERCENTAGE = 100; + + private static final long DEFAULT_TIMEOUT = 1; + + private static final TimeUnit DEFAULT_UNIT = MINUTES; + + @Getter + private final ExecutorService pool; + + private final String batchName; + + private final boolean autoShutdown; + + private final long timeout; + + private final TimeUnit unit; + + // 当任务出错的数量 > 总任务数 * 该百分比,则不再处理后续任务 + private final Integer errorThresholdPercentage; + + public BatchHelper(String batchName) { + this(DEFAULT_POOL, batchName); + } + + public BatchHelper(ExecutorService pool, String batchName) { + this(pool, batchName, DEFAULT_ERROR_THRESHOLD_PERCENTAGE); + } + + + public BatchHelper(ExecutorService pool, String batchName, Integer errorThresholdPercentage) { + this(pool, batchName, DEFAULT_TIMEOUT, DEFAULT_UNIT, errorThresholdPercentage); + } + + /** + * @param nThreads 建议设置为CPU核心数的1~4倍,可获得最佳性能 + * @param batchName 批处理名称 + */ + public BatchHelper(int nThreads, String batchName) { + this(nThreads, batchName, false); + } + + public BatchHelper(int nThreads, String batchName, boolean autoShutdown) { + this(nThreads, batchName, autoShutdown, DEFAULT_ERROR_THRESHOLD_PERCENTAGE); + } + + public BatchHelper(int nThreads, String batchName, boolean autoShutdown, Integer errorThresholdPercentage) { + this(nThreads, batchName, autoShutdown, DEFAULT_TIMEOUT, DEFAULT_UNIT, errorThresholdPercentage); + } + + private BatchHelper(ExecutorService pool, String batchName, long timeout, TimeUnit unit, + Integer errorThresholdPercentage) { + this.batchName = batchName; + this.autoShutdown = false; + this.pool = pool; + this.timeout = timeout; + this.unit = unit; + validatePercentage(errorThresholdPercentage); + this.errorThresholdPercentage = errorThresholdPercentage; + } + + public BatchHelper(int nThreads, String batchName, boolean autoShutdown, long timeout, TimeUnit unit, + Integer errorThresholdPercentage) { + if (nThreads < 1) { + throw new IllegalArgumentException("线程数必须大于等于1"); + } + if (nThreads == 1) { + this.pool = Executors.newSingleThreadExecutor(); + } else { + this.pool = Executors.newFixedThreadPool(nThreads); + } + this.batchName = batchName; + this.autoShutdown = autoShutdown; + this.timeout = timeout; + this.unit = unit; + validatePercentage(errorThresholdPercentage); + this.errorThresholdPercentage = errorThresholdPercentage; + } + + public void process(Collection collection, Consumer handler) { + process(collection, handler, t -> { + }); + } + + public void process(Collection collection, Consumer handler, Consumer finallyHandler) { + process(collection, handler, (t, e) -> log.error(Constants.JOB_MARKER, format("%s error, data = %s", batchName, t)), finallyHandler); + } + + public void process(Collection collection, Consumer handler, BiConsumer onError) { + process(collection, handler, onError, t -> { + }); + } + + /** + * 执行批处理(同步方法) + *

+ * 注: 该方法会阻塞当前线程, 直到批处理完成 + * + * @param collection 待处理记录 + * @param handler 单条处理方法 + * @param onError 自定义单条处理异常后的行为 + */ + public void process(Collection collection, Consumer handler, BiConsumer onError, + Consumer finallyHandler) { + if (pool.isShutdown()) { + throw new RejectedExecutionException("线程池不可用"); + } + + long startTime = currentTimeMillis(); + log.info(Constants.JOB_MARKER, format("%s querySize = %d", batchName, collection.size())); + + AtomicInteger success = new AtomicInteger(0); + AtomicInteger error = new AtomicInteger(0); + Integer errorLimitInclude = collection.size() * errorThresholdPercentage / 100; + + Collection> callableList = collection.stream().map(t -> (Callable) () -> { + if (error.get() > errorLimitInclude) { + return null; + } + try { + handler.accept(t); + success.getAndIncrement(); + } catch (Throwable e) { + log.error(Constants.JOB_MARKER, format("%s error, data = %s", batchName, t), e); + error.getAndIncrement(); + onError.accept(t, e); + } finally { + finallyHandler.accept(t); + } + return null; + }).collect(toList()); + + try { + pool.invokeAll(callableList); + } catch (InterruptedException ignore) { + log.error(Constants.JOB_MARKER, format("%s error", batchName), ignore); + } + long executionMills = currentTimeMillis() - startTime; + log.info(Constants.JOB_MARKER, format("%s successSize = %d", batchName, success.get())); + log.info(Constants.JOB_MARKER, format("%s errorSize = %d", batchName, error.get())); + log.info(Constants.JOB_MARKER, format("%s tookMs = %d", batchName, executionMills)); + + if (autoShutdown) { + shutdownAndAwaitTermination(); + } + } + + private void shutdownAndAwaitTermination() { + pool.shutdown(); + try { + if (!pool.awaitTermination(timeout, unit)) { + pool.shutdownNow(); + if (!pool.awaitTermination(timeout, unit)) { + log.error(Constants.JOB_MARKER, "pool did not terminate"); + } + } + } catch (InterruptedException e) { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + private void validatePercentage(Integer errorThresholdPercentage) { + if (errorThresholdPercentage < 0 || errorThresholdPercentage > 100) { + throw new IllegalArgumentException("错误百分比阈值超出取值范围[0-100]"); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/Idles.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/Idles.java new file mode 100644 index 0000000..bcde0bd --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/concurrent/Idles.java @@ -0,0 +1,23 @@ +package cn.axzo.framework.core.concurrent; + +import cn.axzo.framework.core.InternalException; + +import java.util.concurrent.TimeUnit; + +/** + * @author liyong.tian + * @since 2020/8/12 16:23 + */ +public final class Idles { + + public static void idle(final long duration, final TimeUnit unit) { + try { + unit.sleep(duration); + } catch (InterruptedException e) { + throw new InternalException(e); + } + } + + private Idles() { + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AES.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AES.java new file mode 100644 index 0000000..f896614 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AES.java @@ -0,0 +1,232 @@ +package cn.axzo.framework.core.crypto; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.security.Key; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Base64; +import java.util.UUID; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * AES工具类 + * size指字符串的位数,不是字节数 + * + * @author liyong.tian + * @since 2020/8/12 16:49 + */ +@Slf4j +@SuppressWarnings({"WeakerAccess", "unused"}) +public class AES { + + /** + * 伪随机数生成算法 + */ + private static final String PRNG_ALGORITHM = "SHA1PRNG"; + + private static final String ALGORITHM = "AES"; + + private static final int DEFAULT_KEY_SIZE = 128; + + /** + * AES密钥 + */ + private final Key key; + + private final boolean isUrlSafe; + + private final String padding; + + @Nullable + private final AlgorithmParameterSpec iv; + + private AES(Key key, boolean isUrlSafe, String padding, @Nullable AlgorithmParameterSpec iv) { + this.key = key; + this.isUrlSafe = isUrlSafe; + this.padding = padding; + this.iv = iv; + } + + public static AESBuilder builder() { + return new AESBuilder(); + } + + /** + * 加密 + * + * @param text 明文 + * @return 密文(Base64) + */ + public String encrypt(String text) { + try { + return Ciphers.encrypt(key, iv, text, padding, isUrlSafe); + } catch (Exception e) { + log.error("AES加密失败", e); + throw new AESException("AES加密失败", e); + } + + } + + /** + * 解密 + * + * @param encryptedText 密文(Base64) + * @return 明文 + */ + public String decrypt(String encryptedText) { + try { + return Ciphers.decrypt(key, iv, encryptedText, padding, isUrlSafe); + } catch (Exception e) { + log.error("AES解密失败", e); + throw new AESException("AES解密失败", e); + } + } + + /** + * 指定长度的密钥 + * + * @param key 密钥哈希 + * @param isBase64Encoded 该密钥是否经过Base64加密 + * @param size 密钥位数 + * @return 密钥 + */ + private static Key toKey(String key, boolean isBase64Encoded, int size) { + try { + //必须是8的倍数 + if (size % 8 != 0) { + throw new AESException("密钥位数必须是8的倍数"); + } + byte[] keyBytes; + if (isBase64Encoded) { + keyBytes = Base64.getDecoder().decode(key); + } else { + keyBytes = padWithZeros(key.getBytes(UTF_8), size / 8); + } + return new SecretKeySpec(keyBytes, ALGORITHM); + } catch (Exception e) { + log.error("生成AES密钥失败", e); + throw new AESException("生成AES密钥失败", e); + } + } + + /** + * 随机生成AES密钥 + * + * @param seed 随机种子 + * @param size 密钥位数 + * @return AES密钥 + */ + private static Key generateKey(String seed, int size) { + try { + //随机数 + SecureRandom secureRandom = SecureRandom.getInstance(PRNG_ALGORITHM); + secureRandom.setSeed(seed.getBytes(Charset.defaultCharset())); + + //初始化密钥 + KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); + keyGenerator.init(size, secureRandom); + SecretKey secretKey = keyGenerator.generateKey(); + byte[] bytes = secretKey.getEncoded(); + return new SecretKeySpec(bytes, ALGORITHM); + } catch (Exception e) { + log.error("生成AES密钥失败", e); + throw new AESException("生成AES密钥失败", e); + } + } + + /** + * 生成AES算法向量 + * + * @param iv 向量哈希 + * @return AES算法向量 + */ + private static AlgorithmParameterSpec ivSpec(String iv, int size) { + try { + //必须是8的倍数 + if (size % 8 != 0) { + throw new AESException("算法向量位数必须是8的倍数"); + } + byte[] ivBytes = padWithZeros(iv.getBytes(UTF_8), size / 8); + return new IvParameterSpec(ivBytes); + } catch (Exception e) { + log.error("生成AES算法向量失败", e); + throw new AESException("生成AES算法向量失败", e); + } + } + + /** + * 补位0到指定长度 + */ + private static byte[] padWithZeros(byte[] input, int lenNeed) { + int rest = input.length % lenNeed; + if (rest > 0) { + byte[] result = new byte[input.length + (lenNeed - rest)]; + System.arraycopy(input, 0, result, 0, input.length); + return result; + } + return input; + } + + public static class AESBuilder { + + private String padding = "AES"; + + private Key key; + + private boolean isUrlSafe; + + private AlgorithmParameterSpec iv; + + AESBuilder() { + } + + public AESBuilder genKey(int size) { + return genKey(UUID.randomUUID().toString(), size); + } + + public AESBuilder genKey(String seed, int size) { + this.key = generateKey(seed, size); + return this; + } + + public AESBuilder key(String key, int size) { + this.key = toKey(key, false, size); + return this; + } + + public AESBuilder key(String key, boolean isBase64Encoded, int size) { + this.key = toKey(key, isBase64Encoded, size); + return this; + } + + public AESBuilder isUrlSafe(boolean isUrlSafe) { + this.isUrlSafe = isUrlSafe; + return this; + } + + public AESBuilder padding(String padding) { + this.padding = padding; + return this; + } + + public AESBuilder iv(String iv, int size) { + this.iv = ivSpec(iv, size); + return this; + } + + public AES build() { + if (key == null) { + this.key = generateKey(UUID.randomUUID().toString(), DEFAULT_KEY_SIZE); + } + return new AES(key, isUrlSafe, padding, iv); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AESException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AESException.java new file mode 100644 index 0000000..33a0f8d --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/AESException.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.core.crypto; + +/** + * AES加密异常 + * @author liyong.tian + * @since 2020/8/12 16:51 + */ +public class AESException extends RuntimeException { + + public AESException() { + super(); + } + + public AESException(String message) { + super(message); + } + + public AESException(String message, Throwable cause) { + super(message, cause); + } + + public AESException(Throwable cause) { + super(cause); + } + + protected AESException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/Ciphers.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/Ciphers.java new file mode 100644 index 0000000..9862387 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/Ciphers.java @@ -0,0 +1,63 @@ +package cn.axzo.framework.core.crypto; + +import lombok.experimental.UtilityClass; + +import javax.crypto.Cipher; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @author liyong.tian + * @since 2020/8/12 16:52 + */ +@UtilityClass +public class Ciphers { + + /** + * 加密 + * + * @param text 明文 + * @return 密文(Base64) + */ + public String encrypt(Key key, AlgorithmParameterSpec iv, String text, String padding, boolean isUrlSafe) throws Exception { + Cipher cipher = Cipher.getInstance(padding); + if (iv == null) { + cipher.init(Cipher.ENCRYPT_MODE, key); + } else { + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + } + byte[] bytes = cipher.doFinal(text.getBytes(UTF_8)); + Base64.Encoder encoder = isUrlSafe ? Base64.getUrlEncoder() : Base64.getEncoder(); + return new String(encoder.encode(bytes), UTF_8); + } + + /** + * 解密 + * + * @param encryptedText 密文(Base64) + * @return 明文 + */ + public String decrypt(Key key, AlgorithmParameterSpec iv, String encryptedText, String padding, boolean isUrlSafe) throws Exception { + Cipher cipher = Cipher.getInstance(padding); + if (iv == null) { + cipher.init(Cipher.DECRYPT_MODE, key); + } else { + cipher.init(Cipher.DECRYPT_MODE, key, iv); + } + cipher.init(Cipher.DECRYPT_MODE, key, iv); + Base64.Decoder decoder = isUrlSafe ? Base64.getUrlDecoder() : Base64.getDecoder(); + byte[] bytes = cipher.doFinal(decoder.decode(encryptedText)); + return new String(bytes, UTF_8); + } + + public String encrypt(Key key, String text, String padding, boolean isUrlSafe) throws Exception { + return encrypt(key, null, text, padding, isUrlSafe); + } + + public String decrypt(Key key, String encryptedText, String padding, boolean isUrlSafe) throws Exception { + return decrypt(key, null, encryptedText, padding, isUrlSafe); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESede.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESede.java new file mode 100644 index 0000000..8c80b2c --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESede.java @@ -0,0 +1,85 @@ +package cn.axzo.framework.core.crypto; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESedeKeySpec; +import java.security.Key; +import java.util.Base64; + +/** + * @author liyong.tian + * @since 2020/8/12 16:56 + */ +@Slf4j +public class DESede { + + /** + * 密钥算法 + */ + private static final String ALGORITHM = "DESede"; + + /** + * 密钥哈希(Base64) + */ + @Getter + private final String key; + + /** + * 密钥算法/工作模式/填充方式 + */ + private final String padding; + + private final boolean isUrlSafe; + + public DESede(String desedeKey, String padding) { + this(desedeKey, padding, false); + } + + public DESede(String desedeKey, String padding, boolean isUrlSafe) { + this.key = desedeKey; + this.padding = padding; + this.isUrlSafe = isUrlSafe; + } + + /** + * 加密 + * + * @param text 明文 + * @return 密文(Base64) + */ + public String encrypt(String text) { + try { + return Ciphers.encrypt(toKey(), text, padding, isUrlSafe); + } catch (Exception e) { + log.error("加密失败", e); + throw new DESedeException("加密失败", e); + } + } + + /** + * 解密 + * + * @param encryptedText 密文(Base64) + * @return 明文 + */ + public String decrypt(String encryptedText) { + try { + return Ciphers.decrypt(toKey(), encryptedText, padding, isUrlSafe); + } catch (Exception e) { + log.error("加密失败", e); + throw new DESedeException("加密失败", e); + } + } + + private Key toKey() throws Exception { + byte[] keyBytes = Base64.getDecoder().decode(key); + //实例化Des密钥 + DESedeKeySpec keySpec = new DESedeKeySpec(keyBytes); + //实例化密钥工厂 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); + //生成密钥 + return keyFactory.generateSecret(keySpec); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESedeException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESedeException.java new file mode 100644 index 0000000..e689f37 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/DESedeException.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.core.crypto; + +/** + * DESeds加密异常 + * + * @author liyong.tian + * @since 2020/8/12 16:58 + */ +public class DESedeException extends RuntimeException { + + public DESedeException() { + super(); + } + + public DESedeException(String message) { + super(message); + } + + public DESedeException(String message, Throwable cause) { + super(message, cause); + } + + public DESedeException(Throwable cause) { + super(cause); + } + + protected DESedeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSA.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSA.java new file mode 100644 index 0000000..c50eae4 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSA.java @@ -0,0 +1,153 @@ +package cn.axzo.framework.core.crypto; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +import static jodd.util.StringPool.UTF_8; + +/** + * RSA工具类 + *

+ * 在spring环境中,建议注册为bean实例 + * + * @author liyong.tian + * @since 2020/8/12 16:53 + */ +@Slf4j +public class RSA { + + private static final String ALGORITHM = "RSA"; + private static final String SIGN_ALGORITHM = "SHA1WithRSA"; + + private final PublicKey publicKey; + + private final PrivateKey privateKey; + + /** + * @param publicKey 公钥(Base64) + * @param privateKey 私钥(Base64) + */ + public RSA(String publicKey, String privateKey) { + this.publicKey = loadPublicKey(publicKey); + this.privateKey = loadPrivateKey(privateKey); + } + + /** + * 公钥加密 + * + * @param text 明文 + * @return 密文(Base64) + */ + public String encrypt(String text) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] bytes = cipher.doFinal(text.getBytes(UTF_8)); + return new String(Base64.getEncoder().encode(bytes), UTF_8); + } catch (Exception e) { + log.error("加密失败", e); + throw new RSAException("加密失败", e); + } + } + + /** + * 私钥解密 + * + * @param encryptedText 密文(Base64) + * @return 明文 + */ + public String decrypt(String encryptedText) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText)); + return new String(bytes, UTF_8); + } catch (Exception e) { + log.error("解密失败", e); + throw new RSAException("解密失败", e); + } + } + + /** + * 私钥签名 + * + * @param text 文本 + * @return 签名(Base64) + */ + public String sign(String text) { + try { + Signature signature = Signature.getInstance(SIGN_ALGORITHM); + signature.initSign(privateKey); + signature.update(text.getBytes(UTF_8)); + byte[] bytes = signature.sign(); + return new String(Base64.getEncoder().encode(bytes), UTF_8); + } catch (Exception e) { + log.error("签名失败", e); + throw new RSAException("签名失败", e); + } + } + + /** + * 公钥验签 + * + * @param sign 签名参数 + * @param text 文本 + * @return 验签结果 + */ + public boolean verify(String sign, String text) { + try { + Signature signature = Signature.getInstance(SIGN_ALGORITHM); + signature.initVerify(publicKey); + signature.update(text.getBytes(UTF_8)); + return signature.verify(Base64.getDecoder().decode(sign.getBytes(UTF_8))); + } catch (Exception e) { + log.error("验签失败", e); + return false; + } + } + + /** + * 加载公钥 + * + * @param publicKey 公钥(Base64) + * @return 公钥对象 + */ + private static PublicKey loadPublicKey(String publicKey) { + try { + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey); + KeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + return keyFactory.generatePublic(keySpec); + } catch (Exception e) { + log.error("公钥加载失败", e); + throw new RSAException("公钥加载失败", e); + } + } + + /** + * 加载私钥 + * + * @param privateKey 私钥(Base64) + * @return 私钥对象 + */ + private static PrivateKey loadPrivateKey(String privateKey) { + try { + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey); + KeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + log.error("私钥加载失败", e); + throw new RSAException("私钥加载失败", e); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSAException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSAException.java new file mode 100644 index 0000000..816b5da --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/crypto/RSAException.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.core.crypto; + +/** + * RSA加密异常 + * + * @author liyong.tian + * @since 2020/8/12 16:55 + */ +public class RSAException extends RuntimeException { + + public RSAException() { + super(); + } + + public RSAException(String message) { + super(message); + } + + public RSAException(String message, Throwable cause) { + super(message, cause); + } + + public RSAException(Throwable cause) { + super(cause); + } + + protected RSAException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} \ No newline at end of file diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Cent.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Cent.java new file mode 100644 index 0000000..182bf0c --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Cent.java @@ -0,0 +1,69 @@ +package cn.axzo.framework.core.currency; + +import lombok.Getter; + +import java.beans.ConstructorProperties; +import java.util.Objects; + +/** + * 该模型的序列化规则:存Long 取Cent 展示String + * + * @author liyong.tian + * @since 16/9/19 + */ +@Getter +public class Cent { + + private final Long value; + + private final String yuan; + + @ConstructorProperties({"value", "yuan"}) + public Cent(Long value, String yuan) { + this(value); + if (!Objects.equals(yuan, this.yuan)) { + throw new IllegalArgumentException("元和分不匹配"); + } + } + + public Cent(Long value) { + this.value = value; + this.yuan = toYuan(value); + } + + public Cent(String yuan) { + this.value = toValue(yuan); + this.yuan = toYuan(value); + } + + public String yuan() { + return this.yuan; + } + + public Long value() { + return this.value; + } + + private String toYuan(Long value) { + return PriceUtil.toYuanString(value); + } + + private Long toValue(String yuan) { + return PriceUtil.toCentValue(yuan); + } + + @Override + public String toString() { + return Objects.toString(value); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Long && value == ((Long) obj).longValue(); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Decimals.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Decimals.java new file mode 100644 index 0000000..b58fbd9 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/Decimals.java @@ -0,0 +1,32 @@ +package cn.axzo.framework.core.currency; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ROUND_HALF_UP; +import static java.math.BigInteger.valueOf; + +/** + * 定点数相关转换 + * + * @author liyong.tian + * @since 2017/8/16 上午12:06 + */ +public abstract class Decimals { + + // 一位精度的10 + public static final BigDecimal TEN_ONE_SCALE = new BigDecimal(valueOf(100L), 1); + + // 两位精度的100 + public static final BigDecimal HUNDRED_TWO_SCALE = new BigDecimal(valueOf(10000L), 2); + + // 三位精度的1000 + public static final BigDecimal THOUSAND_THREE_SCALE = new BigDecimal(valueOf(1000000L), 3); + + public static BigDecimal fractionToDecimal(Long numerator, BigDecimal denominator) { + return new BigDecimal(numerator).divide(denominator, denominator.scale(), ROUND_HALF_UP); + } + + public static Long multiplicationToLong(BigDecimal baseValue, BigDecimal multiple) { + return baseValue.multiply(multiple).longValueExact(); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/PriceUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/PriceUtil.java new file mode 100644 index 0000000..ab46ab6 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/currency/PriceUtil.java @@ -0,0 +1,48 @@ +package cn.axzo.framework.core.currency; + +import jodd.util.StringUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.math.BigDecimal; +import java.math.RoundingMode; + +public abstract class PriceUtil { + + // 1元 = 100分 + private final static Integer CENTS_OF_ONE_YUAN = 100; + + @Nonnull + public static String toYuanString(Long cent) { + if (cent == null) { + return ""; + } + BigDecimal realPrice = new BigDecimal(cent).divide(new BigDecimal(CENTS_OF_ONE_YUAN), 2, RoundingMode.HALF_EVEN); + return realPrice.toString(); + } + + @Nullable + public static Double toYuanDouble(Long cent) { + if (cent == null) { + return null; + } + BigDecimal realPrice = new BigDecimal(cent).divide(new BigDecimal(CENTS_OF_ONE_YUAN), 2, RoundingMode.HALF_EVEN); + return realPrice.doubleValue(); + } + + @Nullable + public static Long toCentValue(String yuan) { + if (StringUtil.isBlank(yuan)) { + return null; + } + return new BigDecimal(yuan).multiply(BigDecimal.valueOf(CENTS_OF_ONE_YUAN)).longValueExact(); + } + + @Nullable + public static Long toCentValue(Double yuan) { + if (yuan == null) { + return null; + } + return new BigDecimal(yuan).multiply(BigDecimal.valueOf(CENTS_OF_ONE_YUAN)).longValueExact(); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/EnumStdUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/EnumStdUtil.java new file mode 100644 index 0000000..39919b4 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/EnumStdUtil.java @@ -0,0 +1,128 @@ +package cn.axzo.framework.core.enums; + +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.util.ClassUtil; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * 符合规范的枚举工具 + *

+ * 枚举规范:整型枚举,字符串型枚举 + * + * @author liyong.tian + * @see ICode + * @see IStringCode + * @since 2016/12/18 + */ +public abstract class EnumStdUtil { + + private static LoadingCache, Map> iCodeCache = Caffeine + .newBuilder() + .maximumSize(1000) + .build(enumCls -> ClassUtil.cast(_mappingICode(ClassUtil.cast(enumCls)))); + + private static LoadingCache, Map> iStringCodeCache = Caffeine + .newBuilder() + .maximumSize(1000) + .build(enumCls -> ClassUtil.cast(_mappingIStringCode(ClassUtil.cast(enumCls)))); + + public static & ICode> void cacheICode(Class enumCls) { + iCodeCache.get(enumCls); + } + + public static & IStringCode> void cacheIStringCode(Class enumCls) { + iStringCodeCache.get(enumCls); + } + + /** + * @throws InternalException if code cannot match any enum value + */ + public static & ICode> T parse(Class enumCls, int code) { + return findEnum(enumCls, code).orElseThrow(() -> new InternalException(enumCls.getSimpleName() + " cannot parse code: " + code)); + } + + /** + * @throws InternalException if code cannot match any enum value + */ + public static & IStringCode> T parse(Class enumCls, String code) { + return findEnum(enumCls, code).orElseThrow(() -> new InternalException(enumCls.getSimpleName() + " cannot parse code: " + code)); + } + + public static & ICode> T parse(Class enumCls, int code, T defaultInstance) { + return findEnum(enumCls, code).orElse(defaultInstance); + } + + public static & IStringCode> T parse(Class enumCls, String code, T defaultInstance) { + return findEnum(enumCls, code).orElse(defaultInstance); + } + + public static & ICode> Optional findEnum(Class enumCls, int code) { + if (!enumCls.isEnum()) { + return Optional.empty(); + } + T t = ClassUtil.cast(Objects.requireNonNull(iCodeCache.get(enumCls)).get(code)); + return Optional.ofNullable(t); + } + + public static & IStringCode> Optional findEnum(Class enumCls, String code) { + if (!enumCls.isEnum() || code == null) { + return Optional.empty(); + } + Map map = ClassUtil.cast(Objects.requireNonNull(iStringCodeCache.get(enumCls))); + T t = map.get(code.toLowerCase()); + if (t != null && t.ignoreCase()) { + return Optional.of(t); + } + return Optional.ofNullable(map.get(code)); + } + + public static & ICode> boolean contains(Class enumCls, int code) { + return findEnum(enumCls, code).isPresent(); + } + + public static & IStringCode> boolean contains(Class enumCls, String code) { + return findEnum(enumCls, code).isPresent(); + } + + /*-------------------------------私有方法-------------------------------*/ + + private static & IStringCode> Map _mappingIStringCode(Class enumCls) { + if (!enumCls.isEnum()) { + return new HashMap<>(); + } + T[] enumValues = enumCls.getEnumConstants(); + if (enumValues == null) { + return new HashMap<>(); + } + Map map = new HashMap<>(); + for (T enumValue : enumValues) { + if (enumValue.ignoreCase()) { + map.put(enumValue.getCode().toLowerCase(), enumValue); + } else { + map.put(enumValue.getCode(), enumValue); + } + } + return map; + } + + private static & ICode> Map _mappingICode(Class enumCls) { + if (!enumCls.isEnum()) { + return new HashMap<>(); + } + T[] enumValues = enumCls.getEnumConstants(); + if (enumValues == null) { + return new HashMap<>(); + } + Map map = new HashMap<>(); + for (T enumValue : enumValues) { + map.put(enumValue.getCode(), enumValue); + } + return map; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Enums.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Enums.java new file mode 100644 index 0000000..b976da9 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Enums.java @@ -0,0 +1,59 @@ +package cn.axzo.framework.core.enums; + +import jodd.introspector.CachingIntrospector; +import jodd.introspector.PropertyDescriptor; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +/** + * 枚举工具 + * @see EnumStdUtil + * @author liyong.tian + * @since 2020/8/12 17:03 + */ +@Slf4j +public class Enums { + + public static > Optional getIfPresent(Class enumType, String value) { + if (enumType == null || !enumType.isEnum()) { + return Optional.empty(); + } + return Arrays.stream(enumType.getEnumConstants()).filter(en -> en.toString().equals(value)).findFirst(); + } + + /** + * 获取枚举中指定属性的值 + * + * @param enumCls 枚举类型 + * @param prop Bean属性名 + * @return (枚举值, 指定属性的值) + */ + public static Map, Object> getEnumAndValue(Class enumCls, String prop) { + if (!enumCls.isEnum()) { + throw new IllegalArgumentException("enumCls is not enum type!"); + } + if (jodd.util.StringUtil.isBlank(prop)) { + throw new IllegalArgumentException("prop cannot be blank!"); + } + + Object[] enumValues = enumCls.getEnumConstants(); + if (enumValues == null || enumValues.length == 0) { + return Collections.emptyMap(); + } + Map, Object> result = new LinkedHashMap<>(enumValues.length * 2); + try { + for (Object enumValue : enumValues) { + PropertyDescriptor pd = new CachingIntrospector().lookup(enumCls).getPropertyDescriptor(prop, true); + if (pd == null || pd.getGetter(true) == null) { + continue; + } + result.put((Enum) enumValue, pd.getGetter(true).invokeGetter(enumValue)); + } + } catch (Exception e) { + // ignore... + log.error(e.getMessage(), e); + } + return result; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ICode.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ICode.java new file mode 100644 index 0000000..87a865b --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ICode.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.core.enums; + +import cn.axzo.framework.core.IName; + +/** + * 枚举规范-数值型 + * + * @author liyong.tian + * @since 2016/12/6 + */ +public interface ICode extends IName { + int getCode(); + + String getName(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/IStringCode.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/IStringCode.java new file mode 100644 index 0000000..c323968 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/IStringCode.java @@ -0,0 +1,19 @@ +package cn.axzo.framework.core.enums; + +import cn.axzo.framework.core.IName; + +/** + * 枚举规范-字符串型 + * + * @author liyong.tian + * @since 2016/12/6 + */ +public interface IStringCode extends IName { + String getCode(); + + String getName(); + + default boolean ignoreCase() { + return true; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Reserved.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Reserved.java new file mode 100644 index 0000000..234d820 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/Reserved.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.core.enums; + +import java.lang.annotation.*; + +/** + * 标识接口,用于枚举类 + * + * @author liyong.tian + * @since 2018/3/18 下午6:25 + */ +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Reserved { + +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ReservedEnum.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ReservedEnum.java new file mode 100644 index 0000000..6a9093a --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/ReservedEnum.java @@ -0,0 +1,36 @@ +package cn.axzo.framework.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.annotation.*; + +/** + * 用于描述在一个整形或字符串变量上预设的枚举值 + * + * @author liyong.tian + * @since 2018/3/18 下午4:47 + */ +@Documented +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ReservedEnum { + + Class> using() default None.class; + + @AllArgsConstructor + @Getter + enum None implements ICode { + ; + private int code; + private String name; + } + + @AllArgsConstructor + @Getter + enum NoneStr implements IStringCode { + ; + private String code; + private String name; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumMeta.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumMeta.java new file mode 100644 index 0000000..65c9673 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumMeta.java @@ -0,0 +1,76 @@ +package cn.axzo.framework.core.enums.meta; + +import cn.axzo.framework.core.annotation.Description; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +/** + * @author liyong.tian + * @since 17-8-8. + */ +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode(of = "sourceClass") +public class EnumMeta implements Comparable { + + private final Class> sourceClass; + + private final EnumType enumType; + + private final List intCodes = new ArrayList<>(); + + private final List stringCodes = new ArrayList<>(); + + public void addIntCode(ICode code) { + intCodes.add(new IntCode(code.getCode(), code.getName())); + } + + public void addStringCode(IStringCode stringCode) { + stringCodes.add(new StringCode(stringCode.getCode(), stringCode.getName())); + } + + public void addIntCode(IntCode code) { + intCodes.add(code); + } + + public void addStringCode(StringCode code) { + stringCodes.add(code); + } + + public String getClassName() { + return sourceClass.getName(); + } + + public String getSimpleName() { + return sourceClass.getSimpleName(); + } + + public String getDescription() { + boolean hasDescription = sourceClass.isAnnotationPresent(Description.class); + return hasDescription ? sourceClass.getAnnotation(Description.class).value() : getSimpleName(); + } + + public List getIntCodes() { + return intCodes; + } + + public List getStringCodes() { + return stringCodes; + } + + public boolean isNotDeprecated() { + return !sourceClass.isAnnotationPresent(Deprecated.class); + } + + @Override + public int compareTo(@Nonnull EnumMeta o) { + return this.getSimpleName().compareTo(o.getSimpleName()); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumResourceLoader.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumResourceLoader.java new file mode 100644 index 0000000..955c8a2 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumResourceLoader.java @@ -0,0 +1,94 @@ +package cn.axzo.framework.core.enums.meta; + +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.io.Resources; +import jodd.util.ArraysUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static jodd.util.StringUtil.count; + +/** + * @author liyong.tian + * @since 17-8-11. + */ +public class EnumResourceLoader { + + private final static Comparator PACKAGE_COMPARATOR = comparing(packageName -> count(packageName, '.')); + + public static List findClassPathEnums(List packageNames) { + return packageNames.stream() + .map(EnumResourceLoader::findClassPathEnums) + .flatMap(List::stream) + .collect(toList()); + } + + public static List findClassPathEnums(List packageNames, ClassLoader classLoader) { + return packageNames.stream() + .map(packageName -> findClassPathEnums(packageName, classLoader)) + .flatMap(List::stream) + .collect(toList()); + } + + public static List findClassPathEnums(String... packageNames) { + return Arrays.stream(_mergePackageName(packageNames)) + .map(EnumResourceLoader::findClassPathEnums) + .flatMap(List::stream) + .collect(toList()); + } + + public static List findClassPathEnums(String[] packageNames, ClassLoader classLoader) { + return Arrays.stream(_mergePackageName(packageNames)) + .map(packageName -> findClassPathEnums(packageName, classLoader)) + .flatMap(List::stream) + .collect(toList()); + } + + public static List findClassPathEnums(String packageName) { + return _findEnumMetas(Resources.findEnumClasses(packageName)); + } + + public static List findClassPathEnums(String packageName, ClassLoader classLoader) { + return _findEnumMetas(Resources.findEnumClasses(packageName, classLoader)); + } + + private static List _findEnumMetas(Class>[] classes) { + List enumMetas = new ArrayList<>(); + + Arrays.stream(classes).forEach(clazz -> { + // int code + if (ICode.class.isAssignableFrom(clazz)) { + EnumMeta enumMeta = new EnumMeta(clazz, EnumType.INT_ENUM); + Arrays.stream(clazz.getEnumConstants()).forEach(aEnum -> enumMeta.addIntCode((ICode) aEnum)); + enumMetas.add(enumMeta); + return; + } + + // string code + if (IStringCode.class.isAssignableFrom(clazz)) { + EnumMeta enumMeta = new EnumMeta(clazz, EnumType.STRING_ENUM); + Arrays.stream(clazz.getEnumConstants()).forEach(aEnum -> enumMeta.addStringCode((IStringCode) aEnum)); + enumMetas.add(enumMeta); + } + }); + + return enumMetas; + } + + private static String[] _mergePackageName(String... packageNames) { + String[] sortedPackageNames = Arrays.stream(packageNames).sorted(PACKAGE_COMPARATOR).toArray(String[]::new); + String[] mergedPackageNames = {}; + for (String sortedPackageName : sortedPackageNames) { + if (Arrays.stream(mergedPackageNames).noneMatch(sortedPackageName::startsWith)) { + mergedPackageNames = ArraysUtil.append(mergedPackageNames, sortedPackageName); + } + } + return mergedPackageNames; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumType.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumType.java new file mode 100644 index 0000000..324f2d6 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/EnumType.java @@ -0,0 +1,19 @@ +package cn.axzo.framework.core.enums.meta; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author liyong.tian + * @since 17-8-11. + */ +@Getter +@AllArgsConstructor +public enum EnumType { + + INT_ENUM("数值型枚举"), + + STRING_ENUM("字符串型枚举"); + + private String name; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/IntCode.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/IntCode.java new file mode 100644 index 0000000..4b11702 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/IntCode.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.core.enums.meta; + +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.util.StringUtil; +import lombok.Getter; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.beans.ConstructorProperties; + +/** + * @author liyong.tian + * @since 2017/9/13 上午3:23 + */ +@ToString +@Getter +public class IntCode implements ICode { + + private final int code; + + @NotNull + private final String name; + + @ConstructorProperties({"code", "name"}) + public IntCode(int code, String name) { + this.code = code; + this.name = StringUtil.orEmpty(name); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/StringCode.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/StringCode.java new file mode 100644 index 0000000..9b0fe70 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/enums/meta/StringCode.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.core.enums.meta; + +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.util.StringUtil; +import lombok.Getter; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.beans.ConstructorProperties; + +/** + * @author liyong.tian + * @since 2017/9/13 上午3:24 + */ +@ToString +@Getter +public class StringCode implements IStringCode { + + @NotNull + private final String code; + + @NotNull + private final String name; + + @ConstructorProperties({"code", "name"}) + public StringCode(String code, String name) { + this.code = StringUtil.orEmpty(code); + this.name = StringUtil.orEmpty(name); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Mapping.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Mapping.java new file mode 100644 index 0000000..0f9b472 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Mapping.java @@ -0,0 +1,42 @@ +package cn.axzo.framework.core.function; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.function.Function; + +/** + * @author liyong.tian + * @since 2020/8/28 15:51 + */ +@RequiredArgsConstructor(staticName = "of") +public class Mapping { + + @NonNull + private final Object o; + + private Class type; + + private Mapping(Object o, Class type) { + this.o = o; + this.type = type; + } + + public Mapping filter(Class type) { + if (o.getClass().equals(type)) { + return new Mapping<>(o, type); + } + return new Mapping<>(o); + } + + public Mapping map(Function function) { + if (o.getClass().equals(type)) { + return new Mapping<>(function.apply(type.cast(o)), type); + } + return this; + } + + public Object get() { + return o; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/function/OptionalExt.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/OptionalExt.java new file mode 100644 index 0000000..6cdddc7 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/OptionalExt.java @@ -0,0 +1,59 @@ +package cn.axzo.framework.core.function; + +import lombok.NonNull; + +import java.util.Optional; +import java.util.function.Consumer; + +/** + * 包装Optional对象,封装一些增强特性 + * + * @author liyong.tian + * @since 2020/8/28 15:54 + */ +@SuppressWarnings("all") +public class OptionalExt { + + private final Optional optional; + + private OptionalExt(Optional optional) { + this.optional = optional; + } + + public static OptionalExt of(@NonNull T value) { + return value == null ? of(Optional.empty()) : of(Optional.of(value)); + } + + public static OptionalExt of(Optional optional) { + return new OptionalExt<>(optional); + } + + public static OptionalExt empty() { + return of(Optional.empty()); + } + + public Optional toOptional() { + return optional; + } + + /** + * ifPresent可以和orElse连着用,符合函数式写法 + */ + public OptionalExt ifPresent(Consumer consumer) { + return map(consumer); + } + + /** + * map可以和orElse连着用,符合函数式写法 + */ + public OptionalExt map(Consumer consumer) { + optional.ifPresent(consumer); + return this; + } + + public void orElse(Runner runner) { + if (!optional.isPresent()) { + runner.run(); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Runner.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Runner.java new file mode 100644 index 0000000..abf0420 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/function/Runner.java @@ -0,0 +1,10 @@ +package cn.axzo.framework.core.function; + +/** + * @author liyong.tian + * @since 2020/8/28 16:01 + */ +@FunctionalInterface +public interface Runner { + void run(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceException.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceException.java new file mode 100644 index 0000000..d11db82 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceException.java @@ -0,0 +1,19 @@ +package cn.axzo.framework.core.io; + +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * @author liyong.tian + * @since 2020/8/28 16:04 + */ +public class ResourceException extends RuntimeException { + + public ResourceException(IOException e) { + super(e.getMessage(), e); + } + + public ResourceException(String message, URISyntaxException e) { + super(message, e); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceStrings.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceStrings.java new file mode 100644 index 0000000..78d4cee --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceStrings.java @@ -0,0 +1,71 @@ +package cn.axzo.framework.core.io; + +/** + * @author liyong.tian + * @since 2020/8/28 16:05 + */ +public interface ResourceStrings { + + /** + * URL prefix for loading from the file system: "file:" + */ + String FILE_URL_PREFIX = "file:"; + + /** + * URL prefix for loading from a jar file: "jar:" + */ + String JAR_URL_PREFIX = "jar:"; + + /** + * URL protocol for a file in the file system: "file" + */ + String URL_PROTOCOL_FILE = "file"; + + /** + * URL protocol for an entry from a jar file: "jar" + */ + String URL_PROTOCOL_JAR = "jar"; + + /** + * URL protocol for an entry from a war file: "war" + */ + String URL_PROTOCOL_WAR = "war"; + + /** + * URL protocol for an entry from a zip file: "zip" + */ + String URL_PROTOCOL_ZIP = "zip"; + + /** + * URL protocol for an entry from a WebSphere jar file: "wsjar" + */ + String URL_PROTOCOL_WSJAR = "wsjar"; + + /** + * Separator between JAR URL and file path within the JAR: "!/" + */ + String JAR_URL_SEPARATOR = "!/"; + + /** + * Special separator between WAR URL and jar part on Tomcat + */ + String WAR_URL_SEPARATOR = "*/"; + + /** + * URL protocol for an entry from a JBoss jar file: "vfszip" + */ + String URL_PROTOCOL_VFSZIP = "vfszip"; + + /** + * Pseudo URL prefix for loading from the class path: "classpath:" + */ + String CLASSPATH_URL_PREFIX = "classpath:"; + + /** + * Pseudo URL prefix for all matching resources from the class path: "classpath*:" + * This differs from AntPathResourcePatternFactory's classpath URL prefix in that it + * retrieves all matching resources for a given name (e.g. "/beans.xml"), + * for example in the root of all deployed JAR files. + */ + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceUtil.java new file mode 100644 index 0000000..d88a519 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/ResourceUtil.java @@ -0,0 +1,61 @@ +package cn.axzo.framework.core.io; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +/** + * @author liyong.tian + * @since 2017/7/31 下午3:51 + */ +public abstract class ResourceUtil { + + /** + * Determine whether the given URL points to a resource in the file system, + * i.e. has protocol "file", "vfsfile" or "vfs". + * + * @param url the URL to check + * @return whether the URL has been identified as a file system URL + */ + public static boolean isFileURL(URL url) { + return ResourceStrings.URL_PROTOCOL_FILE.equals(url.getProtocol()); + } + + /** + * Determine whether the given URL points to a resource in a jar file. + * i.e. has protocol "jar", "war, ""zip", "vfszip" or "wsjar". + * + * @param url the URL to check + * @return whether the URL has been identified as a JAR URL + */ + public static boolean isJarURL(URL url) { + String protocol = url.getProtocol(); + return (ResourceStrings.URL_PROTOCOL_JAR.equals(protocol) || ResourceStrings.URL_PROTOCOL_WAR.equals(protocol) || + ResourceStrings.URL_PROTOCOL_ZIP.equals(protocol) || ResourceStrings.URL_PROTOCOL_VFSZIP.equals(protocol) || + ResourceStrings.URL_PROTOCOL_WSJAR.equals(protocol)); + } + + /** + * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the + * given connection, preferring {@code false} but leaving the + * flag at {@code true} for JNLP based resources. + * + * @param con the URLConnection to set the flag on + */ + public static void useCachesIfNecessary(URLConnection con) { + con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); + } + + /** + * Create a URI instance for the given location String, + * replacing spaces with "%20" URI encoding first. + * + * @param location the location String to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the location wasn't a valid URI + */ + public static URI toURI(String location) throws URISyntaxException { + return new URI(location.replaceAll(" ", "%20")); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/Resources.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/Resources.java new file mode 100644 index 0000000..08a78ea --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/Resources.java @@ -0,0 +1,156 @@ +package cn.axzo.framework.core.io; + +import cn.axzo.framework.core.io.factory.ResourceFactory; +import cn.axzo.framework.core.io.factory.ResourcePatternFactory; +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.io.support.AntPathResourcePatternFactory; +import cn.axzo.framework.core.util.PathUtil; +import cn.axzo.framework.core.io.resource.Resource; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import javax.annotation.Nonnull; +import javax.validation.constraints.NotNull; +import java.net.URL; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static java.lang.String.join; + +/** + * @author liyong.tian + * @since 2017/8/1 下午4:36 + */ +public abstract class Resources implements ResourceStrings { + + private static LoadingCache classLoaderResourceFactoryCache = Caffeine + .newBuilder() + .maximumSize(10_000) + .expireAfterWrite(24, TimeUnit.HOURS) + .build(classLoader -> _getResourcePatternFactory(null, classLoader, false)); + + private static LoadingCache, ResourcePatternFactory> relativeClassResourceFactoryCache = Caffeine + .newBuilder() + .maximumSize(10_000) + .expireAfterWrite(24, TimeUnit.HOURS) + .build(relativeClass -> _getResourcePatternFactory(relativeClass, null, false)); + + public static boolean exists(@NotNull String location) { + try { + return getResource(location).exists(); + } catch (Exception ignored) { + return false; + } + } + + public static Resource getResource(@NotNull String location) { + return _getResourceFactory().getResource(location); + } + + public static Resource getResource(@NotNull String location, boolean isFileSystem) { + return _getResourceFactory(isFileSystem).getResource(location); + } + + public static Resource getResource(@NotNull String location, ClassLoader classLoader) { + return _getResourceFactory(classLoader).getResource(location); + } + + public static Resource getResource(@NotNull String location, Class relativeClass) { + return _getResourceFactory(relativeClass).getResource(location); + } + + public static Resource[] findResources(@NotNull String directory, String... fileExtensions) { + Objects.requireNonNull(directory); + if (!directory.endsWith(PathUtil.FOLDER_SEPARATOR)) { + directory = directory + PathUtil.FOLDER_SEPARATOR; + } + String relativePath = "/**/*.{:" + (fileExtensions.length == 0 ? "*" : join("|", fileExtensions)) + "}"; + String locationPattern = CLASSPATH_ALL_URL_PREFIX + PathUtil.applyRelativePath(directory, relativePath); + return findResources(locationPattern); + } + + public static Resource[] findResources(@NotNull String locationPattern) { + return _getResourcePatternFactory().findResources(locationPattern); + } + + public static Resource[] findResources(@NotNull String locationPattern, ClassLoader classLoader) { + return _getResourcePatternFactory(classLoader).findResources(locationPattern); + } + + public static URL[] findURLs(@NotNull String locationPattern) { + return _getResourcePatternFactory().findURLs(locationPattern); + } + + public static Class[] findClasses(@NotNull String packageName) { + return _getResourcePatternFactory().findClasses(packageName); + } + + public static Class[] findClasses(@NotNull String packageName, ClassLoader classLoader) { + return _getResourcePatternFactory(classLoader).findClasses(packageName); + } + + @SuppressWarnings("unchecked") + public static Class>[] findEnumClasses(@NotNull String packageName) { + return (Class>[]) Arrays.stream(findClasses(packageName)) + .filter(Class::isEnum) + .toArray(Class[]::new); + } + + @SuppressWarnings("unchecked") + public static Class>[] findEnumClasses(@NotNull String packageName, ClassLoader classLoader) { + return (Class>[]) Arrays.stream(findClasses(packageName, classLoader)) + .filter(Class::isEnum) + .toArray(Class[]::new); + } + + /*-------------------------------私有方法-------------------------------*/ + + private static ResourceFactory _getResourceFactory() { + return _getResourcePatternFactory(); + } + + private static ResourceFactory _getResourceFactory(Class relativeClass) { + return _getResourcePatternFactory(relativeClass); + } + + private static ResourceFactory _getResourceFactory(ClassLoader cl) { + return _getResourcePatternFactory(cl); + } + + private static ResourceFactory _getResourceFactory(boolean isFileSystem) { + return _getResourcePatternFactory(isFileSystem); + } + + private static ResourcePatternFactory _getResourcePatternFactory() { + return _getResourcePatternFactory(null, null, false); + } + + private static ResourcePatternFactory _getResourcePatternFactory(Class relativeClass) { + return relativeClassResourceFactoryCache.get(relativeClass); + } + + private static ResourcePatternFactory _getResourcePatternFactory(ClassLoader cl) { + return classLoaderResourceFactoryCache.get(cl); + } + + private static ResourcePatternFactory _getResourcePatternFactory(boolean isFileSystem) { + return _getResourcePatternFactory(null, null, isFileSystem); + } + + @Nonnull + private static ResourcePatternFactory _getResourcePatternFactory(Class relativeClass, ClassLoader cl, + boolean isFileSystem) { + ResourcePatternFactory factory; + if (isFileSystem) { + factory = AntPathResourcePatternFactory.FILESYSTEM; + } else if (relativeClass != null) { + factory = new AntPathResourcePatternFactory(relativeClass); + } else if (cl != null) { + factory = new AntPathResourcePatternFactory(cl); + } else { + factory = AntPathResourcePatternFactory.DEFAULT; + } + return factory; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourceFactory.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourceFactory.java new file mode 100644 index 0000000..4b2a649 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourceFactory.java @@ -0,0 +1,41 @@ +package cn.axzo.framework.core.io.factory; + +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.util.ClassUtil; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +/** + * 资源实例工厂 + * + * @author liyong.tian + * @since 2020/8/28 16:07 + */ +public interface ResourceFactory { + + /** + * 根据指定位置返回一个资源实例 + *

+ *

    + *
  • 必须支持完全标准的URL, 例如 "file:C:/test.dat"。 + *
  • 必须支持虚URL, 例如 "classpath:test.dat"。 + *
  • 可选支持文件相对路径, e.g. "WEB-INF/test.dat"。 + *
+ *

+ * 注:一个资源实例并不意味着资源物理存在,你需要调用{@link Resource#exists}来校验存在性 + * + * @param location 资源位置 + * @return 资源实例 + */ + Resource getResource(@NotNull String location); + + /** + * Expose the ClassLoader used by this ResourceFactory. + * + * @return the ClassLoader (only {@code null} if even the system ClassLoader isn't accessible) + * @see ClassUtil#getDefaultClassLoader() + */ + @Nullable + ClassLoader getClassLoader(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourcePatternFactory.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourcePatternFactory.java new file mode 100644 index 0000000..8baf5ef --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/factory/ResourcePatternFactory.java @@ -0,0 +1,51 @@ +package cn.axzo.framework.core.io.factory; + + +import cn.axzo.framework.core.io.ResourceStrings; +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.core.util.PathUtil; + +import javax.validation.constraints.NotNull; +import java.net.URL; +import java.util.Objects; + +import static java.util.stream.Stream.of; +import static jodd.util.StringPool.DOT; + +/** + * @author liyong.tian + * @since 2020/8/28 16:11 + */ +public interface ResourcePatternFactory extends ResourceFactory, ResourceStrings { + + Resource[] findResources(@NotNull String locationPattern); + + default URL[] findURLs(@NotNull String locationPattern) { + return of(findResources(locationPattern)).filter(Resource::exists).map(Resource::getURL).toArray(URL[]::new); + } + + default Class[] findClasses(@NotNull String packageName) { + String packagePath = packageName.replace(DOT, PathUtil.FOLDER_SEPARATOR); + String locationPattern = PathUtil.applyRelativePath(packagePath + PathUtil.FOLDER_SEPARATOR, "**/*.class"); + Resource[] resources = findResources(CLASSPATH_ALL_URL_PREFIX + locationPattern); + return of(resources) + .filter(Resource::exists) + .map(resource -> resource.getURL().toString()) + .filter(url -> url.contains(packagePath)) + .map(resource -> { + String classPath = resource.substring(resource.indexOf(packagePath), resource.lastIndexOf(DOT)); + String className = classPath.replace(PathUtil.FOLDER_SEPARATOR, DOT); + try { + Class clazz = ClassUtil.load(className, getClassLoader()); + // If constructors contains class which not exists, exception will be thrown. + clazz.getConstructors(); + return clazz; + } catch (ClassNotFoundException | LinkageError ignored) { + return null; + } + }) + .filter(Objects::nonNull) + .toArray(Class[]::new); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/AbstractResource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/AbstractResource.java new file mode 100644 index 0000000..4147e5e --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/AbstractResource.java @@ -0,0 +1,99 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.io.ResourceStrings; +import cn.axzo.framework.core.io.ResourceUtil; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.*; + +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * @author liyong.tian + * @since 2017/7/31 下午2:03 + */ +public abstract class AbstractResource implements Resource { + + @Override + public boolean exists() { + try { + URL url = getURL(); + if (ResourceUtil.isFileURL(url)) { + // Proceed with file system resolution + return getFile().exists(); + } else { + // Try a URL connection content-length header + URLConnection con = url.openConnection(); + ResourceUtil.useCachesIfNecessary(con); + if (con instanceof HttpURLConnection) { + ((HttpURLConnection) con).setRequestMethod("HEAD"); + int code = ((HttpURLConnection) con).getResponseCode(); + if (code == HTTP_OK) { + return true; + } else if (code == HTTP_NOT_FOUND) { + return false; + } + if (con.getContentLength() >= 0) { + return true; + } + // no HTTP OK status, and no content-length header: give up + ((HttpURLConnection) con).disconnect(); + return false; + } + if (con.getContentLength() >= 0) { + return true; + } + // Fall back to stream existence: can we open the stream? + InputStream is = getInputStream(); + is.close(); + return true; + } + } catch (IOException ex) { + return false; + } + } + + /** + * This implementation builds a URI based on the URL returned by {@link #getURL()}. + */ + @Nonnull + @Override + public URI getURI() { + URL url = getURL(); + try { + return url.toURI(); + } catch (URISyntaxException ex) { + throw new ResourceException("Invalid URI [" + url + "]", ex); + } + } + + /** + * This implementation returns a File reference for the underlying class path + * resource, provided that it refers to a file in the file system. + */ + @Nonnull + @Override + public File getFile() { + URI uri = getURI(); + if (!ResourceStrings.URL_PROTOCOL_FILE.equals(uri.getScheme())) { + String msg = uri + " cannot be resolved to absolute file path because it does not reside in the file system"; + throw new ResourceException(new FileNotFoundException(msg)); + } + return new File(uri.getSchemeSpecificPart()); + } + + @Nonnull + @Override + public String getDescription() { + return toString(); + } + + @Override + public abstract String toString(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassPathResource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassPathResource.java new file mode 100644 index 0000000..b108cf0 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassPathResource.java @@ -0,0 +1,98 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.core.util.PathUtil; + +import javax.annotation.Nonnull; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URL; +import java.util.Objects; + +/** + * Uses either a given {@link ClassLoader} or a given {@link Class} for loading resources. + * + * @author liyong.tian + * @see ClassLoader#getResource(String) + * @since 2017/7/28 下午4:32 + */ +public class ClassPathResource extends AbstractResource { + + private final String path; + + private final ClassLoader classLoader; + + public ClassPathResource(String path) { + this(path, null); + } + + /** + * ClassLoader始终以绝对路径加载资源,路径不能以"/"开头 + * + * @param path 在classpath下的绝对路径 + * @param classLoader the classLoader to load classpath resource with + */ + public ClassPathResource(String path, ClassLoader classLoader) { + Objects.requireNonNull(path, "path must not be null"); + this.classLoader = classLoader != null ? classLoader : ClassUtil.getDefaultClassLoader(); + this.path = path.startsWith("/") ? path.substring(1) : path; + } + + @Override + public boolean exists() { + return resolveURL() != null; + } + + @Nonnull + @Override + public InputStream getInputStream() { + InputStream is; + if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); + } else { + is = ClassLoader.getSystemResourceAsStream(this.path); + } + if (is == null) { + String msg = getDescription() + " cannot be opened because it does not exist"; + throw new ResourceException(new FileNotFoundException(msg)); + } + return is; + } + + @Nonnull + @Override + public URL getURL() { + URL url = resolveURL(); + if (url == null) { + String msg = getDescription() + " cannot be resolved to URL because it does not exist"; + throw new ResourceException(new FileNotFoundException(msg)); + } + return url; + } + + /** + * Resolves a URL for the underlying class path resource. + * + * @return the resolved URL, or {@code null} if not resolvable + */ + protected URL resolveURL() { + if (this.classLoader != null) { + return this.classLoader.getResource(this.path); + } else { + return ClassLoader.getSystemResource(this.path); + } + } + + @Nonnull + @Override + public Resource createRelative(String relativePath) { + String pathToUse = PathUtil.applyRelativePath(this.path, relativePath); + return new ClassPathResource(pathToUse, this.classLoader); + } + + @Override + public String toString() { + return "classpath resource [" + this.path + "]"; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassRelativeResource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassRelativeResource.java new file mode 100644 index 0000000..bc7a582 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/ClassRelativeResource.java @@ -0,0 +1,65 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.util.PathUtil; + +import javax.annotation.Nonnull; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URL; + +/** + * @author liyong.tian + * @see Class#getResource(String) + * @since 2017/7/31 下午1:47 + */ +public class ClassRelativeResource extends ClassPathResource { + + private final String path; + + private final Class clazz; + + /** + * @param path 若路径以"/"开头,则为绝对路径,反之为clazz包下的相对路径 + * @param clazz 在相对路径的情况下,该类所在的包将作为父目录 + */ + public ClassRelativeResource(String path, Class clazz) { + super(path); + this.path = path; + this.clazz = clazz == null ? Resource.class : clazz; + } + + @Nonnull + @Override + public InputStream getInputStream() { + InputStream is = this.clazz.getResourceAsStream(this.path); + if (is == null) { + String msg = getDescription() + " cannot be opened because it does not exist"; + throw new ResourceException(new FileNotFoundException(msg)); + } + return is; + } + + @Override + protected URL resolveURL() { + return this.clazz.getResource(this.path); + } + + @Nonnull + @Override + public Resource createRelative(String relativePath) { + String pathToUse = PathUtil.applyRelativePath(this.path, relativePath); + return new ClassRelativeResource(pathToUse, this.clazz); + } + + @Override + public String toString() { + String pathToUse; + if (this.path.startsWith("/")) { + pathToUse = this.path; + } else { + pathToUse = PathUtil.applyRelativePath(PathUtil.classPackageAsDirectory(this.clazz), this.path); + } + return "classpath resource [" + pathToUse + "]"; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/FileSystemResource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/FileSystemResource.java new file mode 100644 index 0000000..3e2dc2b --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/FileSystemResource.java @@ -0,0 +1,78 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.util.PathUtil; +import lombok.EqualsAndHashCode; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +/** + * @author liyong.tian + * @since 2017/7/31 下午4:53 + */ +@EqualsAndHashCode(of = "path", callSuper = false) +public class FileSystemResource extends AbstractResource { + + private final File file; + + private final String path; + + public FileSystemResource(String path) { + Objects.requireNonNull(path, "path must not be null"); + this.file = new File(path); + this.path = PathUtil.cleanPath(file.getPath()); + } + + public FileSystemResource(File file) { + Objects.requireNonNull(file, "file must not be null"); + this.file = file; + this.path = PathUtil.cleanPath(file.getPath()); + } + + @Override + public boolean exists() { + return this.file.exists(); + } + + @Override + public String toString() { + return "file [" + this.file.getAbsolutePath() + "]"; + } + + @Nonnull + @Override + public InputStream getInputStream() { + try { + return new FileInputStream(this.file); + } catch (FileNotFoundException e) { + throw new ResourceException(e); + } + } + + @Nonnull + @Override + public URL getURL() { + if (!exists()) { + String msg = getDescription() + " cannot be resolved to URL because it does not exist"; + throw new ResourceException(new FileNotFoundException(msg)); + } + try { + return this.file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new ResourceException(e); + } + } + + @Nonnull + @Override + public Resource createRelative(String relativePath) { + return new FileSystemResource(PathUtil.applyRelativePath(this.path, relativePath)); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/Resource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/Resource.java new file mode 100644 index 0000000..54f0b7d --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/Resource.java @@ -0,0 +1,105 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.function.Consumer; + +/** + * Interface for a resource descriptor that abstracts from the actual + * type of underlying resource, such as a file or class path resource. + *

+ * Note: every Resource must exists and an InputStream can be opened. + * + * @author liyong.tian + * @since 2017/7/28 下午2:41 + */ +public interface Resource { + + /** + * Determine whether this resource actually exists in physical form. + *

+ * This method performs a definitive existence check, whereas the + * existence of a {@code Resource} only guarantees a valid descriptor. + */ + boolean exists(); + + /** + * Consume an {@link InputStream} for the content of an underlying resource. + * + * @throws ResourceException if the content stream could not be opened + */ + default void withInputStream(Consumer consumer) { + try (InputStream inputStream = getInputStream()) { + consumer.accept(inputStream); + } catch (IOException e) { + throw new ResourceException(e); + } + } + + /** + * Return an {@link InputStream} for the content of an underlying resource. + *

+ * It is expected that each call creates a fresh stream. + * This requirement is particularly important when you consider an API such + * as JavaMail, which needs to be able to read the stream multiple times when + * creating mail attachments. For such a use case, it is required + * that each {@code getInputStream()} call returns a fresh stream. + * + * @throws ResourceException if the content stream could not be opened + */ + @Nonnull + InputStream getInputStream(); + + /** + * Return a URL for this resource (must not be {@code null}). + * + * @throws ResourceException if the resource cannot be resolved as URL, + * i.e. if the resource is not available as descriptor + */ + @Nonnull + URL getURL(); + + /** + * Return a URI for this resource. + * + * @throws ResourceException if the resource cannot be resolved as URI, + * i.e. if the resource is not available as descriptor + */ + @Nonnull + URI getURI(); + + /** + * Return a File for this resource. + * + * @throws ResourceException in case of general resolution/reading failures + * @see #getInputStream() + */ + @Nonnull + File getFile(); + + /** + * Create a resource relative to this resource. + * + * @param relativePath the relative path (relative to this resource) + * @return the resource for the relative resource + * @throws ResourceException if the relative resource cannot be determined + */ + @Nonnull + Resource createRelative(String relativePath); + + /** + * Return a description for this resource, to be used for error output when working with the resource. + *

+ * Implementations are also encouraged to return this value from their {@code toString} method. + * + * @see Object#toString() + */ + @Nonnull + String getDescription(); +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/UrlResource.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/UrlResource.java new file mode 100644 index 0000000..16fb01a --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/resource/UrlResource.java @@ -0,0 +1,114 @@ +package cn.axzo.framework.core.io.resource; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.io.ResourceUtil; +import cn.axzo.framework.core.util.PathUtil; +import lombok.EqualsAndHashCode; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Objects; + +/** + * @author liyong.tian + * @since 2017/7/31 下午3:44 + */ +@EqualsAndHashCode(of = "cleanedUrl", callSuper = false) +public class UrlResource extends AbstractResource { + + /** + * Original URL, used for actual access. + */ + private final URL url; + + /** + * Cleaned URL (with normalized path), used for comparisons. + */ + private final URL cleanedUrl; + + /** + * Create a new {@code UrlResource} based on the given URL object. + * + * @param url a URL + */ + public UrlResource(URL url) { + Objects.requireNonNull(url, "URL must not be null"); + this.url = url; + this.cleanedUrl = getCleanedUrl(this.url, url.toString()); + } + + /** + * Create a new {@code UrlResource} based on a URL path. + *

Note: The given path needs to be pre-encoded if necessary. + * + * @param path a URL path + * @throws MalformedURLException if the given URL path is not valid + * @see URL#URL(String) + */ + public UrlResource(String path) throws MalformedURLException { + Objects.requireNonNull(path, "path must not be null"); + this.url = new URL(path); + this.cleanedUrl = getCleanedUrl(this.url, path); + } + + @Override + public String toString() { + return "URL [" + this.url + "]"; + } + + @Nonnull + @Override + public InputStream getInputStream() { + URLConnection con = null; + try { + con = this.url.openConnection(); + ResourceUtil.useCachesIfNecessary(con); + return con.getInputStream(); + } catch (IOException e) { + throw new ResourceException(e); + } finally { + // Close the HTTP connection (if applicable). + if (con != null && con instanceof HttpURLConnection) { + ((HttpURLConnection) con).disconnect(); + } + } + } + + @Nonnull + @Override + public URL getURL() { + return this.url; + } + + @Nonnull + @Override + public Resource createRelative(String relativePath) { + try { + URL url = new URL(this.url, relativePath.startsWith("/") ? relativePath.substring(1) : relativePath); + return new UrlResource(url); + } catch (MalformedURLException e) { + throw new ResourceException(e); + } + } + + /** + * Determine a cleaned URL for the given original URL. + * + * @param originalUrl the original URL + * @param originalPath the original URL path + * @return the cleaned URL + */ + private URL getCleanedUrl(URL originalUrl, String originalPath) { + try { + return new URL(PathUtil.cleanPath(originalPath)); + } catch (MalformedURLException ex) { + // Cleaned URL path cannot be converted to URL -> take original URL. + return originalUrl; + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/AntPathResourcePatternFactory.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/AntPathResourcePatternFactory.java new file mode 100644 index 0000000..51851b8 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/AntPathResourcePatternFactory.java @@ -0,0 +1,420 @@ +package cn.axzo.framework.core.io.support; + +import cn.axzo.framework.core.io.ResourceException; +import cn.axzo.framework.core.io.ResourceUtil; +import cn.axzo.framework.core.io.factory.ResourceFactory; +import cn.axzo.framework.core.io.factory.ResourcePatternFactory; +import cn.axzo.framework.core.io.resource.FileSystemResource; +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.io.resource.UrlResource; +import cn.axzo.framework.core.util.AntPathUtil; +import cn.axzo.framework.core.util.PathUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.net.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipException; + +/** + * @author liyong.tian + * @since 2017/7/28 下午4:30 + */ +@Slf4j +public class AntPathResourcePatternFactory implements ResourcePatternFactory { + + public final static ResourcePatternFactory DEFAULT = + new AntPathResourcePatternFactory(null, null, false); + + public final static ResourcePatternFactory FILESYSTEM = + new AntPathResourcePatternFactory(null, null, true); + + private final ResourceFactory resourceFactory; + + public AntPathResourcePatternFactory(ClassLoader classLoader) { + this(null, classLoader, false); + } + + public AntPathResourcePatternFactory(Class relativeClass) { + this(relativeClass, null, false); + } + + public AntPathResourcePatternFactory(ResourceFactory resourceFactory) { + this.resourceFactory = resourceFactory; + } + + private AntPathResourcePatternFactory(Class relativeClass, ClassLoader classLoader, boolean isFileSystem) { + if (isFileSystem) { + this.resourceFactory = new FileSystemResourceFactory(); + } else { + this.resourceFactory = new DefaultResourceFactory(relativeClass, classLoader); + } + } + + @Nullable + @Override + public ClassLoader getClassLoader() { + return this.resourceFactory.getClassLoader(); + } + + @Override + public Resource getResource(String location) { + return this.resourceFactory.getResource(location); + } + + @Override + public Resource[] findResources(String locationPattern) { + Objects.requireNonNull(locationPattern, "Location pattern must not be null"); + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + // a class path resource (multiple resources for same name possible) + if (AntPathUtil.isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + // a class path resource pattern + return findPathMatchingResources(locationPattern); + } else { + // all class path resources with the given name + String location = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()); + return findAllClassPathResources(location); + } + } else { + // Generally only look for a pattern after a prefix here, + // and on Tomcat only after the "*/" separator for its "war:" protocol. + int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : + locationPattern.indexOf(":") + 1); + if (AntPathUtil.isPattern(locationPattern.substring(prefixEnd))) { + // a file pattern + return findPathMatchingResources(locationPattern); + } else { + // a single resource with the given name + return new Resource[]{getResource(locationPattern)}; + } + } + } + + /*-------------------------------私有方法-------------------------------*/ + + /** + * Find all resources that match the given location pattern via the + * Ant-style PathMatcher. Supports resources in jar files and zip files + * and in the file system. + * + * @param locationPattern the location pattern to match + * @return the result as Resource array + * @see #doFindPathMatchingJarResources + * @see #doFindPathMatchingFileResources + */ + private Resource[] findPathMatchingResources(String locationPattern) { + String rootDirPath = PathUtil.extractRootDir(locationPattern); + String subPattern = locationPattern.substring(rootDirPath.length()); + Resource[] rootDirResources = findResources(rootDirPath); + Set resources = new LinkedHashSet<>(16); + for (Resource rootDirResource : rootDirResources) { + URL rootDirURL = rootDirResource.getURL(); + Set matchingResources = new LinkedHashSet<>(); + try { + if (ResourceUtil.isJarURL(rootDirURL)) { + matchingResources = doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern); + } else { + resources.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); + } + } catch (IOException ignored) { + continue; + } + resources.addAll(matchingResources); + } + return resources.toArray(new Resource[resources.size()]); + } + + /** + * Find all class location resources with the given location via the ClassLoader. + * Delegates to {@link #doFindAllClassPathResources(String)}. + * + * @param location the absolute path within the classpath + * @return the result as Resource array + * @see ClassLoader#getResources + */ + private Resource[] findAllClassPathResources(String location) { + String path = location; + if (path.startsWith("/")) { + path = path.substring(1); + } + Set result = doFindAllClassPathResources(path); + return result.toArray(new Resource[result.size()]); + } + + /** + * Find all resources in jar files that match the given location pattern + * via the Ant-style PathMatcher. + * + * @param rootDirResource the root directory as Resource + * @param rootDirURL the pre-resolved root directory URL + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see JarURLConnection + */ + private Set doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern) + throws IOException { + URLConnection con = rootDirURL.openConnection(); + JarFile jarFile; + String jarFileUrl; + String rootEntryPath; + boolean closeJarFile; + + if (con instanceof JarURLConnection) { + // Should usually be the case for traditional JAR files. + JarURLConnection jarCon = (JarURLConnection) con; + ResourceUtil.useCachesIfNecessary(jarCon); + jarFile = jarCon.getJarFile(); + jarFileUrl = jarCon.getJarFileURL().toExternalForm(); + JarEntry jarEntry = jarCon.getJarEntry(); + rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); + closeJarFile = !jarCon.getUseCaches(); + } else { + // No JarURLConnection -> need to resort to URL file parsing. + // We'll assume URLs of the format "jar:path!/entry", with the protocol + // being arbitrary as long as following the entry format. + // We'll also handle paths with and without leading "file:" prefix. + String urlFile = rootDirURL.getFile(); + try { + int separatorIndex = urlFile.indexOf(WAR_URL_SEPARATOR); + if (separatorIndex == -1) { + separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); + } + if (separatorIndex != -1) { + jarFileUrl = urlFile.substring(0, separatorIndex); + rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars + jarFile = getJarFile(jarFileUrl); + } else { + jarFile = new JarFile(urlFile); + jarFileUrl = urlFile; + rootEntryPath = ""; + } + closeJarFile = true; + } catch (ZipException ex) { + log.debug("Skipping invalid jar classpath entry [" + urlFile + "]"); + return Collections.emptySet(); + } + } + + try { + log.debug("Looking for matching resources in jar file [" + jarFileUrl + "]"); + if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) { + // Root entry path must end with slash to allow for proper matching. + // The Sun JRE does not return a slash here, but BEA JRockit does. + rootEntryPath = rootEntryPath + "/"; + } + Set result = new LinkedHashSet<>(8); + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { + JarEntry entry = entries.nextElement(); + String entryPath = entry.getName(); + if (entryPath.startsWith(rootEntryPath)) { + String relativePath = entryPath.substring(rootEntryPath.length()); + if (AntPathUtil.match(subPattern, relativePath)) { + result.add(rootDirResource.createRelative(relativePath)); + } + } + } + return result; + } finally { + if (closeJarFile) { + jarFile.close(); + } + } + } + + /** + * Find all resources in the file system that match the given location pattern + * via the Ant-style PathMatcher. + * + * @param rootDirResource the root directory as Resource + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + */ + private Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) { + try { + File rootDir = rootDirResource.getFile().getAbsoluteFile(); + Set matchingFiles = retrieveMatchingFiles(rootDir, subPattern); + Set result = new LinkedHashSet<>(matchingFiles.size()); + for (File file : matchingFiles) { + result.add(new FileSystemResource(file)); + } + return result; + } catch (ResourceException ex) { + return Collections.emptySet(); + } + } + + /** + * Find all class location resources with the given path via the ClassLoader. + * Called by {@link #findAllClassPathResources(String)}. + * + * @param path the absolute path within the classpath (never a leading slash) + * @return a mutable Set of matching Resource instances + */ + private Set doFindAllClassPathResources(String path) { + Set result = new LinkedHashSet<>(16); + ClassLoader cl = getClassLoader(); + try { + Enumeration resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); + while (resourceUrls.hasMoreElements()) { + result.add(new UrlResource(resourceUrls.nextElement())); + } + if ("".equals(path)) { + // The above result is likely to be incomplete, i.e. only containing file system references. + // We need to have pointers to each of the jar files on the classpath as well... + addAllClassLoaderJarRoots(cl, result); + } + return result; + } catch (IOException ignored) { + return result; + } + } + + /** + * Search all {@link URLClassLoader} URLs for jar file references and add them to the + * given set of resources in the form of pointers to the root of the jar file content. + * + * @param classLoader the ClassLoader to search (including its ancestors) + * @param result the set of resources to add jar roots to + */ + private void addAllClassLoaderJarRoots(ClassLoader classLoader, Set result) { + if (classLoader instanceof URLClassLoader) { + try { + for (URL url : ((URLClassLoader) classLoader).getURLs()) { + try { + UrlResource jarResource = new UrlResource(JAR_URL_PREFIX + url.toString() + JAR_URL_SEPARATOR); + if (jarResource.exists()) { + result.add(jarResource); + } + } catch (MalformedURLException ex) { + log.debug("Cannot search for matching files underneath [" + url + + "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage()); + } + } + } catch (Exception ex) { + log.debug("Cannot introspect jar files since ClassLoader [" + classLoader + + "] does not support 'getURLs()': " + ex); + } + } + + if (classLoader == ClassLoader.getSystemClassLoader()) { + // "java.class.path" manifest evaluation... + addClassPathManifestEntries(result); + } + + if (classLoader != null) { + try { + // Hierarchy traversal... + addAllClassLoaderJarRoots(classLoader.getParent(), result); + } catch (Exception ex) { + log.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader + + "] does not support 'getParent()': " + ex); + } + } + } + + + /** + * Determine jar file references from the "java.class.path." manifest property and add them + * to the given set of resources in the form of pointers to the root of the jar file content. + * + * @param result the set of resources to add jar roots to + */ + private void addClassPathManifestEntries(Set result) { + try { + String javaClassPathProperty = System.getProperty("java.class.path"); + + for (String path : javaClassPathProperty.split(System.getProperty("path.separator"))) { + try { + String url = JAR_URL_PREFIX + FILE_URL_PREFIX + new File(path).getAbsolutePath() + JAR_URL_SEPARATOR; + UrlResource jarResource = new UrlResource(url); + if (jarResource.exists()) { + result.add(jarResource); + } + } catch (MalformedURLException ex) { + log.debug("Cannot search for matching files underneath [" + path + + "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage()); + } + } + } catch (Exception ex) { + log.debug("Failed to evaluate 'java.class.path' manifest entries: " + ex.getMessage()); + } + } + + /** + * Retrieve files that match the given path pattern, + * checking the given directory and its subdirectories. + * + * @param rootDir the directory to start from + * @param pattern the pattern to match against, + * relative to the root directory + * @return a mutable Set of matching Resource instances + */ + private Set retrieveMatchingFiles(File rootDir, String pattern) { + if (!rootDir.exists()) { + return Collections.emptySet(); + } + if (!rootDir.isDirectory()) { + return Collections.emptySet(); + } + if (!rootDir.canRead()) { + return Collections.emptySet(); + } + String fullPattern = rootDir.getAbsolutePath().replace(File.separator, "/"); + if (!pattern.startsWith("/")) { + fullPattern += "/"; + } + fullPattern = fullPattern + pattern.replace(File.separator, "/"); + Set result = new LinkedHashSet<>(8); + doRetrieveMatchingFiles(fullPattern, rootDir, result); + return result; + } + + /** + * Recursively retrieve files that match the given pattern, + * adding them to the given result list. + * + * @param fullPattern the pattern to match against, + * with prepended root directory path + * @param dir the current directory + * @param result the Set of matching File instances to add to + */ + private void doRetrieveMatchingFiles(String fullPattern, File dir, Set result) { + File[] dirContents = dir.listFiles(); + if (dirContents == null) { + return; + } + Arrays.sort(dirContents); + for (File content : dirContents) { + String currPath = content.getAbsolutePath().replace(File.separator, "/"); + if (content.isDirectory() && AntPathUtil.matchStart(fullPattern, currPath + "/")) { + if (content.canRead()) { + doRetrieveMatchingFiles(fullPattern, content, result); + } + } + if (AntPathUtil.match(fullPattern, currPath)) { + result.add(content); + } + } + } + + /** + * Resolve the given jar file URL into a JarFile object. + */ + private JarFile getJarFile(String jarFileUrl) throws IOException { + if (jarFileUrl.startsWith(FILE_URL_PREFIX)) { + try { + return new JarFile(ResourceUtil.toURI(jarFileUrl).getSchemeSpecificPart()); + } catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new JarFile(jarFileUrl.substring(FILE_URL_PREFIX.length())); + } + } else { + return new JarFile(jarFileUrl); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/DefaultResourceFactory.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/DefaultResourceFactory.java new file mode 100644 index 0000000..c7f9560 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/DefaultResourceFactory.java @@ -0,0 +1,84 @@ +package cn.axzo.framework.core.io.support; + +import cn.axzo.framework.core.io.ResourceStrings; +import cn.axzo.framework.core.io.factory.ResourceFactory; +import cn.axzo.framework.core.io.resource.ClassPathResource; +import cn.axzo.framework.core.io.resource.ClassRelativeResource; +import cn.axzo.framework.core.io.resource.Resource; +import cn.axzo.framework.core.io.resource.UrlResource; +import cn.axzo.framework.core.util.ClassUtil; + +import javax.annotation.Nullable; +import java.net.MalformedURLException; +import java.util.Objects; + +/** + * @author liyong.tian + * @since 2017/8/1 下午1:42 + */ +public class DefaultResourceFactory implements ResourceFactory { + + @Nullable + private final ClassLoader classLoader; + + @Nullable + private final Class relativeClass; + + DefaultResourceFactory() { + this(null, null); + } + + public DefaultResourceFactory(@Nullable Class relativeClass, ClassLoader classLoader) { + this.relativeClass = relativeClass; + this.classLoader = classLoader == null ? ClassUtil.getDefaultClassLoader() : classLoader; + } + + @Override + public Resource getResource(String location) { + Objects.requireNonNull(location, "Location must not be null"); + + if (location.startsWith("/")) { + return getResourceByPath(location); + } + + if (location.startsWith(ResourceStrings.CLASSPATH_URL_PREFIX)) { + return new ClassPathResource(location.substring(ResourceStrings.CLASSPATH_URL_PREFIX.length()), classLoader); + } + + if (location.contains(":")) { + try { + // Try to parse the location as a URL... + return new UrlResource(location); + } catch (MalformedURLException ex) { + // No URL -> resolve as resource path. + return getResourceByPath(location); + } + } else { + // No URL protocol + return getResourceByPath(location); + } + } + + @Nullable + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + + /** + * Return a Resource handle for the resource at the given path. + *

The default implementation supports class path locations. This should + * be appropriate for standalone implementations but can be overridden, + * e.g. for implementations targeted at a Servlet container. + * + * @param path the path to the resource + * @return the corresponding Resource handle + */ + protected Resource getResourceByPath(String path) { + if (relativeClass == null) { + return new ClassPathResource(path, classLoader); + } else { + return new ClassRelativeResource(path, relativeClass); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/FileSystemResourceFactory.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/FileSystemResourceFactory.java new file mode 100644 index 0000000..b510769 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/io/support/FileSystemResourceFactory.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.core.io.support; + +import cn.axzo.framework.core.io.resource.FileSystemResource; +import cn.axzo.framework.core.io.resource.Resource; + +/** + * @author liyong.tian + * @since 2017/8/1 下午1:44 + */ +public class FileSystemResourceFactory extends DefaultResourceFactory { + + @Override + protected Resource getResourceByPath(String path) { + return new FileSystemResource(path); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/math/Interval.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/math/Interval.java new file mode 100644 index 0000000..d26da17 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/math/Interval.java @@ -0,0 +1,22 @@ +package cn.axzo.framework.core.math; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 区间(数学) + * + * @author liyong.tian + * @since 2017/7/19 下午12:58 + */ +@AllArgsConstructor +@Getter +public enum Interval { + + LO_RO("左开右开"), + LO_RC("左开右闭"), + LC_RO("左闭右开"), + LC_RC("左闭右闭"); + + private String name; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/net/FilterUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/FilterUtil.java new file mode 100644 index 0000000..5d71b9c --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/FilterUtil.java @@ -0,0 +1,211 @@ +package cn.axzo.framework.core.net; + +import lombok.experimental.UtilityClass; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static jodd.util.StringPool.ASTERISK; + +/** + * @author liyong.tian + * @since 2017/6/27 + */ +@UtilityClass +public class FilterUtil { + + /** + * Return true if the context-relative request path + * matches the requirements of the specified filter mapping; + * otherwise, return false. + * + * @param urlPatterns Filter mapping being checked + * @param requestPath Context-relative request path of this request + */ + public boolean matchFiltersURL(String requestPath, String... urlPatterns) { + // The flag that indicates this mapping will match all url-patterns + boolean matchAllUrlPatterns = false; + + // Match on context relative request path + String[] testPaths = {}; + + for (String urlPattern : urlPatterns) { + urlPattern = URLDecode(urlPattern, UTF_8); + if (ASTERISK.equals(urlPattern)) { + matchAllUrlPatterns = true; + testPaths = new String[]{}; + break; + } else { + String[] results = new String[testPaths.length + 1]; + System.arraycopy(testPaths, 0, results, 0, testPaths.length); + results[testPaths.length] = URLDecode(urlPattern); + testPaths = results; + } + } + + // Check the specific "*" special URL pattern, which also matches named dispatches + if (matchAllUrlPatterns) return true; + + if (requestPath == null) return false; + + for (String testPath : testPaths) { + if (matchFiltersURL(testPath, requestPath)) { + return true; + } + } + + // No match + return false; + } + + /** + * Return true if the context-relative request path + * matches the requirements of the specified filter mapping; + * otherwise, return false. + * + * @param testPath URL mapping being checked + * @param requestPath Context-relative request path of this request + */ + private boolean matchFiltersURL(String testPath, String requestPath) { + if (testPath == null) return false; + + // Case 1 - Exact Match + if (testPath.equals(requestPath)) return true; + + // Case 2 - Path Match ("/.../*") + if (testPath.equals("/*")) return true; + boolean endWithAny = testPath.endsWith("/*"); + if (endWithAny) { + int testPathLength = testPath.length(); + if (testPath.regionMatches(0, requestPath, 0, testPathLength - 2)) { + if (requestPath.length() == (testPathLength - 2)) { + return true; + } else if ('/' == requestPath.charAt(testPathLength - 2)) { + return true; + } + } + if (testPath.indexOf("/*") == testPathLength - 2) { + return false; + } + } + + // Case 3 - Extension Match + if (testPath.startsWith("*.")) { + int slash = requestPath.lastIndexOf('/'); + int period = requestPath.lastIndexOf('.'); + if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) + && ((requestPath.length() - period) == (testPath.length() - 1))) { + return (testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2)); + } + } + + // Case 4 - Path Match ("/.../*/...") + String[] pathSegments = testPath.split("/\\*"); + if (pathSegments.length >= 2) { + int fromIndex = 0; + for (int i = 0; i < pathSegments.length; i++) { + final String pathSegment = pathSegments[i]; + final int pathSegIndex; + // first + if (i == 0) { + if (requestPath.startsWith(pathSegment)) { + pathSegIndex = 0; + } else { + pathSegIndex = -1; + } + } + // last + else if (i == pathSegments.length - 1) { + if (requestPath.endsWith(pathSegment)) { + pathSegIndex = requestPath.length() - pathSegment.length(); + } else { + pathSegIndex = endWithAny ? requestPath.indexOf(pathSegment, fromIndex) : -1; + } + } + // middle + else { + pathSegIndex = requestPath.indexOf(pathSegment, fromIndex); + } + + // no match + if (pathSegIndex < 0) { + return false; + } + fromIndex = pathSegIndex + pathSegment.length(); + } + return endWithAny || fromIndex == requestPath.length(); + } + + // Case 5 - "Default" Match + return false; // NOTE - Not relevant for selecting filters + } + + /** + * Decode and return the specified URL-encoded String. + * When the byte array is converted to a string, ISO-885901 is used. This + * may be different than some other servers. It is assumed the string is not + * a query string. + * + * @param str The url-encoded string + * @return the decoded string + * @throws IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + private static String URLDecode(String str) { + return URLDecode(str, StandardCharsets.ISO_8859_1); + } + + /** + * Decode and return the specified URL-encoded String. It is assumed the + * string is not a query string. + * + * @param str The url-encoded string + * @param charset The character encoding to use; if null, ISO-8859-1 is + * used. + * @return the decoded string + * @throws IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + private static String URLDecode(String str, Charset charset) { + if (str == null) { + return null; + } + return URLDecode(str.getBytes(StandardCharsets.US_ASCII), charset); + } + + private static String URLDecode(byte[] bytes, Charset charset) { + + if (bytes == null) { + return null; + } + + if (charset == null) { + charset = StandardCharsets.ISO_8859_1; + } + + int len = bytes.length; + int ix = 0; + int ox = 0; + while (ix < len) { + byte b = bytes[ix++]; // Get byte to test + if (b == '%') { + if (ix + 2 > len) { + throw new IllegalArgumentException("The % character must be followed by two hexademical digits"); + } + b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + + convertHexDigit(bytes[ix++])); + } + bytes[ox++] = b; + } + + return new String(bytes, 0, ox, charset); + } + + private static byte convertHexDigit(byte b) { + if ((b >= '0') && (b <= '9')) return (byte) (b - '0'); + if ((b >= 'a') && (b <= 'f')) return (byte) (b - 'a' + 10); + if ((b >= 'A') && (b <= 'F')) return (byte) (b - 'A' + 10); + throw new IllegalArgumentException((char) b + " is not a hexadecimal digit"); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Inets.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Inets.java new file mode 100644 index 0000000..18b42ca --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Inets.java @@ -0,0 +1,177 @@ +package cn.axzo.framework.core.net; + +import cn.axzo.framework.core.FetchException; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author liyong.tian + * @since 2017/2/7 + */ +public abstract class Inets implements Closeable { + + public final static String IP_SYSTEM_KEY = "HOST_IP"; + + private final static String FALLBACK_IP = "0.0.0.0"; + + private final static Inet4Address FALLBACK_ADDRESS = _getInet4AddressByName(FALLBACK_IP, FALLBACK_IP); + + private volatile static Inet4Address localInet4Address; + + private volatile static String localHostAddress; + + private volatile static String localHostName; + + private static AtomicBoolean initialized = new AtomicBoolean(false); + + private static ExecutorService executor = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("axzo-framework-inets"); + thread.setDaemon(true); + return thread; + }); + + @Override + public void close() { + executor.shutdown(); + } + + public static int fetchLocalIpAsInt(int timeoutSeconds) throws FetchException { + Future result = executor.submit((Callable) Inets::fetchLocalIpAsInt); + try { + return result.get(timeoutSeconds, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new FetchException(e); + } + } + + public static String fetchLocalIp(int timeoutSeconds) throws FetchException { + Future result = executor.submit((Callable) Inets::fetchLocalIp); + try { + return result.get(timeoutSeconds, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new FetchException(e); + } + } + + /** + * @return 将ip地址以整数的形式返回 + */ + public static int fetchLocalIpAsInt() { + initialize(); + return ByteBuffer.wrap(localInet4Address.getAddress()).getInt(); + } + + public static String fetchLocalIp() { + initialize(); + return localHostAddress; + } + + public static String fetchLocalHostName() { + initialize(); + return localHostName; + } + + private static void initialize() { + if (initialized.compareAndSet(false, true)) { + localInet4Address = _getPriorLocalInet4Address(); + localHostAddress = localInet4Address.getHostAddress(); + localHostName = localInet4Address.getHostName(); + System.setProperty(IP_SYSTEM_KEY, localHostAddress); + } + } + + /** + * @return 优先获取权重高的IPV4地址 + */ + private static Inet4Address _getPriorLocalInet4Address() { + String ip = System.getProperty(IP_SYSTEM_KEY); // JVM argument: -DHOST_IP=1.2.3.4 + if (ip == null) { + ip = System.getenv(IP_SYSTEM_KEY); // Environment variable: HOST_IP=1.2.3.4 + } + if (ip != null) { + try { + return (Inet4Address) InetAddress.getByName(ip); + } catch (Exception ignored) { + } + } + + List inet4Addresses = _getLocalInet4AddressList(); + Inet4Address local = FALLBACK_ADDRESS; + int maxWeight = -1; + for (Inet4Address inet4Address : inet4Addresses) { + int weight = 0; + if (inet4Address.isSiteLocalAddress()) { + weight += 8; + } + try { + if (inet4Address.isReachable(500)) { + weight += 6; + } + } catch (IOException ignored) { + } + if (inet4Address.isLinkLocalAddress()) { + weight += 4; + } + if (inet4Address.isLoopbackAddress()) { + weight += 2; + } + if (!inet4Address.getHostAddress().equals(inet4Address.getHostName())) { // 有独立的HostName + weight += 1; + } + if (weight > maxWeight) { + maxWeight = weight; + local = inet4Address; + } + } + return local; + } + + + /** + * @return 遍历服务器所有IPV4地址 + */ + private static List _getLocalInet4AddressList() { + List ipList = new ArrayList<>(); + Enumeration allNetInterfaces; + try { + allNetInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException ignored) { + return ipList; + } + while (allNetInterfaces.hasMoreElements()) { + NetworkInterface netInterface = allNetInterfaces.nextElement(); + Enumeration addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress ip = addresses.nextElement(); + if (ip != null && ip instanceof Inet4Address) { + ipList.add((Inet4Address) ip); + } + } + } + return ipList; + } + + private static Inet4Address _getInet4AddressByName(String ip, String fallbackIp) { + try { + return (Inet4Address) InetAddress.getByName(ip); + } catch (Exception e) { + try { + return (Inet4Address) InetAddress.getByName(fallbackIp); + } catch (Exception e1) { + throw new IllegalArgumentException("Cannot find fallbackIp", e1); + } + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Nets.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Nets.java new file mode 100644 index 0000000..359c8e4 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/net/Nets.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.core.net; + +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.io.ResourceUtil; +import lombok.experimental.UtilityClass; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * @author liyong.tian + * @since 2017/11/15 下午4:20 + */ +@UtilityClass +public class Nets { + + public URI uri(String location) { + try { + return ResourceUtil.toURI(location); + } catch (URISyntaxException e) { + throw new InternalException("cannot parse [" + location + "] to uri", e); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Dates.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Dates.java new file mode 100644 index 0000000..387eced --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Dates.java @@ -0,0 +1,202 @@ +package cn.axzo.framework.core.time; + +import cn.axzo.framework.core.Constants; +import cn.axzo.framework.core.math.Interval; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQuery; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.time.ZoneId.systemDefault; +import static java.time.format.DateTimeFormatter.ofPattern; +import static java.time.temporal.ChronoUnit.DAYS; + +/** + * @author liyong.tian + * @since 2017/4/18 + */ +public abstract class Dates { + + public static LocalDate parseDate(String dateStr) { + return parseDate(dateStr, Constants.PATTERN_DATE); + } + + public static LocalDate parseDate(String dateStr, String pattern) { + return ofPattern(pattern).parse(dateStr, LocalDate::from); + } + + public static ZonedDateTime parseZoneDateTime(String dateTimeStr) { + return parseZoneDateTime(dateTimeStr, Constants.PATTERN_DATE_TIME); + } + + public static ZonedDateTime parseZoneDateTime(String dateTimeStr, String pattern) { + LocalDateTime dateTime = ofPattern(pattern).parse(dateTimeStr, LocalDateTime::from); + return dateTime.atZone(systemDefault()); + } + + public static LocalDate toDate(ZonedDateTime dateTime) { + return dateTime.withZoneSameInstant(systemDefault()).toLocalDate(); + } + + public static LocalDateTime toDateTime(ZonedDateTime dateTime) { + return dateTime.withZoneSameInstant(systemDefault()).toLocalDateTime(); + } + + public static ZonedDateTime toZonedDateTime(LocalDate localDate) { + return localDate.atStartOfDay(systemDefault()); + } + + public static ZonedDateTime toZonedDateTime(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return instant.atZone(ZoneId.systemDefault()); + } + + public static DateHelper from(Instant instant) { + return from(instant.toEpochMilli()); + } + + public static DateHelper from(long timestamp) { + return new DateHelper(toZonedDateTime(timestamp)); + } + + public static DateHelper from(LocalDateTime dateTime) { + return new DateHelper(dateTime.atZone(systemDefault())); + } + + public static DateHelper from(ZonedDateTime dateTime) { + return new DateHelper(dateTime); + } + + public static DateHelper from(TemporalAccessor temporal) { + if (temporal instanceof LocalDate) { + return from((LocalDate) temporal); + } + if (temporal instanceof LocalDateTime) { + return from((LocalDateTime) temporal); + } + return new DateHelper(ZonedDateTime.from(temporal)); + } + + public static DateHelper from(LocalDate date) { + return new DateHelper(toZonedDateTime(date)); + } + + public static DateHelper from(String value, String pattern, TemporalQuery query) { + return from(DateTimeFormatter.ofPattern(pattern).parse(value, query)); + } + + public static DateHelper from(int year, int month, int dayOfMonth) { + return from(LocalDate.of(year, month, dayOfMonth)); + } + + public static DateHelper now() { + return new DateHelper(ZonedDateTime.now()); + } + + /** + * 计算两个时间的相差天数(算头不算尾) + */ + public static long calcDays(Temporal startInclusive, Temporal endExclusive) { + return startInclusive.until(endExclusive, DAYS); + } + + public static LocalDate min(LocalDate date, LocalDate... dates) { + return Stream.concat(Stream.of(date), Stream.of(dates)) + .filter(Objects::nonNull) + .min(LocalDate::compareTo) + .orElseThrow(() -> new IllegalArgumentException("dates cannot be all null")); + } + + public static LocalDate max(LocalDate date, LocalDate... dates) { + return Stream.concat(Stream.of(date), Stream.of(dates)) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElseThrow(() -> new IllegalArgumentException("dates cannot be all null")); + } + + public static class DateHelper { + + private final ZonedDateTime dateTime; + + private DateHelper(ZonedDateTime dateTime) { + this.dateTime = dateTime; + } + + public LocalDate asDate() { + return Dates.toDate(dateTime); + } + + public LocalDateTime asDateTime() { + return Dates.toDateTime(dateTime); + } + + public ZonedDateTime asZonedDateTime() { + return dateTime; + } + + public String asDateTimeString() { + return asString(Constants.PATTERN_DATE_TIME); + } + + public String asDateString() { + return asString(Constants.PATTERN_DATE); + } + + public String asString(String pattern) { + return ofPattern(pattern).format(dateTime); + } + + public long asLong() { + return dateTime.toInstant().toEpochMilli(); + } + + public Instant asInstant() { + return dateTime.toInstant(); + } + + public boolean between(LocalDate startDate, LocalDate endDate) { + return between(startDate, endDate, Interval.LC_RO); + } + + public boolean between(LocalDate startDate, LocalDate endDate, Interval interval) { + Objects.requireNonNull(startDate, "startDate cannot be null"); + Objects.requireNonNull(endDate, "endDate cannot be null"); + LocalDate date = asDate(); + switch (interval) { + case LO_RO: + return date.isAfter(startDate) && date.isBefore(endDate); + case LO_RC: + return date.isAfter(startDate) && !date.isAfter(endDate); + case LC_RO: + return !date.isBefore(startDate) && date.isBefore(endDate); + case LC_RC: + default: + return !date.isBefore(startDate) && !date.isAfter(endDate); + } + } + + public boolean between(ZonedDateTime startDateTime, ZonedDateTime endDateTime) { + return between(startDateTime, endDateTime, Interval.LC_RO); + } + + public boolean between(ZonedDateTime startDateTime, ZonedDateTime endDateTime, Interval interval) { + Objects.requireNonNull(startDateTime, "startDate cannot be null"); + Objects.requireNonNull(endDateTime, "endDate cannot be null"); + switch (interval) { + case LO_RC: + return dateTime.isAfter(startDateTime) && !dateTime.isAfter(endDateTime); + case LO_RO: + return dateTime.isAfter(startDateTime) && dateTime.isBefore(endDateTime); + case LC_RO: + return !dateTime.isBefore(startDateTime) && dateTime.isBefore(endDateTime); + case LC_RC: + default: + return !dateTime.isBefore(startDateTime) && !dateTime.isAfter(endDateTime); + } + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/time/DayOfWeek.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/DayOfWeek.java new file mode 100644 index 0000000..27d8f93 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/DayOfWeek.java @@ -0,0 +1,53 @@ +package cn.axzo.framework.core.time; + +import cn.axzo.framework.core.enums.ICode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author liyong.tian + * @since 2018/2/5 上午10:36 + */ +@Getter +@AllArgsConstructor +public enum DayOfWeek implements ICode { + + /** + * The singleton instance for the day-of-week of Monday. + * This has the numeric value of {@code 1}. + */ + MONDAY(1, "星期一"), + /** + * The singleton instance for the day-of-week of Tuesday. + * This has the numeric value of {@code 2}. + */ + TUESDAY(2, "星期二"), + /** + * The singleton instance for the day-of-week of Wednesday. + * This has the numeric value of {@code 3}. + */ + WEDNESDAY(3, "星期三"), + /** + * The singleton instance for the day-of-week of Thursday. + * This has the numeric value of {@code 4}. + */ + THURSDAY(4, "星期四"), + /** + * The singleton instance for the day-of-week of Friday. + * This has the numeric value of {@code 5}. + */ + FRIDAY(5, "星期五"), + /** + * The singleton instance for the day-of-week of Saturday. + * This has the numeric value of {@code 6}. + */ + SATURDAY(6, "星期六"), + /** + * The singleton instance for the day-of-week of Sunday. + * This has the numeric value of {@code 7}. + */ + SUNDAY(7, "星期天"); + + private int code; + private String name; +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Period.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Period.java new file mode 100644 index 0000000..28b37be --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/Period.java @@ -0,0 +1,141 @@ +package cn.axzo.framework.core.time; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import lombok.Getter; + +import javax.annotation.Nonnull; +import java.time.format.DateTimeParseException; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.Integer.parseInt; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +/** + * 指定单位的期数对象 + * + * @author liyong.tian + * @since 2017/3/3 + */ +public class Period implements CharSequence { + + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("P(?:([0-9]+))(?:([YMWD]))", CASE_INSENSITIVE); + + private static final String PERIOD_PREFIX = "P"; + + // 期数 + @Getter + private final int value; + + // 期数单位 + @Getter + private final PeriodUnit unit; + + private final String period; + + private Period(int value, PeriodUnit unit) { + if (unit == null) { + throw new IllegalArgumentException("Period unit cannot be null"); + } + this.value = value; + this.unit = unit; + this.period = PERIOD_PREFIX + value + unit.getCode(); + } + + public static Period of(int value, PeriodUnit unit) { + return new Period(value, unit); + } + + public static Period ofMonths(int months) { + return of(months, PeriodUnit.MONTH); + } + + public static Period ofWeeks(int weeks) { + return of(weeks, PeriodUnit.WEEK); + } + + public static Period ofDays(int days) { + return of(days, PeriodUnit.DAY); + } + + public static Period ofYears(int years) { + return of(years, PeriodUnit.YEAR); + } + + public boolean isZero() { + return this.value == 0; + } + + /** + * Obtains a {@code Period} from a text string such as {@code PnY, PnM, PnW, PnD}. + *

+ * This will parse the string produced by {@code toString()} which is + * based on the ISO-8601 period formats {@code PnY, PnM, PnW, PnD}. + *

+ * The string starts with the ASCII letter "P" in upper case. + * There is then one section, which consisting of a number and a suffix. + * The section has suffixes in ASCII of "Y", "M", "W" and "D" for + * years, months, weeks and days, only accepted in upper case. + * The number part of the section must consist of ASCII digits. + * The number cannot be prefixed by the ASCII negative symbol. + * The number must parse to an {@code int}. + *

+ * For example, the following are valid inputs: + *

+     *   "P2Y"             -- Period.ofYears(2)
+     *   "P3M"             -- Period.ofMonths(3)
+     *   "P4W"             -- Period.ofWeeks(4)
+     *   "P5D"             -- Period.ofDays(5)
+     * 
+ * + * @param text the text to parse, not null + * @return the parsed period, not null + * @throws DateTimeParseException if the text cannot be parsed to a period + */ + public static Period parse(CharSequence text) { + Objects.requireNonNull(text, "text"); + Matcher matcher = PATTERN.matcher(text); + if (matcher.matches()) { + String valueMatch = matcher.group(1); + if (valueMatch != null) { + String unitMatch = matcher.group(2); + PeriodUnit unit = EnumStdUtil.findEnum(PeriodUnit.class, unitMatch) + .orElseThrow(() -> new DateTimeParseException("Text cannot be parsed to a Period", text, 0)); + try { + return of(parseInt(valueMatch), unit); + } catch (NumberFormatException ex) { + throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex); + } + } + } + throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0); + } + + @Override + public int length() { + return period.length(); + } + + @Override + public char charAt(int index) { + return period.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return period.substring(start, end); + } + + /** + * ISO-8601 period formats {@code PnY, PnM, PnW, PnD} + */ + @Nonnull + @Override + public String toString() { + return period; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/time/PeriodUnit.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/PeriodUnit.java new file mode 100644 index 0000000..e500910 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/time/PeriodUnit.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.core.time; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.core.enums.IStringCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Optional; + +/** + * 时间阶段单位 + * + * @author liyong.tian + * @since 2017/2/27 + */ +@AllArgsConstructor +@Getter +public enum PeriodUnit implements IStringCode { + + HOUR("H", "时"), + + DAY("D", "日"), + + WEEK("W", "周"), + + MONTH("M", "月"), + + YEAR("Y", "年"); + + private String code; + private String name; + + public static PeriodUnit parse(String code) { + return EnumStdUtil.parse(PeriodUnit.class, code); + } + + public static PeriodUnit parse(String code, PeriodUnit defaultUnit) { + return EnumStdUtil.parse(PeriodUnit.class, code, defaultUnit); + } + + public static Optional find(String code) { + return EnumStdUtil.findEnum(PeriodUnit.class, code); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/AntPathUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/AntPathUtil.java new file mode 100644 index 0000000..c107292 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/AntPathUtil.java @@ -0,0 +1,346 @@ +package cn.axzo.framework.core.util; + +import jodd.util.StringUtil; +import lombok.Data; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static cn.axzo.framework.core.util.PathUtil.FOLDER_SEPARATOR; +import static java.util.Arrays.stream; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +/** + * @author liyong.tian + * @since 2017/8/1 下午7:32 + */ +public class AntPathUtil { + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final char[] WILDCARD_CHARS = {'*', '?', '{'}; + + private static final Map tokenizedPatternCache = new ConcurrentHashMap<>(256); + + private static final Map stringMatcherCache = new ConcurrentHashMap<>(256); + + public enum MatcherFeature { + CASE_SENSITIVE, TRIM_TOKENS + } + + public static boolean isPattern(String path) { + return path.indexOf('*') != -1 || path.indexOf('?') != -1; + } + + /** + * 全匹配 + *

+ *

    + *
  • /trip/api/*x 匹配 /trip/api/x,/trip/api/ax,/trip/api/abx; 但不匹配 /trip/abc/x
  • + *
  • /trip/a/a?x 匹配 /trip/a/abx;但不匹配 /trip/a/ax,/trip/a/abcx
  • + *
  • /**\/api/alie 匹配 /trip/api/alie,/trip/dax/api/alie;但不匹配 /trip/a/api
  • + *
  • /**\/*.html 匹配所有以.html结尾的路径
  • + *
+ */ + public static boolean match(String antPattern, String path) { + return doMatch(antPattern, path, true, MatcherFeature.CASE_SENSITIVE); + } + + /** + * 部分头匹配 + *

+ *

    + *
  • fonts/*.ttl 可以部分头匹配 fonts
  • + *
  • fonts/**\/*.ttl 可以部分头匹配 fonts/aaa/bbb
  • + *
+ */ + public static boolean matchStart(String antPattern, String path) { + return doMatch(antPattern, path, false, MatcherFeature.CASE_SENSITIVE); + } + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * + * @param antPattern the pattern to match against + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern match as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + private static boolean doMatch(String antPattern, String path, boolean fullMatch, MatcherFeature... features) { + // 快速判断 + if (path.startsWith(FOLDER_SEPARATOR) != antPattern.startsWith(FOLDER_SEPARATOR)) { + return false; + } + + // 数据准备 + boolean caseSensitive = stream(features).anyMatch(feature -> feature == MatcherFeature.CASE_SENSITIVE); + boolean trimTokens = stream(features).anyMatch(feature -> feature == MatcherFeature.TRIM_TOKENS); + String[] pattDirs = tokenizeAntPattern(antPattern, trimTokens); + if (fullMatch && caseSensitive && !isPotentialMatch(path, pattDirs, trimTokens)) { + return false; + } + String[] pathDirs = tokenizePath(path, trimTokens); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], caseSensitive)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (antPattern.endsWith(FOLDER_SEPARATOR) == path.endsWith(FOLDER_SEPARATOR)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(FOLDER_SEPARATOR)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], caseSensitive)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, caseSensitive)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private static boolean isPotentialMatch(String path, String[] pattDirs, boolean trimTokens) { + if (!trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, FOLDER_SEPARATOR); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private static int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private static int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private static boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + private static boolean matchStrings(String antPattern, String str, boolean caseSensitive) { + return getStringMatcher(antPattern).matchStrings(str, caseSensitive); + } + + private static AntPathStringMatcher getStringMatcher(String antPattern) { + AntPathStringMatcher matcher = stringMatcherCache.get(antPattern); + if (matcher == null) { + matcher = new AntPathStringMatcher(antPattern); + if (stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + stringMatcherCache.clear(); + } + stringMatcherCache.put(antPattern, matcher); + } + return matcher; + } + + private static String[] tokenizeAntPattern(String antPattern, boolean trimResults) { + AntPattern pattern = new AntPattern(antPattern, trimResults); + String[] tokenized = tokenizedPatternCache.get(pattern); + if (tokenized == null) { + tokenized = tokenizePath(antPattern, trimResults); + if (tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + tokenizedPatternCache.clear(); + } + tokenizedPatternCache.put(pattern, tokenized); + } + return tokenized; + } + + private static String[] tokenizePath(String path, boolean trimResults) { + String[] tokens = stream(path.split(FOLDER_SEPARATOR)).filter(StringUtil::isNotEmpty).toArray(String[]::new); + if (trimResults) { + StringUtil.trimAll(tokens); + } + return tokens; + } + + @Data + private static class AntPattern { + private final String pattern; + private final boolean trimResults; + } + + private static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{(?:[^/{}])+?}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final Pattern pattern; + + private final Pattern caseInsensitivePattern; + + AntPathStringMatcher(String antPattern) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(antPattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(antPattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("*".equals(match)) { + patternBuilder.append(".*"); + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + } else { + patternBuilder.append('('); + patternBuilder.append(match.substring(colonIdx + 1, match.length() - 1)); + patternBuilder.append(')'); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(antPattern, end, antPattern.length())); + this.pattern = Pattern.compile(patternBuilder.toString()); + this.caseInsensitivePattern = Pattern.compile(patternBuilder.toString(), CASE_INSENSITIVE); + } + + private static String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + private boolean matchStrings(String str, boolean caseSensitive) { + if (caseSensitive) { + return caseInsensitivePattern.matcher(str).matches(); + } + return pattern.matcher(str).matches(); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ArrayUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ArrayUtil.java new file mode 100644 index 0000000..0fa139c --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ArrayUtil.java @@ -0,0 +1,22 @@ +package cn.axzo.framework.core.util; + +import lombok.experimental.UtilityClass; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + * @author liyong.tian + * @since 2017/6/21 + */ +@UtilityClass +public class ArrayUtil { + + public boolean equalsAny(Object base, Object... array) { + return Stream.of(array).anyMatch(o -> Objects.equals(base, o)); + } + + public boolean equalsAll(Object base, Object... array) { + return Stream.of(array).allMatch(o -> Objects.equals(base, o)); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassNameUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassNameUtil.java new file mode 100644 index 0000000..6f3736f --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassNameUtil.java @@ -0,0 +1,100 @@ +package cn.axzo.framework.core.util; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import jodd.util.StringUtil; +import lombok.experimental.UtilityClass; + +import javax.annotation.Nonnull; + +import static java.util.concurrent.TimeUnit.DAYS; +import static jodd.util.StringPool.DOT; + +/** + * @author liyong.tian + * @since 2017/6/29 + */ +@UtilityClass +public class ClassNameUtil { + + private static final int DEFAULT_WIDTH = 40; + + private static LoadingCache typeNameCache = Caffeine.newBuilder() + .maximumSize(10000) + .expireAfterWrite(7, DAYS) + .build(ClassNameUtil::_compressFullTypeName); + + /** + * cn.axzo.example.UserService -> c.q.e.UserService + */ + @Nonnull + public String compressFullTypeName(Class type) { + return compressFullTypeName(type, DEFAULT_WIDTH); + } + + /** + * cn.axzo.framework.example.UserService -> c.q.b.e.UserService + * + * @param width 返回字符串最大长度 + */ + @Nonnull + public String compressFullTypeName(Class type, int width) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + return compressFullTypeName(type.getName(), width); + } + + @Nonnull + public String compressFullTypeName(String fullTypeName) { + return compressFullTypeName(fullTypeName, DEFAULT_WIDTH); + } + + @Nonnull + public String compressFullTypeName(String fullTypeName, int width) { + if (fullTypeName == null) { + throw new IllegalArgumentException("fullTypeName cannot be null"); + } + if (fullTypeName.endsWith(DOT)) { + throw new IllegalArgumentException("fullTypeName is error pattern with a dot ending"); + } + if (width <= 2) { + int index = fullTypeName.lastIndexOf(DOT); + if (index > -1) { + return fullTypeName.substring(index + 1); + } + return fullTypeName; + } + + String compressedName = typeNameCache.get(fullTypeName); + if (compressedName == null) { + throw new IllegalArgumentException("compressedName cannot be null"); + } + while (compressedName.length() > width && compressedName.contains(DOT)) { + compressedName = compressedName.substring(2); + } + return compressedName; + } + + /** + * cn.axzo.framework.example.UserService -> c.q.b.e.UserService + */ + @Nonnull + private String _compressFullTypeName(@Nonnull String fullTypeName) { + String[] segments = StringUtil.split(fullTypeName, DOT); + int total = segments.length; + StringBuilder compressedName = new StringBuilder(); + for (int i = 0; i < total; i++) { + String segment = segments[i]; + if (i < total) { + if (segment != null && segment.length() > 0) { + compressedName.append(segment.charAt(0)); + } + compressedName.append(DOT); + } else { + compressedName.append(segment); + } + } + return compressedName.toString(); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassUtil.java new file mode 100644 index 0000000..0f5f7be --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ClassUtil.java @@ -0,0 +1,265 @@ +package cn.axzo.framework.core.util; + +import cn.axzo.framework.core.InternalException; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.net.URLClassLoader; +import java.util.*; + +import static java.util.stream.Collectors.toSet; + +/** + * @author liyong.tian + * @since 2017/7/28 下午4:54 + */ +public abstract class ClassUtil { + + private static final String ARRAY_SUFFIX = "[]"; + + private static final char INNER_CLASS_SEPARATOR = '$'; + + private static final String INTERNAL_ARRAY_PREFIX = "["; + + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + + private static final Map, Class> primitiveWrapperTypeMap = new IdentityHashMap<>(8); + + private static final Map> primitiveTypeNameMap = new HashMap<>(32); + + /** + * Map with common "java.lang" class name as key and corresponding Class as value. + * Primarily for efficient deserialization of remote invocations. + */ + private static final Map> commonClassCache = new HashMap<>(32); + + private static final Set> WRAPPER_TYPES = Arrays.stream(new Class[]{ + Boolean.class, + Byte.class, + Character.class, + Double.class, + Float.class, + Integer.class, + Long.class, + Short.class, + Void.class + }).collect(toSet()); + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + + Set> primitiveTypes = new HashSet>(32); + primitiveTypes.addAll(primitiveWrapperTypeMap.values()); + primitiveTypes.addAll(Arrays.asList(boolean[].class, byte[].class, char[].class, double[].class, + float[].class, int[].class, long[].class, short[].class)); + primitiveTypes.add(void.class); + for (Class primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + + registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, + Float[].class, Integer[].class, Long[].class, Short[].class); + registerCommonClasses(Number.class, Number[].class, String.class, String[].class, + Object.class, Object[].class, Class.class, Class[].class); + registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, + Error.class, StackTraceElement.class, StackTraceElement[].class); + } + + private static void registerCommonClasses(Class... commonClasses) { + for (Class clazz : commonClasses) { + commonClassCache.put(clazz.getName(), clazz); + } + } + + /** + * May cause {@link ClassCastException} . Be caution to use it. + */ + @SuppressWarnings("unchecked") + public static B cast(A a) { + return (B) a; + } + + public static T newInstance(Class clazz, Object... params) { + try { + return jodd.util.ClassUtil.newInstance(clazz, params); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new InternalException(clazz + " cannot be instantiated"); + } + } + + public static T newInstance(Class clazz) { + try { + return jodd.util.ClassUtil.newInstance(clazz); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new InternalException(clazz + " cannot be instantiated"); + } + } + + @NotNull + public static Class load(@NotNull String className, @Nullable ClassLoader classLoader) + throws ClassNotFoundException { + return load(className, false, classLoader); + } + + /** + * @throws LinkageError if the linkage fails + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails + * @throws ClassNotFoundException if the class cannot be located by + * the specified class loader + */ + @NotNull + public static Class load(@NotNull String className, boolean initialize, @Nullable ClassLoader classLoader) + throws ClassNotFoundException { + return classLoader == null ? Class.forName(className) : Class.forName(className, initialize, classLoader); + } + + public static Optional> loadIfPresent(@Nullable String className) { + if (className == null) { + return Optional.empty(); + } + try { + return Optional.of(Class.forName(className)); + } catch (ClassNotFoundException | LinkageError ignored) { + } + return Optional.empty(); + } + + public static Optional> loadIfPresent(@Nullable String className, @NotNull ClassLoader classLoader) { + if (className == null) { + return Optional.empty(); + } + try { + return Optional.ofNullable(load(className, classLoader)); + } catch (ClassNotFoundException | LinkageError ignored) { + } + return Optional.empty(); + } + + @Nullable + public static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back... + } + if (cl == null) { + // No thread context class loader -> use class loader of this class. + cl = ClassUtil.class.getClassLoader(); + if (cl == null) { + // getClassLoader() returning null indicates the bootstrap ClassLoader + try { + cl = ClassLoader.getSystemClassLoader(); + } catch (Throwable ex) { + // Cannot access system ClassLoader - oh well, maybe the caller can live with null... + } + } + } + return cl; + } + + public void close(URLClassLoader classLoader) { + try { + classLoader.close(); + } catch (Exception ignored) { + } + } + + public static boolean isWrapperType(Class clazz) { + return WRAPPER_TYPES.contains(clazz); + } + + public static boolean isPrimitiveType(Class clazz) { + return clazz != null && (clazz.isPrimitive() || isWrapperType(clazz)); + } + + public static boolean isBasicType(Class clazz) { + return clazz != null && ((String.class.isAssignableFrom(clazz)) || isPrimitiveType(clazz)); + } + + public static boolean isPresent(String className, ClassLoader classLoader) { + try { + forName(className, classLoader); + return true; + } catch (Throwable ex) { + // Class or one of its dependencies is not present... + return false; + } + } + + public static Class forName(String name, ClassLoader classLoader) { + if (name == null) { + throw new IllegalArgumentException("Name must not be null"); + } + + Class clazz = resolvePrimitiveClassName(name); + if (clazz == null) { + clazz = commonClassCache.get(name); + } + if (clazz != null) { + return cast(clazz); + } + + // "java.lang.String[]" style arrays + if (name.endsWith(ARRAY_SUFFIX)) { + String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + Class elementClass = forName(elementClassName, classLoader); + return cast(Array.newInstance(elementClass, 0).getClass()); + } + + // "[Ljava.lang.String;" style arrays + if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + Class elementClass = forName(elementName, classLoader); + return cast(Array.newInstance(elementClass, 0).getClass()); + } + + // "[[I" or "[[Ljava.lang.String;" style arrays + if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + Class elementClass = forName(elementName, classLoader); + return cast(Array.newInstance(elementClass, 0).getClass()); + } + + ClassLoader clToUse = classLoader; + if (clToUse == null) { + clToUse = getDefaultClassLoader(); + } + try { + return (clToUse != null ? cast(clToUse.loadClass(name)) : cast(Class.forName(name))); + } catch (ClassNotFoundException ex) { + int lastDotIndex = name.lastIndexOf(PathUtil.PACKAGE_SEPARATOR); + if (lastDotIndex != -1) { + String innerClassName = + name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); + try { + return (clToUse != null ? cast(clToUse.loadClass(innerClassName)) : cast(Class.forName(innerClassName))); + } catch (ClassNotFoundException ex2) { + // Swallow - let original exception get through + } + } + throw new InternalException(ex); + } + } + + public static Class resolvePrimitiveClassName(String name) { + Class result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 8) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); + } + return result; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullMap.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullMap.java new file mode 100644 index 0000000..3db245d --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullMap.java @@ -0,0 +1,49 @@ +package cn.axzo.framework.core.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/4/6 上午1:34 + */ +public class IgnoreNullMap extends HashMap { + + public IgnoreNullMap(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + } + + public IgnoreNullMap(int initialCapacity) { + super(initialCapacity); + } + + public IgnoreNullMap() { + } + + public IgnoreNullMap(IgnoreNullMap m) { + super(m); + } + + @Override + public V put(K key, V value) { + if (key == null || value == null) { + return super.get(key); + } + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public V putIfAbsent(K key, V value) { + if (key == null || value == null || super.containsKey(key)) { + return super.get(key); + } + return this.put(key, value); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullStringMap.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullStringMap.java new file mode 100644 index 0000000..b8c62ae --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/IgnoreNullStringMap.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.core.util; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/6/4 下午11:15 + */ +public class IgnoreNullStringMap extends IgnoreNullMap { + + public IgnoreNullStringMap() { + } + + public IgnoreNullStringMap(IgnoreNullMap m) { + super(m); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ListUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ListUtil.java new file mode 100644 index 0000000..0b5edf5 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ListUtil.java @@ -0,0 +1,116 @@ +package cn.axzo.framework.core.util; + +import org.javatuples.Pair; +import org.javatuples.Triplet; + +import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static java.lang.Boolean.TRUE; +import static java.util.stream.Collectors.toList; +import static org.javatuples.Triplet.with; + +/** + * @author liyong.tian + * @since 16/9/22 + */ +public class ListUtil { + + public static List intersect(List left, List right) { + return differ(left, right, Objects::equals).getValue1().stream().map(Pair::getValue0).collect(toList()); + } + + public static Triplet, List>, List> differ(List

pList, List qList) { + return differ(pList, qList, Objects::equals); + } + + public static Triplet, List>, List> differ(List

pList, List qList, + BiPredicate equator) { + List

pOnly = pList.stream() + .filter(p -> !contains(qList, p, (q, p1) -> equator.test(p1, q))) + .collect(toList()); + + List> both = pList.stream() + .map(p -> containsGet(qList, p, (q1, p1) -> equator.test(p1, q1)).map(q -> Pair.with(p, q)).orElse(null)) + .filter(Objects::nonNull) + .collect(toList()); + + List qOnly = qList.stream() + .filter(q -> !contains(pList, q, equator)) + .collect(toList()); + + return with(pOnly, both, qOnly); + } + + public static boolean contains(List list, U u, BiPredicate equator) { + return list.stream().anyMatch(t -> equator.test(t, u)); + } + + public static Optional containsGet(List list, U u, BiPredicate equator) { + return list.stream().filter(t -> equator.test(t, u)).findAny(); + } + + /** + * 使用方法: + *

+     *     List orders = new ArrayList<>();
+     *     orders.add(...);
+     *     ...
+     *     orders.stream().filter(distinctByKey(Order::getOrderNo)).collect(toList());
+     * 
+ */ + public static Predicate distinctByKey(Function keySelector) { + Map seen = new HashMap<>(); + return t -> seen.putIfAbsent(keySelector.apply(t), TRUE) == null; + } + + /** + * 并集 + */ + @SafeVarargs + public static List union(List list, List... moreLists) { + if (moreLists.length == 0) { + return new ArrayList<>(list); + } + int size = list.size() + Stream.of(moreLists).mapToInt(List::size).sum(); + List result = new ArrayList<>(5 + size + (size / 10)); + result.addAll(list); + Stream.of(moreLists).forEach(result::addAll); + return result; + } + + /** + * 添加元素到集合 + */ + @SafeVarargs + public static List add(List list, T... moreElements) { + if (moreElements.length == 0) { + return new ArrayList<>(list); + } + int size = list.size() + moreElements.length; + List result = new ArrayList<>(5 + size + (size / 10)); + result.addAll(list); + result.addAll(Arrays.asList(moreElements)); + return result; + } + + /** + * 集合转化成数组 + */ + public static String[] toStringArray(List list) { + if (list == null) { + return null; + } + return list.toArray(new String[0]); + } + + /** + * 判断集合是否为单元素集合 + */ + public static boolean isSingle(List list) { + return list != null && list.size() == 1; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/MapUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/MapUtil.java new file mode 100644 index 0000000..12129c8 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/MapUtil.java @@ -0,0 +1,395 @@ +package cn.axzo.framework.core.util; + +import org.javatuples.Pair; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jodd.util.StringUtil.isNotBlank; + +/** + * @author liyong.tian + * @since 16/9/21 + */ +public class MapUtil { + + @SafeVarargs + public static Map putDefaultIfAbsent(Map map, V defaultValue, K... keys) { + Map newMap = new HashMap<>(map); + Stream.of(keys).forEach(key -> { + if (!newMap.containsKey(key)) { + newMap.put(key, defaultValue); + } + }); + return newMap; + } + + public static List values(Map map, Set keys) { + return new ArrayList<>(subMap(map, keys).values()); + } + + public static Map subMap(Map map, Set keys) { + return subMap(map, keys, key -> key); + } + + public static Map subMap(Map map, Set keys, + Function keyHandler) { + return subMap(map, keys, keyHandler, value -> value); + } + + /** + * @param map 原map + * @param keys 匹配的键 + * @param keyHandler 对key的处理 + * @param valueHandler 对value的处理 + * @return 子map + */ + public static Map subMap(Map map, Set keys, + Function keyHandler, + Function valueHandler) { + Map subMap = new HashMap<>((int) ((float) keys.size() / 0.75F + 1.0F)); + keys.stream() + .filter(map::containsKey) + .map(key -> Pair.with(keyHandler.apply(key), valueHandler.apply(map.get(key)))) + .forEach(pair -> subMap.put(pair.getValue0(), pair.getValue1())); + return subMap; + } + + /** + * 合并两个Map + */ + public static Map merge(Map primaryMap, Map secondaryMap) { + Map map = new HashMap<>(primaryMap); + if (secondaryMap != null && !secondaryMap.isEmpty()) { + map.putAll(secondaryMap); + } + return map; + } + + /** + * Null-safe check if the specified map is empty. + *

+ * Null returns true. + * + * @param map the map to check, may be null + * @return true if empty or null + * @since 3.2 + */ + public static boolean isEmpty(final Map map) { + return map == null || map.isEmpty(); + } + + public static boolean isNotEmpty(final Map map) { + return !isEmpty(map); + } + + /** + * 判断map中的值是否全为空 + *

+ * 若map本身为空,也视为值全为空 + * + * @param nullPredicate 可视为null的自定义条件 + */ + @SafeVarargs + public static boolean isAllNullValues(Map map, Predicate... nullPredicate) { + return isEmpty(map) || map.values().stream().allMatch( + t -> t == null || Stream.of(nullPredicate).anyMatch(predicate -> predicate.test(t)) + ); + } + + public static Map removeNulls(Map source) { + return source.entrySet().stream() + .filter(entry -> entry.getKey() != null) + .filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public static void addMapSet(Map> map, A a, B b) { + addMapSet(map, false, a, b); + } + + @SuppressWarnings("unchecked") + public static void addMapSet(Map> map, boolean ignoreCase, A a, B b) { + + Set setB = map.get(a); + if (setB == null) { + if (ignoreCase && (b instanceof String)) { + setB = (Set) new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + } else { + setB = new LinkedHashSet(); + } + map.put(a, setB); + } + setB.add(b); + } + + public static void addMapMapSet(Map>> map, A a, B b, C c) { + addMapMapSet(map, false, a, b, c); + } + + @SuppressWarnings("unchecked") + public static void addMapMapSet(Map>> map, boolean ignoreCase, A a, B b, C c) { + + Map> mapB = map.get(a); + if (mapB == null) { + if (ignoreCase && (b instanceof String)) { + mapB = (Map>) new TreeMap>(String.CASE_INSENSITIVE_ORDER); + } else { + mapB = new LinkedHashMap<>(); + } + map.put(a, mapB); + } + addMapSet(mapB, ignoreCase, b, c); + } + + + @SuppressWarnings("unchecked") + public static void addMapMapMapSet(Map>>> map, boolean ignoreCase, A a, B b, + C c, D d) { + Map>> mapB = map.get(a); + if (mapB == null) { + if (ignoreCase && (b instanceof String)) { + mapB = (Map>>) new TreeMap>>(String.CASE_INSENSITIVE_ORDER); + } else { + mapB = new LinkedHashMap<>(); + } + map.put(a, mapB); + } + addMapMapSet(mapB, ignoreCase, b, c, d); + } + + public static C getMapMap(Map> map, A a, B b) { + + Map mapB = map.get(a); + if (mapB == null) { + return null; + } + return mapB.get(b); + } + + public static void addMapList(Map> map, A a, B b) { + List listB = map.computeIfAbsent(a, k -> new ArrayList<>()); + listB.add(b); + } + + + public static void addListToMapList(Map> map, A a, List b) { + List listB = map.computeIfAbsent(a, k -> new ArrayList<>()); + listB.addAll(b); + } + + @SuppressWarnings("unchecked") + public static V getValue(Map map, K k) { + V v = null; + try { + v = (V) map.get(k); + } catch (ClassCastException e) { + //LOG.war( "Map value {} was not the expected class", map.get( k ), e ); + } + + return v; + } + + @SuppressWarnings("unchecked") + public static Map map(Object... objects) { + Map map = new LinkedHashMap(); + int i = 0; + while (i < objects.length) { + if (objects[i] instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) objects[i]; + map.put(entry.getKey(), entry.getValue()); + i++; + } else if (objects[i] instanceof Map) { + map.putAll((Map) objects[i]); + i++; + } else if (i < (objects.length - 1)) { + K k = (K) objects[i]; + V v = (V) objects[i + 1]; + map.put(k, v); + i += 2; + } else { + break; + } + } + return map; + } + + private static class SimpleMapEntry implements Map.Entry { + + private final K k; + private V v; + + public SimpleMapEntry(K k, V v) { + this.k = k; + this.v = v; + } + + @Override + public K getKey() { + return k; + } + + @Override + public V getValue() { + return v; + } + + @Override + public V setValue(V v) { + V oldV = this.v; + this.v = v; + return oldV; + } + } + + public static Map.Entry entry(K k, V v) { + return new SimpleMapEntry<>(k, v); + } + + public static K getFirstKey(Map map) { + if (map == null) { + return null; + } + Map.Entry e = map.entrySet().iterator().next(); + if (e != null) { + return e.getKey(); + } + return null; + } + + public static Map filter(Map map, String prefix, boolean removePrefix) { + Map filteredMap = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + if (removePrefix) { + filteredMap.put(entry.getKey().substring(prefix.length()), entry.getValue()); + } else { + filteredMap.put(entry.getKey(), entry.getValue()); + } + } + } + return filteredMap; + } + + public static Map filter(Map map, String prefix) { + return filter(map, prefix, false); + } + + public static Properties filter(Properties properties, String prefix, boolean removePrefix) { + Properties filteredProperties = new Properties(); + for (Map.Entry entry : asMap(properties).entrySet()) { + if (entry.getKey().startsWith(prefix)) { + if (removePrefix) { + filteredProperties.put(entry.getKey().substring(prefix.length()), entry.getValue()); + } else { + filteredProperties.put(entry.getKey(), entry.getValue()); + } + } + } + return filteredProperties; + } + + public static Properties filter(Properties properties, String prefix) { + return filter(properties, prefix, false); + } + + @SuppressWarnings("unchecked") + public static Map asMap(Properties properties) { + return ClassUtil.cast(properties); + } + + public static HashMapBuilder hashMap(S key, T value) { + return new HashMapBuilder().map(key, value); + } + + public static class HashMapBuilder extends HashMap { + private static final long serialVersionUID = 1L; + + public HashMapBuilder() { + } + + public HashMapBuilder map(S key, T value) { + put(key, value); + return this; + } + } + + @SuppressWarnings("unchecked") + public static Map> toMapList(Map m) { + Map> mapList = new LinkedHashMap<>(); + + for (Map.Entry e : m.entrySet()) { + if (e.getValue() instanceof List) { + addListToMapList(mapList, e.getKey(), (List) e.getValue()); + } else { + addMapList(mapList, e.getKey(), e.getValue()); + } + } + + return ClassUtil.cast(mapList); + } + + public static Map putPath(String path, Object value) { + return putPath(null, path, value); + } + + @SuppressWarnings("unchecked") + public static Map putPath(Map map, String path, Object value) { + if (map == null) { + map = new HashMap<>(); + } + + int i = path.indexOf('.'); + if (i < 0) { + ((Map) map).put(path, value); + return map; + } + String segment = path.substring(0, i).trim(); + if (isNotBlank(segment)) { + Object o = map.get(segment); + if ((o != null) && (!(o instanceof Map))) { + return map; + } + Map subMap = (Map) o; + if (subMap == null) { + subMap = new HashMap<>(); + ((Map) map).put(segment, subMap); + } + String subPath = path.substring(i + 1); + if (isNotBlank(subPath)) { + putPath(subMap, subPath, value); + } + } + + return map; + } + + public static Map emptyMapWithKeys(Map map) { + Map newMap = new HashMap<>(); + + for (K k : map.keySet()) { + newMap.put(k, null); + } + + return newMap; + } + + public static boolean hasKeys(Map map, String... keys) { + if (map == null) { + return false; + } + for (String key : keys) { + if (!map.containsKey(key)) { + return false; + } + } + return true; + } + + public static boolean hasKeys(Map map, Set keys) { + return map != null && map.keySet().containsAll(keys); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/PathUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/PathUtil.java new file mode 100644 index 0000000..aadb017 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/PathUtil.java @@ -0,0 +1,153 @@ +package cn.axzo.framework.core.util; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author liyong.tian + * @since 2017/7/31 下午2:56 + */ +public abstract class PathUtil { + + public static final String FOLDER_SEPARATOR = "/"; + + /** + * The package separator character: '.' + */ + public static final char PACKAGE_SEPARATOR = '.'; + + private static final String CURRENT_PATH = "."; + + private static final String TOP_PATH = ".."; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\\\"; + + /** + * The path separator character: '/' + */ + private static final char PATH_SEPARATOR = '/'; + + public static String classPackageAsDirectory(Class clazz) { + if (clazz == null) { + return FOLDER_SEPARATOR; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + if (packageEndIndex == -1) { + return FOLDER_SEPARATOR; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR) + PATH_SEPARATOR; + } + + /** + * Apply the given relative path to the given Java resource path, + * assuming standard Java folder separation (i.e. "/" separators). + * + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + return newPath + FOLDER_SEPARATOR + relativePath; + } + return newPath + relativePath; + } else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and inner simple dots. + *

+ * The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + * + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(String path) { + if (path == null) { + return null; + } + String pathToUse = path.replaceAll(WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + if (prefix.contains("/")) { + prefix = ""; + } else { + pathToUse = pathToUse.substring(prefixIndex + 1); + } + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = pathToUse.split(FOLDER_SEPARATOR); + List pathElements = new LinkedList<>(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (!CURRENT_PATH.equals(element)) { + if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + String.join(FOLDER_SEPARATOR, pathElements); + } + + /** + * Determine the root directory for the given location. + *

+ * Used for determining the starting point for file matching, + * resolving the root directory location to a {@code java.io.File} + * and passing it into {@code retrieveMatchingFiles}, with the + * remainder of the location as pattern. + *

Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", + * for example. + * + * @param location the location to check + * @return the part of the location that denotes the root directory + */ + public static String extractRootDir(String location) { + int prefixEnd = location.indexOf(":") + 1; + int rootDirEnd = location.length(); + while (rootDirEnd > prefixEnd && AntPathUtil.isPattern(location.substring(prefixEnd, rootDirEnd))) { + rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1; + } + if (rootDirEnd == 0) { + rootDirEnd = prefixEnd; + } + return location.substring(0, rootDirEnd); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ReflectUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ReflectUtil.java new file mode 100644 index 0000000..18c82d4 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/ReflectUtil.java @@ -0,0 +1,144 @@ +package cn.axzo.framework.core.util; + +import cn.axzo.framework.core.InternalException; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import lombok.NonNull; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.stream.Collectors.toList; + +/** + * @author liyong.tian + * @since 2016/12/12 + */ +public abstract class ReflectUtil { + + private static LoadingCache, List> methodsCache = Caffeine.newBuilder() + .maximumSize(1000) + .expireAfterWrite(1, DAYS) + .build(ReflectUtil::_getMethods); + + private static LoadingCache, List> declareMethodsCache = Caffeine.newBuilder() + .maximumSize(1000) + .expireAfterWrite(1, DAYS) + .build(ReflectUtil::_getDeclareMethods); + + /** + * 获取父类/父接口中首个泛型的类型,找不到则抛异常{@link InternalException} + * + * @param clazz 当前类的类型 + * @param superClassOrInterface 父类/父接口的类型 + */ + public static Class getSuperGenericType(final Class clazz, final Class superClassOrInterface) { + return getSuperGenericType(clazz, superClassOrInterface, 0); + } + + /** + * 获取父类/父接口中首个泛型的类型,找不到则抛异常{@link InternalException} + * + * @param clazz 当前类的类型 + * @param superClassOrInterface 父类/父接口的类型 + * @param index 指定第几个泛型参数 + */ + public static Class getSuperGenericType(final Class clazz, + final Class superClassOrInterface, + final int index) { + return ReflectUtil.findSuperGenericType(clazz, superClassOrInterface, index) + .orElseThrow(() -> new InternalException(clazz + "must have a generic type")); + } + + /** + * 获取父类/父接口中首个泛型的类型 + * + * @param clazz 当前类的类型 + * @param superClassOrInterface 父类/父接口的类型 + */ + public static Optional> findSuperGenericType(final Class clazz, + final Class superClassOrInterface) { + return findSuperGenericType(clazz, superClassOrInterface, 0); + } + + /** + * 获取父类/父接口中泛型的类型 + * + * @param clazz 当前类的类型 + * @param superClassOrInterface 父类/父接口的类型 + * @param index 指定第几个泛型参数 + */ + @SuppressWarnings("unchecked") + public static Optional> findSuperGenericType(final Class clazz, + final Class superClassOrInterface, + final int index) { + if (clazz == null || superClassOrInterface == null || index < 0) { + return Optional.empty(); + } + Stream genericInterfaces = Stream.of(clazz.getGenericInterfaces()); + Stream genericSuperclass = Stream.of(clazz.getGenericSuperclass()); + List> genericSupertypes = Stream.concat(genericInterfaces, genericSuperclass) + .filter(type -> type instanceof ParameterizedType) + .map(type -> (ParameterizedType) type) + .filter(type -> type.getRawType().getTypeName().equals(superClassOrInterface.getName())) + .flatMap(type -> Stream.of(type.getActualTypeArguments())) + .filter(type -> type instanceof Class) + .map(type -> (Class) type) + .collect(toList()); + if (index > genericSupertypes.size() - 1) { + return Optional.empty(); + } + return Optional.of(genericSupertypes.get(index)); + } + + public static List findMethods(@NonNull Class c, String methodName) { + return findMethods(c, methodName, true); + } + + public static List findMethods(@NonNull Class c, String methodName, boolean includeDefault) { + return _findMethods(c, methodName, includeDefault, true); + } + + public static List findDeclaredMethod(@NonNull Class c, String methodName) { + return findDeclaredMethods(c, methodName, true); + } + + public static List findDeclaredMethods(@NonNull Class c, String methodName, boolean includeDefault) { + return _findMethods(c, methodName, includeDefault, false); + } + + /** + * Returns method from an object, matched by name. This may be considered as + * a slow operation, since methods are matched one by one. + * Returns only accessible methods. + * Only first method is matched. + * + * @param c class to examine + * @param methodName name of the method + * @param includeDefault 是否要包含Java8的默认方法 + * @param publicOnly 是否只支持public方法 + */ + private static List _findMethods(@NonNull Class c, String methodName, boolean includeDefault, + boolean publicOnly) { + List methods = publicOnly ? methodsCache.get(c) : declareMethodsCache.get(c); + Objects.requireNonNull(methods); + return methods.stream() + .filter(method -> includeDefault || !method.isDefault()) + .filter(method -> Objects.equals(methodName, method.getName())) + .collect(toList()); + } + + private static List _getMethods(@NonNull Class c) { + return Stream.of(c.getMethods()).collect(toList()); + } + + private static List _getDeclareMethods(@NonNull Class c) { + return Stream.of(c.getDeclaredMethods()).collect(toList()); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Reflects.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Reflects.java new file mode 100644 index 0000000..98c8599 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Reflects.java @@ -0,0 +1,109 @@ +package cn.axzo.framework.core.util; + +import org.joor.ReflectException; + +import java.lang.reflect.Method; + +/** + * @author liyong.tian + * @since 2017/11/10 上午1:35 + */ +public final class Reflects { + + // --------------------------------------------------------------------- + // Static API used as entrance points to the fluent API + // --------------------------------------------------------------------- + + public static Reflects on(Class clazz) { + return new Reflects(clazz); + } + + // --------------------------------------------------------------------- + // Members + // --------------------------------------------------------------------- + + /** + * The wrapped object + */ + private final Object object; + + /** + * A flag indicating whether the wrapped object is a {@link Class} (for + * accessing static fields and methods), or any other type of {@link Object} + * (for accessing instance fields and methods). + */ + private final boolean isClass; + + // --------------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------------- + + private Reflects(Class type) { + this.object = type; + this.isClass = true; + } + + private Reflects(Object object) { + this.object = object; + this.isClass = false; + } + + public T methodDefaultValue(String methodName, Class returnType) { + return returnType.cast(method(methodName).getDefaultValue()); + } + + public Object methodDefaultValue(String methodName) { + return method(methodName).getDefaultValue(); + } + + public Method method(String methodName) { + try { + return exactMethod(methodName); + } catch (NoSuchMethodException e) { + throw new ReflectException(e); + } + } + + /** + * Get the type of the wrapped object. + * + * @see Object#getClass() + */ + public Class type() { + if (isClass) { + return (Class) object; + } else { + return object.getClass(); + } + } + + /** + * Searches a method with the exact same signature as desired. + *

+ * If a public method is found in the class hierarchy, this method is returned. + * Otherwise a private method with the exact same signature is returned. + * If no exact match could be found, we let the {@code NoSuchMethodException} pass through. + */ + private Method exactMethod(String name, Class... types) throws NoSuchMethodException { + Class type = type(); + + // first priority: find a public method with exact signature match in class hierarchy + try { + return type.getMethod(name, types); + } + // second priority: find a private method with exact signature match on declaring class + catch (NoSuchMethodException e) { + do { + try { + return type.getDeclaredMethod(name, types); + } catch (NoSuchMethodException ignore) { + } + + type = type.getSuperclass(); + } + while (type != null); + + throw new NoSuchMethodException(); + } + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/SetUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/SetUtil.java new file mode 100644 index 0000000..7989b43 --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/SetUtil.java @@ -0,0 +1,96 @@ +package cn.axzo.framework.core.util; + +import cn.axzo.framework.core.InternalException; +import org.javatuples.Pair; +import org.javatuples.Triplet; + +import java.util.*; +import java.util.function.BiPredicate; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; +import static org.javatuples.Pair.with; + +/** + * @author liyong.tian + * @since 2017/3/15 + */ +public class SetUtil { + + public static Set addAll(Set baseSet, Set set) { + Set updated = new HashSet<>(baseSet); + updated.addAll(set); + return updated; + } + + @SafeVarargs + public static Set union(Set... sets) { + Set unionSet = new HashSet<>(); + Stream.of(sets).forEach(unionSet::addAll); + return unionSet; + } + + public static Set replace(Set set, E e) { + Set newSet = new HashSet<>(set); + if (newSet.removeIf(ele -> Objects.equals(ele, e))) { + newSet.add(e); + return newSet; + } + return newSet; + } + + public static Set merge(Set set, E e) { + return merge(set, Collections.singleton(e)); + } + + public static Set merge(Set set1, Set set2) { + return merge(set1, set2, false); + } + + public static Set merge(Set set1, Set set2, boolean allowRepeat) { + int expectSize = set1.size() + set2.size(); + Set set = Stream.concat(set1.stream(), set2.stream()).collect(toSet()); + if (!allowRepeat && set.size() != expectSize) { + throw new InternalException("The two set cannot repeat, set1 = " + set1 + ", set2 = " + set2); + } + return set; + } + + public static boolean match(Set pList, Set qList) { + if (pList.size() != qList.size()) { + return false; + } + Triplet, Set>, Set> triple = differ(pList, qList); + return triple.getValue0().isEmpty() && triple.getValue2().isEmpty() && triple.getValue1().size() == pList.size(); + } + + public static Triplet, Set>, Set> differ(Set pList, Set qList) { + return differ(pList, qList, Objects::equals); + } + + public static Triplet, Set>, Set> differ(Set

pList, Set qList, + BiPredicate equator) { + Set

pOnly = pList.stream() + .filter(p -> !contains(qList, p, (q, p1) -> equator.test(p1, q))) + .collect(toSet()); + + Set> both = pList.stream() + .map(p -> containsGet(qList, p, (q1, p1) -> equator.test(p1, q1)).map(q -> with(p, q)).orElse(null)) + .filter(Objects::nonNull) + .collect(toSet()); + + Set qOnly = qList.stream() + .filter(q -> !contains(pList, q, equator)) + .collect(toSet()); + + return Triplet.with(pOnly, both, qOnly); + } + + public static boolean contains(Set set, U u, BiPredicate equator) { + return set.stream().anyMatch(t -> equator.test(t, u)); + } + + public static Optional containsGet(Set set, U u, BiPredicate equator) { + return set.stream().filter(t -> equator.test(t, u)).findAny(); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringObjectMapBuilder.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringObjectMapBuilder.java new file mode 100644 index 0000000..a71452e --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringObjectMapBuilder.java @@ -0,0 +1,72 @@ +package cn.axzo.framework.core.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/26 上午11:45 + */ +@Deprecated +public class StringObjectMapBuilder { + + private final Map map; + + private boolean immutable; + + private boolean skipNullKey; + + private boolean skipNullValue; + + public StringObjectMapBuilder() { + this.map = new HashMap<>(); + this.immutable = true; + this.skipNullKey = true; + this.skipNullValue = true; + } + + public static StringObjectMapBuilder builder() { + return new StringObjectMapBuilder(); + } + + public StringObjectMapBuilder put(String key, Object value) { + if (skipNullKey && key == null) { + return this; + } + if (skipNullValue && value == null) { + return this; + } + map.put(key, value); + return this; + } + + public Map build() { + if (immutable) { + return Collections.unmodifiableMap(map); + } + return map; + } + + public StringObjectMapBuilder immutable() { + this.immutable = true; + return this; + } + + public StringObjectMapBuilder mutable() { + this.immutable = false; + return this; + } + + public StringObjectMapBuilder skipNullKey(boolean skipNullKey) { + this.skipNullKey = skipNullKey; + return this; + } + + public StringObjectMapBuilder skipNullValue(boolean skipNullValue) { + this.skipNullValue = skipNullValue; + return this; + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringUtil.java new file mode 100644 index 0000000..bd2a97b --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/StringUtil.java @@ -0,0 +1,242 @@ +package cn.axzo.framework.core.util; + +import jodd.util.StringPool; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import static jodd.util.StringPool.EMPTY; +import static jodd.util.StringPool.SPACE; + +/** + * @author liyong.tian + * @since 2016/12/17 + */ +public abstract class StringUtil { + + private static final Pattern INTEGER_PATTERN = Pattern.compile("^0|-?[1-9]\\d*$"); + private static final Pattern POSITIVE_INTEGER_PATTERN = Pattern.compile("^[1-9]\\d*$"); + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + @Nullable + public static String trim(String s) { + return s == null ? null : s.trim(); + } + + @Nullable + public static String trimAndToLowerCase(String s) { + return s == null ? null : s.trim().toLowerCase(); + } + + @Nullable + public static String trimAndRemoveLastChar(String text, Predicate removeLastChar) { + if (text == null) { + return null; + } + text = trim(text); + if (removeLastChar.test(text)) { + return text.substring(0, text.length() - 1); + } + return text; + } + + @Nonnull + public static Integer parseInt(String text) { + if (text == null) { + throw new IllegalArgumentException("Text must not be null"); + } + String trimmed = trim(text); + return isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed); + } + + /** + * 是否为整数:-1,0,1,2... + */ + public static boolean isInteger(String text) { + return text != null && INTEGER_PATTERN.matcher(trim(text)).matches(); + } + + /** + * 是否为正整数:1,2,3... + */ + public static boolean isPositiveInteger(String text) { + return text != null && POSITIVE_INTEGER_PATTERN.matcher(trim(text)).matches(); + } + + /** + * Determine whether the given {@code value} String indicates a hex number, + * i.e. needs to be passed into {@code Integer.decode} instead of + * {@code Integer.valueOf}, etc. + */ + public static boolean isHexNumber(String value) { + int index = (value.startsWith("-") ? 1 : 0); + return value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index); + } + + /** + * Determines if a string is empty (null or zero-length). + */ + public static boolean isEmpty(CharSequence string) { + return (string == null) || (string.length() == 0); + } + + /** + * Determines if a string is not empty (not null and not zero-length). + */ + public static boolean isNotEmpty(CharSequence string) { + return !isEmpty(string); + } + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * 去除字符串里的所有空格 + */ + @Nullable + public static String removeAllWhitespace(String s) { + return s == null ? null : s.replaceAll(SPACE, EMPTY); + } + + /** + * 首字母大写 + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * 首字母小写 + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (isEmpty(str)) { + return str; + } + + char firstChar = str.charAt(0); + char updatedChar; + if (capitalize) { + updatedChar = Character.toUpperCase(firstChar); + } else { + updatedChar = Character.toLowerCase(firstChar); + } + if (firstChar == updatedChar) { + return str; + } + + char[] chars = str.toCharArray(); + chars[0] = updatedChar; + return new String(chars, 0, chars.length); + } + + public static String stringOrSubstringAfterLast(String str, char c) { + if (str == null) { + return null; + } + int i = str.lastIndexOf(c); + if (i != -1) { + return str.substring(i + 1); + } + return str; + } + + public static String orEmpty(String value) { + if (value == null) { + return StringPool.EMPTY; + } + return value; + } + + @Nullable + public static String blankToNull(String value) { + if (jodd.util.StringUtil.isBlank(value)) { + return null; + } + return value; + } + + public static boolean isASCII(String str) { + return str != null && str.getBytes().length == str.length(); + } + + /** + * UTF8字符对应的字节取值范围(首字节表示该字符一共有几个字节) + * 1字节 0xxxxxxx + * 2字节 110xxxxx 10xxxxxx + * 3字节 1110xxxx 10xxxxxx 10xxxxxx + * 4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + * 5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * 6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + public static boolean isUTF8(String str) { + if (str == null) { + return false; + } + + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + + for (byte b : bytes) { + if ((b & 0xf0) == 0xf0) { + // 存在大于等于4字节的字符 + return false; + } + } + + return true; + } + + @Deprecated + public static String removeEmoji(String str) { + return filterBMPChars(str); + } + + /** + * 基本多语言范围(BMP-BasicMultilingualPlane),指1-3字节的UTF8字符的编码范围 + */ + public static String filterBMPChars(String str) { + if (str == null) { + return null; + } + + byte[] bytes1 = str.getBytes(StandardCharsets.UTF_8); + byte[] bytes2 = new byte[bytes1.length]; + + int i = 0; + int j = 0; + while (i < bytes1.length) { + byte b = bytes1[i]; + if ((b & 0xfc) == 0xfc) { + // 6字节字符 + i += 6; + continue; + } + if ((b & 0xf8) == 0xf8) { + // 5字节字符 + i += 5; + continue; + } + if ((b & 0xf0) == 0xf0) { + // 4字节字符 + i += 4; + continue; + } + i++; + bytes2[j++] = b; + } + + byte[] result = Arrays.copyOf(bytes2, j); + return new String(result); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Strings.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Strings.java new file mode 100644 index 0000000..1076d8b --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/Strings.java @@ -0,0 +1,22 @@ +package cn.axzo.framework.core.util; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/22 上午2:36 + */ +public abstract class Strings { + + public static boolean equals(String a, String b) { + return equals(a, b, false); + } + + public static boolean equalsIgnoreCase(String a, String b) { + return equals(a, b, true); + } + + public static boolean equals(String a, String b, boolean ignoreCase) { + return (a == null && b == null) || (a != null && (ignoreCase ? a.equalsIgnoreCase(b) : a.equals(b))); + } +} diff --git a/axzo-common-core/src/main/java/cn/axzo/framework/core/util/UnicodeUtil.java b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/UnicodeUtil.java new file mode 100644 index 0000000..4d163eb --- /dev/null +++ b/axzo-common-core/src/main/java/cn/axzo/framework/core/util/UnicodeUtil.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.core.util; + +/** + * @author liyong.tian + * @since 2018/8/7 + */ +public abstract class UnicodeUtil { + + public static String toUnicode(String string) { + StringBuilder unicode = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + // 取出每一个字符 + char c = string.charAt(i); + String str = Integer.toHexString(c); + switch (4 - str.length()) { + case 0: + str = "\\u" + str; + break; + case 1: + str = "\\u0" + str; + break; + default: + str = String.valueOf(c); + } + unicode.append(str); + } + return unicode.toString(); + } +} diff --git a/axzo-common-dependencies/pom.xml b/axzo-common-dependencies/pom.xml new file mode 100644 index 0000000..3629a13 --- /dev/null +++ b/axzo-common-dependencies/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + + cn.axzo.infra + axzo-dependencies + 2.0.0-SNAPSHOT + + + + axzo-common-dependencies + 1.0.0-SNAPSHOT + pom + Axzo Common Dependencies + + + 1.0.0-SNAPSHOT + + + + + + + cn.axzo.framework + axzo-common-core + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-math + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-validator + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-domain + ${axzo.commons.version} + + + + cn.axzo.framework.framework + axzo-common-context + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-boot + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-web + ${axzo.commons.version} + + + cn.axzo.framework + axzo-common-webmvc + ${axzo.commons.version} + + + + + cn.axzo.framework.logging + log4j2-starter + ${axzo.commons.version} + + + cn.axzo.framework.logging + logback-starter + ${axzo.commons.version} + + + + + cn.axzo.framework.client + retrofit-starter + ${axzo.commons.version} + + + + + cn.axzo.framework.jackson + jackson-datatype-enumstd + ${axzo.commons.version} + + + cn.axzo.framework.jackson + jackson-datatype-fraction + ${axzo.commons.version} + + + cn.axzo.framework.jackson + jackson-datatype-period + ${axzo.commons.version} + + + cn.axzo.framework.jackson + jackson-datatype-string-trim + ${axzo.commons.version} + + + cn.axzo.framework.jackson + jackson-utility + ${axzo.commons.version} + + + cn.axzo.framework.jackson + jackson-starter + ${axzo.commons.version} + + + + + + + + axzo-nexus + + false + + + + axzo-nexus + Nexus Axzo + https://nexus.axzo.cn/content/groups/public/ + + + + + axzo-nexus + Nexus Axzo + https://nexus.axzo.cn/content/groups/public/ + + + + + \ No newline at end of file diff --git a/axzo-common-domain/pom.xml b/axzo-common-domain/pom.xml new file mode 100644 index 0000000..920ac07 --- /dev/null +++ b/axzo-common-domain/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-domain + Axzo Common Domain + + + + + cn.axzo.framework + axzo-common-core + + + cn.axzo.framework + axzo-common-math + + + cn.axzo.framework + axzo-common-validator + + + com.google.guava + guava + + + org.jodd + jodd-props + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-collections4 + + + commons-io + commons-io + + + commons-codec + commons-codec + + + org.apache.commons + commons-text + + + + org.mapstruct + mapstruct + + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + org.springframework.data + spring-data-commons + provided + + + com.alibaba + druid + provided + + + com.netflix.hystrix + hystrix-core + provided + + + io.swagger + swagger-annotations + provided + + + \ No newline at end of file diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/ServiceException.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/ServiceException.java new file mode 100644 index 0000000..9b98df0 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/ServiceException.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.domain; + +import cn.axzo.framework.domain.web.code.IRespCode; +import lombok.Getter; + +import static cn.axzo.framework.domain.web.code.BaseCode.SERVICE_UNAVAILABLE; + +/** + * @Description 业务异常 + * @Author liyong.tian + * @Date 2020/9/7 19:24 + **/ +public class ServiceException extends RuntimeException { + + @Getter + private String code; + + public ServiceException() { + this(SERVICE_UNAVAILABLE); + } + + public ServiceException(IRespCode code) { + super(code.getMessage()); + this.code = code.getRespCode(); + } + + public ServiceException(IRespCode code, String message) { + super(message); + this.code = code.getRespCode(); + } + + public ServiceException(String message) { + super(message); + } + + public ServiceException(String message, Throwable cause) { + super(message, cause); + } + + public ServiceException(Throwable cause) { + super(cause); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/crypto/DESedeEncrypt.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/crypto/DESedeEncrypt.java new file mode 100644 index 0000000..54ec4c7 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/crypto/DESedeEncrypt.java @@ -0,0 +1,127 @@ +package cn.axzo.framework.domain.crypto; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESedeKeySpec; +import java.security.Key; +import java.util.UUID; + +/** + * @Description DES加密解密 + * @Author liyong.tian + * @Date 2020/9/7 19:27 + **/ +@Deprecated +public class DESedeEncrypt { + + /** + * 密钥算法 + */ + private static final String KEY_ALGORITHM = "DESede"; + + private static final String DEFAULT_CIPHER_ALGORITHM = "DESede/ECB/ISO10126Padding"; + + /** + * 转换密钥 + * + * @param key 二进制密钥 + * @return Key 密钥 + */ + private static Key initKey(byte[] key) throws Exception { + DESedeKeySpec desKey = new DESedeKeySpec(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generateSecret(desKey); + } + + /** + * 加密 + * + * @param data 待加密数据 + * @param key 二进制密钥 + * @return byte[] 加密数据 + */ + public static String encrypt(String key, String data) throws Exception { + return encrypt(key.getBytes(), data.getBytes(), DEFAULT_CIPHER_ALGORITHM); + } + + + /** + * 加密 + * + * @param data 待加密数据 + * @param key 二进制密钥 + * @param cipherAlgorithm 加密算法/工作模式/填充方式 + * @return byte[] 加密数据 + */ + public static String encrypt(byte[] key, byte[] data, String cipherAlgorithm) throws Exception { + Key k = initKey(key); + byte[] tmp = encrypt(k, data, cipherAlgorithm); + return Base64.encodeBase64String(tmp); + } + + /** + * 加密 + * + * @param data 待加密数据 + * @param key 密钥 + * @param cipherAlgorithm 加密算法/工作模式/填充方式 + * @return byte[] 加密数据 + */ + public static byte[] encrypt(Key key, byte[] data, String cipherAlgorithm) throws Exception { + // 实例化 + Cipher cipher = Cipher.getInstance(cipherAlgorithm); + // 使用密钥初始化,设置为加密模式 + cipher.init(Cipher.ENCRYPT_MODE, key); + // 执行操作 + return cipher.doFinal(data); + } + + /** + * 解密 + * + * @param data 待解密数据 + * @param key 二进制密钥 + * @return byte[] 解密数据 + */ + public static String decrypt(String key, String data) throws Exception { + return decrypt(key.getBytes(), Base64.decodeBase64(data), DEFAULT_CIPHER_ALGORITHM); + } + + /** + * 解密 + * + * @param data 待解密数据 + * @param key 二进制密钥 + * @param cipherAlgorithm 加密算法/工作模式/填充方式 + * @return byte[] 解密数据 + */ + public static String decrypt(byte[] key, byte[] data, String cipherAlgorithm) throws Exception { + Key k = initKey(key); + return decrypt(k, data, cipherAlgorithm); + } + + /** + * 解密 + * + * @param data 待解密数据 + * @param key 密钥 + * @param cipherAlgorithm 加密算法/工作模式/填充方式 + * @return byte[] 解密数据 + */ + public static String decrypt(Key key, byte[] data, String cipherAlgorithm) throws Exception { + Cipher cipher = Cipher.getInstance(cipherAlgorithm); + cipher.init(Cipher.DECRYPT_MODE, key); + return new String(cipher.doFinal(data)); + } + + /** + * 生成密钥key + */ + public static String generateKey(String targat, String sys) { + StringBuilder build = new StringBuilder(); + String uuid = UUID.randomUUID().toString(); + return build.append(targat).append(Base64.encodeBase64String(uuid.getBytes())).append(sys).toString(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdGenerator.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdGenerator.java new file mode 100644 index 0000000..a12356a --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdGenerator.java @@ -0,0 +1,192 @@ +package cn.axzo.framework.domain.data; + +import cn.axzo.framework.core.FetchException; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.net.Inets; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.locks.ReentrantLock; + +import static cn.axzo.framework.core.Constants.*; +import static java.lang.String.format; +import static java.time.ZoneId.systemDefault; +import static java.util.concurrent.TimeUnit.DAYS; + +/** + * @Description 分布式ID生成器 + * @Author liyong.tian + * @Date 2020/9/7 19:29 + **/ +@Slf4j +public class IdGenerator { + private final static int NODE_ID_BITS = 10; //机器号10位 + private final static int MAX_NODE_ID = ~(-1 << NODE_ID_BITS); //机器号最大值:1023 + + private final ReentrantLock lock = new ReentrantLock(); + private int sequence = 0; + private long lastTimestamp = -1L; + + @Getter + private final long baseTimestamp; //基准时间戳 + private final int nodeId; //机器号 + private final int sequenceBits; //序列号位数 + private final int sequenceMask; //序列号最大值 + @Getter + private final int timestampLeftShift; //时间毫秒数左移位数 + + public IdGenerator() { + this(null); + } + + public IdGenerator(Integer nodeId) { + this(nodeId, null, null); + } + + public IdGenerator(Long baseTimestamp, Integer sequenceBits) { + this(null, baseTimestamp, sequenceBits); + } + + public IdGenerator(Integer nodeId, Long baseTimestamp, Integer sequenceBits) { + //默认值 + final long defaultBaseTimestamp = 1288834974657L; //基准时间戳:2010-11-04T09:42:54.657+08:00[Asia/Shanghai] + final int defaultSequenceBits = 12; + if (nodeId == null) { + nodeId = getDefaultNodeId(); + } + if (baseTimestamp == null) { + baseTimestamp = defaultBaseTimestamp; + } + if (sequenceBits == null) { + sequenceBits = defaultSequenceBits; + } + + //校验 + if (nodeId > MAX_NODE_ID || nodeId < 0) { + throw new IllegalArgumentException(format("node Id can't be greater than %d or less than 0", MAX_NODE_ID)); + } + if (sequenceBits > 12 || sequenceBits < 0) { + throw new IllegalArgumentException(format("sequence bits can't be greater than %d or less than 0", 12)); + } + if (baseTimestamp > currentTimeMillis()) { + throw new IllegalArgumentException("base timestamp can't be greater than now"); + } + + //初始化 + this.baseTimestamp = baseTimestamp; + this.nodeId = nodeId; + this.sequenceBits = sequenceBits; + this.sequenceMask = ~(-1 << sequenceBits); + this.timestampLeftShift = NODE_ID_BITS + sequenceBits; + } + + public String nextNo(String custom) { + return nextNo(ChronoUnit.DAYS, custom); + } + + public String nextNo(ChronoUnit truncatedTo, String custom) { + lock.lock(); + try { + //更新时间戳和序列号 + _updateTimestampAndSequence(); + + //基量时间戳 + ZonedDateTime zonedDateTime = Instant.ofEpochMilli(lastTimestamp).atZone(systemDefault()); + long baseTimestamp = zonedDateTime.truncatedTo(truncatedTo).toInstant().toEpochMilli(); + + //时间戳增量 + long timestampInc = lastTimestamp - baseTimestamp; + + // 000000000000000000000000000000000000000000 0000000000 000000000000 + // timestamp(41b) nodeId(10b) sequence + long suffix = (timestampInc << timestampLeftShift) | (nodeId << sequenceBits) | sequence; + + //时间前缀 + String timePrefix; + switch (truncatedTo) { + case DAYS: + timePrefix = FORMATTER_DATE_COMPACT.format(zonedDateTime); + break; + case HOURS: + timePrefix = FORMATTER_DATE_HOUR_COMPACT.format(zonedDateTime); + break; + case MINUTES: + timePrefix = DateTimeFormatter.ofPattern("yyyyMMddHHmm").format(zonedDateTime); + break; + case SECONDS: + timePrefix = FORMATTER_DATE_TIME_COMPACT.format(zonedDateTime); + break; + default: + throw new InternalException("truncatedTo[" + truncatedTo + "] cannot not support"); + } + return timePrefix + custom + suffix; + } finally { + lock.unlock(); + } + } + + public long nextId() { + lock.lock(); + try { + //更新时间戳和序列号 + _updateTimestampAndSequence(); + + //时间戳增量 + long timestampInc = lastTimestamp - baseTimestamp; + + // 000000000000000000000000000000000000000000 0000000000 000000000000 + // timestamp(41b) nodeId(10b) sequence + return (timestampInc << timestampLeftShift) | (nodeId << sequenceBits) | sequence; + } finally { + lock.unlock(); + } + } + + private void _updateTimestampAndSequence() { + long timestamp = currentTimeMillis(); //获取当前毫秒数 + //如果服务器时间有问题(时钟后退) 报错。 + if (timestamp < lastTimestamp) { + throw new RuntimeException(format( + "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + //如果上次生成时间和当前时间相同,在同一毫秒内 + if (lastTimestamp == timestamp) { + //sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位 + sequence = (sequence + 1) & sequenceMask; + //判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0 + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); //自旋等待到下一毫秒 + } + } else { + sequence = 0; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加 + } + lastTimestamp = timestamp; + } + + private static long tilNextMillis(long lastTimestamp) { + long timestamp = currentTimeMillis(); + while (timestamp <= lastTimestamp) { + timestamp = currentTimeMillis(); + } + return timestamp; + } + + private static long currentTimeMillis() { + return System.currentTimeMillis(); + } + + private static int getDefaultNodeId() { + try { + int ipAddressAsInt = Inets.fetchLocalIpAsInt(1); + return Math.abs(ipAddressAsInt) & MAX_NODE_ID; + } catch (FetchException e) { + log.error("nodeId initialized error", e); + return 0; + } + } +} + diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdHelper.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdHelper.java new file mode 100644 index 0000000..b89fa8a --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/IdHelper.java @@ -0,0 +1,96 @@ +package cn.axzo.framework.domain.data; + +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +import static cn.axzo.framework.core.Constants.PATTERN_DATE_TIME_MILLS_COMPACT; +import static cn.axzo.framework.core.time.Dates.now; +import static jodd.util.StringPool.EMPTY; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 19:48 + **/ +public abstract class IdHelper { + + public static final UUID ZERO_UUID = new UUID(0, 0); + + private static volatile IdGenerator idGenerator; + + /** + * 全局分布式唯一序号 + */ + public static String getNo(ChronoUnit truncatedTo, String custom) { + _init(); + return idGenerator.nextNo(truncatedTo, custom); + } + + /** + * 全局分布式唯一序号 + */ + public static String getNo(String custom) { + _init(); + return idGenerator.nextNo(custom); + } + + /** + * 全局分布式唯一序号 + */ + public static String getNo() { + return getNo(EMPTY); + } + + /** + * 全局分布式唯一序号(年月日开头) + */ + public static long getPrettyId() { + return Long.parseLong(getNo(EMPTY)); + } + + /** + * 全局分布式唯一ID + */ + public static long getId() { + _init(); + return idGenerator.nextId(); + } + + /** + * 32位UUID + */ + public static String get32UUID() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 生成唯一键中的时间戳,默认格式:yyMMddHHmmssSSS + */ + public static String getTimeToken() { + return now().asString(PATTERN_DATE_TIME_MILLS_COMPACT); + } + + /** + * 获取ID生成器 + */ + public static IdGenerator getGenerator() { + _init(); + return idGenerator; + } + + /** + * 重设参数 + */ + public synchronized static void reload(Integer nodeId, Long baseTimestamp, Integer sequenceBits) { + if (nodeId == null && baseTimestamp == null && sequenceBits == null) { + return; + } + idGenerator = new IdGenerator(nodeId, baseTimestamp, sequenceBits); + } + + private synchronized static void _init() { + if (idGenerator == null) { + idGenerator = new IdGenerator(); + } + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/ReentrantDruidDataSource.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/ReentrantDruidDataSource.java new file mode 100644 index 0000000..b044ee2 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/data/ReentrantDruidDataSource.java @@ -0,0 +1,35 @@ +package cn.axzo.framework.domain.data; + +import com.alibaba.druid.pool.DruidDataSource; + +/** + * @Description 允许重复初始化(适配Spring Cloud环境). + * @Author liyong.tian + * @Date 2020/9/7 19:52 + **/ +public class ReentrantDruidDataSource extends DruidDataSource { + + @Override + public void setUrl(String jdbcUrl) { + if (inited) { + return; + } + super.setUrl(jdbcUrl); + } + + @Override + public void setUsername(String username) { + if (inited) { + return; + } + super.setUsername(username); + } + + @Override + public void setPassword(String password) { + if (inited) { + return; + } + super.setPassword(password); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpHeaderUtil.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpHeaderUtil.java new file mode 100644 index 0000000..e64bb1c --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpHeaderUtil.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.domain.http; + +import com.google.common.net.HttpHeaders; +import lombok.experimental.UtilityClass; +import org.jooq.lambda.Seq; + +import java.util.List; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:01 + **/ +@UtilityClass +public class HttpHeaderUtil { + + public boolean isMultipartRequest(List headers) { + return Seq.seq(headers) + .map(String::toLowerCase) + .filter(header -> header.contains(HttpHeaders.CONTENT_TYPE.toLowerCase())) + .anyMatch(header -> header.contains("multipart/form-data")); + } + + public boolean isDownloadResponse(List headers) { + return Seq.seq(headers) + .map(String::toLowerCase) + .anyMatch(header -> header.contains(HttpHeaders.CONTENT_DISPOSITION.toLowerCase())); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpLogFormatter.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpLogFormatter.java new file mode 100644 index 0000000..30a5945 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpLogFormatter.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.domain.http; + +import javax.annotation.Nonnull; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:02 + **/ +public interface HttpLogFormatter { + + @Nonnull + String format(@Nonnull HttpRequestLog requestLog, @Nonnull HttpResponseLog responseLog); + + @Nonnull + String format(@Nonnull HttpRequestLog requestLog); + + @Nonnull + String format(@Nonnull HttpResponseLog responseLog); +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpRequestLog.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpRequestLog.java new file mode 100644 index 0000000..5c5193f --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpRequestLog.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.domain.http; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * @Description 请求日志 + * @Author liyong.tian + * @Date 2020/9/7 20:03 + **/ +@Getter +@Builder +public class HttpRequestLog { + + // 协议 + private String protocol; + + // 请求地址 + private String url; + + // 请求方法 + private String method; + + // 请求头 + @Singular + private List headers; + + // 请求体 + @Nullable + private String body; + + int calcSize() { + int size = protocol.length() + url.length() + method.length(); + for (String header : headers) { + size += header.length(); + } + size += body == null ? 0 : body.length(); + return size; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpResponseLog.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpResponseLog.java new file mode 100644 index 0000000..c481993 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/HttpResponseLog.java @@ -0,0 +1,56 @@ +package cn.axzo.framework.domain.http; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import javax.annotation.Nullable; +import java.util.List; + +import static jodd.util.StringPool.EMPTY; + +/** + * @Description 响应日志 + * @Author liyong.tian + * @Date 2020/9/7 20:04 + **/ +@Getter +@Builder +public class HttpResponseLog { + + // 响应状态码 + private Integer status; + + // 响应信息 + @Builder.Default + private String msg = EMPTY; + + // 响应地址 + private String url; + + // 响应头 + @Singular + private List headers; + + // 响应体 + @Nullable + private String body; + + // 耗时(ms) + private long tookMs; + + // 异常 + @Nullable + private String errorMsg; + + int calcSize() { + int size = String.valueOf(status).length() + msg.length() + url.length(); + for (String header : headers) { + size += header.length(); + } + size += body == null ? 0 : body.length(); + size += String.valueOf(tookMs).length(); + size += errorMsg == null ? 0 : errorMsg.length(); + return size; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/JsonHttpLogFormatter.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/JsonHttpLogFormatter.java new file mode 100644 index 0000000..b8deaf5 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/JsonHttpLogFormatter.java @@ -0,0 +1,80 @@ +package cn.axzo.framework.domain.http; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:05 + **/ +public enum JsonHttpLogFormatter implements HttpLogFormatter { + + INSTANCE; + + @Nonnull + @Override + public String format(@Nonnull HttpRequestLog requestLog, @Nonnull HttpResponseLog responseLog) { + Objects.requireNonNull(requestLog, "HttpRequestLog cannot be null"); + Objects.requireNonNull(responseLog, "HttpResponseLog cannot be null"); + StringBuilder sb = new StringBuilder(requestLog.calcSize() + responseLog.calcSize() + 200); + _append(sb, requestLog); + _append(sb, responseLog); + return sb.toString(); + } + + @Nonnull + @Override + public String format(@Nonnull HttpRequestLog requestLog) { + Objects.requireNonNull(requestLog, "HttpRequestLog cannot be null"); + StringBuilder sb = new StringBuilder(requestLog.calcSize() + 100); + _append(sb, requestLog); + return sb.toString(); + } + + @Nonnull + @Override + public String format(@Nonnull HttpResponseLog responseLog) { + Objects.requireNonNull(responseLog, "HttpResponseLog cannot be null"); + StringBuilder sb = new StringBuilder(responseLog.calcSize() + 100); + _append(sb, responseLog); + return sb.toString(); + } + + private void _append(StringBuilder sb, @Nonnull HttpRequestLog log) { + // --> 表示发送 + String headers = String.join(",\n\t\t", log.getHeaders()); + sb.append("\n-->request start\n{"); + sb.append("\n\turl: ").append(log.getUrl()).append(","); + sb.append("\n\tmethod: ").append(log.getMethod()).append(","); + sb.append("\n\theaders: ").append("[\n\t\t").append(headers).append("\n\t]").append(","); + if (log.getBody() != null) { + sb.append("\n\tbody: ").append(log.getBody().replaceAll("\\n", "")).append(","); + } + sb.append("\n\tprotocol: ").append(log.getProtocol()); + sb.append("\n}\n-->request end"); + } + + private void _append(StringBuilder sb, @Nonnull HttpResponseLog log) { + // <--表示接收 + if (log.getErrorMsg() == null) { + String headers = "[\n\t\t" + String.join(",\n\t\t", log.getHeaders()) + "\n\t]"; + sb.append("\n<--response start\n{"); + sb.append("\n\tstatus: ").append(log.getStatus()).append(","); + sb.append("\n\tmsg: ").append(log.getMsg()).append(","); + sb.append("\n\turl: ").append(log.getUrl()).append(","); + sb.append("\n\theaders: ").append(headers).append(","); + if (log.getBody() != null) { + sb.append("\n\tbody: ").append(log.getBody().replaceAll("\\n", "")).append(","); + } + sb.append("\n\ttookMs: ").append(log.getTookMs()).append("ms"); + sb.append("\n}\n<--response end"); + } else { + sb.append("\n<--response start\n{"); + sb.append("\n\turl: ").append(log.getUrl()).append(","); + sb.append("\n\terrorMsg: ").append(log.getErrorMsg()); + sb.append("\n\ttookMs: ").append(log.getTookMs()).append("ms"); + sb.append("\n}\n<--response end"); + } + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/VerboseHttpLogFormatter.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/VerboseHttpLogFormatter.java new file mode 100644 index 0000000..9bc895e --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/http/VerboseHttpLogFormatter.java @@ -0,0 +1,60 @@ +package cn.axzo.framework.domain.http; + +import com.google.common.base.Joiner; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Objects; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:06 + **/ +public enum VerboseHttpLogFormatter implements HttpLogFormatter { + + INSTANCE; + + private static final String T_HEAD = "║ "; + + private static final String F_BREAK = " %n"; + private static final String F_URL = " %s"; + private static final String F_TIME = T_HEAD + "Elapsed Time: %.1fms"; + private static final String F_HEADERS = T_HEAD + "Headers: %s"; + private static final String F_RESPONSE = "Response: %d"; + private static final String F_BODY = "Body: %s"; + + private static final String U_BREAKER = F_BREAK + "╔═════════════════════════════════════════════════════════════════════════" + F_BREAK + T_HEAD; + private static final String D_BREAKER = "╚═════════════════════════════════════════════════════════════════════════" + F_BREAK; + + private static final String F_REQUEST_WITHOUT_BODY = F_URL + F_BREAK + F_HEADERS + F_BREAK + T_HEAD; + private static final String F_REQUEST_WITH_BODY = F_URL + F_BREAK + F_HEADERS + F_BREAK + T_HEAD + F_BODY + F_BREAK; + private static final String F_RESPONSE_WITH_BODY = T_HEAD + F_BREAK + F_TIME + F_BREAK + T_HEAD + F_RESPONSE + F_BREAK + F_HEADERS + F_BODY + F_BREAK + D_BREAKER; + + @Nonnull + public String format(@Nonnull HttpRequestLog log) { + if (Objects.equals(log.getMethod(), "GET")) { + String pattern = U_BREAKER + "GET: " + F_REQUEST_WITHOUT_BODY; + return String.format(pattern, log.getUrl(), convertHeaders(log.getHeaders())); + } else { + String pattern = U_BREAKER + log.getMethod() + ": " + F_REQUEST_WITH_BODY; + return String.format(pattern, log.getUrl(), convertHeaders(log.getHeaders()), log.getBody()); + } + } + + @Nonnull + public String format(@Nonnull HttpResponseLog log) { + double tookMs = log.getTookMs() + 0.0; + return String.format(F_RESPONSE_WITH_BODY, tookMs, log.getStatus(), convertHeaders(log.getHeaders()), log.getBody()); + } + + private String convertHeaders(List headers) { + return Joiner.on("\n" + T_HEAD).join(headers); + } + + @Nonnull + @Override + public String format(@Nonnull HttpRequestLog requestLog, @Nonnull HttpResponseLog responseLog) { + return format(requestLog) + format(responseLog); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/AbstractPageRequest.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/AbstractPageRequest.java new file mode 100644 index 0000000..9fdca0d --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/AbstractPageRequest.java @@ -0,0 +1,153 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.domain.sort.Order; +import cn.axzo.framework.domain.sort.Sort; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import javax.annotation.Nullable; +import java.util.function.Function; + +import static cn.axzo.framework.domain.sort.SortDefaults.RESORT_STRATEGY; + + +/** + * @author liyong.tian + * @since 2017/2/15 + */ +@ToString +@EqualsAndHashCode +public abstract class AbstractPageRequest implements Pageable { + + private final int page; + private final int size; + + @Getter + private final int defaultPageSize; + @Getter + private final int maxPageSize; + + private final boolean needTotal; + private final boolean needContent; + + @Getter + private final boolean fixEdge; + private final PageableVerbose verbose; + @Getter + private final boolean pageNumberOneIndexed; + private final ResortStrategy resortStrategy; + + private Sort sort; + + public AbstractPageRequest(int page, int size) { + this(page, size, + PageDefaults.PAGE_SIZE, + PageDefaults.MAX_PAGE_SIZE, + PageDefaults.NEED_TOTAL, + PageDefaults.NEED_CONTENT, + PageDefaults.IS_FIX_EDGE, + PageDefaults.PAGEABLE_VERBOSE, + PageDefaults.PAGE_NUMBER_ONE_INDEXED, + RESORT_STRATEGY + ); + } + + /** + * Creates a new {@link AbstractPageRequest}. + * + * @param page 页码 + * @param size 每页数量 + * @param needTotal 是否需要查询总记录数 + * @param needContent 是否需要查询记录列表 + * @param fixEdge 是否纠正分页边界错误,比如当page<起始页时,自动设置page=起始页 + * @param verbose 返回冗余数据范围 + * @param pageNumberOneIndexed 页码是否从1开始 + * @param resortStrategy 全局重排序策略 + * @param orders 排序规则 + */ + public AbstractPageRequest(final int page, + final int size, + final int defaultPageSize, + final int maxPageSize, + final boolean needTotal, + final boolean needContent, + final boolean fixEdge, + final PageableVerbose verbose, + final boolean pageNumberOneIndexed, + final ResortStrategy resortStrategy, + final Order... orders) { + this.defaultPageSize = defaultPageSize; + this.maxPageSize = maxPageSize; + this.needTotal = needTotal; + this.needContent = needContent; + this.verbose = verbose == null ? PageDefaults.PAGEABLE_VERBOSE : verbose; + this.fixEdge = fixEdge; + this.pageNumberOneIndexed = pageNumberOneIndexed; + this.resortStrategy = resortStrategy == null ? RESORT_STRATEGY : resortStrategy; + this.size = size < 1 ? defaultPageSize : size > maxPageSize ? maxPageSize : size; + if (fixEdge) { + this.page = page < getFirstPageNumber() ? getFirstPageNumber() : page; + } else { + this.page = page; + } + if (this.page < getFirstPageNumber()) { + throw new PageRequestException("page number must not be less than " + getFirstPageNumber()); + } + if (orders.length > 0) { + this.sort = new Sort(orders); + } + } + + @Override + public int getPageSize() { + return size; + } + + @Override + public int getPageNumber() { + return page; + } + + @Override + public boolean needTotal() { + return needTotal; + } + + @Override + public boolean needContent() { + return needContent; + } + + @Override + public PageableVerbose verbose() { + return verbose; + } + + @Override + public ResortStrategy resortStrategy() { + return resortStrategy; + } + + @Nullable + @Override + public Sort getSort(boolean applyResortStrategy) { + return sort == null ? null : applyResortStrategy ? sort.resort(resortStrategy) : sort; + } + + @Override + public void resort(String property, String... newProperties) { + Sort sort = getSort(); + if (sort != null) { + this.sort = sort.resort(property, newProperties); + } + } + + @Override + public void resort(String property, Function resortHandler) { + Sort sort = getSort(); + if (sort != null) { + this.sort = sort.resort(property, resortHandler); + } + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Page.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Page.java new file mode 100644 index 0000000..aae6f97 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Page.java @@ -0,0 +1,146 @@ +package cn.axzo.framework.domain.page; + + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +/** + * 分页查询结果 + * + * @author liyong.tian + * @since 2017/2/15 + */ +public interface Page extends Iterable { + + /** + * 结果集 + */ + List getContent(); + + /** + * 总数(如果不做count查询,则为null) + */ + Long getTotal(); + + /** + * 总页数(如果不做count查询,则如果结果集大小不等于每页数量,就返回当前页,反之,返回null) + */ + @Nullable + default Integer getLastPageNumber() { + if (getTotal() == null) { + return getContent().size() != current().getPageSize() ? current().getPageNumber() : null; + } + return PageUtil.getLastPageNumber(getTotal(), current().getPageSize()); + } + + /** + * 当前页是否为第一页 + */ + default boolean isFirst() { + return current().getPageNumber() == current().getFirstPageNumber(); + } + + /** + * 当前页是否为最后一页(如果不做count查询,则只要结果集大小不等于每页数量,就返回false) + */ + default boolean isLast() { + if (getLastPageNumber() == null) { + return getContent().size() != current().getPageSize(); + } + return current().getPageNumber() == getLastPageNumber(); + } + + /** + * 当前页是否有上一页 + */ + default boolean hasPrevious() { + return current().getPageNumber() > current().getFirstPageNumber(); + } + + /** + * 当前页是否有下一页(如果不做count查询,则只要结果集大小等于每页数量,就返回true) + */ + default boolean hasNext() { + if (getLastPageNumber() == null) { + return getContent().size() == current().getPageSize(); + } + return current().getPageNumber() < getLastPageNumber(); + } + + /** + * 第一页 + */ + @Nonnull + default Pageable first() { + return current().jumpTo(current().getFirstPageNumber()); + } + + /** + * 上一页 + */ + @Nonnull + default Pageable previous() { + return hasPrevious() ? current().jumpTo(current().getPageNumber() - 1) : first(); + } + + /** + * 当前页 + */ + @Nonnull + Pageable current(); + + /** + * 下一页 + * 1.如果有下一页,则返回下一页 + * 2.如果没有最后一页,则始终跳至下一页 + *

+ * 注:若无count查询,则等到结果集数量小于每页数量,一定会产生最后一页 + */ + @Nonnull + default Pageable next() { + if (hasNext()) { + return current().jumpTo(current().getPageNumber() + 1); + } + Pageable last = last(); + return last == null ? current().jumpTo(current().getPageNumber() + 1) : last; + } + + /** + * 最后一页( + *

+ * 1.如果没有下一页,则当前页就是最后一页 + * 2.如果不做count查询,则为null + *

+ * 注:若无count查询,则等到结果集数量小于每页数量,一定会产生最后一页 + */ + default Pageable last() { + if (!hasNext()) { + return current().copy(); + } + return getLastPageNumber() == null ? null : current().jumpTo(getLastPageNumber()); + } + + /** + * Returns a new {@link Page} with the content of the current one mapped by the given {@link Function}. + * + * @param mapper must not be {@literal null}. + */ + @Nonnull + Page map(Function mapper); + + @Nonnull + Page mapAll(Function, List> mapper); + + @Nonnull + @Override + default Iterator iterator() { + if (getContent() == null) { + return new ArrayList().iterator(); + } + return getContent().iterator(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageBuilder.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageBuilder.java new file mode 100644 index 0000000..0f87219 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageBuilder.java @@ -0,0 +1,111 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.domain.sort.Direction; +import cn.axzo.framework.domain.sort.Order; +import cn.axzo.framework.domain.sort.Sort; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Stream; + +import static cn.axzo.framework.domain.page.PageDefaults.*; +import static cn.axzo.framework.domain.sort.SortDefaults.RESORT_STRATEGY; +import static com.google.common.collect.Lists.newArrayList; + +/** + * @author liyong.tian + * @since 2017/2/17 + */ +public class PageBuilder { + private int page; + private int size = PAGE_SIZE; + private int defaultPageSize = PAGE_SIZE; + private int maxPageSize = MAX_PAGE_SIZE; + + private boolean needTotal = NEED_TOTAL; + private boolean needContent = NEED_CONTENT; + private boolean fixEdge = IS_FIX_EDGE; + private PageableVerbose verbose = PAGEABLE_VERBOSE; + private boolean pageNumberOneIndexed = PAGE_NUMBER_ONE_INDEXED; + private ResortStrategy resortStrategy = RESORT_STRATEGY; + private List orders = newArrayList(); + + private PageBuilder(int page) { + this.page = page; + } + + public static PageBuilder firstPage() { + return page(1); + } + + public static PageBuilder page(int page) { + return new PageBuilder(page); + } + + public PageBuilder size(int size) { + this.size = size; + return this; + } + + public PageBuilder defaultPageSize(int defaultPageSize) { + this.defaultPageSize = defaultPageSize; + return this; + } + + public PageBuilder maxPageSize(int maxPageSize) { + this.maxPageSize = maxPageSize; + return this; + } + + public PageBuilder needTotal(boolean needTotal) { + this.needTotal = needTotal; + return this; + } + + public PageBuilder needContent(boolean needContent) { + this.needContent = needContent; + return this; + } + + public PageBuilder fixEdge(boolean fixEdge) { + this.fixEdge = fixEdge; + return this; + } + + public PageBuilder verbose(PageableVerbose verbose) { + this.verbose = verbose; + return this; + } + + public PageBuilder pageNumberOneIndexed(boolean pageNumberOneIndexed) { + this.pageNumberOneIndexed = pageNumberOneIndexed; + this.page = Math.max(PageUtil.getFirstPageNumber(pageNumberOneIndexed), this.page); + return this; + } + + public PageBuilder resortStrategy(ResortStrategy resortStrategy) { + this.resortStrategy = resortStrategy; + return this; + } + + public PageBuilder sort(Direction direction, String... fields) { + Stream.of(fields).forEach(field -> orders.add(new Order(direction, field))); + return this; + } + + public PageBuilder sort(@Nullable Sort sort) { + if (sort != null) { + sort.forEach(orders::add); + } + return this; + } + + public Pageable build() { + return new PageRequest( + page, size, defaultPageSize, maxPageSize, + needTotal, needContent, + fixEdge, verbose, pageNumberOneIndexed, resortStrategy, + orders.toArray(new Order[]{}) + ); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageConfigKey.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageConfigKey.java new file mode 100644 index 0000000..7f07a9f --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageConfigKey.java @@ -0,0 +1,9 @@ +package cn.axzo.framework.domain.page; + +/** + * @author liyong.tian + * @since 2017/4/6 + */ +public enum PageConfigKey { + NEED_TOTAL, NEED_CONTENT, FIX_EDGE, PAGE_NUMBER_ONE_INDEXED +} \ No newline at end of file diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageDefaults.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageDefaults.java new file mode 100644 index 0000000..e176c51 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageDefaults.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.domain.page; + +/** + * @author liyong.tian + * @since 2017/2/17 + */ +public interface PageDefaults { + + // 最大页码 + int MAX_PAGE_SIZE = 2000; + + // 默认每页显示数量 + int PAGE_SIZE = 20; + + // 默认要查询总记录数 + boolean NEED_TOTAL = true; + + // 默认要查询记录列表 + boolean NEED_CONTENT = true; + + // 默认不自动修正分页参数 + boolean IS_FIX_EDGE = false; + + // 默认不返回分页冗余信息 + PageableVerbose PAGEABLE_VERBOSE = PageableVerbose.NONE; + + // 默认起始页为1 + boolean PAGE_NUMBER_ONE_INDEXED = true; +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageImpl.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageImpl.java new file mode 100644 index 0000000..d253ba6 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageImpl.java @@ -0,0 +1,59 @@ +package cn.axzo.framework.domain.page; + +import lombok.Data; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * @author liyong.tian + * @since 2017/2/15 + */ +@Data +public class PageImpl implements Page { + + private final List content = new ArrayList<>(); + private final Long total; + private final Pageable current; + + public PageImpl(List content, Pageable pageable) { + this(content, pageable, null); + } + + public PageImpl(List content, Pageable pageable, Long total) { + if (content == null) { + throw new IllegalArgumentException("Content must not be null!"); + } + if (pageable == null) { + throw new IllegalArgumentException("Pageable must not be null!"); + } + this.content.addAll(content); + this.current = pageable; + this.total = total; + } + + @Nonnull + @Override + public Pageable current() { + return current; + } + + @Nonnull + @Override + public Page map(Function mapper) { + List result = new ArrayList<>(content.size()); + for (T element : this) { + result.add(mapper.apply(element)); + } + return new PageImpl<>(result, current, total); + } + + @Nonnull + @Override + public Page mapAll(Function, List> mapper) { + List result = mapper.apply(content); + return new PageImpl<>(result, current, total); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequest.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequest.java new file mode 100644 index 0000000..4cfeb46 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequest.java @@ -0,0 +1,113 @@ +package cn.axzo.framework.domain.page; + + +import cn.axzo.framework.domain.sort.Order; +import cn.axzo.framework.domain.sort.SortDefaults; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static cn.axzo.framework.domain.page.PageConfigKey.PAGE_NUMBER_ONE_INDEXED; + +/** + * @author liyong.tian + * @since 2017/2/15 + */ +public class PageRequest extends AbstractPageRequest { + + public PageRequest(int page) { + super(page, PageDefaults.PAGE_SIZE); + } + + public PageRequest(int page, int size) { + super(page, size); + } + + /** + * @see PageBuilder + */ + PageRequest(int page, + int size, + int defaultPageSize, + int maxPageSize, + boolean needTotal, + boolean needContent, + boolean fixEdge, + PageableVerbose verbose, + boolean pageNumberOneIndexed, + ResortStrategy resortStrategy, + Order... orders) { + super(page, size, defaultPageSize, maxPageSize, needTotal, needContent, fixEdge, verbose, pageNumberOneIndexed, + resortStrategy, orders); + } + + @Nonnull + @Override + public Pageable jumpTo(int page) { + return PageBuilder.page(page) + .size(getPageSize()) + .defaultPageSize(getDefaultPageSize()) + .maxPageSize(getMaxPageSize()) + .needTotal(needTotal()) + .needContent(needContent()) + .fixEdge(isFixEdge()) + .verbose(verbose()) + .pageNumberOneIndexed(isPageNumberOneIndexed()) + .resortStrategy(resortStrategy()) + .sort(getSort()) + .build(); + } + + @Nonnull + @Override + public Pageable config(PageConfigKey key, boolean enabled) { + return PageBuilder.page(getPageNumber()) + .size(getPageSize()) + .defaultPageSize(getDefaultPageSize()) + .maxPageSize(getMaxPageSize()) + .needTotal(key == PageConfigKey.NEED_TOTAL ? enabled : needTotal()) + .needContent(key == PageConfigKey.NEED_CONTENT ? enabled : needTotal()) + .fixEdge(key == PageConfigKey.FIX_EDGE ? enabled : isFixEdge()) + .verbose(verbose()) + .pageNumberOneIndexed(key == PAGE_NUMBER_ONE_INDEXED ? enabled : isPageNumberOneIndexed()) + .resortStrategy(resortStrategy()) + .sort(getSort()) + .build(); + } + + @Nonnull + @Override + public Pageable config(@Nullable PageableVerbose verbose) { + verbose = verbose == null ? PageDefaults.PAGEABLE_VERBOSE : verbose; + return PageBuilder.page(getPageNumber()) + .size(getPageSize()) + .defaultPageSize(getDefaultPageSize()) + .maxPageSize(getMaxPageSize()) + .needTotal(needTotal()) + .needContent(needTotal()) + .fixEdge(isFixEdge()) + .verbose(verbose) + .pageNumberOneIndexed(isPageNumberOneIndexed()) + .resortStrategy(resortStrategy()) + .sort(getSort()) + .build(); + } + + @Nonnull + @Override + public Pageable config(@Nullable ResortStrategy strategy) { + strategy = strategy == null ? SortDefaults.RESORT_STRATEGY : strategy; + return PageBuilder.page(getPageNumber()) + .size(getPageSize()) + .defaultPageSize(getDefaultPageSize()) + .maxPageSize(getMaxPageSize()) + .needTotal(needTotal()) + .needContent(needTotal()) + .fixEdge(isFixEdge()) + .verbose(verbose()) + .pageNumberOneIndexed(isPageNumberOneIndexed()) + .resortStrategy(strategy) + .sort(getSort()) + .build(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequestException.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequestException.java new file mode 100644 index 0000000..13b8c40 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageRequestException.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.domain.page; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/4/12 下午6:06 + */ +public class PageRequestException extends RuntimeException { + public PageRequestException() { + super(); + } + + public PageRequestException(String message) { + super(message); + } + + public PageRequestException(String message, Throwable cause) { + super(message, cause); + } + + public PageRequestException(Throwable cause) { + super(cause); + } + + protected PageRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageUtil.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageUtil.java new file mode 100644 index 0000000..6f72eb7 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageUtil.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.domain.page; + +/** + * @author liyong.tian + * @since 2017/2/16 + */ +public abstract class PageUtil { + + public static int getFirstPageNumber() { + return getFirstPageNumber(PageDefaults.PAGE_NUMBER_ONE_INDEXED); + } + + public static int getFirstPageNumber(boolean pageNumberOneIndexed) { + return pageNumberOneIndexed ? 1 : 0; + } + + public static int getLastPageNumber(long total, int pageSize) { + return (int) Math.ceil((double) total / (double) pageSize); + } + + public static int getOffset(int firstPageNumber, int pageNumber, int pageSize) { + return (pageNumber - firstPageNumber) * pageSize; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageVerbose.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageVerbose.java new file mode 100644 index 0000000..73b1049 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageVerbose.java @@ -0,0 +1,88 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.domain.sort.SortOrderDTO; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.jooq.lambda.Seq; + +import java.util.List; +import java.util.Optional; + +/** + * 分页冗余信息 + * + * @author liyong.tian + * @since 2018/2/1 上午11:22 + */ +@Data +public class PageVerbose { + + @ApiModelProperty(value = "当前页码", position = 1) + private Integer currentPage; + + @ApiModelProperty(value = "当前记录数", position = 2) + private Integer currentSize; + + @ApiModelProperty(value = "是否为第一页", position = 3) + private Boolean isFirst; + + @ApiModelProperty(value = "是否为最后一页", position = 4) + private Boolean isLast; + + @ApiModelProperty(value = "是否有前一页", position = 5) + private Boolean hasPrevious; + + @ApiModelProperty(value = "是否有下一页", position = 6) + private Boolean hasNext; + + @ApiModelProperty(value = "总页数", position = 7) + private Integer totalPage; + + @ApiModelProperty(value = "当前排序规则", position = 8) + private List sort; + + @ApiModelProperty(value = "每页数量", position = 9) + private Integer pageSize; + + private PageVerbose(Page page) { + Pageable current = page.current(); + switch (current.verbose()) { + case PAGE: + setPage(page, current); + break; + case SORT: + setSort(current); + break; + case ALL: + setPage(page, current); + setSort(current); + break; + default: + throw new IllegalArgumentException("No verbose config"); + } + } + + private void setPage(Page page, Pageable current) { + this.currentPage = current.getPageNumber(); + this.currentSize = page.getContent().size(); + this.isFirst = page.isFirst(); + this.isLast = page.isLast(); + this.hasPrevious = page.hasPrevious(); + this.hasNext = page.hasNext(); + this.totalPage = page.getLastPageNumber(); + this.pageSize = current.getPageSize(); + } + + private void setSort(Pageable current) { + if (current.getSort() != null) { + this.sort = Seq.seq(current.getSort().iterator()).map(SortOrderDTO::new).toList(); + } + } + + public static Optional of(Page page) { + if (page.current().verbose() == PageableVerbose.NONE) { + return Optional.empty(); + } + return Optional.of(new PageVerbose(page)); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Pageable.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Pageable.java new file mode 100644 index 0000000..7dbab75 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/Pageable.java @@ -0,0 +1,209 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.domain.sort.Order; +import cn.axzo.framework.domain.sort.Sort; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Abstract interface for pagination information. + * + * @author liyong.tian + * @since 2017/2/15 + */ +public interface Pageable { + + /** + * Returns the page to be returned. + * + * @return the page to be returned. + */ + int getPageNumber(); + + /** + * Returns the number of items to be returned. + * + * @return the number of items of that page + */ + int getPageSize(); + + /** + * Returns the offset to be taken according to the underlying page and page size. + * + * @return the offset to be taken + */ + default int getOffset() { + return PageUtil.getOffset(getFirstPageNumber(), getPageNumber(), getPageSize()); + } + + /** + * Returns the sorting parameters. + */ + @Nullable + default Sort getSort() { + return getSort(false); + } + + /** + * Returns the sorting parameters. + */ + @Nullable + Sort getSort(boolean applyResortStrategy); + + /** + * 是否需要查询总记录数 + */ + boolean needTotal(); + + /** + * 是否需要查询记录列表 + */ + boolean needContent(); + + /** + * 是否纠正分页边界错误,比如当page<1时,自动设置page=1 + *

+ * 不合法的分页参数情况(如果fixEdge=false,则前3个应抛异常): + * 1.page < first_page_number + * 2.size < 1 + * 3.size > max_page_size + * 4.page > last_page_number(做count查询后才能发现) + */ + boolean isFixEdge(); + + default Pageable fixEdge(Long total) { + if (isFixEdge() && total != null && getOffset() > total) { + return jumpTo(PageUtil.getLastPageNumber(total, getPageSize())); + } + return this; + } + + /** + * 分页冗余选项 + */ + @NotNull + PageableVerbose verbose(); + + /** + * 全局重排序策略 + */ + ResortStrategy resortStrategy(); + + /** + * 页码是否从1开始 + */ + boolean isPageNumberOneIndexed(); + + default int getFirstPageNumber() { + return PageUtil.getFirstPageNumber(isPageNumberOneIndexed()); + } + + @Nonnull + Pageable jumpTo(int page); + + @Nonnull + default Pageable copy() { + return jumpTo(getPageNumber()); + } + + @Nonnull + default Pageable enable(PageConfigKey key) { + return config(key, true); + } + + @Nonnull + default Pageable disabled(PageConfigKey key) { + return config(key, false); + } + + @Nonnull + Pageable config(PageConfigKey key, boolean enabled); + + @Nonnull + Pageable config(@Nullable PageableVerbose verbose); + + @Nonnull + Pageable config(@Nullable ResortStrategy strategy); + + @NonNull + default org.springframework.data.domain.Pageable toSpringPageable() { + Sort sort = getSort(); + if (sort == null) { + if (isPageNumberOneIndexed()) { + return org.springframework.data.domain.PageRequest.of(getPageNumber() - 1, getPageSize()); + } else { + return org.springframework.data.domain.PageRequest.of(getPageNumber(), getPageSize()); + } + } else { + if (isPageNumberOneIndexed()) { + return org.springframework.data.domain.PageRequest.of(getPageNumber() - 1, getPageSize(), + sort.toSpringSort()); + } else { + return org.springframework.data.domain.PageRequest.of(getPageNumber(), getPageSize(), + sort.toSpringSort()); + } + } + } + + default List toSortQueryStrings() { + Sort sort = getSort(); + if (sort == null) { + return Collections.emptyList(); + } + return sort.toQueryStrings(); + } + + default List toSortQueryStrings(String separator) { + Sort sort = getSort(); + if (sort == null) { + return Collections.emptyList(); + } + return sort.toQueryStrings(separator); + } + + default String toSortStrings() { + Sort sort = getSort(); + if (sort == null) { + return null; + } + return StringUtils.join(toSortQueryStrings(" "), ","); + } + + @Deprecated + default Map toQueryMap() { + Map map = new HashMap<>(); + map.put("pageSize", getPageSize()); + map.put("pageNum", getPageNumber()); + map.put("sort", toSortQueryStrings()); + map.put("needTotal", needTotal()); + map.put("needContent", needContent()); + map.put("verbose", verbose()); + map.put("fixEdge", isFixEdge()); + return map; + } + + /** + * 手动重排序,将指定属性重命名 + * + * @param property 待替换的排序规则的属性名 + * @param newProperties 获取重排序后的新规则 + */ + void resort(String property, String... newProperties); + + /** + * 手动重排序,将指定属性的排序规则替换成新的排序规则 + * + * @param property 待替换的排序规则的属性名 + * @param resortHandler 获取重排序后的新规则 + */ + void resort(String property, Function resortHandler); +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageableVerbose.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageableVerbose.java new file mode 100644 index 0000000..16beae2 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/PageableVerbose.java @@ -0,0 +1,23 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.core.annotation.Description; +import cn.axzo.framework.core.enums.IStringCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author liyong.tian + * @since 2018/2/1 上午11:17 + */ +@Getter +@AllArgsConstructor +@Description("分页冗余选项") +public enum PageableVerbose implements IStringCode { + NONE("none", "不返回冗余"), + ALL("all", "返回全部冗余"), + PAGE("page", "返回当前分页详情"), + SORT("sort", "返回当前排序规则"); + + private String code; + private String name; +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategies.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategies.java new file mode 100644 index 0000000..bbba14c --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategies.java @@ -0,0 +1,31 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.core.NamingStrategy; +import cn.axzo.framework.domain.sort.Order; + +import javax.annotation.Nonnull; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/22 上午12:05 + */ +public enum ResortStrategies implements ResortStrategy { + + SAME_CASE { + @Override + public Order transfer(@Nonnull Order order) { + String property = NamingStrategy.SAME_CASE.translate(order.getProperty()); + return order.with(property); + } + }, + + SNAKE_CASE { + @Override + public Order transfer(@Nonnull Order order) { + String property = NamingStrategy.SNAKE_CASE.translate(order.getProperty()); + return order.with(property); + } + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategy.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategy.java new file mode 100644 index 0000000..53906f1 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/page/ResortStrategy.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.domain.page; + +import cn.axzo.framework.domain.sort.Order; + +import javax.annotation.Nonnull; + +/** + * 重排序策略 + * + * @author liyong.tian + * @since 2018/3/21 下午11:03 + */ +public interface ResortStrategy { + Order transfer(@Nonnull Order order); +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Direction.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Direction.java new file mode 100644 index 0000000..35fd801 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Direction.java @@ -0,0 +1,38 @@ +package cn.axzo.framework.domain.sort; + +import java.util.Locale; + +/** + * @author liyong.tian + * @since 2017/2/16 + */ +public enum Direction { + + ASC, DESC; + + /** + * Returns the {@link Direction} enum for the given {@link String} value. + * + * @throws IllegalArgumentException in case the given value cannot be parsed into an enum value. + */ + public static Direction fromString(String value) { + try { + return Direction.valueOf(value.toUpperCase(Locale.US)); + } catch (Exception e) { + throw new IllegalArgumentException(String.format( + "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).", value), e); + } + } + + /** + * Returns the {@link Direction} enum for the given {@link String} or null if it cannot be parsed into an enum + * value. + */ + public static Direction fromStringOrNull(String value) { + try { + return fromString(value); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Order.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Order.java new file mode 100644 index 0000000..be00e1d --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Order.java @@ -0,0 +1,147 @@ +package cn.axzo.framework.domain.sort; + +import jodd.util.StringUtil; +import lombok.Data; + +import static jodd.util.StringUtil.isBlank; + +/** + * PropertyPath implements the pairing of an {@link Direction} and a property. It is used to provide input for + * {@link Sort} + * + * @author liyong.tian + * @since 2017/2/16 + */ +@Data +public class Order { + public static final Direction DEFAULT_DIRECTION = Direction.ASC; + private static final boolean DEFAULT_IGNORE_CASE = false; + + private final Direction direction; + private final String property; + private final boolean ignoreCase; + private final NullHandling nullHandling; + + public Order(String property) { + this(DEFAULT_DIRECTION, property); + } + + public Order(Direction direction, String property) { + this(direction, property, DEFAULT_IGNORE_CASE, null); + } + + public Order(Direction direction, String property, NullHandling nullHandling) { + this(direction, property, DEFAULT_IGNORE_CASE, nullHandling); + } + + /** + * Creates a new {@link Order} instance. if order is {@literal null} then order defaults to + * {@link Order#DEFAULT_DIRECTION} + * + * @param direction can be {@literal null}, will default to {@link Order#DEFAULT_DIRECTION} + * @param property must not be {@literal null} or empty. + * @param ignoreCase true if sorting should be case insensitive. false if sorting should be case sensitive. + * @param nullHandling can be {@literal null}, will default to {@link NullHandling#NATIVE}. + */ + private Order(Direction direction, String property, boolean ignoreCase, NullHandling nullHandling) { + if (isBlank(property)) { + throw new IllegalArgumentException("Property must not null or empty!"); + } + this.direction = direction == null ? DEFAULT_DIRECTION : direction; + this.property = property; + this.ignoreCase = ignoreCase; + this.nullHandling = nullHandling == null ? NullHandling.NATIVE : nullHandling; + } + + /** + * Returns a new {@link Order} with case insensitive sorting enabled. + */ + public Order ignoreCase() { + return new Order(direction, property, true, nullHandling); + } + + /** + * Returns a {@link Order} with the given property. + */ + public Order with(String property) { + if (StringUtil.isNotBlank(property)) { + return new Order(direction, property, ignoreCase, nullHandling); + } + return this; + } + + /** + * Returns a {@link Order} with the given {@link NullHandling}. + * + * @param nullHandling can be {@literal null}. + */ + public Order with(NullHandling nullHandling) { + return new Order(direction, this.property, ignoreCase, nullHandling); + } + + /** + * Returns a {@link Order} with {@link NullHandling#NULLS_FIRST} as null handling hint. + */ + public Order nullsFirst() { + return with(NullHandling.NULLS_FIRST); + } + + /** + * Returns a {@link Order} with {@link NullHandling#NULLS_LAST} as null handling hint. + */ + public Order nullsLast() { + return with(NullHandling.NULLS_LAST); + } + + /** + * Returns a {@link Order} with {@link NullHandling#NATIVE} as null handling hint. + */ + public Order nullsNative() { + return with(NullHandling.NATIVE); + } + + /** + * Returns whether sorting for this property shall be ascending. + */ + public boolean isAscending() { + return this.direction == Direction.ASC; + } + + /** + * Enumeration for null handling hints that can be used in {@link Order} expressions. + * + * @author Thomas Darimont + * @since 1.8 + */ + public enum NullHandling { + + /** + * Lets the data store decide what to do with nulls. + */ + NATIVE, + + /** + * A hint to the used data store to order entries with null values before non null entries. + */ + NULLS_FIRST, + + /** + * A hint to the used data store to order entries with null values after non null entries. + */ + NULLS_LAST + } + + public String toQueryString() { + return this.property + "," + this.direction.toString().toLowerCase(); + } + + public String toQueryString(String separator) { + return this.property + separator + this.direction.toString().toLowerCase(); + } + + org.springframework.data.domain.Sort.Order toSpringOrder() { + org.springframework.data.domain.Sort.Direction direction = + org.springframework.data.domain.Sort.Direction.fromString(this.direction.name()); + return new org.springframework.data.domain.Sort.Order(direction, this.property); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Sort.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Sort.java new file mode 100644 index 0000000..77e0594 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/Sort.java @@ -0,0 +1,154 @@ +package cn.axzo.framework.domain.sort; + +import cn.axzo.framework.core.util.ListUtil; +import cn.axzo.framework.domain.page.ResortStrategy; +import lombok.ToString; +import lombok.val; +import org.jooq.lambda.Seq; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; + +import static java.util.stream.Collectors.toList; + +/** + * @author liyong.tian + * @since 2017/2/16 + */ +@ToString +public class Sort implements Iterable { + + private final List orders; + + public Sort(Order... orders) { + this(Arrays.asList(orders)); + } + + public Sort(List orders) { + this(orders, true); + } + + public Sort(String... properties) { + this(Order.DEFAULT_DIRECTION, properties); + } + + public Sort(Direction direction, String... properties) { + this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties)); + } + + public Sort(Direction direction, List properties) { + this(_buildOrders(direction, properties), true); + } + + /** + * Creates a new {@link Sort} instance. + * + * @param orders 多个排序规则 + * @param clean 是否清理错误的规则 + */ + private Sort(List orders, boolean clean) { + this.orders = clean ? _clean(orders) : orders; + } + + /** + * Returns a new {@link Sort} consisting of the {@link Order}s of the current {@link Sort} combined with the given + * ones. + */ + public Sort and(@Nullable Sort sort) { + if (sort == null) { + return this; + } + ArrayList these = new ArrayList<>(this.orders); + for (Order order : sort) { + these.add(order); + } + return new Sort(these); + } + + /** + * Returns the order registered for the given property. + */ + public Optional findOrderFor(String property) { + for (Order order : this) { + if (order.getProperty().equals(property)) { + return Optional.of(order); + } + } + return Optional.empty(); + } + + @Nonnull + public Iterator iterator() { + return this.orders.iterator(); + } + + public List toQueryStrings() { + return this.orders.stream().map(Order::toQueryString).collect(toList()); + } + + public List toQueryStrings(String separator) { + return this.orders.stream().map(order -> order.toQueryString(separator)).collect(toList()); + } + + /** + * 全局重排序 + */ + public Sort resort(ResortStrategy strategy) { + Objects.requireNonNull(strategy); + val orders = Seq.seq(this.orders).map(strategy::transfer).toList(); + return new Sort(orders, true); + } + + /** + * 修改指定属性的排序规则 + */ + public Sort resort(String property, String... newProperties) { + return resort(property, order -> new Sort(order.getDirection(), newProperties)); + } + + /** + * 修改指定属性的排序规则 + */ + public Sort resort(String property, Function resortHandler) { + Objects.requireNonNull(property); + Objects.requireNonNull(resortHandler); + return removeOrderFor(property).map(order -> and(resortHandler.apply(order))).orElse(this); + } + + public Optional removeOrderFor(String property) { + for (int i = 0; i < orders.size(); i++) { + final Order order = orders.get(i); + if (order.getProperty().equals(property)) { + orders.remove(i); + return Optional.of(order); + } + } + return Optional.empty(); + } + + public org.springframework.data.domain.Sort toSpringSort() { + return org.springframework.data.domain.Sort.by(this.orders.stream().map(Order::toSpringOrder).collect(toList())); + } + + /*-------------------------------私有方法-------------------------------*/ + + /** + * 清理错误的排序规则,比如a,asc&&a,desc,只保留a,asc + */ + private List _clean(List orders) { + return orders.stream().filter(ListUtil.distinctByKey(Order::getProperty)).collect(toList()); + } + + private static List _buildOrders(Direction direction, List properties) { + if (properties == null || properties.isEmpty()) { + throw new IllegalArgumentException("You have to provide at least one property to sort by!"); + } + List orders = new ArrayList<>(properties.size()); + for (String property : properties) { + orders.add(new Order(direction, property)); + } + return orders; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortDefaults.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortDefaults.java new file mode 100644 index 0000000..3dce124 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortDefaults.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.domain.sort; + +import cn.axzo.framework.domain.page.ResortStrategies; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/21 下午11:14 + */ +public interface SortDefaults { + + // 默认排序字段的命名规则:驼峰 -> 下划线 + ResortStrategies RESORT_STRATEGY = ResortStrategies.SAME_CASE; +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortLimit.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortLimit.java new file mode 100644 index 0000000..f105205 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortLimit.java @@ -0,0 +1,25 @@ +package cn.axzo.framework.domain.sort; + +import java.lang.annotation.*; + +/** + * 排序限制 + * + * @author liyong.tian + * @since 2017/10/4 下午12:31 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface SortLimit { + + /** + * 同sort规则一样,对客户端请求过来的排序规则进行过滤 + */ + String[] value() default {}; + + /** + * Specifies if sort parameter is required or not. + */ + boolean required() default false; +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortOrderDTO.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortOrderDTO.java new file mode 100644 index 0000000..9fb6bc8 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/sort/SortOrderDTO.java @@ -0,0 +1,25 @@ +package cn.axzo.framework.domain.sort; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +/** + * @author liyong.tian + * @since 2018/1/24 下午9:54 + */ +@Data +@RequiredArgsConstructor +public class SortOrderDTO { + + @ApiModelProperty("字段") + private final String property; + + @ApiModelProperty("顺序(ASC:升序 DESC:降序)") + private final Direction direction; + + public SortOrderDTO(Order order) { + this.direction = order.getDirection(); + this.property = order.getProperty(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ApiException.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ApiException.java new file mode 100644 index 0000000..79381ad --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ApiException.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.domain.web; + +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import lombok.Getter; + +import static cn.axzo.framework.domain.web.code.BaseCode.UNAVAILABLE_FOR_LEGAL_REASONS; +import static java.lang.String.format; + +/** + * @Description 接口访问异常 + * @Author liyong.tian + * @Date 2020/9/7 20:16 + **/ +public class ApiException extends RuntimeException { + + @Getter + private final String code; + + @Getter + private final boolean badRequest; + + public ApiException(IRespCode code, Object... args) { + this(false, code, args); + } + + public ApiException(IRespCode message) { + this(false, message, UNAVAILABLE_FOR_LEGAL_REASONS); + } + + public ApiException(String message, IRespCode code, Object... args) { + this(false, message, code, args); + } + + ApiException(boolean badRequest, IRespCode code, Object... args) { + this(badRequest, null, code, args); + } + + ApiException(boolean badRequest, String message, IRespCode code, Object... args) { + super(format(message == null ? code.getMessage() : message, args)); + this.code = code.getRespCode(); + this.badRequest = badRequest; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/BadRequestException.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/BadRequestException.java new file mode 100644 index 0000000..b40ff80 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/BadRequestException.java @@ -0,0 +1,19 @@ +package cn.axzo.framework.domain.web; + +import cn.axzo.framework.domain.web.code.IRespCode; + +/** + * @Description 接口参数异常 + * @Author liyong.tian + * @Date 2020/9/7 20:17 + **/ +public class BadRequestException extends ApiException { + + public BadRequestException(IRespCode code, Object... args) { + super(true, code, args); + } + + public BadRequestException(String message, IRespCode code, Object... args) { + super(true, message, code, args); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ClientException.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ClientException.java new file mode 100644 index 0000000..b88ad1d --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/ClientException.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.domain.web; + +import cn.axzo.framework.domain.web.code.IRespCode; +import com.netflix.hystrix.exception.HystrixBadRequestException; +import lombok.Getter; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:18 + **/ +public class ClientException extends HystrixBadRequestException { + + @Getter + private String code; + + public ClientException(IRespCode code) { + super(code.getMessage()); + this.code = code.getRespCode(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/BaseCode.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/BaseCode.java new file mode 100644 index 0000000..39915e8 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/BaseCode.java @@ -0,0 +1,55 @@ +package cn.axzo.framework.domain.web.code; + +import cn.axzo.framework.core.annotation.Description; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.stream.Stream; + +/** + * @Description 基础响应码 + * @Author liyong.tian + * @Date 2020/9/7 20:22 + **/ +@Getter +@AllArgsConstructor +@Description("基础错误码") +public enum BaseCode implements IModuleRespCode{ + + SUCCESS("000", "成功", 200) { + @Override + public String getRespCode() { + return "0"; + } + }, + BAD_REQUEST("400", "请求参数错误", 400), + UNAUTHORIZED("401", "未授权", 401), + FORBIDDEN("403", "拒绝访问", 403), + NOT_FOUND("404", "URI Not Found", 404), + METHOD_NOT_ALLOWED("405", "请求方法错误", 405), + NOT_ACCEPTABLE("406", "Not Acceptable", 406), + CONFLICT("409", "资源冲突", 409), + PAYLOAD_TOO_LARGE("413", "Payload Too Large", 413), + UNSUPPORTED_MEDIA_TYPE("415", "不支持的请求头类型", 415), + UNAVAILABLE_FOR_LEGAL_REASONS("451", "通用业务异常", 451), + SERVER_ERROR("500", "当前服务不可用,请稍后重试", 500), + SERVICE_UNAVAILABLE("503", "当前服务不可用,请稍后重试", 503); + + private String code; + private String message; + private int status; + + public static BaseCode parse(int status) { + return Stream.of(values()).filter(baseCode -> baseCode.getStatus() == status).findFirst().orElse(SERVER_ERROR); + } + + @Override + public String getProjectCode() { + return INTERNAL_PROJECT_CODE; + } + + @Override + public String getModuleCode() { + return DEFAULT_MODULE_CODE; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IModuleRespCode.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IModuleRespCode.java new file mode 100644 index 0000000..aa3f67c --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IModuleRespCode.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.domain.web.code; + +/** + * @Description 响应码规范:一共8位,取值范围0~9,3位项目编号+2位模块编号+3位自定义编号 + * @Author liyong.tian + * @Date 2020/9/7 20:24 + **/ +public interface IModuleRespCode extends IProjectRespCode { + + // 默认模块编号:00 + String DEFAULT_MODULE_CODE = "00"; + + String getModuleCode(); + + @Override + default String getRespCode() { + return getProjectCode() + getModuleCode() + getCode(); + } + + @Override + default String getName() { + return getMessage(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IProjectRespCode.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IProjectRespCode.java new file mode 100644 index 0000000..ea0233a --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IProjectRespCode.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.domain.web.code; + +/** + * @Description 响应码规范:一共8位,取值范围0~9,3位项目编号+2位模块编号+3位自定义编号 + * @Author liyong.tian + * @Date 2020/9/7 20:24 + **/ +public interface IProjectRespCode extends IRespCode { + + // 基础项目专用编号:100 + String INTERNAL_PROJECT_CODE = "100"; + + // 临时项目编号:999 + String TEMP_PROJECT_CODE = "999"; + + String getProjectCode(); + + @Override + default String getRespCode() { + return getProjectCode() + getCode(); + } + + @Override + default String getName() { + return getMessage(); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IRespCode.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IRespCode.java new file mode 100644 index 0000000..916c9a4 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/IRespCode.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.domain.web.code; + +import cn.axzo.framework.core.enums.IStringCode; + +/** + * @Description 响应码规范:一共8位,取值范围0~9,3位项目编号+2位模块编号+3位自定义编号 + * @Author liyong.tian + * @Date 2020/9/7 20:19 + **/ +public interface IRespCode extends IStringCode { + + @Override + String getCode(); + + String getMessage(); + + @Override + default String getName() { + return getMessage(); + } + + default String getRespCode() { + return getCode(); + } + + default boolean useResourceBundle() { + return false; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/RespCode.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/RespCode.java new file mode 100644 index 0000000..8c35a9f --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/code/RespCode.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.domain.web.code; + +import lombok.Data; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:26 + **/ +@Data +public class RespCode implements IRespCode { + + private final String code; + + private final String message; +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/http/HttpStatus.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/http/HttpStatus.java new file mode 100644 index 0000000..4f4225f --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/http/HttpStatus.java @@ -0,0 +1,541 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 cn.axzo.framework.domain.web.http; + +/** + * Enumeration of HTTP status codes. + * + *

The HTTP status code series can be retrieved via {@link #series()}. + * + * @author Arjen Poutsma + * @author Sebastien Deleuze + * @author Brian Clozel + * @since 3.0 + * @see Series + * @see HTTP Status Code Registry + * @see List of HTTP status codes - Wikipedia + */ +public enum HttpStatus { + + // 1xx Informational + + /** + * {@code 100 Continue}. + * @see HTTP/1.1: Semantics and Content, section 6.2.1 + */ + CONTINUE(100, "Continue"), + /** + * {@code 101 Switching Protocols}. + * @see HTTP/1.1: Semantics and Content, section 6.2.2 + */ + SWITCHING_PROTOCOLS(101, "Switching Protocols"), + /** + * {@code 102 Processing}. + * @see WebDAV + */ + PROCESSING(102, "Processing"), + /** + * {@code 103 Checkpoint}. + * @see A proposal for supporting + * resumable POST/PUT HTTP requests in HTTP/1.0 + */ + CHECKPOINT(103, "Checkpoint"), + + // 2xx Success + + /** + * {@code 200 OK}. + * @see HTTP/1.1: Semantics and Content, section 6.3.1 + */ + OK(200, "OK"), + /** + * {@code 201 Created}. + * @see HTTP/1.1: Semantics and Content, section 6.3.2 + */ + CREATED(201, "Created"), + /** + * {@code 202 Accepted}. + * @see HTTP/1.1: Semantics and Content, section 6.3.3 + */ + ACCEPTED(202, "Accepted"), + /** + * {@code 203 Non-Authoritative Information}. + * @see HTTP/1.1: Semantics and Content, section 6.3.4 + */ + NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"), + /** + * {@code 204 No Content}. + * @see HTTP/1.1: Semantics and Content, section 6.3.5 + */ + NO_CONTENT(204, "No Content"), + /** + * {@code 205 Reset Content}. + * @see HTTP/1.1: Semantics and Content, section 6.3.6 + */ + RESET_CONTENT(205, "Reset Content"), + /** + * {@code 206 Partial Content}. + * @see HTTP/1.1: Range Requests, section 4.1 + */ + PARTIAL_CONTENT(206, "Partial Content"), + /** + * {@code 207 Multi-Status}. + * @see WebDAV + */ + MULTI_STATUS(207, "Multi-Status"), + /** + * {@code 208 Already Reported}. + * @see WebDAV Binding Extensions + */ + ALREADY_REPORTED(208, "Already Reported"), + /** + * {@code 226 IM Used}. + * @see Delta encoding in HTTP + */ + IM_USED(226, "IM Used"), + + // 3xx Redirection + + /** + * {@code 300 Multiple Choices}. + * @see HTTP/1.1: Semantics and Content, section 6.4.1 + */ + MULTIPLE_CHOICES(300, "Multiple Choices"), + /** + * {@code 301 Moved Permanently}. + * @see HTTP/1.1: Semantics and Content, section 6.4.2 + */ + MOVED_PERMANENTLY(301, "Moved Permanently"), + /** + * {@code 302 Found}. + * @see HTTP/1.1: Semantics and Content, section 6.4.3 + */ + FOUND(302, "Found"), + /** + * {@code 302 Moved Temporarily}. + * @see HTTP/1.0, section 9.3 + * @deprecated in favor of {@link #FOUND} which will be returned from {@code HttpStatus.valueOf(302)} + */ + @Deprecated + MOVED_TEMPORARILY(302, "Moved Temporarily"), + /** + * {@code 303 See Other}. + * @see HTTP/1.1: Semantics and Content, section 6.4.4 + */ + SEE_OTHER(303, "See Other"), + /** + * {@code 304 Not Modified}. + * @see HTTP/1.1: Conditional Requests, section 4.1 + */ + NOT_MODIFIED(304, "Not Modified"), + /** + * {@code 305 Use Proxy}. + * @see HTTP/1.1: Semantics and Content, section 6.4.5 + * @deprecated due to security concerns regarding in-band configuration of a proxy + */ + @Deprecated + USE_PROXY(305, "Use Proxy"), + /** + * {@code 307 Temporary Redirect}. + * @see HTTP/1.1: Semantics and Content, section 6.4.7 + */ + TEMPORARY_REDIRECT(307, "Temporary Redirect"), + /** + * {@code 308 Permanent Redirect}. + * @see RFC 7238 + */ + PERMANENT_REDIRECT(308, "Permanent Redirect"), + + // --- 4xx Client Error --- + + /** + * {@code 400 Bad Request}. + * @see HTTP/1.1: Semantics and Content, section 6.5.1 + */ + BAD_REQUEST(400, "Bad Request"), + /** + * {@code 401 Unauthorized}. + * @see HTTP/1.1: Authentication, section 3.1 + */ + UNAUTHORIZED(401, "Unauthorized"), + /** + * {@code 402 Payment Required}. + * @see HTTP/1.1: Semantics and Content, section 6.5.2 + */ + PAYMENT_REQUIRED(402, "Payment Required"), + /** + * {@code 403 Forbidden}. + * @see HTTP/1.1: Semantics and Content, section 6.5.3 + */ + FORBIDDEN(403, "Forbidden"), + /** + * {@code 404 Not Found}. + * @see HTTP/1.1: Semantics and Content, section 6.5.4 + */ + NOT_FOUND(404, "Not Found"), + /** + * {@code 405 Method Not Allowed}. + * @see HTTP/1.1: Semantics and Content, section 6.5.5 + */ + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + /** + * {@code 406 Not Acceptable}. + * @see HTTP/1.1: Semantics and Content, section 6.5.6 + */ + NOT_ACCEPTABLE(406, "Not Acceptable"), + /** + * {@code 407 Proxy Authentication Required}. + * @see HTTP/1.1: Authentication, section 3.2 + */ + PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), + /** + * {@code 408 Request Timeout}. + * @see HTTP/1.1: Semantics and Content, section 6.5.7 + */ + REQUEST_TIMEOUT(408, "Request Timeout"), + /** + * {@code 409 Conflict}. + * @see HTTP/1.1: Semantics and Content, section 6.5.8 + */ + CONFLICT(409, "Conflict"), + /** + * {@code 410 Gone}. + * @see HTTP/1.1: Semantics and Content, section 6.5.9 + */ + GONE(410, "Gone"), + /** + * {@code 411 Length Required}. + * @see HTTP/1.1: Semantics and Content, section 6.5.10 + */ + LENGTH_REQUIRED(411, "Length Required"), + /** + * {@code 412 Precondition failed}. + * @see HTTP/1.1: Conditional Requests, section 4.2 + */ + PRECONDITION_FAILED(412, "Precondition Failed"), + /** + * {@code 413 Payload Too Large}. + * @since 4.1 + * @see HTTP/1.1: Semantics and Content, section 6.5.11 + */ + PAYLOAD_TOO_LARGE(413, "Payload Too Large"), + /** + * {@code 413 Request Entity Too Large}. + * @see HTTP/1.1, section 10.4.14 + * @deprecated in favor of {@link #PAYLOAD_TOO_LARGE} which will be returned from {@code HttpStatus.valueOf(413)} + */ + @Deprecated + REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"), + /** + * {@code 414 URI Too Long}. + * @since 4.1 + * @see HTTP/1.1: Semantics and Content, section 6.5.12 + */ + URI_TOO_LONG(414, "URI Too Long"), + /** + * {@code 414 Request-URI Too Long}. + * @see HTTP/1.1, section 10.4.15 + * @deprecated in favor of {@link #URI_TOO_LONG} which will be returned from {@code HttpStatus.valueOf(414)} + */ + @Deprecated + REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"), + /** + * {@code 415 Unsupported Media Type}. + * @see HTTP/1.1: Semantics and Content, section 6.5.13 + */ + UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + /** + * {@code 416 Requested Range Not Satisfiable}. + * @see HTTP/1.1: Range Requests, section 4.4 + */ + REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable"), + /** + * {@code 417 Expectation Failed}. + * @see HTTP/1.1: Semantics and Content, section 6.5.14 + */ + EXPECTATION_FAILED(417, "Expectation Failed"), + /** + * {@code 418 I'm a teapot}. + * @see HTCPCP/1.0 + */ + I_AM_A_TEAPOT(418, "I'm a teapot"), + /** + * @deprecated See WebDAV Draft Changes + */ + @Deprecated + INSUFFICIENT_SPACE_ON_RESOURCE(419, "Insufficient Space On Resource"), + /** + * @deprecated See WebDAV Draft Changes + */ + @Deprecated + METHOD_FAILURE(420, "Method Failure"), + /** + * @deprecated See WebDAV Draft Changes + */ + @Deprecated + DESTINATION_LOCKED(421, "Destination Locked"), + /** + * {@code 422 Unprocessable Entity}. + * @see WebDAV + */ + UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), + /** + * {@code 423 Locked}. + * @see WebDAV + */ + LOCKED(423, "Locked"), + /** + * {@code 424 Failed Dependency}. + * @see WebDAV + */ + FAILED_DEPENDENCY(424, "Failed Dependency"), + /** + * {@code 426 Upgrade Required}. + * @see Upgrading to TLS Within HTTP/1.1 + */ + UPGRADE_REQUIRED(426, "Upgrade Required"), + /** + * {@code 428 Precondition Required}. + * @see Additional HTTP Status Codes + */ + PRECONDITION_REQUIRED(428, "Precondition Required"), + /** + * {@code 429 Too Many Requests}. + * @see Additional HTTP Status Codes + */ + TOO_MANY_REQUESTS(429, "Too Many Requests"), + /** + * {@code 431 Request Header Fields Too Large}. + * @see Additional HTTP Status Codes + */ + REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), + /** + * {@code 451 Unavailable For Legal Reasons}. + * @see + * An HTTP Status Code to Report Legal Obstacles + * @since 4.3 + */ + UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), + + // --- 5xx Server Error --- + + /** + * {@code 500 Internal Server Error}. + * @see HTTP/1.1: Semantics and Content, section 6.6.1 + */ + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + /** + * {@code 501 Not Implemented}. + * @see HTTP/1.1: Semantics and Content, section 6.6.2 + */ + NOT_IMPLEMENTED(501, "Not Implemented"), + /** + * {@code 502 Bad Gateway}. + * @see HTTP/1.1: Semantics and Content, section 6.6.3 + */ + BAD_GATEWAY(502, "Bad Gateway"), + /** + * {@code 503 Service Unavailable}. + * @see HTTP/1.1: Semantics and Content, section 6.6.4 + */ + SERVICE_UNAVAILABLE(503, "Service Unavailable"), + /** + * {@code 504 Gateway Timeout}. + * @see HTTP/1.1: Semantics and Content, section 6.6.5 + */ + GATEWAY_TIMEOUT(504, "Gateway Timeout"), + /** + * {@code 505 HTTP Version Not Supported}. + * @see HTTP/1.1: Semantics and Content, section 6.6.6 + */ + HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported"), + /** + * {@code 506 Variant Also Negotiates} + * @see Transparent Content Negotiation + */ + VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"), + /** + * {@code 507 Insufficient Storage} + * @see WebDAV + */ + INSUFFICIENT_STORAGE(507, "Insufficient Storage"), + /** + * {@code 508 Loop Detected} + * @see WebDAV Binding Extensions + */ + LOOP_DETECTED(508, "Loop Detected"), + /** + * {@code 509 Bandwidth Limit Exceeded} + */ + BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded"), + /** + * {@code 510 Not Extended} + * @see HTTP Extension Framework + */ + NOT_EXTENDED(510, "Not Extended"), + /** + * {@code 511 Network Authentication Required}. + * @see Additional HTTP Status Codes + */ + NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); + + + private final int value; + + private final String reasonPhrase; + + + HttpStatus(int value, String reasonPhrase) { + this.value = value; + this.reasonPhrase = reasonPhrase; + } + + + /** + * Return the integer value of this status code. + */ + public int value() { + return this.value; + } + + /** + * Return the reason phrase of this status code. + */ + public String getReasonPhrase() { + return this.reasonPhrase; + } + + /** + * Whether this status code is in the HTTP series + * {@link Series#INFORMATIONAL}. + * This is a shortcut for checking the value of {@link #series()}. + */ + public boolean is1xxInformational() { + return Series.INFORMATIONAL.equals(series()); + } + + /** + * Whether this status code is in the HTTP series + * {@link Series#SUCCESSFUL}. + * This is a shortcut for checking the value of {@link #series()}. + */ + public boolean is2xxSuccessful() { + return Series.SUCCESSFUL.equals(series()); + } + + /** + * Whether this status code is in the HTTP series + * {@link Series#REDIRECTION}. + * This is a shortcut for checking the value of {@link #series()}. + */ + public boolean is3xxRedirection() { + return Series.REDIRECTION.equals(series()); + } + + + /** + * Whether this status code is in the HTTP series + * {@link Series#CLIENT_ERROR}. + * This is a shortcut for checking the value of {@link #series()}. + */ + public boolean is4xxClientError() { + return Series.CLIENT_ERROR.equals(series()); + } + + /** + * Whether this status code is in the HTTP series + * {@link Series#SERVER_ERROR}. + * This is a shortcut for checking the value of {@link #series()}. + */ + public boolean is5xxServerError() { + return Series.SERVER_ERROR.equals(series()); + } + + /** + * Returns the HTTP status series of this status code. + * @see Series + */ + public Series series() { + return Series.valueOf(this); + } + + /** + * Return a string representation of this status code. + */ + @Override + public String toString() { + return Integer.toString(this.value); + } + + + /** + * Return the enum constant of this type with the specified numeric value. + * @param statusCode the numeric value of the enum to be returned + * @return the enum constant with the specified numeric value + * @throws IllegalArgumentException if this enum has no constant for the specified numeric value + */ + public static HttpStatus valueOf(int statusCode) { + for (HttpStatus status : values()) { + if (status.value == statusCode) { + return status; + } + } + throw new IllegalArgumentException("No matching constant for [" + statusCode + "]"); + } + + + /** + * Enumeration of HTTP status series. + *

Retrievable via {@link HttpStatus#series()}. + */ + public enum Series { + + INFORMATIONAL(1), + SUCCESSFUL(2), + REDIRECTION(3), + CLIENT_ERROR(4), + SERVER_ERROR(5); + + private final int value; + + Series(int value) { + this.value = value; + } + + /** + * Return the integer value of this status series. Ranges from 1 to 5. + */ + public int value() { + return this.value; + } + + public static Series valueOf(int status) { + int seriesCode = status / 100; + for (Series series : values()) { + if (series.value == seriesCode) { + return series; + } + } + throw new IllegalArgumentException("No matching constant for [" + status + "]"); + } + + public static Series valueOf(HttpStatus status) { + return valueOf(status.value); + } + } + +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiCoreResult.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiCoreResult.java new file mode 100644 index 0000000..75874e5 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiCoreResult.java @@ -0,0 +1,92 @@ +package cn.axzo.framework.domain.web.result; + +import cn.axzo.framework.domain.web.ApiException; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.beans.Transient; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.stream.Stream.of; +import static jodd.util.StringUtil.isNotBlank; + +/** + * @Description 注:code只是业务角度的响应码,相当于HTTP状态码的扩展集,可以展示更明确的错误原因,不是业务数据里的状态 + * @Author liyong.tian + * @Date 2020/9/7 20:28 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@JsonPropertyOrder({"code", "msg", "data"}) +public abstract class ApiCoreResult implements Result { + + @ApiModelProperty(value = "业务码", required = true, position = 1) + protected String code; + + @ApiModelProperty(value = "业务码说明", required = true, example = "成功", position = 2) + protected String msg; + + @ApiModelProperty(value = "业务数据", position = 100) + protected E data; + + @Transient + protected IRespCode[] getOKCodes() { + return new IRespCode[]{BaseCode.SUCCESS}; + } + + @Transient + public boolean isSuccess() { + // 兼容 + return code.equals("00000000") || of(getOKCodes()).anyMatch(respOKCode -> Objects.equals(this.code, respOKCode.getRespCode())); + } + + @Transient + public boolean isError() { + return !isSuccess(); + } + + @Transient + public IRespCode getRespCode() { + return new IRespCode() { + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return msg; + } + }; + } + + public RuntimeException decomposeException() { + return new ApiException(getRespCode()); + } + + @Override + public Map toMap() { + Map map = new LinkedHashMap<>(); + map.put("code", code); + map.put("msg", msg); + map.put("data", data); + return map; + } + + private static final String GLOBAL_SUCCESS_CODE = System.getProperty("apiresult.success_code"); + + public String getCode() { + if (isNotBlank(GLOBAL_SUCCESS_CODE) && isSuccess()) { + return GLOBAL_SUCCESS_CODE; + } + return this.code; + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiListResult.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiListResult.java new file mode 100644 index 0000000..f1598d6 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiListResult.java @@ -0,0 +1,68 @@ +package cn.axzo.framework.domain.web.result; + +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.beans.ConstructorProperties; +import java.util.List; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:31 + **/ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiListResult extends ApiCoreResult> { + + public static ApiListResult ok() { + return ok(null); + } + + public static ApiListResult ok(List data) { + return build(BaseCode.SUCCESS.getRespCode(), BaseCode.SUCCESS.getMessage(), data); + } + + public static ApiListResult err(IRespCode code) { + return err(code, code.getMessage()); + } + + public static ApiListResult err(IRespCode code, String message) { + return err(code.getRespCode(), message); + } + + public static ApiListResult err(String message) { + return err(BaseCode.SERVER_ERROR.getRespCode(), message); + } + + public static ApiListResult err(IRespCode code, List data) { + return build(code.getRespCode(), code.getMessage(), data); + } + + public static ApiListResult err(String code, String message) { + if (code == null) { + return err(message); + } + return build(code, message, null); + } + + public static ApiListResult build(IRespCode code) { + return build(code, null); + } + + public static ApiListResult build(IRespCode code, List data) { + return build(code.getRespCode(), code.getMessage(), data); + } + + public static ApiListResult build(String code, String message, List data) { + return new ApiListResult<>(code, message, data); + } + + @ConstructorProperties({"code", "message", "data"}) + public ApiListResult(String code, String message, List data) { + super(code, message, data); + } +} + diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiPageResult.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiPageResult.java new file mode 100644 index 0000000..905dbe2 --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiPageResult.java @@ -0,0 +1,151 @@ +package cn.axzo.framework.domain.web.result; + +import cn.axzo.framework.domain.page.Page; +import cn.axzo.framework.domain.page.PageImpl; +import cn.axzo.framework.domain.page.PageVerbose; +import cn.axzo.framework.domain.page.Pageable; +import cn.axzo.framework.domain.web.code.IRespCode; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.google.common.collect.Lists; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.beans.ConstructorProperties; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.axzo.framework.domain.web.code.BaseCode.SUCCESS; +import static com.google.common.collect.Lists.newArrayList; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:32 + **/ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@JsonPropertyOrder({"total", "code", "msg", "data", "pageNum", "pageSize", "verbose"}) +public class ApiPageResult extends ApiCoreResult> { + + @ApiModelProperty(value = "总记录数", position = 101, required = true) + private final Long total; + + @ApiModelProperty(value = "当前页", position = 102) + private final Integer pageNum; + + @ApiModelProperty(value = "每页显示数量", position = 103) + private final Integer pageSize; + + @ApiModelProperty(value = "分页冗余信息", position = 104) + private final PageVerbose verbose; + + public static ApiPageResult empty() { + return ok(newArrayList(), 0L); + } + + public static ApiPageResult ok(Page page) { + Pageable current = page.current(); + + // data + List data = page.getContent(); + if (!current.needContent() && data.isEmpty()) { + data = null; + } + + // pageNum & pageSize + Integer pageNum = null; + Integer pageSize = null; + if (current.isFixEdge() && current.needContent()) { + pageNum = current.getPageNumber(); + pageSize = current.getPageSize(); + } + + // verbose + PageVerbose verbose = PageVerbose.of(page).orElse(null); + return ok(data, page.getTotal(), pageNum, pageSize, verbose); + } + +// public static ApiPageResult ok(PageInfo pageInfo) { +// return ok(pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPageSize()); +// } + + public static ApiPageResult ok(org.springframework.data.domain.Page page) { + return ok(page.getContent(), page.getTotalElements()); + } + + public static ApiPageResult ok(List data, Long total) { + return build(total, SUCCESS.getRespCode(), SUCCESS.getMessage(), data, + null, null, null); + } + + public static ApiPageResult ok(List data, Long total, Integer pageNumber, Integer pageSize) { + return build(total, SUCCESS.getRespCode(), SUCCESS.getMessage(), data, pageNumber, pageSize, null); + } + + public static ApiPageResult ok(List data, Long total, Integer pageNumber, Integer pageSize, + PageVerbose verbose) { + return build(total, SUCCESS.getRespCode(), SUCCESS.getMessage(), data, pageNumber, pageSize, verbose); + } + + public static ApiPageResult err(IRespCode code) { + return build(code); + } + + public static ApiPageResult err(IRespCode code, String message) { + return err(code.getRespCode(), message); + } + + public static ApiPageResult err(String code, String message) { + return build(null, code, message, null, null, null, null); + } + + public static ApiPageResult build(IRespCode code) { + return build(null, code, null, null, null); + } + + public static ApiPageResult build(Long total, IRespCode code, List data, + Integer pageNum, Integer pageSize) { + return build(total, code.getRespCode(), code.getMessage(), data, pageNum, pageSize, null); + } + + public static ApiPageResult build(Long total, String code, String message, List data, + Integer pageNum, Integer pageSize) { + return new ApiPageResult<>(total, code, message, data, pageNum, pageSize, null); + } + + public static ApiPageResult build(Long total, String code, String message, List data, + Integer pageNum, Integer pageSize, PageVerbose verbose) { + return new ApiPageResult<>(total, code, message, data, pageNum, pageSize, verbose); + } + + @ConstructorProperties({"total", "code", "message", "data", "pageNum", "pageSize", "verbose"}) + public ApiPageResult(Long total, String code, String message, List data, Integer pageNum, Integer pageSize, + PageVerbose verbose) { + super(code, message, data); + this.total = total; + this.pageNum = pageNum; + this.pageSize = pageSize; + this.verbose = verbose; + } + + @Override + public Map toMap() { + Map map = new LinkedHashMap<>(); + map.put("code", code); + map.put("msg", msg); + map.put("total", total); + map.put("data", data); + map.put("pageNum", pageNum); + map.put("pageSize", pageSize); + map.put("verbose", verbose); + return map; + } + + public Page toPage(Pageable pageable) { + return new PageImpl<>(data == null ? Lists.newArrayList() : data, pageable, total); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiResult.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiResult.java new file mode 100644 index 0000000..2e29c1d --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/ApiResult.java @@ -0,0 +1,85 @@ +package cn.axzo.framework.domain.web.result; + +import cn.axzo.framework.domain.web.ApiException; +import cn.axzo.framework.domain.web.code.IRespCode; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.beans.ConstructorProperties; +import java.util.function.Function; + +import static cn.axzo.framework.domain.web.code.BaseCode.SERVER_ERROR; +import static cn.axzo.framework.domain.web.code.BaseCode.SUCCESS; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:33 + **/ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiResult extends ApiCoreResult { + + public static ApiResult ok() { + return ok(null); + } + + public static ApiResult ok(E data) { + return build(SUCCESS, data); + } + + public static ApiResult err(IRespCode code) { + return err(code, code.getMessage()); + } + + public static ApiResult err(IRespCode code, String message) { + return err(code.getRespCode(), message); + } + + public static ApiResult err(IRespCode code, E data) { + return build(code.getRespCode(), code.getMessage(), data); + } + + public static ApiResult err(String message) { + return err(SERVER_ERROR.getRespCode(), message); + } + + public static ApiResult err(String code, String message) { + if (code == null) { + return err(message); + } + return build(code, message, null); + } + + public static ApiResult build(IRespCode code) { + return build(code, null); + } + + public static ApiResult build(IRespCode code, E data) { + return build(code.getRespCode(), code.getMessage(), data); + } + + public static ApiResult build(String code, String message, E data) { + return new ApiResult<>(code, message, data); + } + + public static ApiResult with(Throwable e) { + if (e != null && e instanceof ApiException) { + return err(((ApiException) e).getCode(), e.getMessage()); + } + if (e != null) { + return err(SERVER_ERROR, e.getMessage()); + } + return err(SERVER_ERROR); + } + + @ConstructorProperties({"code", "msg", "data"}) + public ApiResult(String code, String message, E data) { + super(code, message, data); + } + + public ApiResult map(Function mapper) { + T data = mapper.apply(getData()); + return new ApiResult<>(getCode(), getMsg(), data); + } +} diff --git a/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/Result.java b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/Result.java new file mode 100644 index 0000000..1fef88c --- /dev/null +++ b/axzo-common-domain/src/main/java/cn/axzo/framework/domain/web/result/Result.java @@ -0,0 +1,13 @@ +package cn.axzo.framework.domain.web.result; + +import java.util.Map; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 20:28 + **/ +public interface Result { + + Map toMap(); +} diff --git a/axzo-common-framework/axzo-common-context/pom.xml b/axzo-common-framework/axzo-common-context/pom.xml new file mode 100644 index 0000000..e4bb04a --- /dev/null +++ b/axzo-common-framework/axzo-common-context/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + axzo-common-framework + cn.axzo.framework.framework + 1.0.0-SNAPSHOT + + + axzo-common-context + Axzo Common Context + + + + cn.axzo.framework + axzo-common-domain + + + org.springframework + spring-context + + + org.springframework + spring-context-support + + + javax.cache + cache-api + + + + + org.springframework.data + spring-data-commons + true + + + diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/Placeholders.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/Placeholders.java new file mode 100644 index 0000000..ad11521 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/Placeholders.java @@ -0,0 +1,11 @@ +package cn.axzo.framework.context; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:30 + **/ +public interface Placeholders { + + String APPLICATION_NAME = "${spring.application.name:${vcap.application.name:${spring.config.name:application}}}"; +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/ClientFormatterRegistrar.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/ClientFormatterRegistrar.java new file mode 100644 index 0000000..5f6e340 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/ClientFormatterRegistrar.java @@ -0,0 +1,18 @@ +package cn.axzo.framework.context.client; + +import cn.axzo.framework.context.convert.EnumStdConverters; +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:55 + **/ +public class ClientFormatterRegistrar implements FormatterRegistrar { + + @Override + public void registerFormatters(FormatterRegistry registry) { + registry.addConverter(new EnumStdConverters.EnumStdToStringConverter()); + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/IQueryMap.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/IQueryMap.java new file mode 100644 index 0000000..67a93e7 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/IQueryMap.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.context.client; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:52 + **/ +public interface IQueryMap { + + default QueryMap toQueryMap() { + QueryMap.Builder builder = QueryMap.builder(); + append(builder); + return builder.build(); + } + + void append(QueryMap.Builder builder); +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/QueryMap.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/QueryMap.java new file mode 100644 index 0000000..f004e6b --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/client/QueryMap.java @@ -0,0 +1,64 @@ +package cn.axzo.framework.context.client; + +import cn.axzo.framework.core.util.IgnoreNullMap; +import org.jooq.lambda.Seq; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; + +import javax.annotation.Nonnull; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:53 + **/ +public class QueryMap extends IgnoreNullMap { + + private static FormattingConversionService conversionService = new DefaultFormattingConversionService(); + + static { + new ClientFormatterRegistrar().registerFormatters(conversionService); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public Object put(String key, Object value) { + if (value == null) { + return super.put(key, null); + } + if (value instanceof Iterable) { + return super.put(key, convert((Iterable) value)); + } + return super.put(key, convert(value)); + } + + private Iterable convert(Iterable values) { + return Seq.seq(values).map(this::convert).toList(); + } + + private Object convert(@Nonnull Object value) { + if (value instanceof String) { + return value; + } + if (conversionService.canConvert(value.getClass(), String.class)) { + return conversionService.convert(value, String.class); + } + return value; + } + + public static class Builder { + private QueryMap map = new QueryMap(); + + public Builder put(String key, Object value) { + map.put(key, value); + return this; + } + + public QueryMap build() { + return map; + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/EnumStdConverters.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/EnumStdConverters.java new file mode 100644 index 0000000..0d8a6c9 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/EnumStdConverters.java @@ -0,0 +1,236 @@ +package cn.axzo.framework.context.convert; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.util.StringUtil; +import com.google.common.base.Enums; +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import lombok.val; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.*; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:31 + **/ +@ParametersAreNonnullByDefault +public class EnumStdConverters { + + public static Collection getConvertersToRegister() { + List converters = new ArrayList<>(); + converters.add(new EnumStdToSimpleTypeConverter()); + converters.add(new IntegerToEnumConverterFactory()); + converters.add(new StringToEnumConverterFactory()); + return converters; + } + + public static class EnumStdToStringConverter extends EnumStdToSimpleTypeConverter { + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + Object target = super.convert(source, sourceType, targetType); + if (String.class.equals(targetType.getType())) { + return Objects.toString(target); + } + return target; + } + } + + @WritingConverter + public static class EnumStdToSimpleTypeConverter implements ConditionalGenericConverter { + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + val type = sourceType.getObjectType(); + return type.isEnum() && (ICode.class.isAssignableFrom(type) || IStringCode.class.isAssignableFrom(type)); + } + + @Nullable + @Override + public Set getConvertibleTypes() { + return Sets.newHashSet( + new ConvertiblePair(Enum.class, Integer.class), + new ConvertiblePair(Enum.class, String.class) + ); + } + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Object target = source; + if (source instanceof ICode) { + target = ((ICode) source).getCode(); + } + if (source instanceof IStringCode) { + target = ((IStringCode) source).getCode(); + } + return target; + } + } + + @SuppressWarnings("unchecked") + @ReadingConverter + public static class IntegerToEnumConverterFactory implements ConverterFactory { + + @Nonnull + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + if (enumType == null) { + throw new IllegalArgumentException("The target type " + targetType.getName() + " does not refer to an enum"); + } + if (ICode.class.isAssignableFrom(enumType)) { + return new IntegerToICode(enumType); + } + if (IStringCode.class.isAssignableFrom(enumType)) { + return new IntegerToIStringCode(enumType); + } + return new IntegerToEnum(enumType); + } + } + + @SuppressWarnings("unchecked") + @ReadingConverter + public static final class StringToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + if (enumType == null) { + throw new IllegalArgumentException("The target type " + targetType.getName() + " does not refer to an enum"); + } + if (ICode.class.isAssignableFrom(enumType)) { + return new StringToICode(enumType); + } + if (IStringCode.class.isAssignableFrom(enumType)) { + return new StringToIStringCode(enumType); + } + return new StringToEnum(enumType); + } + } + + private static class IntegerToICode & ICode> implements Converter { + + private final Class enumType; + + IntegerToICode(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Integer source) { + return EnumStdUtil.findEnum(enumType, source) + .orElseGet(() -> enumType.getEnumConstants()[source]); + } + } + + private static class IntegerToIStringCode & IStringCode> implements Converter { + + private final Class enumType; + + IntegerToIStringCode(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Integer source) { + return EnumStdUtil.findEnum(enumType, Objects.toString(source)) + .orElseGet(() -> enumType.getEnumConstants()[source]); + } + } + + private static class IntegerToEnum implements Converter { + + private final Class enumType; + + IntegerToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Integer source) { + return this.enumType.getEnumConstants()[source]; + } + } + + private static class StringToICode & ICode> implements Converter { + + private final Class enumType; + + StringToICode(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (Strings.isNullOrEmpty(source)) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + if (StringUtil.isInteger(source)) { + return EnumStdUtil.findEnum(enumType, StringUtil.parseInt(source)) + .orElseGet(() -> Enums.getIfPresent(enumType, source.trim()).orNull()); + } + return Enums.getIfPresent(enumType, source.trim()).orNull(); + } + } + + private static class StringToIStringCode & IStringCode> implements Converter { + + private final Class enumType; + + StringToIStringCode(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (Strings.isNullOrEmpty(source)) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + return EnumStdUtil.findEnum(enumType, source) + .orElseGet(() -> Enums.getIfPresent(enumType, source.trim()).orNull()); + } + } + + private static class StringToEnum> implements Converter { + + private final Class enumType; + + StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (Strings.isNullOrEmpty(source)) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + return Enums.getIfPresent(enumType, source.trim()).orNull(); + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/StringTrimmerConverter.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/StringTrimmerConverter.java new file mode 100644 index 0000000..8355b1c --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/convert/StringTrimmerConverter.java @@ -0,0 +1,55 @@ +package cn.axzo.framework.context.convert; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +/** + * @Description 去除字符串首尾空格 + * @Author liyong.tian + * @Date 2020/9/8 17:33 + **/ +public class StringTrimmerConverter implements Converter { + + private final String charsToDelete; + + private final boolean emptyAsNull; + + /** + * Create a new StringTrimmerConverter. + * + * @param emptyAsNull {@code true} if an empty String is to be + * transformed into {@code null} + */ + public StringTrimmerConverter(boolean emptyAsNull) { + this(null, emptyAsNull); + } + + /** + * Create a new StringTrimmerConverter. + * + * @param charsToDelete a set of characters to delete, in addition to + * trimming an input String. Useful for deleting unwanted line breaks: + * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @param emptyAsNull {@code true} if an empty String is to be + * transformed into {@code null} + */ + public StringTrimmerConverter(String charsToDelete, boolean emptyAsNull) { + this.charsToDelete = charsToDelete; + this.emptyAsNull = emptyAsNull; + } + + @Override + public String convert(String source) { + if (source == null) { + return null; + } + String value = source.trim(); + if (this.charsToDelete != null) { + value = StringUtils.deleteAny(value, this.charsToDelete); + } + if (this.emptyAsNull && "".equals(value)) { + return null; + } + return value; + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/CustomFormatterRegistrar.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/CustomFormatterRegistrar.java new file mode 100644 index 0000000..d7a0eeb --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/CustomFormatterRegistrar.java @@ -0,0 +1,42 @@ +package cn.axzo.framework.context.format; + +import cn.axzo.framework.context.convert.EnumStdConverters; +import cn.axzo.framework.context.convert.StringTrimmerConverter; +import cn.axzo.framework.context.format.time.InstantFormatter; +import cn.axzo.framework.context.format.time.LocalDateTimeFormatter; +import cn.axzo.framework.context.format.time.ZonedDateTimeFormatter; +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.Jsr310DateTimeFormatAnnotationFormatterFactory; + +/** + * @Description 统一注册自定义的值转换器 + * @Author liyong.tian + * @Date 2020/9/8 17:35 + **/ +public class CustomFormatterRegistrar implements FormatterRegistrar { + + @Override + public void registerFormatters(FormatterRegistry registry) { + addConverters(registry); + + addFormatters(registry); + + addFormattersForFieldAnnotation(registry); + } + + private void addConverters(FormatterRegistry registry) { + registry.addConverter(new StringTrimmerConverter(false)); + registry.addConverterFactory(new EnumStdConverters.StringToEnumConverterFactory()); + } + + private void addFormatters(FormatterRegistry registry) { + registry.addFormatter(new LocalDateTimeFormatter()); + registry.addFormatter(new ZonedDateTimeFormatter()); + registry.addFormatter(new InstantFormatter()); + } + + private void addFormattersForFieldAnnotation(FormatterRegistry registry) { + registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory()); + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/InstantFormatter.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/InstantFormatter.java new file mode 100644 index 0000000..8de7a44 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/InstantFormatter.java @@ -0,0 +1,47 @@ +package cn.axzo.framework.context.format.time; + +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.lang.UsesJava8; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Locale; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:37 + **/ +@UsesJava8 +public class InstantFormatter implements Formatter { + + private org.springframework.format.datetime.standard.InstantFormatter delegate = + new org.springframework.format.datetime.standard.InstantFormatter(); + + @Override + public Instant parse(String text, Locale locale) throws ParseException { + return new InstantFormatterParser().parse(text, locale); + } + + @Override + public String print(Instant object, Locale locale) { + return Long.toString(object.toEpochMilli()); + } + + @UsesJava8 + private class InstantFormatterParser implements Parser { + + @Override + public Instant parse(String text, Locale locale) throws ParseException { + if (StringUtils.trimAllWhitespace(text).matches("^[1-9]\\d+$")) { + long mills = NumberUtils.parseNumber(text, Long.class); + return Instant.ofEpochMilli(mills); + } + // 如果不是Long值, 则委派给内置的转换器执行, 实现了对内置转换器的扩展 + return delegate.parse(text, locale); + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/LocalDateTimeFormatter.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/LocalDateTimeFormatter.java new file mode 100644 index 0000000..fd4e822 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/LocalDateTimeFormatter.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.context.format.time; + +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.datetime.standard.DateTimeFormatterFactory; +import org.springframework.format.datetime.standard.TemporalAccessorPrinter; +import org.springframework.lang.UsesJava8; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +import java.text.ParseException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +import static java.time.ZoneId.systemDefault; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:39 + **/ +@UsesJava8 +public class LocalDateTimeFormatter implements Formatter { + + private final DateTimeFormatter formatter; + + public LocalDateTimeFormatter() { + DateTimeFormatter fallback = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); + this.formatter = (new DateTimeFormatterFactory()).createDateTimeFormatter(fallback); + } + + public LocalDateTime parse(String text, Locale locale) throws ParseException { + return (new LocalDateTimeParser()).parse(text, locale); + } + + public String print(LocalDateTime localDateTime, Locale locale) { + return (new TemporalAccessorPrinter(this.formatter)).print(localDateTime.atZone(systemDefault()), locale); + } + + @UsesJava8 + private class LocalDateTimeParser implements Parser { + private LocalDateTimeParser() { + } + + public LocalDateTime parse(String text, Locale locale) { + if(StringUtils.trimAllWhitespace(text).matches("^[1-9]\\d+$")) { + long mills = NumberUtils.parseNumber(text, Long.class); + return LocalDateTime.ofInstant(Instant.ofEpochMilli(mills), systemDefault()); + } + // 如果不是Long值, 则委派给内置的转换器执行, 实现了对内置转换器的扩展 + return LocalDateTime.parse(text, LocalDateTimeFormatter.this.formatter); + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/ZonedDateTimeFormatter.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/ZonedDateTimeFormatter.java new file mode 100644 index 0000000..c3a3f6c --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/format/time/ZonedDateTimeFormatter.java @@ -0,0 +1,58 @@ +package cn.axzo.framework.context.format.time; + +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.datetime.standard.DateTimeFormatterFactory; +import org.springframework.format.datetime.standard.TemporalAccessorPrinter; +import org.springframework.lang.UsesJava8; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +import java.text.ParseException; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +import static java.time.ZoneId.systemDefault; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:40 + **/ +@UsesJava8 +public class ZonedDateTimeFormatter implements Formatter { + + private final DateTimeFormatter formatter; + + public ZonedDateTimeFormatter() { + DateTimeFormatter fallback = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); + this.formatter = new DateTimeFormatterFactory().createDateTimeFormatter(fallback); + } + + @Override + public ZonedDateTime parse(String text, Locale locale) throws ParseException { + return new ZonedDateTimeParser().parse(text, locale); + } + + @Override + public String print(ZonedDateTime object, Locale locale) { + return new TemporalAccessorPrinter(formatter).print(object, locale); + } + + @UsesJava8 + private class ZonedDateTimeParser implements Parser { + + @Override + public ZonedDateTime parse(String text, Locale locale) { + if (StringUtils.trimAllWhitespace(text).matches("^[1-9]\\d+$")) { + long mills = NumberUtils.parseNumber(text, Long.class); + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(mills), systemDefault()); + } + // 如果不是Long值, 则委派给内置的转换器执行, 实现了对内置转换器的扩展 + return ZonedDateTime.parse(text, ZonedDateTimeFormatter.this.formatter); + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidator.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidator.java new file mode 100644 index 0000000..d271432 --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidator.java @@ -0,0 +1,47 @@ +package cn.axzo.framework.context.validation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.ConversionService; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.SmartValidator; + +import static java.lang.String.format; +import static jodd.util.StringUtil.isNotBlank; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:48 + **/ +@Slf4j +@RequiredArgsConstructor +public class SpringValidator { + + private final SmartValidator smartValidator; + + private final ConversionService conversionService; + + public void validate(Object target) { + validate(target, "object"); + } + + public void validate(Object target, String objectName) { + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(target, objectName); + errors.initConversion(conversionService); + smartValidator.validate(target, errors); + if (errors.hasErrors()) { + SpringValidatorException exception = new SpringValidatorException(errors); + String message; + FieldError error = exception.getBindingResult().getFieldError(); + if (isNotBlank(error.getDefaultMessage())) { + message = format("%s %s", error.getField(), error.getDefaultMessage()); + } else { + message = "Validation refused"; + } + log.warn(objectName + "校验失败," + message); + throw exception; + } + } +} diff --git a/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidatorException.java b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidatorException.java new file mode 100644 index 0000000..3f36e7b --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/main/java/cn/axzo/framework/context/validation/SpringValidatorException.java @@ -0,0 +1,224 @@ +package cn.axzo.framework.context.validation; + +import lombok.Getter; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.util.Assert; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import java.beans.PropertyEditor; +import java.util.List; +import java.util.Map; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/8 17:50 + **/ +public class SpringValidatorException extends RuntimeException implements BindingResult { + + @Getter + private final BindingResult bindingResult; + + public SpringValidatorException(BindingResult bindingResult) { + Assert.notNull(bindingResult, "BindingResult must not be null"); + this.bindingResult = bindingResult; + } + + @Override + public Object getTarget() { + return bindingResult.getTarget(); + } + + @Override + public Map getModel() { + return bindingResult.getModel(); + } + + @Override + public Object getRawFieldValue(String field) { + return bindingResult.getRawFieldValue(field); + } + + @Override + public PropertyEditor findEditor(String field, Class valueType) { + return bindingResult.findEditor(field, valueType); + } + + @Override + public PropertyEditorRegistry getPropertyEditorRegistry() { + return bindingResult.getPropertyEditorRegistry(); + } + + @Override + public void addError(ObjectError error) { + bindingResult.addError(error); + } + + @Override + public String[] resolveMessageCodes(String errorCode) { + return bindingResult.resolveMessageCodes(errorCode); + } + + @Override + public String[] resolveMessageCodes(String errorCode, String field) { + return bindingResult.resolveMessageCodes(errorCode, field); + } + + @Override + public void recordSuppressedField(String field) { + bindingResult.recordSuppressedField(field); + } + + @Override + public String[] getSuppressedFields() { + return bindingResult.getSuppressedFields(); + } + + @Override + public String getObjectName() { + return bindingResult.getObjectName(); + } + + @Override + public void setNestedPath(String nestedPath) { + bindingResult.setNestedPath(nestedPath); + } + + @Override + public String getNestedPath() { + return bindingResult.getNestedPath(); + } + + @Override + public void pushNestedPath(String subPath) { + bindingResult.pushNestedPath(subPath); + } + + @Override + public void popNestedPath() throws IllegalStateException { + bindingResult.popNestedPath(); + } + + @Override + public void reject(String errorCode) { + bindingResult.reject(errorCode); + } + + @Override + public void reject(String errorCode, String defaultMessage) { + bindingResult.reject(errorCode, defaultMessage); + } + + @Override + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + bindingResult.reject(errorCode, errorArgs, defaultMessage); + } + + @Override + public void rejectValue(String field, String errorCode) { + bindingResult.reject(field, errorCode); + } + + @Override + public void rejectValue(String field, String errorCode, String defaultMessage) { + bindingResult.rejectValue(field, errorCode, defaultMessage); + } + + @Override + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + + @Override + public void addAllErrors(Errors errors) { + bindingResult.addAllErrors(errors); + } + + @Override + public boolean hasErrors() { + return bindingResult.hasErrors(); + } + + @Override + public int getErrorCount() { + return bindingResult.getErrorCount(); + } + + @Override + public List getAllErrors() { + return bindingResult.getAllErrors(); + } + + @Override + public boolean hasGlobalErrors() { + return bindingResult.hasGlobalErrors(); + } + + @Override + public int getGlobalErrorCount() { + return bindingResult.getGlobalErrorCount(); + } + + @Override + public List getGlobalErrors() { + return bindingResult.getGlobalErrors(); + } + + @Override + public ObjectError getGlobalError() { + return bindingResult.getGlobalError(); + } + + @Override + public boolean hasFieldErrors() { + return bindingResult.hasFieldErrors(); + } + + @Override + public int getFieldErrorCount() { + return bindingResult.getFieldErrorCount(); + } + + @Override + public List getFieldErrors() { + return bindingResult.getFieldErrors(); + } + + @Override + public FieldError getFieldError() { + return bindingResult.getFieldError(); + } + + @Override + public boolean hasFieldErrors(String field) { + return bindingResult.hasFieldErrors(field); + } + + @Override + public int getFieldErrorCount(String field) { + return bindingResult.getFieldErrorCount(field); + } + + @Override + public List getFieldErrors(String field) { + return bindingResult.getFieldErrors(field); + } + + @Override + public FieldError getFieldError(String field) { + return bindingResult.getFieldError(field); + } + + @Override + public Object getFieldValue(String field) { + return bindingResult.getFieldValue(field); + } + + @Override + public Class getFieldType(String field) { + return bindingResult.getFieldType(field); + } +} diff --git a/axzo-common-framework/axzo-common-context/src/test/java/cn/axzo/framework/AppTest.java b/axzo-common-framework/axzo-common-context/src/test/java/cn/axzo/framework/AppTest.java new file mode 100644 index 0000000..186647a --- /dev/null +++ b/axzo-common-framework/axzo-common-context/src/test/java/cn/axzo/framework/AppTest.java @@ -0,0 +1,20 @@ +package cn.axzo.framework; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test for simple App. + */ +public class AppTest +{ + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() + { + assertTrue( true ); + } +} diff --git a/axzo-common-framework/pom.xml b/axzo-common-framework/pom.xml new file mode 100644 index 0000000..3193757 --- /dev/null +++ b/axzo-common-framework/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + cn.axzo.framework.framework + axzo-common-framework + pom + Axzo Common Framework + + + axzo-common-context + + diff --git a/axzo-common-jackson/jackson-datatype-enumstd/pom.xml b/axzo-common-jackson/jackson-datatype-enumstd/pom.xml new file mode 100644 index 0000000..3dba5fb --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/pom.xml @@ -0,0 +1,42 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-datatype-enumstd + Axzo Common Jackson DataType EnumStd + + + + cn/axzo/framework/jackson/datatype/enumstd + ${project.groupId}.datatype.enumstd + + + + + cn.axzo.framework + axzo-common-core + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumFormat.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumFormat.java new file mode 100644 index 0000000..b9ad880 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumFormat.java @@ -0,0 +1,18 @@ +package cn.axzo.framework.jackson.datatype.enumstd; + +import java.lang.annotation.*; + +/** + * @author liyong.tian + * @since 2020/8/28 17:41 + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnumFormat { + + String nameSuffix() default "Name"; + + // 是否为内嵌对象,如果是内嵌对象,则其他配置不作数 + boolean nested() default false; +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumStdModule.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumStdModule.java new file mode 100644 index 0000000..87b4d3f --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/EnumStdModule.java @@ -0,0 +1,35 @@ +package cn.axzo.framework.jackson.datatype.enumstd; + +import cn.axzo.framework.jackson.datatype.enumstd.deser.EnumDeserializerModifier; +import cn.axzo.framework.jackson.datatype.enumstd.ser.EnumSerializerModifier; +import cn.axzo.framework.jackson.datatype.enumstd.ser.IntegerEnumFormatSerializer; +import cn.axzo.framework.jackson.datatype.enumstd.ser.StringEnumFormatSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * @author liyong.tian + * @since 2020/8/28 17:42 + */ +public class EnumStdModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public EnumStdModule() { + super(cn.axzo.framework.jackson.datatype.enumstd.PackageVersion.VERSION); + + // first serializer + setSerializerModifier(new EnumSerializerModifier()); + addSerializer(Integer.class, new IntegerEnumFormatSerializer(Integer.class)); + addSerializer(Integer.TYPE, new IntegerEnumFormatSerializer(Integer.TYPE)); + addSerializer(String.class, new StringEnumFormatSerializer()); + + // then deserializer + setDeserializerModifier(new EnumDeserializerModifier()); + } + + // yes, will try to avoid duplicate registations (if MapperFeature enabled) + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/JavaTypeUtil.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/JavaTypeUtil.java new file mode 100644 index 0000000..40a7f2b --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/JavaTypeUtil.java @@ -0,0 +1,50 @@ +package cn.axzo.framework.jackson.datatype.enumstd; + +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import com.fasterxml.jackson.databind.JavaType; +import lombok.experimental.UtilityClass; +import lombok.val; + +/** + * @author liyong.tian + * @since 2020/8/28 17:44 + */ +@UtilityClass +public class JavaTypeUtil { + + public boolean isEnumStdType(JavaType type) { + if (type == null) { + return false; + } + if (type.hasContentType()) { + type = type.getContentType(); + } + val rawClass = type.getRawClass(); + return type.isEnumType() + && (ICode.class.isAssignableFrom(rawClass) || IStringCode.class.isAssignableFrom(rawClass)); + } + + public boolean isIntType(JavaType type) { + if (type == null) { + return false; + } + if (type.hasContentType()) { + type = type.getContentType(); + } + val rawClass = type.getRawClass(); + + return int.class.equals(rawClass) || Integer.class.equals(rawClass); + } + + public boolean isStringType(JavaType type) { + if (type == null) { + return false; + } + if (type.hasContentType()) { + type = type.getContentType(); + } + val rawClass = type.getRawClass(); + return String.class.equals(rawClass); + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java new file mode 100644 index 0000000..f624532 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson.datatype.enumstd; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "1.0.0-SNAPSHOT", "cn.axzo.framework.jackson", "jackson-datatype-enumstd"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java.in b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java.in new file mode 100644 index 0000000..8b656b7 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumDeserializerModifier.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumDeserializerModifier.java new file mode 100644 index 0000000..ccc7e36 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumDeserializerModifier.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.jackson.datatype.enumstd.deser; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.std.EnumDeserializer; + +/** + * @author liyong.tian + * @since 2020/8/28 17:51 + */ +public class EnumDeserializerModifier extends BeanDeserializerModifier { + + @Override + public JsonDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, + JsonDeserializer deserializer) { + if (deserializer instanceof EnumDeserializer) { + return new EnumStdDeserializer<>(type.getRawClass(), (EnumDeserializer) deserializer); + } + return deserializer; + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumStdDeserializer.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumStdDeserializer.java new file mode 100644 index 0000000..e4f51be --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/deser/EnumStdDeserializer.java @@ -0,0 +1,83 @@ +package cn.axzo.framework.jackson.datatype.enumstd.deser; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.util.StringUtil; +import cn.axzo.framework.jackson.datatype.enumstd.JavaTypeUtil; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.EnumDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import javax.annotation.Nullable; + +import java.io.IOException; + +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT; +import static com.fasterxml.jackson.databind.DeserializationFeature.READ_ENUMS_USING_TO_STRING; + +/** + * @see EnumDeserializerModifier + * @author liyong.tian + * @since 2020/8/28 18:00 + */ +public class EnumStdDeserializer & IStringCode, I extends Enum & ICode> + extends StdScalarDeserializer implements ContextualDeserializer { + @Nullable + private Class _codeClass; + + @Nullable + private Class _stringCodeClass; + + private final EnumDeserializer _enumDeserializer; + + @SuppressWarnings("unchecked") + public EnumStdDeserializer(Class rawCls, EnumDeserializer enumDeserializer) { + super(rawCls); + this._enumDeserializer = enumDeserializer; + if (rawCls.isEnum()) { + if (ICode.class.isAssignableFrom(rawCls)) { + this._codeClass = (Class) rawCls; + } else if (IStringCode.class.isAssignableFrom(rawCls)) { + this._stringCodeClass = (Class) rawCls; + } + } + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + if (property != null && JavaTypeUtil.isEnumStdType(property.getType())) { + return this; + } + return _enumDeserializer; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (!ctxt.isEnabled(READ_ENUMS_USING_TO_STRING)) { + String text = p.getText(); + if (_codeClass != null) { + if (p.getCurrentToken() == VALUE_NUMBER_INT || StringUtil.isInteger(text)) { + return EnumStdUtil.findEnum(_codeClass, StringUtil.parseInt(text)).orElse(null); + } + } + if (_stringCodeClass != null) { + return EnumStdUtil.findEnum(_stringCodeClass, text).orElse(null); + } + } + // 委托给Jackson自带的枚举反序列化器 + return _enumDeserializer.deserialize(p, ctxt); + } + + /** + * 将此反序列化器缓存起来,避免反复创建 + */ + @Override + public boolean isCachable() { + return true; + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumSerializerModifier.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumSerializerModifier.java new file mode 100644 index 0000000..4f41016 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumSerializerModifier.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.jackson.datatype.enumstd.ser; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.std.EnumSerializer; + +/** + * @author liyong.tian + * @since 2020/8/28 17:53 + */ +public class EnumSerializerModifier extends BeanSerializerModifier { + + @Override + public JsonSerializer modifyEnumSerializer(SerializationConfig config, JavaType valueType, + BeanDescription beanDesc, JsonSerializer serializer) { + if (serializer instanceof EnumSerializer) { + return new EnumStdSerializer((EnumSerializer) serializer); + } + return serializer; + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumStdSerializer.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumStdSerializer.java new file mode 100644 index 0000000..c88f298 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/EnumStdSerializer.java @@ -0,0 +1,100 @@ +package cn.axzo.framework.jackson.datatype.enumstd.ser; + +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.enums.meta.IntCode; +import cn.axzo.framework.core.enums.meta.StringCode; +import cn.axzo.framework.jackson.datatype.enumstd.EnumFormat; +import cn.axzo.framework.jackson.datatype.enumstd.JavaTypeUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.EnumSerializer; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import javax.annotation.Nullable; +import java.io.IOException; + +/** + * @see EnumSerializerModifier + * @author liyong.tian + * @since 2020/8/28 17:54 + */ +public class EnumStdSerializer extends StdScalarSerializer> implements ContextualSerializer { + + private final EnumSerializer _enumSerializer; + + @Nullable + private final EnumFormat enumFormat; + + EnumStdSerializer(EnumSerializer enumSerializer) { + this(enumSerializer, null); + } + + private EnumStdSerializer(EnumSerializer enumSerializer, @Nullable EnumFormat enumFormat) { + super(enumSerializer.getEnumValues().getEnumClass(), false); + this._enumSerializer = enumSerializer; + this.enumFormat = enumFormat; + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + if (property != null && JavaTypeUtil.isEnumStdType(property.getType())) { + EnumFormat enumFormat = property.getAnnotation(EnumFormat.class); + if (enumFormat == null) { + enumFormat = property.getContextAnnotation(EnumFormat.class); + } + if (enumFormat != null) { + return new EnumStdSerializer(_enumSerializer, enumFormat); + } + return this; + } + return _enumSerializer; + } + + @Override + public void serialize(Enum value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) { + return; + } + if (value instanceof ICode) { + serializeICode((ICode) value, gen); + return; + } + if (value instanceof IStringCode) { + serializeIStringCode((IStringCode) value, gen); + return; + } + _enumSerializer.serialize(value, gen, provider); + } + + private void serializeICode(ICode code, JsonGenerator gen) throws IOException { + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName == null || enumFormat == null) { + gen.writeNumber(code.getCode()); + return; + } + if (enumFormat.nested()) { + gen.writeObject(new IntCode(code.getCode(), code.getName())); + } else { + gen.writeNumber(code.getCode()); + gen.writeStringField(fieldName + enumFormat.nameSuffix(), code.getName()); + } + } + + private void serializeIStringCode(IStringCode code, JsonGenerator gen) throws IOException { + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName == null || enumFormat == null) { + gen.writeString(code.getCode()); + return; + } + if (enumFormat.nested()) { + gen.writeObject(new StringCode(code.getCode(), code.getName())); + } else { + gen.writeString(code.getCode()); + gen.writeStringField(fieldName + enumFormat.nameSuffix(), code.getName()); + } + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/IntegerEnumFormatSerializer.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/IntegerEnumFormatSerializer.java new file mode 100644 index 0000000..534f45b --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/IntegerEnumFormatSerializer.java @@ -0,0 +1,95 @@ +package cn.axzo.framework.jackson.datatype.enumstd.ser; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.ReservedEnum; +import cn.axzo.framework.core.enums.meta.IntCode; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.jackson.datatype.enumstd.EnumFormat; +import cn.axzo.framework.jackson.datatype.enumstd.JavaTypeUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.NumberSerializers.IntegerSerializer; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Optional; + +import static jodd.util.StringPool.EMPTY; + +/** + * @author liyong.tian + * @since 2020/8/28 17:57 + */ +public class IntegerEnumFormatSerializer extends StdScalarSerializer implements ContextualSerializer { + + private final IntegerSerializer _integerSerializer; + + @Nullable + private final EnumFormat enumFormat; + + @Nonnull + private Class> using; + + public IntegerEnumFormatSerializer(Class type) { + this(type, null, null); + } + + private IntegerEnumFormatSerializer(Class type, + @Nullable EnumFormat enumFormat, + @Nullable Class> using) { + super(type, false); + this._integerSerializer = new IntegerSerializer(type); + this.enumFormat = enumFormat; + this.using = using == null ? ReservedEnum.NoneStr.class : using; + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + if (property != null && JavaTypeUtil.isIntType(property.getType())) { + EnumFormat enumFormat = property.getAnnotation(EnumFormat.class); + if (enumFormat == null) { + enumFormat = property.getContextAnnotation(EnumFormat.class); + } + if (enumFormat != null) { + ReservedEnum reservedEnum = property.getAnnotation(ReservedEnum.class); + if (reservedEnum != null) { + return new IntegerEnumFormatSerializer(handledType(), enumFormat, reservedEnum.using()); + } + return new IntegerEnumFormatSerializer(handledType(), enumFormat, null); + } + } + return _integerSerializer; + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) { + return; + } + if (enumFormat != null && value instanceof Integer && ICode.class.isAssignableFrom(using)) { + Integer code = (Integer) value; + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName == null) { + gen.writeNumber(code); + return; + } + + Optional optional = EnumStdUtil.findEnum(ClassUtil.cast(using), code); + String name = optional.map(ICode::getName).orElse(EMPTY); + if (enumFormat.nested()) { + gen.writeObject(new IntCode(code, name)); + } else { + gen.writeNumber(code); + gen.writeStringField(fieldName + enumFormat.nameSuffix(), name); + } + return; + } + _integerSerializer.serialize(value, gen, provider); + } +} diff --git a/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/StringEnumFormatSerializer.java b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/StringEnumFormatSerializer.java new file mode 100644 index 0000000..5edb6f9 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-enumstd/src/main/java/cn/axzo/framework/jackson/datatype/enumstd/ser/StringEnumFormatSerializer.java @@ -0,0 +1,94 @@ +package cn.axzo.framework.jackson.datatype.enumstd.ser; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.enums.ReservedEnum; +import cn.axzo.framework.core.enums.meta.StringCode; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.jackson.datatype.enumstd.JavaTypeUtil; +import cn.axzo.framework.jackson.datatype.enumstd.EnumFormat; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import com.fasterxml.jackson.databind.ser.std.StringSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Optional; + +import static jodd.util.StringPool.EMPTY; + +/** + * @author liyong.tian + * @since 2020/8/28 17:59 + */ +public class StringEnumFormatSerializer extends StdScalarSerializer implements ContextualSerializer { + + private final StringSerializer _stringSerializer; + + @Nullable + private final EnumFormat enumFormat; + + @Nonnull + private Class> using; + + public StringEnumFormatSerializer() { + this(null, null); + } + + private StringEnumFormatSerializer(@Nullable EnumFormat enumFormat, + @Nullable Class> using) { + super(String.class, false); + this._stringSerializer = new StringSerializer(); + this.enumFormat = enumFormat; + this.using = using == null ? ReservedEnum.NoneStr.class : using; + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + if (property != null && JavaTypeUtil.isStringType(property.getType())) { + EnumFormat enumFormat = property.getAnnotation(EnumFormat.class); + if (enumFormat == null) { + enumFormat = property.getContextAnnotation(EnumFormat.class); + } + if (enumFormat != null) { + ReservedEnum reservedEnum = property.getAnnotation(ReservedEnum.class); + if (reservedEnum != null) { + return new StringEnumFormatSerializer(enumFormat, reservedEnum.using()); + } + return new StringEnumFormatSerializer(enumFormat, null); + } + } + return _stringSerializer; + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) { + return; + } + if (value instanceof String && enumFormat != null && IStringCode.class.isAssignableFrom(using)) { + String code = (String) value; + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName == null) { + gen.writeString(code); + return; + } + + Optional optional = EnumStdUtil.findEnum(ClassUtil.cast(using), code); + String name = optional.map(IStringCode::getName).orElse(EMPTY); + if (enumFormat.nested()) { + gen.writeObject(new StringCode(code, name)); + } else { + gen.writeString(code); + gen.writeStringField(fieldName + enumFormat.nameSuffix(), name); + } + return; + } + _stringSerializer.serialize(value, gen, provider); + } +} diff --git a/axzo-common-jackson/jackson-datatype-fraction/pom.xml b/axzo-common-jackson/jackson-datatype-fraction/pom.xml new file mode 100644 index 0000000..3503394 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/pom.xml @@ -0,0 +1,47 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-datatype-fraction + + + + cn/axzo/framework/jackson/datatype/fraction + ${project.groupId}.datatype.fraction + + + + + cn.axzo.framework + axzo-common-core + + + cn.axzo.framework + axzo-common-math + + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/FractionModule.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/FractionModule.java new file mode 100644 index 0000000..b463247 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/FractionModule.java @@ -0,0 +1,38 @@ +package cn.axzo.framework.jackson.datatype.fraction; + +import cn.axzo.framework.jackson.datatype.fraction.deser.BigFractionDeserializer; +import cn.axzo.framework.jackson.datatype.fraction.deser.FractionDeserializer; +import cn.axzo.framework.jackson.datatype.fraction.ser.BigFractionSerializer; +import cn.axzo.framework.jackson.datatype.fraction.ser.FractionSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.apache.commons.math3.fraction.BigFraction; +import org.apache.commons.math3.fraction.Fraction; + +import static cn.axzo.framework.jackson.datatype.fraction.PackageVersion.VERSION; + +/** + * @author liyong.tian + * @since 2020/8/28 18:09 + */ +public class FractionModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public FractionModule() { + super(VERSION); + + // first serializer + addSerializer(Fraction.class, new FractionSerializer()); + addSerializer(BigFraction.class, new BigFractionSerializer()); + + // deserializers + addDeserializer(Fraction.class, new FractionDeserializer()); + addDeserializer(BigFraction.class, new BigFractionDeserializer()); + } + + // yes, will try to avoid duplicate registations (if MapperFeature enabled) + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } +} diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java new file mode 100644 index 0000000..2a4eb78 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson.datatype.fraction; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "1.0.0-SNAPSHOT", "cn.axzo.framework.jackson", "jackson-datatype-fraction"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java.in b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java.in new file mode 100644 index 0000000..8b656b7 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/BigFractionDeserializer.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/BigFractionDeserializer.java new file mode 100644 index 0000000..409c616 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/BigFractionDeserializer.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.jackson.datatype.fraction.deser; + +import cn.axzo.framework.math.fraction.BigFractions; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.apache.commons.math3.fraction.BigFraction; + +import java.io.IOException; + +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static jodd.util.StringPool.COMMA; + +/** + * long类型的分数反序列化器 + *

+ * 百分之一的表示形式: + * 1/100 + * [1,100] + * + * @author jearton + * @since 2017/3/3 + */ +public class BigFractionDeserializer extends JsonDeserializer { + + @Override + public BigFraction deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.isExpectedStartArrayToken()) { + StringBuilder sb = new StringBuilder(); + JsonToken t = p.currentToken(); + while (t != null) { + sb.append(p.getText()); + if (t != START_ARRAY & (t = p.nextToken()) != END_ARRAY & t != null) { + sb.append(COMMA); + } + } + return BigFractions.parse(sb.toString()); + } + return BigFractions.parse(p.getText()); + } +} diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/FractionDeserializer.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/FractionDeserializer.java new file mode 100644 index 0000000..f2c3c90 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/deser/FractionDeserializer.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.jackson.datatype.fraction.deser; + +import cn.axzo.framework.math.fraction.Fractions; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.apache.commons.math3.fraction.Fraction; + +import java.io.IOException; + +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static jodd.util.StringPool.COMMA; + +/** + * int类型的分数反序列化器 + *

+ * 百分之一的表示形式: + * 1/100 + * [1,100] + * + * @author jearton + * @since 2017/3/2 + */ +public class FractionDeserializer extends JsonDeserializer { + + @Override + public Fraction deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.isExpectedStartArrayToken()) { + StringBuilder sb = new StringBuilder(); + JsonToken t = p.currentToken(); + while (t != null) { + sb.append(p.getText()); + if (t != START_ARRAY & (t = p.nextToken()) != END_ARRAY & t != null) { + sb.append(COMMA); + } + } + return Fractions.parse(sb.toString()); + } + return Fractions.parse(p.getText()); + } +} diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/BigFractionSerializer.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/BigFractionSerializer.java new file mode 100644 index 0000000..7ad1e13 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/BigFractionSerializer.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.jackson.datatype.fraction.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.commons.math3.fraction.BigFraction; + +import java.io.IOException; + +/** + * long类型的分数反序列化器 + *

+ * 百分之一的表示形式: + * 1/100 + * [1,100] + * + * @author jearton + * @since 2017/3/2 + */ +public class BigFractionSerializer extends JsonSerializer { + + @Override + public void serialize(BigFraction value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + return; + } + gen.writeString(value.toString()); + } +} diff --git a/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/FractionSerializer.java b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/FractionSerializer.java new file mode 100644 index 0000000..175772d --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-fraction/src/main/java/cn/axzo/framework/jackson/datatype/fraction/ser/FractionSerializer.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.jackson.datatype.fraction.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.commons.math3.fraction.Fraction; + +import java.io.IOException; + +/** + * int类型的分数反序列化器 + *

+ * 百分之一的表示形式: + * 1/100 + * [1,100] + * + * @author jearton + * @since 2017/3/2 + */ +public class FractionSerializer extends JsonSerializer { + + @Override + public void serialize(Fraction value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + return; + } + gen.writeString(value.toString()); + } +} diff --git a/axzo-common-jackson/jackson-datatype-period/pom.xml b/axzo-common-jackson/jackson-datatype-period/pom.xml new file mode 100644 index 0000000..8a69dbb --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-period/pom.xml @@ -0,0 +1,43 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-datatype-period + Axzo Common Jackson DataType Period + + + + cn/axzo/framework/jackson/datatype/period + ${project.groupId}.datatype.period + + + + + cn.axzo.framework + axzo-common-core + + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java new file mode 100644 index 0000000..47c2dcf --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson.datatype.period; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "1.0.0-SNAPSHOT", "cn.axzo.framework.jackson", "jackson-datatype-period"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java.in b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java.in new file mode 100644 index 0000000..8b656b7 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PeriodModule.java b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PeriodModule.java new file mode 100644 index 0000000..b9074b3 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/PeriodModule.java @@ -0,0 +1,28 @@ +package cn.axzo.framework.jackson.datatype.period; + +import cn.axzo.framework.core.time.Period; +import cn.axzo.framework.jackson.datatype.period.deser.PeriodDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import static cn.axzo.framework.jackson.datatype.period.PackageVersion.VERSION; +/** + * @author liyong.tian + * @since 2017/3/4 + */ +public class PeriodModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public PeriodModule() { + super(VERSION); + + // then deserializer + addDeserializer(Period.class, new PeriodDeserializer()); + } + + // yes, will try to avoid duplicate registations (if MapperFeature enabled) + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } +} diff --git a/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/deser/PeriodDeserializer.java b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/deser/PeriodDeserializer.java new file mode 100644 index 0000000..83b1f34 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-period/src/main/java/cn/axzo/framework/jackson/datatype/period/deser/PeriodDeserializer.java @@ -0,0 +1,25 @@ +package cn.axzo.framework.jackson.datatype.period.deser; + +import cn.axzo.framework.core.time.Period; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import jodd.util.StringUtil; + +import java.io.IOException; + +/** + * @author liyong.tian + * @since 2017/3/4 + */ +public class PeriodDeserializer extends JsonDeserializer { + + @Override + public Period deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = p.getText(); + if (StringUtil.isBlank(text)) { + return null; + } + return Period.parse(text); + } +} diff --git a/axzo-common-jackson/jackson-datatype-string-trim/pom.xml b/axzo-common-jackson/jackson-datatype-string-trim/pom.xml new file mode 100644 index 0000000..59cdf49 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/pom.xml @@ -0,0 +1,36 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-datatype-string-trim + Axzo Common Jackson DataType String Trim + + + + cn/axzo/framework/jackson/datatype/string + ${project.groupId}.datatype.string + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java new file mode 100644 index 0000000..03eeeda --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson.datatype.string; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "1.0.0-SNAPSHOT", "cn.axzo.framework.jackson", "jackson-datatype-string-trim"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java.in b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java.in new file mode 100644 index 0000000..8b656b7 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/TrimModule.java b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/TrimModule.java new file mode 100644 index 0000000..eccd074 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/TrimModule.java @@ -0,0 +1,32 @@ +package cn.axzo.framework.jackson.datatype.string; + +import cn.axzo.framework.jackson.datatype.string.deser.TrimDeserializerModifier; +import cn.axzo.framework.jackson.datatype.string.ser.TrimSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import static cn.axzo.framework.jackson.datatype.string.PackageVersion.VERSION; + +/** + * @author jearton + * @since 2017/1/6 + */ +public class TrimModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public TrimModule() { + super(VERSION); + + // first serializer + addSerializer(String.class, new TrimSerializer()); + + // then deserializer + setDeserializerModifier(new TrimDeserializerModifier()); + } + + // yes, will try to avoid duplicate registations (if MapperFeature enabled) + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } +} diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializer.java b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializer.java new file mode 100644 index 0000000..d70dfe0 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializer.java @@ -0,0 +1,33 @@ +package cn.axzo.framework.jackson.datatype.string.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; + +import java.io.IOException; + +/** + * @author jearton + * @since 2017/1/6 + */ +public class TrimDeserializer extends StdScalarDeserializer { + + private StringDeserializer _stringDeserializer; + + public TrimDeserializer(StringDeserializer stringDeserializer) { + super(String.class); + this._stringDeserializer = stringDeserializer; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = _stringDeserializer.deserialize(p, ctxt); + return text == null ? null : text.trim(); + } +} diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializerModifier.java b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializerModifier.java new file mode 100644 index 0000000..89a5b1a --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/deser/TrimDeserializerModifier.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.jackson.datatype.string.deser; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; + +/** + * @author jearton + * @since 2017/1/6 + */ +public class TrimDeserializerModifier extends BeanDeserializerModifier { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { + if (deserializer instanceof StringDeserializer) { + return new TrimDeserializer((StringDeserializer) deserializer); + } + return deserializer; + } +} diff --git a/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/ser/TrimSerializer.java b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/ser/TrimSerializer.java new file mode 100644 index 0000000..1e3bf33 --- /dev/null +++ b/axzo-common-jackson/jackson-datatype-string-trim/src/main/java/cn/axzo/framework/jackson/datatype/string/ser/TrimSerializer.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.jackson.datatype.string.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author jearton + * @since 2017/1/6 + */ +public class TrimSerializer extends StdScalarSerializer { + + public TrimSerializer() { + super(String.class, false); + } + + @Override + public boolean isEmpty(SerializerProvider prov, String value) { + return (value == null) || (value.length() == 0); + } + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value != null) { + gen.writeString(value.trim()); + } + } + + @Override + public JsonNode getSchema(SerializerProvider provider, Type typeHint) { + return createSchemaNode("cn/axzo/framework/jackson/datatype/string", true); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + visitStringFormat(visitor, typeHint); + } +} diff --git a/axzo-common-jackson/jackson-starter/pom.xml b/axzo-common-jackson/jackson-starter/pom.xml new file mode 100644 index 0000000..099fbc0 --- /dev/null +++ b/axzo-common-jackson/jackson-starter/pom.xml @@ -0,0 +1,89 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-starter + Axzo Common Jackson Starter + + + cn/axzo/framework/jackson + ${project.groupId} + + + + + + cn.axzo.framework.jackson + jackson-utility + + + cn.axzo.framework.jackson + jackson-datatype-enumstd + + + cn.axzo.framework.jackson + jackson-datatype-fraction + + + cn.axzo.framework.jackson + jackson-datatype-period + + + cn.axzo.framework.jackson + jackson-datatype-string-trim + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + + + org.apache.geronimo.bundles + json + + + + + org.json + json + + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java b/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java new file mode 100644 index 0000000..0918924 --- /dev/null +++ b/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "1.0.0-SNAPSHOT", "cn.axzo.framework.jackson", "jackson-starter"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java.in b/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java.in new file mode 100644 index 0000000..8b656b7 --- /dev/null +++ b/axzo-common-jackson/jackson-starter/src/main/java/cn/axzo/framework/jackson/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-utility/pom.xml b/axzo-common-jackson/jackson-utility/pom.xml new file mode 100644 index 0000000..e74dde0 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/pom.xml @@ -0,0 +1,160 @@ + + + + axzo-common-jackson + cn.axzo.framework.jackson + 1.0.0-SNAPSHOT + + 4.0.0 + + jackson-utility + Axzo Common Jackson Utility + + + + + com.fasterxml.jackson.module + jackson-module-afterburner + + + + + cn.axzo.framework.jackson + jackson-datatype-enumstd + provided + + + cn.axzo.framework.jackson + jackson-datatype-fraction + provided + + + cn.axzo.framework.jackson + jackson-datatype-period + provided + + + cn.axzo.framework.jackson + jackson-datatype-string-trim + provided + + + com.fasterxml.jackson.module + jackson-module-parameter-names + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + provided + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + + io.swagger + swagger-annotations + provided + + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-dataTypePackage + + replace + + generate-sources + + + + + ${basedir}/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java.in + + + ${project.basedir}/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java + + + + + @package@ + ${project.groupId}.utility + + + + @java8ParameterNamesPackage@ + com.fasterxml.jackson.module.paramnames + + + + @javaTimePackage@ + com.fasterxml.jackson.datatype.jsr310 + + + + @java8Package@ + com.fasterxml.jackson.datatype.jdk8 + + + + @hppcPackage@ + com.fasterxml.jackson.datatype.hppc + + + + @jsonOrgPackage@ + com.fasterxml.jackson.datatype.jsonorg + + + + @fractionPackage@ + ${project.groupId}.datatype.fraction + + + + @periodPackage@ + ${project.groupId}.datatype.period + + + + @stringTrimPackage@ + ${project.groupId}.datatype.string + + + + @enumStdPackage@ + ${project.groupId}.datatype.enumstd + + + + + + + \ No newline at end of file diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/Attributes.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/Attributes.java new file mode 100644 index 0000000..780e3d7 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/Attributes.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.SerializerProvider; + +import javax.validation.constraints.NotNull; + +/** + * @author liyong.tian + * @since 2020/8/28 18:40 + */ +public class Attributes { + + private final SerializerProvider provider; + + private final DeserializationContext ctxt; + + private Attributes(SerializerProvider provider, DeserializationContext ctxt) { + this.provider = provider; + this.ctxt = ctxt; + } + + public static Attributes from(@NotNull SerializerProvider provider) { + return new Attributes(provider, null); + } + + public static Attributes from(@NotNull DeserializationContext ctxt) { + return new Attributes(null, ctxt); + } + + @NotNull + public T getAttribute(String key, Class targetType, @NotNull T defaultValue) { + if (provider != null) { + if (provider.getAttribute(key) == null) { + return defaultValue; + } + return targetType.cast(provider.getAttribute(key)); + } + if (ctxt.getAttribute(key) == null) { + return defaultValue; + } + return targetType.cast(ctxt.getAttribute(key)); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/DynamicJSON.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/DynamicJSON.java new file mode 100644 index 0000000..5573586 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/DynamicJSON.java @@ -0,0 +1,132 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import jodd.util.StringUtil; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * @author liyong.tian + * @since 2018/9/25 + */ +public class DynamicJSON { + + private static final Pattern PATTERN_DOT = Pattern.compile("\\."); + + /** + * The largest power of two that can be represented as an {@code int}. + * + * @since 10.0 + */ + private static final int MAX_POWER_OF_TWO = 1 << (Integer.SIZE - 2); + + private static LoadingCache splitFieldCache = Caffeine.newBuilder() + .maximumSize(10000) + .build(DynamicJSON::splitField); + + /*** + * 过滤出需要的字段 + * @param fields 内嵌对象或数组,用.号隔离 + */ + public static void filter(JsonNode node, String... fields) { + Map treeFields = toTreeFields(fields); + if (!treeFields.isEmpty()) { + filter(node, treeFields); + } + } + + private static Map toTreeFields(String[] fields) { + List fieldGroups = new ArrayList<>(fields.length); + for (String field : fields) { + if (StringUtil.isBlank(field)) { + continue; + } + fieldGroups.add(splitFieldCache.get(field)); + } + + Map resultMap = new HashMap<>(capacity(fields.length)); + for (String[] names : fieldGroups) { + if (names.length == 0) { + continue; + } + String fieldName = names[0]; + TreeField treeField = resultMap.get(fieldName); + if (treeField == null) { + treeField = new TreeField(fieldName); + resultMap.put(fieldName, treeField); + } + for (int i = 1; i < names.length; i++) { + String name = names[i]; + TreeField matched = treeField.getChild(name); + if (matched == null) { + matched = new TreeField(name); + treeField.addChild(matched); + } + treeField = matched; + } + } + return resultMap; + } + + private static String[] splitField(String field) { + return PATTERN_DOT.split(field); + } + + private static void filter(JsonNode node, Map treeFields) { + if (!node.isContainerNode()) { + return; + } + if (node.isObject()) { + ObjectNode objectNode = (ObjectNode) node; + if (treeFields.isEmpty()) { + return; + } + Iterator> fields = objectNode.fields(); + List removing = new ArrayList<>(); + while (fields.hasNext()) { + Map.Entry entry = fields.next(); + String fieldName = entry.getKey(); + TreeField treeField = treeFields.get(fieldName); + if (treeField == null) { + removing.add(fieldName); + } else { + filter(entry.getValue(), treeField.getChildren()); + } + } + objectNode.remove(removing); + return; + } + if (node.isArray()) { + ArrayNode arrayNode = (ArrayNode) node; + Iterator elements = arrayNode.elements(); + while (elements.hasNext()) { + filter(elements.next(), treeFields); + } + } + } + + /** + * Returns a capacity that is sufficient to keep the map from being resized as long as it grows no + * larger than expectedSize and the load factor is ≥ its default (0.75). + */ + private static int capacity(int expectedSize) { + if (expectedSize < 3) { + if (expectedSize < 0) { + throw new IllegalArgumentException(expectedSize + " cannot be negative but was: expectedSize"); + } + return expectedSize + 1; + } + if (expectedSize < MAX_POWER_OF_TWO) { + // This is the calculation used in JDK8 to resize when a putAll + // happens; it seems to be the most conservative calculation we + // can make. 0.75 is the default load factor. + return (int) ((float) expectedSize / 0.75F + 1.0F); + } + return Integer.MAX_VALUE; // any large value + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/FilterBMPChars.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/FilterBMPChars.java new file mode 100644 index 0000000..b281bf1 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/FilterBMPChars.java @@ -0,0 +1,22 @@ +package cn.axzo.framework.jackson.utility; + +import cn.axzo.framework.jackson.utility.deser.FilterBMPCharsDeserializer; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.lang.annotation.*; + +/** + * 保留1-3字节的UTF8字符,去除4字节及以上的字符 + * + * @author liyong.tian + * @since 2018/9/26 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +@JacksonAnnotationsInside +@JsonDeserialize(using = FilterBMPCharsDeserializer.class) +public @interface FilterBMPChars { + +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSON.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSON.java new file mode 100644 index 0000000..ecc7b27 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSON.java @@ -0,0 +1,342 @@ +package cn.axzo.framework.jackson.utility; + +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.jackson.utility.initializer.JSONInitializer; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.type.TypeFactory; +import lombok.val; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.function.Function; + +/** + * JSON常用操作工具类 + * + * @author liyong.tian + * @since 2017/3/17 + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class JSON { + + private static ObjectMapper objectMapper; + + private static TypeFactory typeFactory; + + static { + objectMapper = new JsonMapper(); + JSONInitializer.init(objectMapper); + typeFactory = objectMapper.getTypeFactory(); + } + + public static ObjectMapper objectMapper() { + return objectMapper.copy(); + } + + public static TypeFactory typeFactory() { + return typeFactory; + } + + /*-----------------------deserialization-----------------------*/ + + public static T parseObject(String json, Class clazz) { + return parseObject(json, clazz, e -> { + throw new JSONException(e.getMessage(), e); + }); + } + + public static T parseObject(String json, Class clazz, Function fallback) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readValue(json, clazz); + } catch (IOException e) { + return fallback.apply(e); + } + } + + public static T parseObject(String json, JavaType type) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readValue(json, type); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static T parseObject(String json, TypeReference type) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readValue(json, type); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static T parseObject(InputStream src, Class type) { + Objects.requireNonNull(src, "src cannot be null"); + try { + return objectMapper.readValue(src, type); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static T parseObject(ByteBuffer buffer, Class clazz) { + Objects.requireNonNull(buffer, "byte buffer cannot be null"); + Objects.requireNonNull(clazz, "clazz cannot be null"); + try { + return objectMapper.readValue(buffer.array(), + buffer.arrayOffset() + buffer.position(), buffer.remaining(), clazz); + } catch (Exception e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static T parseObject(byte[] bytes, Class clazz) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + Objects.requireNonNull(clazz, "clazz cannot be null"); + try { + return objectMapper.readValue(bytes, clazz); + } catch (Exception e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static Optional parseOptional(String json, Class clazz) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, clazz)); + } + + public static Optional parseOptional(String json, JavaType type) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, type)); + } + + public static Optional parseOptional(String json, TypeReference type) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, type)); + } + + public static E[] parseArray(String json, Class elementClass) { + if (json == null) return ClassUtil.cast(Array.newInstance(elementClass, 0)); + try { + return objectMapper.readValue(json, typeFactory.constructArrayType(elementClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static List parseList(String json, Class elementClass) { + if (json == null) return Collections.emptyList(); + try { + return objectMapper.readValue(json, typeFactory.constructCollectionType(List.class, elementClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static List parseList(InputStream src, Class elementClass) { + if (src == null) return Collections.emptyList(); + try { + return objectMapper.readValue(src, typeFactory.constructCollectionType(List.class, elementClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static Set parseSet(String json, Class elementClass) { + if (json == null) return Collections.emptySet(); + try { + return objectMapper.readValue(json, typeFactory.constructCollectionType(Set.class, elementClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static Map parseTextMap(String json) { + return parseMap(json, HashMap.class, String.class, String.class); + } + + public static Map parseMap(String json, Class keyClass, Class valueClass) { + return parseMap(json, HashMap.class, keyClass, valueClass); + } + + public static Map parseMap(String json, Class mapClass, + Class keyClass, Class valueClass) { + if (json == null) return Collections.emptyMap(); + try { + return objectMapper.readValue(json, typeFactory.constructMapType(mapClass, keyClass, valueClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static JsonNode parseNode(String json) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readTree(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static JsonNode parseNode(InputStream in) { + Objects.requireNonNull(in, "input stream cannot be null"); + try { + return objectMapper.readTree(in); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static JSONReader reader() { + return new JSONReader(objectMapper); + } + + public static JSONReader reader(Module module) { + return new JSONReader(objectMapper, module); + } + + public static JSONReader reader(Module... modules) { + return new JSONReader(objectMapper, modules); + } + + public static JSONReader reader(DeserializationFeature f) { + return reader(f, true); + } + + public static JSONReader reader(DeserializationFeature first, DeserializationFeature... f) { + return new JSONReader(objectMapper, true, first, f); + } + + public static JSONReader reader(DeserializationFeature f, boolean featureState) { + return new JSONReader(objectMapper, featureState, f); + } + + /*-----------------------serialization-----------------------*/ + + public static String toJSONString(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + /** + * @param filterFields filter the fields to be included. + */ + public static String toJSONString(JsonNode node, String... filterFields) { + DynamicJSON.filter(node, filterFields); + return toJSONString(node); + } + + /** + * @param filterFields filter the fields to be included. + */ + public static String toJSONString(Object object, String... filterFields) { + if (filterFields == null || filterFields.length == 0) { + return toJSONString(object); + } + JsonNode node = toTree(object); + DynamicJSON.filter(node, filterFields); + return toJSONString(node); + } + + public static String toJSONString(String key, Object value) { + val map = new HashMap(); + map.put(key, value); + return toJSONString(map); + } + + public static String toPrettyJSONString(Object object) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + @Deprecated + public static byte[] toJSONBytes(Object object) { + return toBytes(object); + } + + public static byte[] toBytes(Object object) { + try { + return objectMapper.writeValueAsBytes(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static ByteBuffer toByteBuffer(Object object) { + return ByteBuffer.wrap(toBytes(object)); + } + + public static JSONWriter writer() { + return new JSONWriter(objectMapper); + } + + public static JSONWriter writer(Module module) { + return new JSONWriter(objectMapper, module); + } + + public static JSONWriter writer(Module... modules) { + return new JSONWriter(objectMapper, modules); + } + + public static JSONWriter writer(SerializationFeature f) { + return writer(f, true); + } + + public static JSONWriter writer(SerializationFeature first, SerializationFeature... f) { + return new JSONWriter(objectMapper, true, first, f); + } + + public static JSONWriter writer(SerializationFeature f, boolean featureState) { + return new JSONWriter(objectMapper, featureState, f); + } + + /*-----------------------convert-----------------------*/ + + public static Map toMap(Object fromValue) { + Objects.requireNonNull(fromValue, "fromValue cannot be null"); + return objectMapper.convertValue(fromValue, new TypeReference>() { + }); + } + + public static JsonNode toTree(Object fromValue) { + Objects.requireNonNull(fromValue, "fromValue cannot be null"); + return objectMapper.valueToTree(fromValue); + } + + public static T toObject(Object fromValue, TypeReference toValueTypeRef) { + Objects.requireNonNull(fromValue, "fromValue cannot be null"); + return objectMapper.convertValue(fromValue, toValueTypeRef); + } + + public static T toObject(Map fromValue, Class clazz) { + Objects.requireNonNull(fromValue, "fromValue cannot be null"); + return objectMapper.convertValue(fromValue, clazz); + } + + @Deprecated + public static JsonNode toNode(Object object) { + return toTree(object); + } + + @Deprecated + public static JsonNode parseNode(Object obj) { + return toTree(obj); + } + + @Deprecated + public static T parseObject(Map map, Class clazz) { + return toObject(map, clazz); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONException.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONException.java new file mode 100644 index 0000000..0e923af --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONException.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.jackson.utility; + +public class JSONException extends RuntimeException { + + public JSONException(){ + super(); + } + + public JSONException(String message){ + super(message); + } + + public JSONException(String message, Throwable cause){ + super(message, cause); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONFeatured.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONFeatured.java new file mode 100644 index 0000000..74272a0 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONFeatured.java @@ -0,0 +1,125 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import jodd.util.ArraysUtil; + +import javax.annotation.Nullable; +import java.util.*; + +import static jodd.util.StringPool.EMPTY; +import static jodd.util.StringUtil.isNotBlank; + +/** + * @author liyong.tian + * @since 2017/9/6 上午10:52 + */ +public abstract class JSONFeatured> { + + @Nullable + private Set _registeredModuleTypes; + + ObjectMapper objectMapper; + + JsonFactory jsonFactory; + + MapperFeature[] withMapperFeatures = {}; + + MapperFeature[] withoutMapperFeatures = {}; + + @Nullable + Map attributes; + + @Nullable + List withoutAttributes; + + JSONFeatured(ObjectMapper mapper) { + this.objectMapper = mapper; + this.jsonFactory = objectMapper.getFactory(); + } + + JSONFeatured(ObjectMapper mapper, Module... modules) { + this.objectMapper = registerModules(mapper, modules); + this.jsonFactory = objectMapper.getFactory(); + } + + public T enable(MapperFeature... features) { + this.withMapperFeatures = ArraysUtil.insert(withMapperFeatures, features, withMapperFeatures.length); + return self(); + } + + public T disable(MapperFeature... features) { + this.withoutMapperFeatures = ArraysUtil.insert(withoutMapperFeatures, features, withoutMapperFeatures.length); + return self(); + } + + public T enable(JsonFactory.Feature... features) { + this.jsonFactory = jsonFactory.copy().setCodec(objectMapper); + for (JsonFactory.Feature feature : features) { + this.jsonFactory.enable(feature); + } + return self(); + } + + public T disable(JsonFactory.Feature... features) { + this.jsonFactory = jsonFactory.copy().setCodec(objectMapper); + for (JsonFactory.Feature feature : features) { + this.jsonFactory.disable(feature); + } + return self(); + } + + public T set(String key, Object value) { + if (attributes == null) { + this.attributes = new HashMap<>(); + } + if (isNotBlank(key) && value != null) { + this.attributes.put(key, value); + } + return self(); + } + + public T unset(String key) { + if (withoutAttributes == null) { + this.withoutAttributes = new ArrayList<>(); + } + if (isNotBlank(key)) { + this.withoutAttributes.add(key); + } + return self(); + } + + public T module(Module module) { + this.objectMapper = registerModules(objectMapper, module); + this.jsonFactory = jsonFactory.setCodec(objectMapper); + return self(); + } + + public T modules(Module... modules) { + this.objectMapper = registerModules(objectMapper, modules); + this.jsonFactory = jsonFactory.setCodec(objectMapper); + return self(); + } + + private ObjectMapper registerModules(ObjectMapper mapper, Module... modules) { + if (modules.length == 0) { + return mapper; + } + if (_registeredModuleTypes == null) { + this._registeredModuleTypes = new HashSet<>(); + } + for (Module module : modules) { + _registeredModuleTypes.add(module.getTypeId().toString()); + } + String moduleId = _registeredModuleTypes.stream().reduce(EMPTY, String::concat); + String key = moduleId + ":" + mapper.getClass(); + return JacksonCache.MODULE_ID_MAPPER_CACHE.get(key, k -> mapper.copy().registerModules(modules)); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONReader.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONReader.java new file mode 100644 index 0000000..0b933ba --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONReader.java @@ -0,0 +1,255 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.type.TypeFactory; +import jodd.util.ArraysUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.util.*; + +/** + * The instance can be reused + * + * @author liyong.tian + * @since 2017/9/6 上午4:00 + */ +public class JSONReader extends JSONFeatured { + + private final TypeFactory typeFactory; + + private DeserializationFeature[] withFeatures = {}; + + private DeserializationFeature[] withoutFeatures = {}; + + private JsonParser.Feature[] withParserFeatures = {}; + + private JsonParser.Feature[] withoutParserFeatures = {}; + + JSONReader(ObjectMapper mapper) { + super(mapper); + this.typeFactory = mapper.getTypeFactory(); + } + + JSONReader(ObjectMapper mapper, Module... modules) { + super(mapper, modules); + this.typeFactory = mapper.getTypeFactory(); + } + + JSONReader(ObjectMapper mapper, boolean featureState, DeserializationFeature first, DeserializationFeature... f) { + super(mapper); + this.typeFactory = mapper.getTypeFactory(); + if (featureState) { + withFeatures = ArraysUtil.append(f, first); + } else { + withoutFeatures = ArraysUtil.append(f, first); + } + } + + public T parseObject(String json, Class clazz) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readerFor(clazz) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public T parseObject(String json, JavaType type) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readerFor(type) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public T parseObject(String json, TypeReference type) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.readerFor(type) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public T parseObject(InputStream src, Class type) { + Objects.requireNonNull(src, "src cannot be null"); + try { + return objectMapper.readerFor(type) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(src); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public Optional parseOptional(String json, Class clazz) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, clazz)); + } + + public Optional parseOptional(String json, JavaType type) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, type)); + } + + public Optional parseOptional(String json, TypeReference type) { + if (json == null) return Optional.empty(); + return Optional.of(parseObject(json, type)); + } + + @SuppressWarnings("unchecked") + public E[] parseArray(String json, Class elementClass) { + if (json == null) return (E[]) Array.newInstance(elementClass, 0); + try { + return objectMapper.readerFor(typeFactory.constructArrayType(elementClass)) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public List parseList(String json, Class elementClass) { + if (json == null) return Collections.emptyList(); + try { + return objectMapper.readerFor(typeFactory.constructCollectionType(List.class, elementClass)) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public List parseList(InputStream src, Class elementClass) { + if (src == null) return Collections.emptyList(); + try { + return objectMapper.readerFor(typeFactory.constructCollectionType(List.class, elementClass)) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(src); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public Set parseSet(String json, Class elementClass) { + if (json == null) return Collections.emptySet(); + try { + return objectMapper.readerFor(typeFactory.constructCollectionType(Set.class, elementClass)) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public Map parseMap(String json, Class keyClass, Class valueClass) { + return parseMap(json, HashMap.class, keyClass, valueClass); + } + + public Map parseMap(String json, Class mapClass, + Class keyClass, Class valueClass) { + if (json == null) return Collections.emptyMap(); + try { + return objectMapper.readerFor(typeFactory.constructMapType(mapClass, keyClass, valueClass)) + .with(jsonFactory) + .with(deserializationConfig()) + .readValue(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public JsonNode parseNode(String json) { + Objects.requireNonNull(json, "json string cannot be null"); + try { + return objectMapper.reader() + .with(jsonFactory) + .with(deserializationConfig()) + .readTree(json); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public JsonNode parseNode(InputStream in) { + Objects.requireNonNull(in, "input stream cannot be null"); + try { + return objectMapper.reader() + .with(jsonFactory) + .with(deserializationConfig()) + .readTree(in); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public JSONReader enable(DeserializationFeature... features) { + this.withFeatures = ArraysUtil.insert(withFeatures, features, withFeatures.length); + return this; + } + + public JSONReader disable(DeserializationFeature... features) { + this.withoutFeatures = ArraysUtil.insert(withoutFeatures, features, withoutFeatures.length); + return this; + } + + public JSONReader enable(JsonParser.Feature... features) { + this.withParserFeatures = ArraysUtil.insert(withParserFeatures, features, withParserFeatures.length); + return this; + } + + public JSONReader disable(JsonParser.Feature... features) { + this.withoutParserFeatures = ArraysUtil.insert(withoutParserFeatures, features, withoutParserFeatures.length); + return this; + } + + private DeserializationConfig deserializationConfig() { + DeserializationConfig config = objectMapper.getDeserializationConfig(); + if (withFeatures.length > 0) { + config = config.withFeatures(withFeatures); + } + if (withoutFeatures.length > 0) { + config = config.withoutFeatures(withoutFeatures); + } + if (withMapperFeatures.length > 0) { + config = config.with(withMapperFeatures); + } + if (withoutMapperFeatures.length > 0) { + config = config.without(withoutMapperFeatures); + } + if (withParserFeatures.length > 0) { + config = config.withFeatures(withParserFeatures); + } + if (withoutParserFeatures.length > 0) { + config = config.withoutFeatures(withoutParserFeatures); + } + if (attributes != null && !attributes.isEmpty()) { + config = config.withAttributes(attributes); + } + if (withoutAttributes != null) { + for (Object key : withoutAttributes) { + config = config.withoutAttribute(key); + } + } + return config; + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONWriter.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONWriter.java new file mode 100644 index 0000000..4f26d4a --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JSONWriter.java @@ -0,0 +1,135 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.*; +import jodd.util.ArraysUtil; + +import java.io.IOException; + +import static jodd.util.ArraysUtil.insert; + +/** + * The instance can be reused + * + * @author liyong.tian + * @since 2017/9/6 上午4:53 + */ +public class JSONWriter extends JSONFeatured { + + private SerializationFeature[] withFeatures = {}; + + private SerializationFeature[] withoutFeatures = {}; + + private JsonGenerator.Feature[] withGeneratorFeatures = {}; + + private JsonGenerator.Feature[] withoutGeneratorFeatures = {}; + + JSONWriter(ObjectMapper mapper) { + super(mapper); + } + + JSONWriter(ObjectMapper mapper, Module... modules) { + super(mapper, modules); + } + + JSONWriter(ObjectMapper mapper, boolean featureState, SerializationFeature first, SerializationFeature... f) { + super(mapper); + if (featureState) { + withFeatures = ArraysUtil.append(f, first); + } else { + withoutFeatures = ArraysUtil.append(f, first); + } + } + + public String toJSONString(Object object) { + try { + return writer().writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public byte[] toJSONBytes(Object object) { + try { + return writer().writeValueAsBytes(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public String toPrettyJSONString(Object object) { + try { + return prettyWriter().writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public JSONWriter enable(SerializationFeature... features) { + this.withFeatures = insert(withFeatures, features, withFeatures.length); + return this; + } + + public JSONWriter disable(SerializationFeature... features) { + this.withoutFeatures = insert(withoutFeatures, features, withoutFeatures.length); + return this; + } + + @Override + public JSONWriter enable(MapperFeature... features) { + if (features.length > 0) { + this.objectMapper = this.objectMapper.copy().enable(features); + } + return super.enable(features); + } + + @Override + public JSONWriter disable(MapperFeature... features) { + if (features.length > 0) { + this.objectMapper = this.objectMapper.copy().disable(features); + } + return super.disable(features); + } + + public JSONWriter enable(JsonGenerator.Feature... features) { + this.withGeneratorFeatures = insert(withGeneratorFeatures, features, withGeneratorFeatures.length); + return this; + } + + public JSONWriter disable(JsonGenerator.Feature... features) { + this.withoutGeneratorFeatures = insert(withoutGeneratorFeatures, features, withoutGeneratorFeatures.length); + return this; + } + + private ObjectWriter writer() { + return configWriter(objectMapper.writer()); + } + + private ObjectWriter prettyWriter() { + return configWriter(objectMapper.writerWithDefaultPrettyPrinter()); + } + + private ObjectWriter configWriter(ObjectWriter writer) { + if (withFeatures.length > 0) { + writer = writer.withFeatures(withFeatures); + } + if (withoutFeatures.length > 0) { + writer = writer.withoutFeatures(withoutFeatures); + } + if (withGeneratorFeatures.length > 0) { + writer = writer.withFeatures(withGeneratorFeatures); + } + if (withoutGeneratorFeatures.length > 0) { + writer = writer.withoutFeatures(withoutGeneratorFeatures); + } + if (attributes != null && !attributes.isEmpty()) { + writer = writer.withAttributes(attributes); + } + if (withoutAttributes != null) { + for (Object key : withoutAttributes) { + writer = writer.withoutAttribute(key); + } + } + return writer.with(jsonFactory); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonCache.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonCache.java new file mode 100644 index 0000000..535d29d --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonCache.java @@ -0,0 +1,16 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.experimental.UtilityClass; + +/** + * @author liyong.tian + * @since 2017/9/9 上午12:42 + */ +@UtilityClass +final class JacksonCache { + + final static Cache MODULE_ID_MAPPER_CACHE = Caffeine.newBuilder().maximumSize(10_00).build(); +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java new file mode 100644 index 0000000..7220316 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.jackson.utility; + +/** + * Automatically generated from JacksonDataTypePackage.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public class JacksonDataTypePackage { + + public final static String JAVA8_PARAMETER_NAMES_PACKAGE = "com.fasterxml.jackson.module.paramnames"; + + public final static String JAVA_TIME_PACKAGE = "com.fasterxml.jackson.datatype.jsr310"; + + public final static String JAVA8_PACKAGE = "com.fasterxml.jackson.datatype.jdk8"; + + public final static String HPPC_PACKAGE = "com.fasterxml.jackson.datatype.hppc"; + + public final static String FRACTION_PACKAGE = "cn.axzo.framework.jackson.datatype.fraction"; + + public final static String PERIOD_PACKAGE = "cn.axzo.framework.jackson.datatype.period"; + + public final static String STRING_TRIM_PACKAGE = "cn.axzo.framework.jackson.datatype.string"; + + public final static String ENUM_STD_PACKAGE = "cn.axzo.framework.jackson.datatype.enumstd"; + + public final static String JSON_ORG_PACKAGE = "com.fasterxml.jackson.datatype.jsonorg"; +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java.in b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java.in new file mode 100644 index 0000000..f3fe92a --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JacksonDataTypePackage.java.in @@ -0,0 +1,27 @@ +package @package@; + +/** + * Automatically generated from JacksonDataTypePackage.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public class JacksonDataTypePackage { + + public final static String JAVA8_PARAMETER_NAMES_PACKAGE = "@java8ParameterNamesPackage@"; + + public final static String JAVA_TIME_PACKAGE = "@javaTimePackage@"; + + public final static String JAVA8_PACKAGE = "@java8Package@"; + + public final static String HPPC_PACKAGE = "@hppcPackage@"; + + public final static String FRACTION_PACKAGE = "@fractionPackage@"; + + public final static String PERIOD_PACKAGE = "@periodPackage@"; + + public final static String STRING_TRIM_PACKAGE = "@stringTrimPackage@"; + + public final static String ENUM_STD_PACKAGE = "@enumStdPackage@"; + + public final static String JSON_ORG_PACKAGE = "@jsonOrgPackage@"; +} \ No newline at end of file diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JsonMapper.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JsonMapper.java new file mode 100644 index 0000000..b408988 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/JsonMapper.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.jackson.utility; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +/** + * @author liyong.tian + * @since 2017/8/14 下午11:01 + */ +public class JsonMapper extends ObjectMapper { + + public JsonMapper() { + } + + public JsonMapper(JsonMapper src) { + super(src); + } + + @SuppressWarnings("unchecked") + @Override + public T readValue(String content, Class valueType) throws JsonProcessingException { + try { + return super.readValue(content, valueType); + } catch (IOException e) { + if (valueType == String.class) { + try { + return (T) super.writeValueAsString(super.readValue(content, Object.class)); + } catch (IOException ignored) { + return (T) content; + } + } + throw e; + } + } + + @SuppressWarnings("unchecked") + @Override + public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException { + if (toValueType != String.class) { + return super.convertValue(fromValue, toValueType); + } else { + try { + return (T) super.writeValueAsString(fromValue); + } catch (JsonProcessingException e) { + return super.convertValue(fromValue, toValueType); + } + } + } + + @Override + public JsonMapper copy() { + super._checkInvalidCopy(JsonMapper.class); + return new JsonMapper(this); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/TreeField.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/TreeField.java new file mode 100644 index 0000000..a578592 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/TreeField.java @@ -0,0 +1,28 @@ +package cn.axzo.framework.jackson.utility; + +import lombok.Data; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author liyong.tian + * @since 2018/9/25 + */ +@Data +class TreeField { + + private final String name; + + private final Map children = new HashMap<>(); + + void addChild(TreeField treeField) { + this.children.put(treeField.getName(), treeField); + } + + @Nullable + TreeField getChild(String fieldName) { + return this.children.get(fieldName); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XML.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XML.java new file mode 100644 index 0000000..ccce55b --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XML.java @@ -0,0 +1,294 @@ +package cn.axzo.framework.jackson.utility; + +import cn.axzo.framework.core.FetchException; +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.net.Inets; +import cn.axzo.framework.jackson.utility.initializer.XMLInitializer; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import static cn.axzo.framework.core.Constants.*; +import static java.lang.String.format; +import static java.time.ZoneId.systemDefault; +import static java.time.temporal.ChronoUnit.DAYS; + +/** + * @author liyong.tian + * @since 2018/1/4 下午5:46 + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class XML { + + private static XmlMapper xmlMapper; + + private static TypeFactory typeFactory; + + static { + xmlMapper = new XmlMapper(); + XMLInitializer.init(xmlMapper); + typeFactory = xmlMapper.getTypeFactory(); + } + + public static XmlMapper xmlMapper() { + return xmlMapper.copy(); + } + + /*-----------------------deserialization-----------------------*/ + + public static T parseObject(String xml, Class clazz) { + Objects.requireNonNull(xml, "xml string cannot be null"); + try { + return xmlMapper.readValue(xml, clazz); + } catch (IOException e) { + throw new XMLException(e.getMessage(), e); + } + } + + public static Map parseTextMap(String xml) { + return parseMap(xml, HashMap.class, String.class, String.class); + } + + public static Map parseMap(String xml, Class mapClass, + Class keyClass, Class valueClass) { + if (xml == null) return Collections.emptyMap(); + try { + return xmlMapper().readValue(xml, typeFactory.constructMapType(mapClass, keyClass, valueClass)); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + /*-----------------------serialization-----------------------*/ + + public static String toXMLString(Object object) { + try { + return xmlMapper.writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static byte[] toXMLBytes(Object object) { + try { + return xmlMapper.writeValueAsBytes(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + public static String toPrettyXMLString(Object object) { + try { + return xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } catch (IOException e) { + throw new JSONException(e.getMessage(), e); + } + } + + @Deprecated + public static String toJSONString(Object object) { + return toXMLString(object); + } + + @Deprecated + public static byte[] toJSONBytes(Object object) { + return toXMLBytes(object); + } + + @Deprecated + public static String toPrettyJSONString(Object object) { + return toPrettyXMLString(object); + } + + /*-----------------------convert-----------------------*/ + + public static Map toMap(Object object) { + return xmlMapper.convertValue(object, new TypeReference>() { + }); + } + + /** + * 分布式ID生成器 + * Created by liyong.tian on 2016/12/17. + */ + @Slf4j + public static class IdGenerator { + private final static int NODE_ID_BITS = 10; //机器号10位 + private final static int MAX_NODE_ID = ~(-1 << NODE_ID_BITS); //机器号最大值:1023 + + private final ReentrantLock lock = new ReentrantLock(); + private int sequence = 0; + private long lastTimestamp = -1L; + + @Getter + private final long baseTimestamp; //基准时间戳 + private final int nodeId; //机器号 + private final int sequenceBits; //序列号位数 + private final int sequenceMask; //序列号最大值 + @Getter + private final int timestampLeftShift; //时间毫秒数左移位数 + + public IdGenerator() { + this(null); + } + + public IdGenerator(Integer nodeId) { + this(nodeId, null, null); + } + + public IdGenerator(Long baseTimestamp, Integer sequenceBits) { + this(null, baseTimestamp, sequenceBits); + } + + public IdGenerator(Integer nodeId, Long baseTimestamp, Integer sequenceBits) { + //默认值 + final long defaultBaseTimestamp = 1288834974657L; //基准时间戳:2010-11-04T09:42:54.657+08:00[Asia/Shanghai] + final int defaultSequenceBits = 12; + if (nodeId == null) { + nodeId = getDefaultNodeId(); + } + if (baseTimestamp == null) { + baseTimestamp = defaultBaseTimestamp; + } + if (sequenceBits == null) { + sequenceBits = defaultSequenceBits; + } + + //校验 + if (nodeId > MAX_NODE_ID || nodeId < 0) { + throw new IllegalArgumentException(format("node Id can't be greater than %d or less than 0", MAX_NODE_ID)); + } + if (sequenceBits > 12 || sequenceBits < 0) { + throw new IllegalArgumentException(format("sequence bits can't be greater than %d or less than 0", 12)); + } + if (baseTimestamp > currentTimeMillis()) { + throw new IllegalArgumentException("base timestamp can't be greater than now"); + } + + //初始化 + this.baseTimestamp = baseTimestamp; + this.nodeId = nodeId; + this.sequenceBits = sequenceBits; + this.sequenceMask = ~(-1 << sequenceBits); + this.timestampLeftShift = NODE_ID_BITS + sequenceBits; + } + + public String nextNo(String custom) { + return nextNo(DAYS, custom); + } + + public String nextNo(ChronoUnit truncatedTo, String custom) { + lock.lock(); + try { + //更新时间戳和序列号 + _updateTimestampAndSequence(); + + //基量时间戳 + ZonedDateTime zonedDateTime = Instant.ofEpochMilli(lastTimestamp).atZone(systemDefault()); + long baseTimestamp = zonedDateTime.truncatedTo(truncatedTo).toInstant().toEpochMilli(); + + //时间戳增量 + long timestampInc = lastTimestamp - baseTimestamp; + + // 000000000000000000000000000000000000000000 0000000000 000000000000 + // timestamp(41b) nodeId(10b) sequence + long suffix = (timestampInc << timestampLeftShift) | (nodeId << sequenceBits) | sequence; + + //时间前缀 + String timePrefix; + switch (truncatedTo) { + case DAYS: + timePrefix = FORMATTER_DATE_COMPACT.format(zonedDateTime); + break; + case HOURS: + timePrefix = FORMATTER_DATE_HOUR_COMPACT.format(zonedDateTime); + break; + case MINUTES: + timePrefix = DateTimeFormatter.ofPattern("yyyyMMddHHmm").format(zonedDateTime); + break; + case SECONDS: + timePrefix = FORMATTER_DATE_TIME_COMPACT.format(zonedDateTime); + break; + default: + throw new InternalException("truncatedTo[" + truncatedTo + "] cannot not support"); + } + return timePrefix + custom + suffix; + } finally { + lock.unlock(); + } + } + + public long nextId() { + lock.lock(); + try { + //更新时间戳和序列号 + _updateTimestampAndSequence(); + + //时间戳增量 + long timestampInc = lastTimestamp - baseTimestamp; + + // 000000000000000000000000000000000000000000 0000000000 000000000000 + // timestamp(41b) nodeId(10b) sequence + return (timestampInc << timestampLeftShift) | (nodeId << sequenceBits) | sequence; + } finally { + lock.unlock(); + } + } + + private void _updateTimestampAndSequence() { + long timestamp = currentTimeMillis(); //获取当前毫秒数 + //如果服务器时间有问题(时钟后退) 报错。 + if (timestamp < lastTimestamp) { + throw new RuntimeException(format( + "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + //如果上次生成时间和当前时间相同,在同一毫秒内 + if (lastTimestamp == timestamp) { + //sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位 + sequence = (sequence + 1) & sequenceMask; + //判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0 + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); //自旋等待到下一毫秒 + } + } else { + sequence = 0; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加 + } + lastTimestamp = timestamp; + } + + private static long tilNextMillis(long lastTimestamp) { + long timestamp = currentTimeMillis(); + while (timestamp <= lastTimestamp) { + timestamp = currentTimeMillis(); + } + return timestamp; + } + + private static long currentTimeMillis() { + return System.currentTimeMillis(); + } + + private static int getDefaultNodeId() { + try { + int ipAddressAsInt = Inets.fetchLocalIpAsInt(1); + return Math.abs(ipAddressAsInt) & MAX_NODE_ID; + } catch (FetchException e) { + IdGenerator.log.error("nodeId initialized error", e); + return 0; + } + } + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XMLException.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XMLException.java new file mode 100644 index 0000000..438fb0b --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/XMLException.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.jackson.utility; + +/** + * @author liyong.tian + * @since 2018/1/4 下午5:54 + */ +public class XMLException extends RuntimeException { + + public XMLException(){ + super(); + } + + public XMLException(String message){ + super(message); + } + + public XMLException(String message, Throwable cause){ + super(message, cause); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/FilterBMPCharsDeserializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/FilterBMPCharsDeserializer.java new file mode 100644 index 0000000..821be48 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/FilterBMPCharsDeserializer.java @@ -0,0 +1,26 @@ +package cn.axzo.framework.jackson.utility.deser; + +import cn.axzo.framework.core.util.StringUtil; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; + +import java.io.IOException; + +/** + * @author liyong.tian + * @since 2018/9/26 + */ +public class FilterBMPCharsDeserializer extends StdScalarDeserializer { + + protected FilterBMPCharsDeserializer() { + super(String.class); + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = StringDeserializer.instance.deserialize(p, ctxt); + return StringUtil.filterBMPChars(text); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/PrimitiveOrWrapperDeserializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/PrimitiveOrWrapperDeserializer.java new file mode 100644 index 0000000..40f27b6 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/deser/PrimitiveOrWrapperDeserializer.java @@ -0,0 +1,51 @@ +package cn.axzo.framework.jackson.utility.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import java.io.IOException; + +/** + * @author liyong.tian + * @since 2017/9/8 下午10:34 + */ +public abstract class PrimitiveOrWrapperDeserializer extends StdScalarDeserializer { + private static final long serialVersionUID = 1L; + + protected final T _nullValue; + protected final boolean _primitive; + + protected PrimitiveOrWrapperDeserializer(Class vc, T nvl) { + super(vc); + _nullValue = nvl; + _primitive = vc.isPrimitive(); + } + + @Override + public abstract T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException; + + @Override + public final T getNullValue(DeserializationContext ctxt) throws JsonMappingException { + if (_primitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + ctxt.reportInputMismatch(this, + "Can not map JSON null into type %s (set DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + handledType().toString()); + } + return _nullValue; + } + + @Override + public T getEmptyValue(DeserializationContext ctxt) throws JsonMappingException { + // [databind#1095]: Should not allow coercion from into null from Empty String + // either, if `null` not allowed + if (_primitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + ctxt.reportInputMismatch(this, + "Can not map Empty String as null into type %s (set DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + handledType().toString()); + } + return _nullValue; + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JSONInitializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JSONInitializer.java new file mode 100644 index 0000000..1c59296 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JSONInitializer.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.jackson.utility.initializer; + +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import lombok.experimental.UtilityClass; + +/** + * @author liyong.tian + * @since 2018/9/27 + */ +@UtilityClass +public class JSONInitializer { + + public void init(ObjectMapper objectMapper) { + JacksonInitializer.init(objectMapper); + + //Annotation Introspector + JacksonInitializer.doOnClassPresent("io.swagger.annotations.ApiModelProperty", () -> { + AnnotationIntrospector ai = new JacksonAnnotationIntrospector() { + @Override + protected boolean _isIgnorable(Annotated a) { + return JacksonInitializer.isSwaggerHidden(a) || super._isIgnorable(a); + } + }; + JacksonInitializer.appendAnnotationIntrospector(objectMapper, ai); + }); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JacksonInitializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JacksonInitializer.java new file mode 100644 index 0000000..ca39521 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/JacksonInitializer.java @@ -0,0 +1,93 @@ +package cn.axzo.framework.jackson.utility.initializer; + +import cn.axzo.framework.jackson.datatype.enumstd.EnumStdModule; +import cn.axzo.framework.jackson.datatype.fraction.FractionModule; +import cn.axzo.framework.jackson.datatype.period.PeriodModule; +import cn.axzo.framework.jackson.datatype.string.TrimModule; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.datatype.hppc.HppcModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import io.swagger.annotations.ApiModelProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; + +import static cn.axzo.framework.jackson.utility.JacksonDataTypePackage.*; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT; +import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER; +import static com.fasterxml.jackson.databind.DeserializationFeature.*; +import static com.fasterxml.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION; +import static com.fasterxml.jackson.databind.MapperFeature.PROPAGATE_TRANSIENT_MARKER; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS; + +/** + * @author liyong.tian + * @since 2018/1/4 下午5:48 + */ +class JacksonInitializer { + + private static ClassLoader moduleClassLoader = JacksonInitializer.class.getClassLoader(); + + static void init(ObjectMapper objectMapper) { + List modules = new ArrayList<>(); + + // Bytecode optimization + modules.add(new AfterburnerModule()); + + // JDK + doOnClassPresent(JAVA8_PARAMETER_NAMES_PACKAGE + ".ParameterNamesModule", + () -> modules.add(new ParameterNamesModule(JsonCreator.Mode.DEFAULT))); + doOnClassPresent(JAVA_TIME_PACKAGE + ".JavaTimeModule", () -> modules.add(new JavaTimeModule())); + doOnClassPresent(JAVA8_PACKAGE + ".Jdk8Module", () -> modules.add(new Jdk8Module())); + + // Third part + doOnClassPresent(HPPC_PACKAGE + ".HppcModule", () -> modules.add(new HppcModule())); + doOnClassPresent(JSON_ORG_PACKAGE + ".JsonOrgModule", () -> modules.add(new JsonOrgModule())); + + // Customized + doOnClassPresent(FRACTION_PACKAGE + ".FractionModule", () -> modules.add(new FractionModule())); + doOnClassPresent(PERIOD_PACKAGE + ".PeriodModule", () -> modules.add(new PeriodModule())); + doOnClassPresent(STRING_TRIM_PACKAGE + ".TrimModule", () -> modules.add(new TrimModule())); + doOnClassPresent(ENUM_STD_PACKAGE + ".EnumStdModule", () -> modules.add(new EnumStdModule())); + + objectMapper.setSerializationInclusion(NON_ABSENT); + objectMapper.setTimeZone(TimeZone.getDefault()); + objectMapper.disable(FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.disable(DEFAULT_VIEW_INCLUSION); + objectMapper.disable(READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + objectMapper.disable(WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); + objectMapper.disable(ACCEPT_FLOAT_AS_INT); + objectMapper.enable(ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER); + objectMapper.enable(ACCEPT_SINGLE_VALUE_AS_ARRAY); + objectMapper.enable(PROPAGATE_TRANSIENT_MARKER); + objectMapper.registerModules(modules); + } + + static void appendAnnotationIntrospector(ObjectMapper objectMapper, AnnotationIntrospector ai) { + objectMapper.setConfig(objectMapper.getSerializationConfig().withAppendedAnnotationIntrospector(ai)); + objectMapper.setConfig(objectMapper.getDeserializationConfig().withAppendedAnnotationIntrospector(ai)); + } + + static boolean isSwaggerHidden(Annotated annotated) { + ApiModelProperty apiModelProperty = annotated.getAnnotation(ApiModelProperty.class); + return apiModelProperty != null && apiModelProperty.hidden(); + } + + static void doOnClassPresent(String className, Runnable runnable) { + try { + Class.forName(className, true, moduleClassLoader); + runnable.run(); + } catch (ClassNotFoundException e) { + // Class or one of its dependencies is not present... + } + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/XMLInitializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/XMLInitializer.java new file mode 100644 index 0000000..da4d1c4 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/initializer/XMLInitializer.java @@ -0,0 +1,30 @@ +package cn.axzo.framework.jackson.utility.initializer; + +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlAnnotationIntrospector; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.experimental.UtilityClass; + +/** + * @author liyong.tian + * @since 2018/9/27 + */ +@UtilityClass +public class XMLInitializer { + + public void init(XmlMapper xmlMapper) { + JacksonInitializer.init(xmlMapper); + + //Annotation Introspector + JacksonInitializer.doOnClassPresent("io.swagger.annotations.ApiModelProperty", () -> { + AnnotationIntrospector ai = new JacksonXmlAnnotationIntrospector() { + @Override + protected boolean _isIgnorable(Annotated a) { + return JacksonInitializer.isSwaggerHidden(a) || super._isIgnorable(a); + } + }; + JacksonInitializer.appendAnnotationIntrospector(xmlMapper, ai); + }); + } +} diff --git a/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/ser/ListWithCommaSerializer.java b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/ser/ListWithCommaSerializer.java new file mode 100644 index 0000000..84d19d7 --- /dev/null +++ b/axzo-common-jackson/jackson-utility/src/main/java/cn/axzo/framework/jackson/utility/ser/ListWithCommaSerializer.java @@ -0,0 +1,52 @@ +package cn.axzo.framework.jackson.utility.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContainerSerializer; +import com.fasterxml.jackson.databind.type.SimpleType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.jooq.lambda.Seq; + +import java.io.IOException; +import java.util.List; + +import static jodd.util.StringPool.COMMA; + +/** + * @author liyong.tian + * @since 2017/9/25 上午2:28 + */ +public class ListWithCommaSerializer extends ContainerSerializer> { + + public ListWithCommaSerializer() { + super(TypeFactory.defaultInstance().constructCollectionType(List.class, Object.class)); + } + + @Override + public JavaType getContentType() { + return SimpleType.constructUnsafe(Object.class); + } + + @Override + public JsonSerializer getContentSerializer() { + return null; + } + + @Override + public boolean hasSingleElement(List value) { + return value.size() == 1; + } + + @Override + protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { + return null; + } + + @Override + public void serialize(List value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(Seq.toString(value.stream(), COMMA)); + } +} diff --git a/axzo-common-jackson/pom.xml b/axzo-common-jackson/pom.xml new file mode 100644 index 0000000..a83c3d1 --- /dev/null +++ b/axzo-common-jackson/pom.xml @@ -0,0 +1,97 @@ + + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + 4.0.0 + + cn.axzo.framework.jackson + axzo-common-jackson + pom + Axzo Common Jackson Parent + + + jackson-datatype-enumstd + jackson-datatype-fraction + jackson-datatype-period + jackson-datatype-string-trim + jackson-utility + jackson-starter + + + + + + + ${project.basedir}/src/main/java/${pkgVersion.dir}/PackageVersion.java.in + ${project.basedir}/src/main/java/${pkgVersion.dir}/PackageVersion.java + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + + process-packageVersion + + replace + + + + + + ${pkgVersion.tpl.in} + ${pkgVersion.tpl.out} + + + + @package@ + ${pkgVersion.package} + + + + @projectversion@ + ${project.version} + + + + @projectgroupid@ + ${project.groupId} + + + + @projectartifactid@ + ${project.artifactId} + + + + + + + + \ No newline at end of file diff --git a/axzo-common-loggings/log4j2-starter/pom.xml b/axzo-common-loggings/log4j2-starter/pom.xml new file mode 100644 index 0000000..69fc665 --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + axzo-common-loggings + cn.axzo.framework.logging + 1.0.0-SNAPSHOT + + + log4j2-starter + Axzo Common Logging Log4j2 Starter + + + + cn.axzo.framework + axzo-common-core + + + org.springframework.boot + spring-boot-starter-log4j2 + + + + + com.lmax + disruptor + + + + cn.axzo.framework.jackson + jackson-utility + true + + + \ No newline at end of file diff --git a/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/filter/LoggerFilter.java b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/filter/LoggerFilter.java new file mode 100644 index 0000000..f8e9b96 --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/filter/LoggerFilter.java @@ -0,0 +1,102 @@ +package cn.axzo.framework.logging.log4j2.filter; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/7 19:03 + **/ +@Plugin(name = "LoggerFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true) +public class LoggerFilter extends AbstractFilter { + + private final static String MATCH_ALL = "*"; + + private final LoadingCache loggerFilterResultCache; + + private final String name; + + private LoggerFilter(final String name, final Long cacheSize, final Result onMatch, final Result onMismatch) { + super(onMatch, onMismatch); + this.name = name; + this.loggerFilterResultCache = Caffeine.newBuilder() + .maximumSize(cacheSize) + .build(loggerName -> + name.equals(MATCH_ALL) ? onMatch : loggerName.startsWith(name) ? onMatch : onMismatch); + } + + @Override + public Result filter(final Logger logger, final Level level, final Marker marker, final String msg, + final Object... params) { + return filter(logger.getName()); + } + + @Override + public Result filter(final Logger logger, final Level level, final Marker marker, final Object msg, + final Throwable t) { + return filter(logger.getName()); + } + + @Override + public Result filter(final Logger logger, final Level level, final Marker marker, final Message msg, + final Throwable t) { + return filter(logger.getName()); + } + + @Override + public Result filter(final LogEvent event) { + return filter(event.getLoggerName()); + } + + private Result filter(final String loggerName) { + if (loggerName != null) { + return loggerFilterResultCache.get(loggerName); + } + return onMismatch; + } + + @Override + public String toString() { + return name; + } + + /** + * Create the MarkerFilter. + * + * @param name The logger name to match. + * @param match The action to take if a match occurs. + * @param mismatch The action to take if no match occurs. + * @return A LoggerFilter. + */ + @PluginFactory + public static LoggerFilter createFilter( + @PluginAttribute(value = "name", defaultString = MATCH_ALL) final String name, + @PluginAttribute(value = "cacheSize", defaultLong = 10_000L) final Long cacheSize, + @PluginAttribute("onMatch") final Result match, + @PluginAttribute("onMismatch") final Result mismatch) { + if (name == null) { + LOGGER.error("A logger name must be provided for LoggerFilter"); + return null; + } + if (cacheSize == null) { + LOGGER.error("cacheSize must be provided for LoggerFilter"); + return null; + } + if (cacheSize < 0) { + return new LoggerFilter(name, 0L, match, mismatch); + } + return new LoggerFilter(name, cacheSize, match, mismatch); + } +} diff --git a/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdPatternConverter.java b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdPatternConverter.java new file mode 100644 index 0000000..a56b3ec --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdPatternConverter.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.logging.log4j2.pattern; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.pattern.ConverterKeys; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; + +/** + * 升级到2.9之后,可以删除 + */ +@Plugin(name = "ProcessIdPatternConverter", category = "Converter") +@ConverterKeys({"pid", "processId"}) +public final class ProcessIdPatternConverter extends LogEventPatternConverter { + private static final String DEFAULT_DEFAULT_VALUE = "???"; + private final String pid; + + /** + * Private constructor. + */ + private ProcessIdPatternConverter(final String... options) { + super("Process ID", "pid"); + final String defaultValue = options.length > 0 ? options[0] : DEFAULT_DEFAULT_VALUE; + String discoveredPid = ProcessIdUtil.getProcessId(); + pid = discoveredPid.equals(ProcessIdUtil.DEFAULT_PROCESSID) ? defaultValue : discoveredPid; + } + + /** + * Returns the process ID. + * + * @return the process ID + */ + public String getProcessId() { + return pid; + } + + public static void main(final String[] args) { + System.out.println(new ProcessIdPatternConverter().pid); + } + + /** + * Obtains an instance of ProcessIdPatternConverter. + * + * @param options options, currently ignored, may be null. + * @return instance of ProcessIdPatternConverter. + */ + public static ProcessIdPatternConverter newInstance(final String[] options) { + return new ProcessIdPatternConverter(options); + } + + /** + * {@inheritDoc} + */ + @Override + public void format(final LogEvent event, final StringBuilder toAppendTo) { + toAppendTo.append(pid); + } +} \ No newline at end of file diff --git a/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdUtil.java b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdUtil.java new file mode 100644 index 0000000..c562815 --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/src/main/java/cn/axzo/framework/logging/log4j2/pattern/ProcessIdUtil.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.logging.log4j2.pattern; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; + +/** + * @author liyong.tian + * @since 2018/9/19 + */ +class ProcessIdUtil { + + static final String DEFAULT_PROCESSID = "-"; + + static String getProcessId() { + try { + return ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; // likely works on most platforms + } catch (final Exception ex) { + try { + return new File("/proc/self").getCanonicalFile().getName(); // try a Linux-specific way + } catch (final IOException ignoredUseDefault) { + // Ignore exception. + } + } + return DEFAULT_PROCESSID; + } +} diff --git a/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2-sample.xml b/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2-sample.xml new file mode 100644 index 0000000..25bff88 --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2-sample.xml @@ -0,0 +1,58 @@ + + + + /data/logs + app_name + debug + + %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%5p}{ERROR=red,DEBUG=green} %magenta{%pid} --- [%10t] %cyan{%c{1.}} : %m%n%xEx + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- [%10t] %c{1.} : %m%n%xEx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2.StatusLogger.properties b/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2.StatusLogger.properties new file mode 100644 index 0000000..a1ebc8a --- /dev/null +++ b/axzo-common-loggings/log4j2-starter/src/main/resources/log4j2.StatusLogger.properties @@ -0,0 +1,10 @@ +# 关闭log4j2在加载配置文件期间,由于找不到配置文件而打印的错误日志 + +# log4j2官网描述 +# Before a configuration is found, +# status logger level can be controlled with system property:org.apache.logging.log4j.simplelog.StatusLogger.level. +# After a configuration is found, +# status logger level can be controlled in the configuration file with the "status" attribute, +# for example: . + +org.apache.logging.log4j.simplelog.StatusLogger.level=off \ No newline at end of file diff --git a/axzo-common-loggings/logback-starter/pom.xml b/axzo-common-loggings/logback-starter/pom.xml new file mode 100644 index 0000000..94e8047 --- /dev/null +++ b/axzo-common-loggings/logback-starter/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + axzo-common-loggings + cn.axzo.framework.logging + 1.0.0-SNAPSHOT + + + logback-starter + Axzo Common Logging logback Starter + + + + org.springframework.boot + spring-boot-starter-logging + + + \ No newline at end of file diff --git a/axzo-common-loggings/logback-starter/src/main/java/cn/axzo/framework/logging/logback/MarkerFilter.java b/axzo-common-loggings/logback-starter/src/main/java/cn/axzo/framework/logging/logback/MarkerFilter.java new file mode 100644 index 0000000..01b8734 --- /dev/null +++ b/axzo-common-loggings/logback-starter/src/main/java/cn/axzo/framework/logging/logback/MarkerFilter.java @@ -0,0 +1,52 @@ +package cn.axzo.framework.logging.logback; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.AbstractMatcherFilter; +import ch.qos.logback.core.spi.FilterReply; + + +/** + * @Description 基于Marker的日志过滤器 + * @Author liyong.tian + * @Date 2020/9/7 18:35 + **/ +public class MarkerFilter extends AbstractMatcherFilter { + + /** + * 标记名称 + */ + private String marker; + + @Override + public FilterReply decide(ILoggingEvent event) { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + if (isBlank(marker)) { + return FilterReply.DENY; + } + + if (event.getMarker() != null && event.getMarker().contains(marker)) { + return FilterReply.ACCEPT; + } else { + return FilterReply.DENY; + } + } + + private static boolean isBlank(final CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + return true; + } + + public void setMarker(String marker) { + this.marker = marker; + } +} diff --git a/axzo-common-loggings/pom.xml b/axzo-common-loggings/pom.xml new file mode 100644 index 0000000..f54dbc4 --- /dev/null +++ b/axzo-common-loggings/pom.xml @@ -0,0 +1,22 @@ + + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + 4.0.0 + + cn.axzo.framework.logging + axzo-common-loggings + pom + Axzo Common Logging Parent + + + + log4j2-starter + logback-starter + + \ No newline at end of file diff --git a/axzo-common-math/pom.xml b/axzo-common-math/pom.xml new file mode 100644 index 0000000..a39907a --- /dev/null +++ b/axzo-common-math/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-math + Axzo Common Math + + + + + cn.axzo.framework + axzo-common-core + + + org.apache.commons + commons-math3 + + + + diff --git a/axzo-common-math/src/main/java/cn/axzo/framework/math/Maths.java b/axzo-common-math/src/main/java/cn/axzo/framework/math/Maths.java new file mode 100644 index 0000000..0d288c2 --- /dev/null +++ b/axzo-common-math/src/main/java/cn/axzo/framework/math/Maths.java @@ -0,0 +1,12 @@ +package cn.axzo.framework.math; + +/** + * @author liyong.tian + * @since 2020/8/28 17:27 + */ +public class Maths { + + public static int log10(int a) { + return (int) Math.log10(a); + } +} diff --git a/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/BigFractions.java b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/BigFractions.java new file mode 100644 index 0000000..9bd5fdb --- /dev/null +++ b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/BigFractions.java @@ -0,0 +1,72 @@ +package cn.axzo.framework.math.fraction; + +import org.apache.commons.math3.fraction.BigFraction; +import org.apache.commons.math3.fraction.Fraction; + +import javax.annotation.Nonnull; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; + +import static java.lang.Long.parseLong; + +/** + * @author liyong.tian + * @since 2017/3/4 + */ +public class BigFractions implements FractionRules { + + public static BigFraction from(Fraction fraction){ + return new BigFraction(fraction.getNumerator(), fraction.getDenominator()); + } + + /** + * @param fraction 分数 + * @param roundingMode 舍入规则 + */ + public static Long toLong(BigFraction fraction, RoundingMode roundingMode) { + if (fraction == null || roundingMode == null) { + throw new IllegalArgumentException("fraction or roundingMode cannot be null"); + } + return fraction.bigDecimalValue(0, roundingMode.ordinal()).longValue(); + } + + @Nonnull + public static BigFraction of(long numerator, long denominator) { + return new BigFraction(numerator, denominator); + } + + /** + * For example, the following are valid inputs: + *

+     *   "1"                  -- new BigFraction(1)
+     *   "1/1000"             -- new BigFraction(1,1000)
+     *   "[1]"                -- new BigFraction(1)
+     *   "[1,1000]"           -- new BigFraction(1,1000)
+     * 
+ * + * @throws NumberFormatException if text cannot match the pattern: num/num or [num,num] + */ + @Nonnull + public static BigFraction parse(String text) { + Objects.requireNonNull(text, "text"); + return parse(text, FractionRules.getMatcher(text)); + } + + private static BigFraction parse(String text, Matcher matcher) { + if (!matcher.matches()) { + throw new NumberFormatException(ERROR_PATTERN_MSG + text); + } + String numMatch = matcher.group(1); + String denMatch = matcher.group(2); + try { + if (denMatch == null) { + return new BigFraction(parseLong(numMatch)); + } + return new BigFraction(parseLong(numMatch), parseLong(denMatch)); + } catch (Exception ex) { + throw new NumberFormatException(ERROR_PATTERN_MSG + text); + } + } +} + diff --git a/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/FractionRules.java b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/FractionRules.java new file mode 100644 index 0000000..f1f6428 --- /dev/null +++ b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/FractionRules.java @@ -0,0 +1,34 @@ +package cn.axzo.framework.math.fraction; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static jodd.util.StringPool.LEFT_SQ_BRACKET; +import static jodd.util.StringPool.RIGHT_SQ_BRACKET; + +/** + * @author liyong.tian + * @since 2017/3/4 + */ +public interface FractionRules { + + String ERROR_PATTERN_MSG = "Text cannot match the pattern: num/num or [num,num], text = "; + + /** + * The pattern for parsing. + */ + Pattern SLASH_PATTERN = Pattern.compile("(?:([\\-|+]?[0-9]+))(?:\\s*/\\s*(?:([\\-|+]?[1-9][0-9]*)))?"); + Pattern ARRAY_PATTERN = Pattern.compile("\\[\\s*(?:([\\-|+]?[0-9]+))\\s*(?:,\\s*(?:([\\-|+]?[1-9][0-9]*))\\s*)?]"); + + static boolean isValid(String text) { + return text != null && (SLASH_PATTERN.matcher(text).matches() || ARRAY_PATTERN.matcher(text).matches()); + } + + static Matcher getMatcher(String text) { + if (text.startsWith(LEFT_SQ_BRACKET) && text.endsWith(RIGHT_SQ_BRACKET)) { + return ARRAY_PATTERN.matcher(text); + } + return SLASH_PATTERN.matcher(text); + } +} + diff --git a/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/Fractions.java b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/Fractions.java new file mode 100644 index 0000000..8c6886b --- /dev/null +++ b/axzo-common-math/src/main/java/cn/axzo/framework/math/fraction/Fractions.java @@ -0,0 +1,55 @@ +package cn.axzo.framework.math.fraction; + +import org.apache.commons.math3.fraction.Fraction; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.regex.Matcher; + +import static java.lang.Integer.parseInt; + +/** + * @author liyong.tian + * @since 2017/3/3 + */ +public abstract class Fractions implements FractionRules { + + @Nonnull + public static Fraction of(int numerator, int denominator) { + return new Fraction(numerator, denominator); + } + + /** + * For example, the following are valid inputs: + *
+     *   "1"                  -- new Fraction(1)
+     *   "1/1000"             -- new Fraction(1,1000)
+     *   "[1]"                -- new Fraction(1)
+     *   "[1,1000]"           -- new Fraction(1,1000)
+     *   "[-1,1000]"          -- new Fraction(-1,1000)
+     * 
+ * + * @throws NumberFormatException if text cannot match the pattern: num/num or [num,num] + */ + @Nonnull + public static Fraction parse(String text) { + Objects.requireNonNull(text, "text"); + return parse(text, FractionRules.getMatcher(text)); + } + + private static Fraction parse(String text, Matcher matcher) { + if (!matcher.matches()) { + throw new NumberFormatException(ERROR_PATTERN_MSG + text); + } + String numMatch = matcher.group(1); + String denMatch = matcher.group(2); + try { + if (denMatch == null) { + return new Fraction(parseInt(numMatch)); + } + return new Fraction(parseInt(numMatch), parseInt(denMatch)); + } catch (Exception ex) { + throw new NumberFormatException(ERROR_PATTERN_MSG + text); + } + } +} diff --git a/axzo-common-validator/pom.xml b/axzo-common-validator/pom.xml new file mode 100644 index 0000000..6354142 --- /dev/null +++ b/axzo-common-validator/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-validator + Axzo Common Validator + + + + cn.axzo.framework + axzo-common-core + + + cn.axzo.framework + axzo-common-math + + + org.glassfish + javax.el + + + org.hibernate.validator + hibernate-validator + + + diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/ASCII.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/ASCII.java new file mode 100644 index 0000000..81fc8e2 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/ASCII.java @@ -0,0 +1,22 @@ +package cn.axzo.framework.validator.constraints; + +import cn.axzo.framework.validator.constraintvalidators.ASCIIConstraintValidator; + +import javax.validation.Constraint; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author liyong.tian + * @since 2020/9/4 17:53 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {ASCIIConstraintValidator.class}) +public @interface ASCII { +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMax.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMax.java new file mode 100644 index 0000000..5c3037c --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMax.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.validator.constraints; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 分数最大值 + *

+ * 支持的类型有: + *

    + *
  • {@code Fraction}
  • + *
  • {@code BigFraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * @author liyong.tian + * @since 2020/9/4 18:15 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {}) +public @interface FractionMax { + String message() default "{javax.validation.constraints.FractionMax.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * 1 + * 1/10000 + * [1,10000] + */ + String value(); + + /** + * 开闭区间,默认为闭区间 + */ + boolean inclusive() default true; + + /** + * Defines several {@code @FractionMax} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + FractionMax[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMin.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMin.java new file mode 100644 index 0000000..2602ea0 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/FractionMin.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.validator.constraints; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 分数最大值 + *

+ * 支持的类型有: + *

    + *
  • {@code Fraction}
  • + *
  • {@code BigFraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * @author liyong.tian + * @since 2020/9/4 18:17 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {}) +public @interface FractionMin { + String message() default "{javax.validation.constraints.FractionMin.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * 1 + * 1/10000 + * [1,10000] + */ + String value(); + + /** + * 开闭区间,默认为闭区间 + */ + boolean inclusive() default true; + + /** + * Defines several {@code @FractionMin} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + FractionMin[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMax.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMax.java new file mode 100644 index 0000000..b123be7 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMax.java @@ -0,0 +1,39 @@ +package cn.axzo.framework.validator.constraints; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author liyong.tian + * @since 2017/3/14 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {}) +public @interface PeriodMax { + + String message() default "{javax.validation.constraints.PeriodMax.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int value(); + + /** + * Defines several {@code @PeriodMax} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + PeriodMax[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMin.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMin.java new file mode 100644 index 0000000..dcae19a --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/PeriodMin.java @@ -0,0 +1,41 @@ +package cn.axzo.framework.validator.constraints; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 期数最小值 + * + * @author liyong.tian + * @since 2017/3/4 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {}) +public @interface PeriodMin { + + String message() default "{javax.validation.constraints.PeriodMin.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int value(); + + /** + * Defines several {@code @PeriodMin} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + PeriodMin[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/Phone.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/Phone.java new file mode 100644 index 0000000..45c646c --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/Phone.java @@ -0,0 +1,41 @@ +package cn.axzo.framework.validator.constraints; + +import cn.axzo.framework.validator.constraintvalidators.PhoneConstraintValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/6/7 下午1:14 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {PhoneConstraintValidator.class}) +public @interface Phone { + + String message() default "{javax.validation.constraints.Phone.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * Defines several {@code @FractionMax} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + Phone[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/SizeIn.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/SizeIn.java new file mode 100644 index 0000000..d12a35b --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/SizeIn.java @@ -0,0 +1,46 @@ +package cn.axzo.framework.validator.constraints; + +import cn.axzo.framework.validator.constraintvalidators.SizeInConstraintValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/6/7 下午1:14 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {SizeInConstraintValidator.class}) +public @interface SizeIn { + + String message() default "{javax.validation.constraints.SizeIn.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * 离散取值 + */ + int[] value(); + + /** + * Defines several {@code @FractionMax} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + SizeIn[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/UTF8.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/UTF8.java new file mode 100644 index 0000000..9207764 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraints/UTF8.java @@ -0,0 +1,35 @@ +package cn.axzo.framework.validator.constraints; + +import cn.axzo.framework.validator.constraintvalidators.UTF8ConstraintValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {UTF8ConstraintValidator.class}) +public @interface UTF8 { + + String message() default "{javax.validation.constraints.UTF8.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * Defines several {@code @FractionMax} annotations on the same element. + */ + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) + @Retention(RUNTIME) + @Documented + @interface List { + UTF8[] value(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/ASCIIConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/ASCIIConstraintValidator.java new file mode 100644 index 0000000..6fb90df --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/ASCIIConstraintValidator.java @@ -0,0 +1,23 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.core.util.StringUtil; +import cn.axzo.framework.validator.constraints.ASCII; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author liyong.tian + * @since 2020/9/4 18:04 + */ +public class ASCIIConstraintValidator implements ConstraintValidator { + + @Override + public void initialize(ASCII constraintAnnotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || StringUtil.isASCII(value); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMaxConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMaxConstraintValidator.java new file mode 100644 index 0000000..24c6c6e --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMaxConstraintValidator.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.math.fraction.BigFractions; +import cn.axzo.framework.validator.constraints.FractionMax; +import org.apache.commons.math3.fraction.BigFraction; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * The annotated element must be a number whose value must be higher or + * equal to the specified minimum. + *

+ * Supported types are: + *

    + *
  • {@code BigFraction}
  • + *
  • {@code Fraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * + * @author liyong.tian + * @since 2017/3/4 + */ +public class BigFractionMaxConstraintValidator implements ConstraintValidator { + + private BigFraction maxValue; + private boolean inclusive; + + @Override + public void initialize(FractionMax fractionMax) { + this.maxValue = BigFractions.parse(fractionMax.value()); + this.inclusive = fractionMax.inclusive(); + } + + @Override + public boolean isValid(BigFraction value, ConstraintValidatorContext context) { + // null values are valid + if (value == null) { + return true; + } + int comparisonResult = value.compareTo(maxValue); + return inclusive ? comparisonResult <= 0 : comparisonResult < 0; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMinConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMinConstraintValidator.java new file mode 100644 index 0000000..40ef7b8 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/BigFractionMinConstraintValidator.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.math.fraction.BigFractions; +import cn.axzo.framework.validator.constraints.FractionMin; +import org.apache.commons.math3.fraction.BigFraction; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * The annotated element must be a number whose value must be higher or + * equal to the specified minimum. + *

+ * Supported types are: + *

    + *
  • {@code BigFraction}
  • + *
  • {@code Fraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * + * @author liyong.tian + * @since 2017/3/4 + */ +public class BigFractionMinConstraintValidator implements ConstraintValidator { + + private BigFraction minValue; + private boolean inclusive; + + @Override + public void initialize(FractionMin fractionMin) { + this.minValue = BigFractions.parse(fractionMin.value()); + this.inclusive = fractionMin.inclusive(); + } + + @Override + public boolean isValid(BigFraction value, ConstraintValidatorContext context) { + // null values are valid + if (value == null) { + return true; + } + int comparisonResult = value.compareTo(minValue); + return inclusive ? comparisonResult >= 0 : comparisonResult > 0; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMaxConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMaxConstraintValidator.java new file mode 100644 index 0000000..edffb52 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMaxConstraintValidator.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.math.fraction.Fractions; +import cn.axzo.framework.validator.constraints.FractionMax; +import org.apache.commons.math3.fraction.Fraction; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * The annotated element must be a number whose value must be higher or + * equal to the specified minimum. + *

+ * Supported types are: + *

    + *
  • {@code BigFraction}
  • + *
  • {@code Fraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * + * @author liyong.tian + * @since 2017/3/4 + */ +public class FractionMaxConstraintValidator implements ConstraintValidator { + + private Fraction maxValue; + private boolean inclusive; + + @Override + public void initialize(FractionMax fractionMax) { + this.maxValue = Fractions.parse(fractionMax.value()); + this.inclusive = fractionMax.inclusive(); + } + + @Override + public boolean isValid(Fraction value, ConstraintValidatorContext context) { + // null values are valid + if (value == null) { + return true; + } + int comparisonResult = value.compareTo(maxValue); + return inclusive ? comparisonResult <= 0 : comparisonResult < 0; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMinConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMinConstraintValidator.java new file mode 100644 index 0000000..2a2f60f --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/FractionMinConstraintValidator.java @@ -0,0 +1,45 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.math.fraction.Fractions; +import cn.axzo.framework.validator.constraints.FractionMin; +import org.apache.commons.math3.fraction.Fraction; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * The annotated element must be a number whose value must be higher or + * equal to the specified minimum. + *

+ * Supported types are: + *

    + *
  • {@code BigFraction}
  • + *
  • {@code Fraction}
  • + *
+ *

+ * {@code null} elements are considered valid. + * + * @author liyong.tian + * @since 2017/3/4 + */ +public class FractionMinConstraintValidator implements ConstraintValidator { + + private Fraction minValue; + private boolean inclusive; + + @Override + public void initialize(FractionMin fractionMin) { + this.minValue = Fractions.parse(fractionMin.value()); + this.inclusive = fractionMin.inclusive(); + } + + @Override + public boolean isValid(Fraction value, ConstraintValidatorContext context) { + // null values are valid + if (value == null) { + return true; + } + int comparisonResult = value.compareTo(minValue); + return inclusive ? comparisonResult >= 0 : comparisonResult > 0; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMaxConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMaxConstraintValidator.java new file mode 100644 index 0000000..4d0a264 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMaxConstraintValidator.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.core.time.Period; +import cn.axzo.framework.validator.constraints.PeriodMax; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author liyong.tian + * @since 2017/3/14 + */ +public class PeriodMaxConstraintValidator implements ConstraintValidator { + + private int maxValue; + + @Override + public void initialize(PeriodMax constraintAnnotation) { + this.maxValue = constraintAnnotation.value(); + } + + @Override + public boolean isValid(Period value, ConstraintValidatorContext context) { + // null values are valid + return value == null || maxValue >= value.getValue(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMinConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMinConstraintValidator.java new file mode 100644 index 0000000..261c724 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PeriodMinConstraintValidator.java @@ -0,0 +1,28 @@ +package cn.axzo.framework.validator.constraintvalidators; + + +import cn.axzo.framework.core.time.Period; +import cn.axzo.framework.validator.constraints.PeriodMin; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author liyong.tian + * @since 2017/3/14 + */ +public class PeriodMinConstraintValidator implements ConstraintValidator { + + private int minValue; + + @Override + public void initialize(PeriodMin constraintAnnotation) { + this.minValue = constraintAnnotation.value(); + } + + @Override + public boolean isValid(Period value, ConstraintValidatorContext context) { + // null values are valid + return value == null || value.getValue() >= minValue; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PhoneConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PhoneConstraintValidator.java new file mode 100644 index 0000000..6b57a54 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/PhoneConstraintValidator.java @@ -0,0 +1,27 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.validator.constraints.Phone; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/6/7 下午1:47 + */ +public class PhoneConstraintValidator implements ConstraintValidator { + + private final static Pattern PHONE_PATTERN = Pattern.compile("(\\d{3}-\\d{8}|\\d{4}-\\d{7})|^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$"); + + @Override + public void initialize(Phone constraintAnnotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return PHONE_PATTERN.matcher(value).matches(); + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/SizeInConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/SizeInConstraintValidator.java new file mode 100644 index 0000000..0e34549 --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/SizeInConstraintValidator.java @@ -0,0 +1,42 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.validator.constraints.SizeIn; +import lombok.val; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.List; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/6/7 下午1:47 + */ +public class SizeInConstraintValidator implements ConstraintValidator> { + + private int[] values; + + @Override + public void initialize(SizeIn constraintAnnotation) { + this.values = constraintAnnotation.value(); + } + + @Override + public boolean isValid(List value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + int size = value.size(); + for (int i : values) { + if (i == size) { + return true; + } + } + val hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class); + hibernateContext.addMessageParameter("values", Arrays.toString(values)); + return false; + } +} diff --git a/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/UTF8ConstraintValidator.java b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/UTF8ConstraintValidator.java new file mode 100644 index 0000000..2cadc4e --- /dev/null +++ b/axzo-common-validator/src/main/java/cn/axzo/framework/validator/constraintvalidators/UTF8ConstraintValidator.java @@ -0,0 +1,24 @@ +package cn.axzo.framework.validator.constraintvalidators; + +import cn.axzo.framework.core.util.StringUtil; +import cn.axzo.framework.validator.constraints.UTF8; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author zhoufeng.qi + * @since 2018/09/11 14:22 + */ +public class UTF8ConstraintValidator implements ConstraintValidator { + + @Override + public void initialize(UTF8 constraintAnnotation) { + + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || StringUtil.isUTF8(value); + } +} diff --git a/axzo-common-validator/src/main/resources/ContributorValidationMessages.properties b/axzo-common-validator/src/main/resources/ContributorValidationMessages.properties new file mode 100644 index 0000000..7e7e449 --- /dev/null +++ b/axzo-common-validator/src/main/resources/ContributorValidationMessages.properties @@ -0,0 +1,8 @@ +javax.validation.constraints.PeriodMax.message = must be less than or equal to {value} +javax.validation.constraints.PeriodMin.message = must be greater than or equal to {value} +javax.validation.constraints.FractionMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value} +javax.validation.constraints.FractionMin.message = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value} +javax.validation.constraints.SizeIn.message = size must be one of{values} +javax.validation.constraints.Phone.message = phone is invalid +javax.validation.constraints.ASCII.message = must be ascii string +javax.validation.constraints.UTF8.message = must be utf8 string \ No newline at end of file diff --git a/axzo-common-validator/src/main/resources/ContributorValidationMessages_en_US.properties b/axzo-common-validator/src/main/resources/ContributorValidationMessages_en_US.properties new file mode 100644 index 0000000..7e7e449 --- /dev/null +++ b/axzo-common-validator/src/main/resources/ContributorValidationMessages_en_US.properties @@ -0,0 +1,8 @@ +javax.validation.constraints.PeriodMax.message = must be less than or equal to {value} +javax.validation.constraints.PeriodMin.message = must be greater than or equal to {value} +javax.validation.constraints.FractionMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value} +javax.validation.constraints.FractionMin.message = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value} +javax.validation.constraints.SizeIn.message = size must be one of{values} +javax.validation.constraints.Phone.message = phone is invalid +javax.validation.constraints.ASCII.message = must be ascii string +javax.validation.constraints.UTF8.message = must be utf8 string \ No newline at end of file diff --git a/axzo-common-validator/src/main/resources/ContributorValidationMessages_zh_CN.properties b/axzo-common-validator/src/main/resources/ContributorValidationMessages_zh_CN.properties new file mode 100644 index 0000000..7365a36 --- /dev/null +++ b/axzo-common-validator/src/main/resources/ContributorValidationMessages_zh_CN.properties @@ -0,0 +1,8 @@ +javax.validation.constraints.PeriodMax.message = \u5fc5\u987b\u5c0f\u4e8e\u6216\u7b49\u4e8e{value} +javax.validation.constraints.PeriodMin.message = \u5fc5\u987b\u5927\u4e8e\u6216\u7b49\u4e8e{value} +javax.validation.constraints.FractionMax.message = \u5fc5\u987b\u5c0f\u4e8e${inclusive == true ? '\u6216\u7b49\u4e8e' : ''}{value} +javax.validation.constraints.FractionMin.message = \u5fc5\u987b\u5927\u4e8e${inclusive == true ? '\u6216\u7b49\u4e8e' : ''}{value} +javax.validation.constraints.SizeIn.message = \u4e2a\u6570\u5fc5\u987b\u4e3a\u5176\u4e2d\u4e4b\u4e00{values} +javax.validation.constraints.Phone.message = \u624b\u673a\u53f7\u683c\u5f0f\u4e0d\u6b63\u786e +javax.validation.constraints.ASCII.message = \u5fc5\u987b\u662f\u0041\u0053\u0043\u0049\u0049\u5b57\u7b26\u4e32 +javax.validation.constraints.UTF8.message = \u5fc5\u987b\u662f\u0055\u0054\u0046\u0038\u5b57\u7b26\u4e32 \ No newline at end of file diff --git a/axzo-common-validator/src/main/resources/META-INF/services/javax.validation.ConstraintValidator b/axzo-common-validator/src/main/resources/META-INF/services/javax.validation.ConstraintValidator new file mode 100644 index 0000000..b9d5920 --- /dev/null +++ b/axzo-common-validator/src/main/resources/META-INF/services/javax.validation.ConstraintValidator @@ -0,0 +1,22 @@ +# Assuming a custom constraint annotation @FractionMax +FractionMaxConstraintValidator +BigFractionMaxConstraintValidator + +# Assuming a custom constraint annotation @FractionMin +FractionMinConstraintValidator +BigFractionMinConstraintValidator + +# Assuming a custom constraint annotation @PeriodMax +PeriodMaxConstraintValidator + +# Assuming a custom constraint annotation @PeriodMin +PeriodMinConstraintValidator + +# Assuming a custom constraint annotation @SizeIn +SizeInConstraintValidator + +# Assuming a custom constraint annotation @ASCII +ASCIIConstraintValidator + +# Assuming a custom constraint annotation @UTF8 +UTF8ConstraintValidator \ No newline at end of file diff --git a/axzo-common-validator/src/test/java/cn/axzo/framework/AppTest.java b/axzo-common-validator/src/test/java/cn/axzo/framework/AppTest.java new file mode 100644 index 0000000..186647a --- /dev/null +++ b/axzo-common-validator/src/test/java/cn/axzo/framework/AppTest.java @@ -0,0 +1,20 @@ +package cn.axzo.framework; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test for simple App. + */ +public class AppTest +{ + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() + { + assertTrue( true ); + } +} diff --git a/axzo-common-web/pom.xml b/axzo-common-web/pom.xml new file mode 100644 index 0000000..73172fc --- /dev/null +++ b/axzo-common-web/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + + axzo-common-web + Axzo Common Web + + + + + cn.axzo.framework.framework + axzo-common-context + + + cn.axzo.framework + axzo-common-domain + + + org.springframework + spring-web + + + + org.springframework.data + spring-data-commons + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + provided + + + \ No newline at end of file diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/HandlerMethods.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/HandlerMethods.java new file mode 100644 index 0000000..bddc464 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/HandlerMethods.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.web.dynamic; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import static java.util.stream.Collectors.toList; + +/** + * @author liyong.tian + * @since 2018/7/19 + */ +public abstract class HandlerMethods { + + public static List resolveRequestEntityMethods(Function function) { + return Arrays.stream(function.getClass().getDeclaredMethods()) + .filter(method -> method.getAnnotation(TargetApi.class) != null) + .map(method -> new RequestEntityHandlerMethod(function, method)) + .collect(toList()); + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/RequestEntityHandlerMethod.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/RequestEntityHandlerMethod.java new file mode 100644 index 0000000..800c3a7 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/RequestEntityHandlerMethod.java @@ -0,0 +1,140 @@ +package cn.axzo.framework.web.dynamic; + +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.domain.ServiceException; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.util.UriTemplate; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; + +/** + * @author liyong.tian + * @since 2018/7/19 + */ +public class RequestEntityHandlerMethod extends HandlerMethod { + + private final HttpMethod httpMethod; + + private final UriTemplate uriTemplate; + + private final Function bean; + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public RequestEntityHandlerMethod(Function bean, Method method) { + super(bean, method); + TargetApi targetApi = getMethodAnnotation(TargetApi.class); + this.httpMethod = targetApi.method(); + this.uriTemplate = new UriTemplate(targetApi.path()); + this.bean = bean; + } + + public boolean match(RequestEntity entity) { + return Objects.equals(this.httpMethod, entity.getMethod()) && uriTemplate.matches(entity.getUrl().getPath()); + } + + public R doInvoke(Object... providedArgs) { + //解析方法参数 + Object[] args = resolveArguments(providedArgs); + + //调用方法 + ReflectionUtils.makeAccessible(getBridgedMethod()); + try { + Object returnValue = getBridgedMethod().invoke(getBean(), args); + //转换返回值 + returnValue = bean.apply(returnValue); + return ClassUtil.cast(returnValue); + } catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } else if (targetException instanceof Error) { + throw (Error) targetException; + } else if (targetException instanceof Exception) { + throw new ServiceException(targetException); + } else { + String text = getInvocationErrorMessage(args); + throw new IllegalStateException(text, targetException); + } + } catch (IllegalAccessException e) { + throw new ServiceException(e); + } + } + + @Override + public Function getBean() { + return this.bean; + } + + private Object[] resolveArguments(Object[] providedArgs) { + providedArgs = Arrays.stream(providedArgs).map(bean).toArray(Object[]::new); + MethodParameter[] parameters = getMethodParameters(); + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + args[i] = resolveProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + String errorText = "Could not resolve method parameter at index " + + parameter.getParameterIndex() + " in " + parameter.getMethod().toGenericString() + + ": " + getArgumentResolutionErrorMessage(i); + throw new IllegalStateException(errorText); + } + return args; + } + + /** + * Attempt to resolve a method parameter from the list of provided argument values. + */ + private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) { + if (providedArgs == null) { + return null; + } + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + return null; + } + + private String getArgumentResolutionErrorMessage(int index) { + Class paramType = getMethodParameters()[index].getParameterType(); + return "No suitable resolver for argument " + index + " of type '" + paramType.getName() + "'"; + } + + private String getInvocationErrorMessage(Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage()); + sb.append("Resolved arguments: \n"); + for (int i = 0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + + private String getDetailedErrorMessage() { + return "Failed to invoke handler method\n" + + "HandlerMethod details: \n" + + "Controller [" + getBeanType().getName() + "]\n" + + "Method [" + getBridgedMethod().toGenericString() + "]\n"; + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/TargetApi.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/TargetApi.java new file mode 100644 index 0000000..ded149d --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/dynamic/TargetApi.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.web.dynamic; + +import org.springframework.http.HttpMethod; + +import java.lang.annotation.*; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 11:08 + **/ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TargetApi { + + HttpMethod method(); + + String path(); +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiCoreEntity.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiCoreEntity.java new file mode 100644 index 0000000..baa17c2 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiCoreEntity.java @@ -0,0 +1,283 @@ +package cn.axzo.framework.web.http; + +import cn.axzo.framework.domain.web.result.ApiCoreResult; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.http.*; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author liyong.tian + * @since 2017/11/14 下午5:36 + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public abstract class ApiCoreEntity> extends HttpEntity { + + private final Object status; + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * + * @param body the entity body + * @param headers the entity headers + * @param status the status code + */ + public ApiCoreEntity(R body, MultiValueMap headers, HttpStatus status) { + super(body, headers); + Assert.notNull(status, "HttpStatus must not be null"); + this.status = status; + } + + /** + * Return the HTTP status code of the response. + * + * @return the HTTP status as an HttpStatus enum entry + */ + public HttpStatus getStatusCode() { + if (this.status instanceof HttpStatus) { + return (HttpStatus) this.status; + } else { + return HttpStatus.valueOf((Integer) this.status); + } + } + + /** + * Return the HTTP status code of the response. + * + * @return the HTTP status as an int value + * @since 4.3 + */ + public int getStatusCodeValue() { + if (this.status instanceof HttpStatus) { + return ((HttpStatus) this.status).value(); + } else { + return (Integer) this.status; + } + } + + /** + * @author liyong.tian + * @since 2017/11/14 下午9:42 + */ + protected abstract static class AbstractBodySetter> implements BodySetter { + + protected final HttpStatus status; + + protected final HttpHeaders headers = new HttpHeaders(); + + protected AbstractBodySetter(HttpStatus status) { + this.status = status; + } + + @Override + public B contentLength(long contentLength) { + this.headers.setContentLength(contentLength); + return asSetter(); + } + + @Override + public B contentType(MediaType contentType) { + this.headers.setContentType(contentType); + return asSetter(); + } + + @Override + public B header(String headerName, String... headerValues) { + for (String headerValue : headerValues) { + this.headers.add(headerName, headerValue); + } + return asSetter(); + } + + @Override + public B headers(HttpHeaders headers) { + if (headers != null) { + this.headers.putAll(headers); + } + return asSetter(); + } + + @Override + public B allow(HttpMethod... allowedMethods) { + this.headers.setAllow(new LinkedHashSet<>(Arrays.asList(allowedMethods))); + return asSetter(); + } + + @Override + public B eTag(String etag) { + if (etag != null) { + if (!etag.startsWith("\"") && !etag.startsWith("W/\"")) { + etag = "\"" + etag; + } + if (!etag.endsWith("\"")) { + etag = etag + "\""; + } + } + this.headers.setETag(etag); + return asSetter(); + } + + @Override + public B lastModified(long lastModified) { + this.headers.setLastModified(lastModified); + return asSetter(); + } + + @Override + public B location(URI location) { + this.headers.setLocation(location); + return asSetter(); + } + + @Override + public B cacheControl(CacheControl cacheControl) { + String ccValue = cacheControl.getHeaderValue(); + if (ccValue != null) { + this.headers.setCacheControl(cacheControl.getHeaderValue()); + } + return asSetter(); + } + + @Override + public B varyBy(String... requestHeaders) { + this.headers.setVary(Arrays.asList(requestHeaders)); + return asSetter(); + } + + @SuppressWarnings("unchecked") + private B asSetter() { + return (B) this; + } + } + + /** + * Defines a builder that adds a body to the response entity. + * + * @author liyong.tian + * @since 2017/11/14 下午8:05 + */ + private interface BodySetter> extends HeadersSetter { + + /** + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + * + * @param contentLength the content length + * @return this builder + * @see HttpHeaders#setContentLength(long) + */ + B contentLength(long contentLength); + + /** + * Set the {@linkplain MediaType media type} of the body, as specified by the + * {@code Content-Type} header. + * + * @param contentType the content type + * @return this builder + * @see HttpHeaders#setContentType(MediaType) + */ + B contentType(MediaType contentType); + } + + /** + * Defines a builder that adds headers to the response entity. + * + * @param the builder subclass + * @author liyong.tian + * @since 2017/11/14 下午7:44 + */ + private interface HeadersSetter> { + + /** + * Add the given, single header value under the given name. + * + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + * @see HttpHeaders#add(String, String) + */ + B header(String headerName, String... headerValues); + + /** + * Copy the given headers into the entity's headers map. + * + * @param headers the existing HttpHeaders to copy from + * @return this builder + * @see HttpHeaders#add(String, String) + * @since 4.1.2 + */ + B headers(HttpHeaders headers); + + /** + * Set the set of allowed {@link HttpMethod HTTP methods}, as specified + * by the {@code Allow} header. + * + * @param allowedMethods the allowed methods + * @return this builder + * @see HttpHeaders#setAllow(Set) + */ + B allow(HttpMethod... allowedMethods); + + /** + * Set the entity tag of the body, as specified by the {@code ETag} header. + * + * @param etag the new entity tag + * @return this builder + * @see HttpHeaders#setETag(String) + */ + B eTag(String etag); + + /** + * Set the time the resource was last changed, as specified by the + * {@code Last-Modified} header. + *

The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + * + * @param lastModified the last modified date + * @return this builder + * @see HttpHeaders#setLastModified(long) + */ + B lastModified(long lastModified); + + /** + * Set the location of a resource, as specified by the {@code Location} header. + * + * @param location the location + * @return this builder + * @see HttpHeaders#setLocation(URI) + */ + B location(URI location); + + /** + * Set the caching directives for the resource, as specified by the HTTP 1.1 + * {@code Cache-Control} header. + *

A {@code CacheControl} instance can be built like + * {@code CacheControl.maxAge(3600).cachePublic().noTransform()}. + * + * @param cacheControl a builder for cache-related HTTP response headers + * @return this builder + * @see RFC-7234 Section 5.2 + * @since 4.2 + */ + B cacheControl(CacheControl cacheControl); + + /** + * Configure one or more request header names (e.g. "Accept-Language") to + * add to the "Vary" response header to inform clients that the response is + * subject to content negotiation and variances based on the value of the + * given request headers. The configured request header names are added only + * if not already present in the response "Vary" header. + * + * @param requestHeaders request header names + * @since 4.3 + */ + B varyBy(String... requestHeaders); + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiEntity.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiEntity.java new file mode 100644 index 0000000..f81426a --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiEntity.java @@ -0,0 +1,188 @@ +package cn.axzo.framework.web.http; + +import cn.axzo.framework.core.net.Nets; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.result.ApiResult; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author liyong.tian + * @since 2017/11/14 下午7:55 + */ +public class ApiEntity extends ApiCoreEntity> { + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * + * @param body the entity body + * @param headers the entity headers + * @param status the status code + */ + ApiEntity(ApiResult body, MultiValueMap headers, HttpStatus status) { + super(body, headers, status); + } + + public ApiEntity map(Function mapper) { + ApiResult result = getBody().map(mapper); + return status(getStatusCode()).headers(getHeaders()).wrapper(result); + } + + public static ApiEntity of(ResponseEntity entity) { + return status(entity.getStatusCode()).headers(entity.getHeaders()).ok(entity.getBody()); + } + + /** + * Create a builder with the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created builder + * @since 4.1 + */ + public static ApiEntity ok() { + return status(HttpStatus.OK).ok(); + } + + /** + * A shortcut for creating a {@code ResponseEntity} with the given body and + * the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created {@code ResponseEntity} + * @since 4.1 + */ + public static ApiEntity ok(E data) { + return status(HttpStatus.OK).ok(data); + } + + public static ApiEntity build(IRespCode code, E data) { + return status(HttpStatus.OK).build(code, data); + } + + public static ApiEntityBuilder status(@Nonnull HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); + return new ApiEntityBuilder(status); + } + + public static ApiEntityBuilder header(String headerName, String... headerValues) { + return status(HttpStatus.OK).header(headerName, headerValues); + } + + public static ApiEntityBuilder headers(HttpHeaders headers) { + return status(HttpStatus.OK).headers(headers); + } + + /** + * Create a new builder with a {@linkplain HttpStatus#CREATED CREATED} status + * and a location header set to the given URI. + * + * @param location the location URI + * @return the created builder + * @since 4.1 + */ + public static ApiEntityBuilder created(URI location) { + return status(HttpStatus.CREATED).location(location); + } + + public static ApiEntityBuilder created(String location) { + return status(HttpStatus.CREATED).location(Nets.uri(location)); + } + + /** + * Create a builder with an {@linkplain HttpStatus#ACCEPTED ACCEPTED} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiEntityBuilder accepted() { + return status(HttpStatus.ACCEPTED); + } + + /** + * Create a builder with a {@linkplain HttpStatus#BAD_REQUEST BAD_REQUEST} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiEntityBuilder badRequest() { + return status(HttpStatus.BAD_REQUEST); + } + + /** + * Create a builder with a {@linkplain HttpStatus#NOT_FOUND NOT_FOUND} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiEntityBuilder notFound() { + return status(HttpStatus.NOT_FOUND); + } + + public static class ApiEntityBuilder extends AbstractBodySetter { + + private ApiEntityBuilder(HttpStatus status) { + super(status); + } + + public ApiEntity ok() { + return wrapper(ApiResult.ok()); + } + + public ApiEntity ok(E data) { + return wrapper(ApiResult.ok(data)); + } + + @SuppressWarnings("all") + public ApiEntity okOrNotFound(Optional optional) { + return optional.map(o -> wrapper(ApiResult.ok(o))).orElseGet(() -> { + return wrapper(ApiResult.ok()); + }); + } + + public ApiEntity err(IRespCode code) { + return wrapper(ApiResult.err(code)); + } + + public ApiEntity err(IRespCode code, String message) { + return wrapper(ApiResult.err(code, message)); + } + + public ApiEntity err(IRespCode code, E data) { + return wrapper(ApiResult.err(code, data)); + } + + public ApiEntity err(String message) { + return wrapper(ApiResult.err(message)); + } + + public ApiEntity err(String code, String message) { + return wrapper(ApiResult.err(code, message)); + } + + public ApiEntity with(Throwable e) { + return wrapper(ApiResult.with(e)); + } + + public ApiEntity build(IRespCode code) { + return build(code, null); + } + + public ApiEntity build(IRespCode code, E data) { + return wrapper(ApiResult.build(code, data)); + } + + public ApiEntity build(String code, String message, E data) { + return wrapper(ApiResult.build(code, message, data)); + } + + ApiEntity wrapper(ApiResult result) { + return new ApiEntity<>(result, super.headers, super.status); + } + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiListEntity.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiListEntity.java new file mode 100644 index 0000000..b831541 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiListEntity.java @@ -0,0 +1,162 @@ +package cn.axzo.framework.web.http; + +import cn.axzo.framework.core.net.Nets; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.result.ApiListResult; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.List; + +/** + * @author liyong.tian + * @since 2017/11/14 下午8:00 + */ +public class ApiListEntity extends ApiCoreEntity, ApiListResult> { + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * + * @param body the entity body + * @param headers the entity headers + * @param status the status code + */ + ApiListEntity(ApiListResult body, MultiValueMap headers, HttpStatus status) { + super(body, headers, status); + } + + /** + * Create a builder with the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created builder + * @since 4.1 + */ + public static ApiListEntity ok() { + return status(HttpStatus.OK).ok(); + } + + /** + * A shortcut for creating a {@code ResponseEntity} with the given body and + * the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created {@code ResponseEntity} + * @since 4.1 + */ + public static ApiListEntity ok(List data) { + return status(HttpStatus.OK).ok(data); + } + + public static ApiListEntityBuilder status(@Nonnull HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); + return new ApiListEntityBuilder(status); + } + + public static ApiListEntityBuilder header(String headerName, String... headerValues) { + return status(HttpStatus.OK).header(headerName, headerValues); + } + + public static ApiListEntityBuilder headers(HttpHeaders headers) { + return status(HttpStatus.OK).headers(headers); + } + + /** + * Create a new builder with a {@linkplain HttpStatus#CREATED CREATED} status + * and a location header set to the given URI. + * + * @param location the location URI + * @return the created builder + * @since 4.1 + */ + public static ApiListEntityBuilder created(URI location) { + return status(HttpStatus.CREATED).location(location); + } + + public static ApiListEntityBuilder created(String location) { + return status(HttpStatus.CREATED).location(Nets.uri(location)); + } + + /** + * Create a builder with an {@linkplain HttpStatus#ACCEPTED ACCEPTED} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiListEntityBuilder accepted() { + return status(HttpStatus.ACCEPTED); + } + + /** + * Create a builder with a {@linkplain HttpStatus#BAD_REQUEST BAD_REQUEST} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiListEntityBuilder badRequest() { + return status(HttpStatus.BAD_REQUEST); + } + + /** + * Create a builder with a {@linkplain HttpStatus#NOT_FOUND NOT_FOUND} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiListEntityBuilder notFound() { + return status(HttpStatus.NOT_FOUND); + } + + public static class ApiListEntityBuilder extends AbstractBodySetter { + + private ApiListEntityBuilder(HttpStatus status) { + super(status); + } + + public ApiListEntity ok() { + return wrapper(ApiListResult.ok()); + } + + public ApiListEntity ok(List data) { + return wrapper(ApiListResult.ok(data)); + } + + public ApiListEntity err(IRespCode code) { + return wrapper(ApiListResult.err(code)); + } + + public ApiListEntity err(IRespCode code, String message) { + return wrapper(ApiListResult.err(code, message)); + } + + public ApiListEntity err(String message) { + return wrapper(ApiListResult.err(message)); + } + + public ApiListEntity err(IRespCode code, List data) { + return wrapper(ApiListResult.err(code, data)); + } + + public ApiListEntity err(String code, String message) { + return wrapper(ApiListResult.err(code, message)); + } + + public ApiListEntity build(IRespCode code) { + return wrapper(ApiListResult.build(code)); + } + + public ApiListEntity build(IRespCode code, List data) { + return wrapper(ApiListResult.build(code, data)); + } + + public ApiListEntity build(String code, String message, List data) { + return wrapper(ApiListResult.build(code, message, data)); + } + + private ApiListEntity wrapper(ApiListResult result) { + return new ApiListEntity<>(result, super.headers, super.status); + } + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiPageEntity.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiPageEntity.java new file mode 100644 index 0000000..015ab77 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/ApiPageEntity.java @@ -0,0 +1,181 @@ +package cn.axzo.framework.web.http; + +import com.github.pagehelper.PageInfo; +import cn.axzo.framework.core.net.Nets; +import cn.axzo.framework.domain.page.Page; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.result.ApiPageResult; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.List; + +/** + * @author liyong.tian + * @since 2017/11/14 下午7:52 + */ +public class ApiPageEntity extends ApiCoreEntity, ApiPageResult> { + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * + * @param body the entity body + * @param headers the entity headers + * @param status the status code + */ + public ApiPageEntity(ApiPageResult body, MultiValueMap headers, HttpStatus status) { + super(body, headers, status); + } + + /** + * Create a builder with the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created builder + * @since 4.1 + */ + public static ApiPageEntity empty() { + return status(HttpStatus.OK).empty(); + } + + /** + * A shortcut for creating a {@code ResponseEntity} with the given body and + * the status set to {@linkplain HttpStatus#OK OK}. + * + * @return the created {@code ResponseEntity} + * @since 4.1 + */ + public static ApiPageEntity ok(Page page) { + return status(HttpStatus.OK).ok(page); + } + + public static ApiPageEntity ok(org.springframework.data.domain.Page page) { + return status(HttpStatus.OK).ok(page); + } + +// public static ApiPageEntity ok(PageInfo pageInfo) { +// return status(HttpStatus.OK).ok(pageInfo); +// } + + public static ApiPageEntityBuilder status(@Nonnull HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); + return new ApiPageEntityBuilder(status); + } + + public static ApiPageEntityBuilder header(String headerName, String... headerValues) { + return status(HttpStatus.OK).header(headerName, headerValues); + } + + public static ApiPageEntityBuilder headers(HttpHeaders headers) { + return status(HttpStatus.OK).headers(headers); + } + + /** + * Create a new builder with a {@linkplain HttpStatus#CREATED CREATED} status + * and a location header set to the given URI. + * + * @param location the location URI + * @return the created builder + * @since 4.1 + */ + public static ApiPageEntityBuilder created(URI location) { + return status(HttpStatus.CREATED).location(location); + } + + public static ApiPageEntityBuilder created(String location) { + return status(HttpStatus.CREATED).location(Nets.uri(location)); + } + + /** + * Create a builder with an {@linkplain HttpStatus#ACCEPTED ACCEPTED} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiPageEntityBuilder accepted() { + return status(HttpStatus.ACCEPTED); + } + + /** + * Create a builder with a {@linkplain HttpStatus#BAD_REQUEST BAD_REQUEST} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiPageEntityBuilder badRequest() { + return status(HttpStatus.BAD_REQUEST); + } + + /** + * Create a builder with a {@linkplain HttpStatus#NOT_FOUND NOT_FOUND} status. + * + * @return the created builder + * @since 4.1 + */ + public static ApiPageEntityBuilder notFound() { + return status(HttpStatus.NOT_FOUND); + } + + public static class ApiPageEntityBuilder extends AbstractBodySetter { + + private ApiPageEntityBuilder(HttpStatus status) { + super(status); + } + + public ApiPageEntity empty() { + return wrapper(ApiPageResult.empty()); + } + + public ApiPageEntity ok(Page page) { + return wrapper(ApiPageResult.ok(page)); + } + + public ApiPageEntity ok(org.springframework.data.domain.Page page) { + return wrapper(ApiPageResult.ok(page)); + } + +// public ApiPageEntity ok(PageInfo pageInfo) { +// return wrapper(ApiPageResult.ok(pageInfo)); +// } + + public ApiPageEntity ok(List data, Long total) { + return wrapper(ApiPageResult.ok(data, total)); + } + + public ApiPageEntity ok(List data, Long total, Integer pageNumber, Integer pageSize) { + return wrapper(ApiPageResult.ok(data, total, pageNumber, pageSize)); + } + + public ApiPageEntity err(IRespCode code) { + return wrapper(ApiPageResult.err(code)); + } + + public ApiPageEntity err(IRespCode code, String message) { + return wrapper(ApiPageResult.err(code, message)); + } + + public ApiPageEntity err(String code, String message) { + return wrapper(ApiPageResult.err(code, message)); + } + + public ApiPageEntity build(IRespCode code) { + return wrapper(ApiPageResult.build(code)); + } + + public ApiPageEntity build(Long total, IRespCode code, List data, Integer pageNum, Integer pageSize) { + return wrapper(ApiPageResult.build(total, code, data, pageNum, pageSize)); + } + + public ApiPageEntity build(Long total, String code, String message, List data, + Integer pageNum, Integer pageSize) { + return wrapper(ApiPageResult.build(total, code, message, data, pageNum, pageSize)); + } + + private ApiPageEntity wrapper(ApiPageResult result) { + return new ApiPageEntity<>(result, super.headers, super.status); + } + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/ConverterUtil.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/ConverterUtil.java new file mode 100644 index 0000000..acb1757 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/ConverterUtil.java @@ -0,0 +1,23 @@ +package cn.axzo.framework.web.http.converter; + +import lombok.experimental.UtilityClass; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; + +import java.util.List; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/2 下午1:58 + */ +@UtilityClass +public class ConverterUtil { + + public void removeXmlConverters(List> converters) { + converters.removeIf(converter -> converter instanceof AbstractXmlHttpMessageConverter); + converters.removeIf(converter -> converter instanceof MappingJackson2XmlHttpMessageConverter); + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/FormDataHttpMessageConverter.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/FormDataHttpMessageConverter.java new file mode 100644 index 0000000..eaecdfe --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/http/converter/FormDataHttpMessageConverter.java @@ -0,0 +1,57 @@ +package cn.axzo.framework.web.http.converter; + +import cn.axzo.framework.core.InternalException; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; + +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; +import static org.springframework.http.MediaType.TEXT_PLAIN; + +/** + * 解析form-data方式提交的value值 + */ +public class FormDataHttpMessageConverter extends AbstractHttpMessageConverter { + + private final ConversionService conversionService; + + private final static MediaType[] supportedMediaTypes = { + APPLICATION_OCTET_STREAM, + TEXT_PLAIN + }; + + public FormDataHttpMessageConverter(ConversionService conversionService) { + super(Charset.defaultCharset(), supportedMediaTypes); + this.conversionService = conversionService; + } + + @Override + protected boolean supports(Class clazz) { + return conversionService.canConvert(String.class, clazz); + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, + HttpMessageNotReadableException { + String body = StreamUtils.copyToString(inputMessage.getBody(), getDefaultCharset()); + return conversionService.convert(body, clazz); + } + + @Override + protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException { + throw new InternalException("will not happen"); + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/DefaultMultipartFile.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/DefaultMultipartFile.java new file mode 100644 index 0000000..d51c014 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/DefaultMultipartFile.java @@ -0,0 +1,90 @@ +package cn.axzo.framework.web.multipart; + +import jodd.util.StringUtil; +import lombok.Getter; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.MimeType; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static java.net.URLConnection.guessContentTypeFromName; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; + +/** + * @author liyong.tian + * @since 2017/11/16 下午10:54 + */ +public class DefaultMultipartFile implements MultipartFile { + + public static final String DEFAULT_FORM_DATA_NAME = "file"; + + @Getter + private final String name; + + @Getter + private final String originalFilename; + + @Getter + private final String contentType; + + private final byte[] payload; + + public DefaultMultipartFile(String filename, byte[] payload) { + this(DEFAULT_FORM_DATA_NAME, filename, payload); + } + + public DefaultMultipartFile(String name, String filename, byte[] payload) { + this(name, filename, guessMimeType(filename), payload); + } + + public DefaultMultipartFile(String name, String filename, MimeType mimeType, byte[] payload) { + if (payload == null) { + throw new IllegalArgumentException("Payload cannot be null."); + } + this.name = name; + this.originalFilename = filename; + this.contentType = mimeType.toString(); + this.payload = payload; + } + + @Override + public boolean isEmpty() { + return payload.length == 0; + } + + @Override + public long getSize() { + return payload.length; + } + + @Override + public byte[] getBytes() { + return payload; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(payload); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + FileCopyUtils.copy(payload, dest); + } + + public static MimeType guessMimeType(String filename) { + if (StringUtil.isBlank(filename)) { + throw new IllegalArgumentException("Filename cannot be null."); + } + String type = guessContentTypeFromName(filename); + try { + return MimeType.valueOf(type); + } catch (Exception e) { + return APPLICATION_OCTET_STREAM; + } + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/Multipart.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/Multipart.java new file mode 100644 index 0000000..9c91cd1 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/multipart/Multipart.java @@ -0,0 +1,74 @@ +package cn.axzo.framework.web.multipart; + +import lombok.experimental.UtilityClass; +import org.springframework.util.MimeType; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static org.springframework.util.FileCopyUtils.copyToByteArray; + +/** + * @author liyong.tian + * @since 2017/11/16 下午11:10 + */ +@UtilityClass +public class Multipart { + + /*---------------------------------------File---------------------------------------*/ + public MultipartFile file(File file) { + return file(DefaultMultipartFile.DEFAULT_FORM_DATA_NAME, file); + } + + public MultipartFile file(String name, File file) { + return file(name, file, DefaultMultipartFile.guessMimeType(file.getName())); + } + + public MultipartFile file(String name, File file, MimeType mimeType) { + try { + return new DefaultMultipartFile(name, file.getName(), mimeType, copyToByteArray(file)); + } catch (IOException e) { + throw new IllegalArgumentException("File cannot be parsed correctly."); + } + } + + /*------------------------------------InputStream------------------------------------*/ + public MultipartFile file(String filename, InputStream inputStream) { + return file(DefaultMultipartFile.DEFAULT_FORM_DATA_NAME, filename, inputStream); + } + + public MultipartFile file(String filename, MimeType mimeType, InputStream inputStream) { + return file(DefaultMultipartFile.DEFAULT_FORM_DATA_NAME, filename, mimeType, inputStream); + } + + public MultipartFile file(String name, String filename, InputStream inputStream) { + return file(name, filename, DefaultMultipartFile.guessMimeType(filename), inputStream); + } + + public MultipartFile file(String name, String filename, MimeType mimeType, InputStream inputStream) { + try { + return new DefaultMultipartFile(name, filename, mimeType, copyToByteArray(inputStream)); + } catch (IOException e) { + throw new IllegalArgumentException("InputStream cannot be parsed correctly."); + } + } + + /*------------------------------------Byte Array------------------------------------*/ + public MultipartFile file(String filename, byte[] payload) { + return file(DefaultMultipartFile.DEFAULT_FORM_DATA_NAME, filename, payload); + } + + public MultipartFile file(String filename, MimeType mimeType, byte[] payload) { + return file(DefaultMultipartFile.DEFAULT_FORM_DATA_NAME, filename, mimeType, payload); + } + + public MultipartFile file(String name, String filename, byte[] payload) { + return file(name, filename, DefaultMultipartFile.guessMimeType(filename), payload); + } + + public MultipartFile file(String name, String filename, MimeType mimeType, byte[] payload) { + return new DefaultMultipartFile(name, filename, mimeType, payload); + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageAnnotationUtils.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageAnnotationUtils.java new file mode 100644 index 0000000..bcd3c28 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageAnnotationUtils.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.web.page; + +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.util.Objects; + +/** + * @author liyong.tian + * @since 2017/2/17 + */ +class PageAnnotationUtils { + /** + * Returns the value of the given specific property of the given annotation. If the value of that property is the + * properties default, we fall back to the value of the {@code value} attribute. + * + * @param annotation must not be {@literal null}. + * @param property must not be {@literal null} or empty. + */ + @SuppressWarnings("unchecked") + public static T getSpecificPropertyOrDefaultFromValue(Annotation annotation, String property) { + Object propertyDefaultValue = AnnotationUtils.getDefaultValue(annotation, property); + Object propertyValue = AnnotationUtils.getValue(annotation, property); + if (Objects.equals(propertyDefaultValue, propertyValue)) { + return (T) AnnotationUtils.getValue(annotation); + } + return (T) propertyValue; + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageableArgumentResolver.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageableArgumentResolver.java new file mode 100644 index 0000000..ba45398 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/PageableArgumentResolver.java @@ -0,0 +1,199 @@ +package cn.axzo.framework.web.page; + +import cn.axzo.framework.core.enums.EnumStdUtil; +import cn.axzo.framework.domain.page.PageBuilder; +import cn.axzo.framework.domain.page.Pageable; +import cn.axzo.framework.domain.page.PageableVerbose; +import cn.axzo.framework.domain.sort.Direction; +import cn.axzo.framework.domain.sort.Sort; +import lombok.Data; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import static cn.axzo.framework.core.util.StringUtil.isInteger; +import static cn.axzo.framework.core.util.StringUtil.isPositiveInteger; +import static cn.axzo.framework.domain.page.PageUtil.getFirstPageNumber; +import static java.lang.Boolean.parseBoolean; +import static java.lang.Integer.parseInt; +import static org.springframework.util.StringUtils.hasText; + +/** + * @author liyong.tian + * @since 2017/2/17 + */ +@Data +public class PageableArgumentResolver implements HandlerMethodArgumentResolver { + + private final RestPageProperties properties; + + private final SortArgumentResolver sortResolver; + + /** + * Constructs an instance of this resolver with the specified {@link SortArgumentResolver}. + * + * @param sortResolver The sort resolver to use + */ + public PageableArgumentResolver(RestPageProperties properties, @Nullable SortArgumentResolver sortResolver) { + this.sortResolver = sortResolver == null ? new SortArgumentResolver(properties) : sortResolver; + this.properties = properties; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Pageable.class.equals(parameter.getParameterType()); + } + + @Override + public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + assertPageableUniqueness(parameter); + + String pageString = webRequest.getParameter(properties.getPageParameterName()); + String pageSizeString = webRequest.getParameter(properties.getSizeParameterName()); + String needTotalString = webRequest.getParameter(properties.getNeedTotalParameterName()); + String needContentString = webRequest.getParameter(properties.getNeedContentParameterName()); + String verboseString = webRequest.getParameter(properties.getVerboseParameterName()); + String fixEdgeString = webRequest.getParameter(properties.getFixEdgeParameterName()); + + Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(parameter); + int page = isInteger(pageString) ? parseInt(pageString) : defaultOrFallback.getPageNumber(); + int size = isPositiveInteger(pageSizeString) ? parseInt(pageSizeString) : defaultOrFallback.getPageSize(); + + boolean needTotal = hasText(needTotalString) ? parseBoolean(needTotalString) : defaultOrFallback.needTotal(); + boolean needContent = hasText(needContentString) ? parseBoolean(needContentString) : defaultOrFallback.needContent(); + PageableVerbose verbose; + if (hasText(verboseString)) { + verbose = EnumStdUtil.findEnum(PageableVerbose.class, verboseString).orElseGet(defaultOrFallback::verbose); + } else { + verbose = defaultOrFallback.verbose(); + } + boolean fixEdge = hasText(fixEdgeString) ? parseBoolean(fixEdgeString) : defaultOrFallback.isFixEdge(); + + Sort sort = sortResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + sort = sort == null ? defaultOrFallback.getSort() : sort; + return PageBuilder.page(page) + .size(size) + .defaultPageSize(properties.getDefaultPageSize()) + .maxPageSize(properties.getMaxPageSize()) + .needTotal(needTotal) + .needContent(needContent) + .fixEdge(fixEdge) + .verbose(verbose) + .pageNumberOneIndexed(properties.isOneIndexedParameters()) + .resortStrategy(properties.getResortStrategy()) + .sort(sort) + .build(); + } + + @Nonnull + private Pageable getDefaultFromAnnotationOrFallback(MethodParameter parameter) { + PageableDefault defaults = parameter.getParameterAnnotation(PageableDefault.class); + Integer page; + Integer size; + Sort sort = null; + if (defaults == null) { + page = getFirstPageNumber(properties.isOneIndexedParameters()); + size = properties.getDefaultPageSize(); + } else { + page = defaults.page(); + if (page == 0 && properties.isOneIndexedParameters()) { + page = getFirstPageNumber(true); + } + size = PageAnnotationUtils.getSpecificPropertyOrDefaultFromValue(defaults, "size"); + if (defaults.sort().length > 0) { + sort = new Sort(Direction.fromString(defaults.direction().name()), defaults.sort()); + } + } + return PageBuilder.page(page) + .size(size) + .defaultPageSize(properties.getDefaultPageSize()) + .maxPageSize(properties.getMaxPageSize()) + .fixEdge(properties.isFixEdge()) + .pageNumberOneIndexed(properties.isOneIndexedParameters()) + .resortStrategy(properties.getResortStrategy()) + .sort(sort) + .build(); + } + + /** + * Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}. + * + * @param parameter must not be {@literal null}. + */ + private static void assertPageableUniqueness(MethodParameter parameter) { + Method method = parameter.getMethod(); + if (containsMoreThanOnePageableParameter(method)) { + Annotation[][] annotations = method.getParameterAnnotations(); + assertQualifiersFor(method.getParameterTypes(), annotations); + } + } + + /** + * Returns whether the given {@link Method} has more than one {@link Pageable} parameter. + * + * @param method must not be {@literal null}. + */ + private static boolean containsMoreThanOnePageableParameter(Method method) { + boolean pageableFound = false; + for (Class type : method.getParameterTypes()) { + if (pageableFound && type.equals(Pageable.class)) { + return true; + } + if (type.equals(Pageable.class)) { + pageableFound = true; + } + } + return false; + } + + /** + * Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to + * distinguish them from each other. + * + * @param parameterTypes must not be {@literal null}. + * @param annotations must not be {@literal null}. + */ + private static void assertQualifiersFor(Class[] parameterTypes, Annotation[][] annotations) { + Set values = new HashSet<>(); + for (int i = 0; i < annotations.length; i++) { + if (Pageable.class.equals(parameterTypes[i])) { + Qualifier qualifier = findAnnotation(annotations[i]); + if (null == qualifier) { + throw new IllegalStateException( + "Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier"); + } + if (values.contains(qualifier.value())) { + throw new IllegalStateException("Values of the user Qualifiers must be unique!"); + } + values.add(qualifier.value()); + } + } + } + + /** + * Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the + * array does not contain a {@link Qualifier} annotation. + * + * @param annotations must not be {@literal null}. + */ + @Nullable + private static Qualifier findAnnotation(Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof Qualifier) { + return (Qualifier) annotation; + } + } + return null; + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/page/RestPageProperties.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/RestPageProperties.java new file mode 100644 index 0000000..e0f495e --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/RestPageProperties.java @@ -0,0 +1,102 @@ +package cn.axzo.framework.web.page; + + +import cn.axzo.framework.domain.page.ResortStrategies; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import static cn.axzo.framework.domain.page.PageDefaults.*; +import static cn.axzo.framework.domain.sort.SortDefaults.RESORT_STRATEGY; + +//import org.springframework.validation.annotation.Validated; + + +/** + * @author liyong.tian + * @since 2017/1/22 + */ +@Data +@Validated +public class RestPageProperties { + + /** + * Default size of pages. + */ + @NotNull + private Integer defaultPageSize = PAGE_SIZE; + + /** + * Maximum size of pages. + */ + @NotNull + private Integer maxPageSize = 100; + + /** + * 全局配置是否自动修正不合法的分页参数 + */ + private boolean fixEdge = IS_FIX_EDGE; + + /** + * Configures the parameter name to be used to find the page number in the request. + */ + @Deprecated + private String pageParamName; + + /** + * Configures the parameter name to be used to find the page number in the request. + */ + @NotBlank + private String pageParameterName = "page"; + + /** + * Configures the parameter name to be used to find the page size in the request. + */ + @NotBlank + private String sizeParameterName = "size"; + + /** + * 排序参数名 + */ + @NotBlank + private String sortParameterName = "sort"; + + /** + * 是否查询总数的参数名 + */ + @NotBlank + private String needTotalParameterName = "needTotal"; + + /** + * 是否查询总数的参数名 + */ + @NotBlank + private String needContentParameterName = "needContent"; + + /** + * 是否返回分页冗余信息的参数名 + */ + @NotBlank + private String verboseParameterName = "verbose"; + + /** + * 是否查询总数的参数名 + */ + @NotBlank + private String fixEdgeParameterName = "fixEdge"; + + /** + * Configures whether to expose and assume 1-based page number indexes in the request parameters. + */ + private boolean oneIndexedParameters = PAGE_NUMBER_ONE_INDEXED; + + @NotNull + private ResortStrategies resortStrategy = RESORT_STRATEGY; + + @SuppressWarnings("deprecation") + public String getPageParameterName() { + return pageParamName != null ? pageParamName : pageParameterName; + } +} diff --git a/axzo-common-web/src/main/java/cn.axzo.framework.web/page/SortArgumentResolver.java b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/SortArgumentResolver.java new file mode 100644 index 0000000..cc820c9 --- /dev/null +++ b/axzo-common-web/src/main/java/cn.axzo.framework.web/page/SortArgumentResolver.java @@ -0,0 +1,135 @@ +package cn.axzo.framework.web.page; + +import cn.axzo.framework.domain.sort.Direction; +import cn.axzo.framework.domain.sort.Order; +import cn.axzo.framework.domain.sort.Sort; +import jodd.util.StringUtil; +import lombok.Data; +import org.springframework.core.MethodParameter; +import org.springframework.data.web.SortDefault; +import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static cn.axzo.framework.domain.sort.Direction.fromStringOrNull; +import static cn.axzo.framework.web.page.PageAnnotationUtils.getSpecificPropertyOrDefaultFromValue; +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; +import static jodd.util.StringUtil.isBlank; + +/** + * @author liyong.tian + * @since 2017/2/17 + */ +@Data +public class SortArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName(); + private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName(); + + private final RestPageProperties properties; + private Sort fallbackSort = null; + + public SortArgumentResolver(RestPageProperties properties) { + this.properties = properties; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Sort.class.equals(parameter.getParameterType()); + } + + @Override + public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String[] sortStrings = webRequest.getParameterValues(properties.getSortParameterName()); + if (sortStrings == null) { + return _getDefaultFromAnnotationOrFallback(parameter); + } + sortStrings = Stream.of(sortStrings).filter(StringUtil::isNotBlank).toArray(String[]::new); + if (sortStrings.length == 0) { + return _getDefaultFromAnnotationOrFallback(parameter); + } + return _parseParameterIntoSort(sortStrings); + } + + /** + * Reads the default {@link Sort} to be used from the given {@link MethodParameter}. Rejects the parameter if both an + * {@link SortDefaults} and {@link SortDefault} annotation is found as we cannot build a reliable {@link Sort} + * instance then (property ordering). + * + * @param parameter will never be {@literal null}. + * @return the default {@link Sort} instance derived from the parameter annotations or the configured fallback-sort + */ + @Nullable + private Sort _getDefaultFromAnnotationOrFallback(MethodParameter parameter) { + SortDefaults sortDefaults = parameter.getParameterAnnotation(SortDefaults.class); + SortDefault sortDefault = parameter.getParameterAnnotation(SortDefault.class); + if (sortDefaults != null && sortDefault != null) { + throw new IllegalArgumentException( + format("Cannot use both @%s and @%s on parameter %s! Move %s into %s to define sorting order!", + SORT_DEFAULTS_NAME, SORT_DEFAULT_NAME, parameter.toString(), SORT_DEFAULT_NAME, SORT_DEFAULTS_NAME)); + } + if (sortDefault != null) { + List orders = _constructOrders(sortDefault); + return orders.isEmpty() ? null : new Sort(orders); + } + if (sortDefaults != null) { + List orders = _constructOrders(sortDefaults.value()); + return orders.isEmpty() ? null : new Sort(orders); + } + return fallbackSort; + } + + /** + * Parses the given sort expressions into a {@link Sort} instance. The implementation expects the sources to be a + * concatenation of Strings using the given delimiter. If the last element can be parsed into a {@link Direction} it's + * considered a {@link Direction} and a simple property otherwise. + * + * @param sortStrings will never be {@literal null}. + */ + @Nullable + private Sort _parseParameterIntoSort(@Nonnull String[] sortStrings) { + List orders = new ArrayList<>(); + for (String part : sortStrings) { + if (isBlank(part)) { + continue; + } + String[] elements = part.split(","); + Direction direction = elements.length == 0 ? null : fromStringOrNull(elements[elements.length - 1]); + for (int i = 0; i < elements.length; i++) { + if (i == elements.length - 1 && direction != null) { + continue; + } + String property = elements[i]; + if (isBlank(property)) { + continue; + } + orders.add(new Order(direction, property)); + } + } + return orders.isEmpty() ? null : new Sort(orders); + } + + private List _constructOrders(SortDefault... sortDefaults) { + return Stream.of(sortDefaults) + .filter(Objects::nonNull) + .flatMap(sortDefault -> { + String[] fields = getSpecificPropertyOrDefaultFromValue(sortDefault, "sort"); + if (fields.length == 0) { + fields = getSpecificPropertyOrDefaultFromValue(sortDefault, "value"); + } + Direction direction = fromStringOrNull(sortDefault.direction().name()); + return Stream.of(fields).map(field -> new Order(direction, field)); + }) + .collect(toList()); + } +} diff --git a/axzo-common-webmvc/pom.xml b/axzo-common-webmvc/pom.xml new file mode 100644 index 0000000..d1a7453 --- /dev/null +++ b/axzo-common-webmvc/pom.xml @@ -0,0 +1,76 @@ + + + + axzo-framework-commons + cn.axzo.framework + 1.0.0-SNAPSHOT + + 4.0.0 + + axzo-common-webmvc + Axzo Common Web MVC + + + + cn.axzo.framework + axzo-common-web + + + cn.axzo.framework.jackson + jackson-starter + + + org.springframework + spring-webmvc + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + javax.servlet + javax.servlet-api + provided + + + io.springfox + springfox-swagger2 + provided + + + org.mapstruct + mapstruct + + + + + org.mapstruct + mapstruct-jdk8 + provided + + + io.springfox + springfox-bean-validators + provided + + + org.springframework.security + spring-security-web + provided + + + org.springframework.security.oauth + spring-security-oauth2 + provided + + + io.undertow + undertow-core + provided + + + \ No newline at end of file diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersConfigurer.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersConfigurer.java new file mode 100644 index 0000000..f5b3fd7 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersConfigurer.java @@ -0,0 +1,52 @@ +package cn.axzo.framework.web.servlet.configurer; + +import cn.axzo.framework.web.http.converter.ConverterUtil; +import cn.axzo.framework.web.http.converter.FormDataHttpMessageConverter; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.core.Ordered; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.FormatterRegistry; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 16:13 + **/ +@RequiredArgsConstructor +public class HttpMessageConvertersConfigurer implements WebMvcConfigurer, Ordered { + + private final HttpMessageConvertersProperties properties; + + private final CompletableFuture conversionServiceFuture = new CompletableFuture<>(); + + @Override + public void extendMessageConverters(List> converters) { + if (properties.isExcludeXml()) { + ConverterUtil.removeXmlConverters(converters); + } + + //增加自定义Converter + conversionServiceFuture.whenComplete((conversionService, e) -> { + val httpMessageConverter = new FormDataHttpMessageConverter(conversionService); + converters.add(httpMessageConverter); + }); + } + + @Override + public void addFormatters(FormatterRegistry registry) { + if (registry instanceof ConversionService) { + conversionServiceFuture.complete((ConversionService) registry); + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersProperties.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersProperties.java new file mode 100644 index 0000000..c85ee56 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/configurer/HttpMessageConvertersProperties.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.web.servlet.configurer; + +import lombok.Data; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 16:15 + **/ +@Data +public class HttpMessageConvertersProperties { + + // 是否排除xml相关的converters + private boolean excludeXml = false; +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAware.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAware.java new file mode 100644 index 0000000..b175c62 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAware.java @@ -0,0 +1,13 @@ +package cn.axzo.framework.web.servlet.context; + +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 16:21 + **/ +public interface RequestMappingHandlerAdapterLazyAware { + + void setRequestMappingHandlerAdapter(RequestMappingHandlerAdapter handlerAdapter); +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAwareProcessor.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAwareProcessor.java new file mode 100644 index 0000000..6f6f835 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/context/RequestMappingHandlerAdapterLazyAwareProcessor.java @@ -0,0 +1,38 @@ +package cn.axzo.framework.web.servlet.context; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +import java.util.concurrent.CompletableFuture; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 16:23 + **/ +@RequiredArgsConstructor +public class RequestMappingHandlerAdapterLazyAwareProcessor implements BeanPostProcessor { + + private CompletableFuture future = new CompletableFuture<>(); + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RequestMappingHandlerAdapter && !future.isDone()) { + this.future.complete((RequestMappingHandlerAdapter) bean); + } + if (bean instanceof RequestMappingHandlerAdapterLazyAware) { + this.future.whenComplete((result, e) -> { + RequestMappingHandlerAdapterLazyAware aware = (RequestMappingHandlerAdapterLazyAware) bean; + aware.setRequestMappingHandlerAdapter(result); + }); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/CustomServletRequestWrapper.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/CustomServletRequestWrapper.java new file mode 100644 index 0000000..61d8257 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/CustomServletRequestWrapper.java @@ -0,0 +1,116 @@ +package cn.axzo.framework.web.servlet.filter; + +import org.apache.commons.io.IOUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * @Description 包装HttpServletRequest + * @Author liyong.tian + * @Date 2020/9/9 16:27 + **/ +public class CustomServletRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + /** + * Construct a wrapper for the specified request. + * + * @param request The request to be wrapped + */ + CustomServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + body = IOUtils.toByteArray(super.getInputStream()); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() { + return new RequestBodyCachingInputStream(body); + } + + private class RequestBodyCachingInputStream extends ServletInputStream { + private byte[] body; + private int lastIndexRetrieved = -1; + private ReadListener listener; + + RequestBodyCachingInputStream(byte[] body) { + this.body = body; + } + + @Override + public int read() throws IOException { + if (isFinished()) { + return -1; + } + int i = body[lastIndexRetrieved + 1]; + lastIndexRetrieved++; + if (isFinished() && listener != null) { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + throw e; + } + } + return i; + } + + @Override + public boolean isFinished() { + return lastIndexRetrieved == body.length - 1; + } + + @Override + public boolean isReady() { + // This implementation will never block + // We also never need to call the readListener from this method, as this method will never return false + return isFinished(); + } + + @Override + public void setReadListener(ReadListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener can not be null"); + } + if (this.listener != null) { + throw new IllegalArgumentException("listener has been set"); + } + this.listener = listener; + if (!isFinished()) { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } else { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } + } + + @Override + public int available() { + return body.length - lastIndexRetrieved - 1; + } + + @Override + public void close() { + lastIndexRetrieved = body.length - 1; + body = null; + } + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedBadRequestFilter.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedBadRequestFilter.java new file mode 100644 index 0000000..aaf5c21 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedBadRequestFilter.java @@ -0,0 +1,189 @@ +package cn.axzo.framework.web.servlet.filter; + +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.web.http.ApiEntity; +import cn.axzo.framework.web.servlet.context.RequestMappingHandlerAdapterLazyAware; +import io.undertow.server.RequestTooBigException; +import io.undertow.server.handlers.form.MultiPartParserDefinition; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +import javax.annotation.Nullable; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static cn.axzo.framework.core.util.ClassUtil.getDefaultClassLoader; +import static cn.axzo.framework.domain.web.code.BaseCode.BAD_REQUEST; +import static cn.axzo.framework.domain.web.code.BaseCode.PAYLOAD_TOO_LARGE; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 16:31 + **/ +@Slf4j +@RequiredArgsConstructor +@SuppressWarnings("unused") +public class OrderedBadRequestFilter extends OncePerRequestFilter implements Ordered, RequestMappingHandlerAdapterLazyAware { + + private static boolean isUndertow = ClassUtil.isPresent("io.undertow.Undertow", getDefaultClassLoader()); + + /** + * 使用懒加载方式,避免改变在SpringMVC里原本的加载顺序 + */ + private RequestMappingHandlerAdapter handlerAdapter; + + private final static String ERROR_REQUEST_INVALID = "Request is invalid"; + + private final static String ERROR_FORM_DATA_NOT_ENCODED = "Request form data is not encoded"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + try { + request.getParameterNames(); + } catch (Exception e) { + log.warn(ERROR_REQUEST_INVALID, e); + _handleExceptionReturn(request, response, e); + return; //到此结束,不再继续 + } + + filterChain.doFilter(request, response); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 10; + } + + @Override + public void setRequestMappingHandlerAdapter(RequestMappingHandlerAdapter handlerAdapter) { + this.handlerAdapter = handlerAdapter; + } + + private void _handleExceptionReturn(HttpServletRequest request, + HttpServletResponse response, + Exception ex) throws IOException { + if (handlerAdapter == null) { + log.error("RequestMappingHandlerAdapter initialized error"); + _outputWithoutMessage(response, INTERNAL_SERVER_ERROR); + return; + } + + ApiEntity returnValue = _decideReturnValue(ex); + MethodParameter returnType = new ReturnValueMethodParameter(returnValue); + val returnValueHandler = _getReturnValueHandler(returnType); + if (returnValueHandler == null) { + log.error("No return value handler found"); + _outputWithoutMessage(response, INTERNAL_SERVER_ERROR); + return; + } + + try { + ModelAndViewContainer container = new ModelAndViewContainer(); + NativeWebRequest webRequest = new ServletWebRequest(request, response); + returnValueHandler.handleReturnValue(returnValue, returnType, container, webRequest); + } catch (HttpMediaTypeNotAcceptableException e) { + log.error("Handle return value error", e); + _outputWithoutMessage(response, HttpStatus.NOT_ACCEPTABLE); + } catch (Exception e) { + log.error("Handle return value error", e); + _outputWithoutMessage(response, INTERNAL_SERVER_ERROR); + } + } + + private void _outputWithoutMessage(HttpServletResponse response, HttpStatus status) throws IOException { + response.setStatus(status.value()); + try (val writer = response.getWriter()) { + writer.flush(); + } + } + + @Nullable + private HandlerMethodReturnValueHandler _getReturnValueHandler(MethodParameter returnType) { + return handlerAdapter.getReturnValueHandlers() + .stream() + .filter(handler -> handler.supportsReturnType(returnType)) + .findFirst() + .orElse(null); + } + + private ApiEntity _decideReturnValue(Exception ex) { + ApiEntity returnValue; + if (ex instanceof IllegalArgumentException) { + return ApiEntity.badRequest().err(BAD_REQUEST, ex.getMessage()); + } else if (ex instanceof StringIndexOutOfBoundsException) { + return ApiEntity.badRequest().err(BAD_REQUEST, ERROR_FORM_DATA_NOT_ENCODED); + } else if (ex instanceof IllegalStateException) { + return _decideReturnValue((IllegalStateException) ex); + } else if (ex instanceof RuntimeException) { + return _decideReturnValue((RuntimeException) ex); + } + return ApiEntity.status(INTERNAL_SERVER_ERROR).err(BaseCode.SERVER_ERROR); + } + + private ApiEntity _decideReturnValue(IllegalStateException ex) { + if (isUndertow) { + Throwable cause = ex.getCause(); + if (cause instanceof RequestTooBigException) { + return ApiEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).err(PAYLOAD_TOO_LARGE, cause.getMessage()); + } + if (cause instanceof MultiPartParserDefinition.FileTooLargeException) { + return ApiEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).err(PAYLOAD_TOO_LARGE, cause.getMessage()); + } + } + return ApiEntity.badRequest().err(BAD_REQUEST, ex.getMessage()); + } + + private ApiEntity _decideReturnValue(RuntimeException ex) { + if (ex.getCause() instanceof IOException) { + return ApiEntity.badRequest().err(BAD_REQUEST, ex.getCause().getMessage()); + } + return ApiEntity.badRequest().err(BAD_REQUEST, ex.getMessage()); + } + + /** + * A MethodParameter for a HandlerMethod return type based on an actual return value. + */ + private class ReturnValueMethodParameter extends SynthesizingMethodParameter { + + private final Object returnValue; + + ReturnValueMethodParameter(Object returnValue) { + super(OrderedBadRequestFilter.this.getClass().getMethods()[0], -1); + this.returnValue = returnValue; + } + + ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + } + + @Override + public Class getParameterType() { + return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); + } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedTimerFilter.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedTimerFilter.java new file mode 100644 index 0000000..958b437 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/OrderedTimerFilter.java @@ -0,0 +1,36 @@ +package cn.axzo.framework.web.servlet.filter; + +import org.springframework.core.Ordered; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @Description 记录请求的初始时间 + * @Author liyong.tian + * @Date 2020/9/9 16:54 + **/ +public class OrderedTimerFilter extends OncePerRequestFilter implements Ordered { + + public final static String ATTRIBUTE_START_TIME = OrderedTimerFilter.class.getName() + ".startTime"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + request.setAttribute(ATTRIBUTE_START_TIME, System.nanoTime()); + filterChain.doFilter(request, response); + } finally { + request.removeAttribute(ATTRIBUTE_START_TIME); + } + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestMappingInfoFilter.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestMappingInfoFilter.java new file mode 100644 index 0000000..5633337 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestMappingInfoFilter.java @@ -0,0 +1,44 @@ +package cn.axzo.framework.web.servlet.filter; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.Ordered; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:03 + **/ +@RequiredArgsConstructor +public class RequestMappingInfoFilter extends OncePerRequestFilter implements Ordered { + + private final Set producibleMediaTypes; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + if (producibleMediaTypes != null && !producibleMediaTypes.isEmpty()) { + request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producibleMediaTypes); + } + filterChain.doFilter(request, response); + } finally { + request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + } + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestReplaceFilter.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestReplaceFilter.java new file mode 100644 index 0000000..592e914 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/filter/RequestReplaceFilter.java @@ -0,0 +1,25 @@ +package cn.axzo.framework.web.servlet.filter; + +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @Description 替换Request对象 + * @Author liyong.tian + * @Date 2020/9/9 17:05 + **/ +public class RequestReplaceFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!(request instanceof CustomServletRequestWrapper)) { + request = new CustomServletRequestWrapper(request); + } + filterChain.doFilter(request, response); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/HttpLogInterceptor.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/HttpLogInterceptor.java new file mode 100644 index 0000000..0cb55fc --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/HttpLogInterceptor.java @@ -0,0 +1,76 @@ +package cn.axzo.framework.web.servlet.interceptor; + +import cn.axzo.framework.core.net.FilterUtil; +import cn.axzo.framework.domain.http.HttpHeaderUtil; +import cn.axzo.framework.domain.http.HttpLogFormatter; +import cn.axzo.framework.domain.http.HttpRequestLog; +import cn.axzo.framework.web.servlet.interceptor.config.InterceptorProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static cn.axzo.framework.core.Constants.API_MARKER; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @Description 只有对请求对象可重复读的二次封装, 才能应用该拦截器 + * @Author liyong.tian + * @Date 2020/9/9 17:07 + **/ +@Slf4j +@RequiredArgsConstructor +public class HttpLogInterceptor extends HandlerInterceptorAdapter { + + private final InterceptorProperties properties; + + private final HttpLogFormatter formatter; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + //校验 + String requestURI = request.getRequestURI(); + if (FilterUtil.matchFiltersURL(requestURI, properties.getDenyPatterns())) { + return true; + } + + //拼接日志 + HttpRequestLog.HttpRequestLogBuilder logBuilder = HttpRequestLog.builder(); + // protocol + logBuilder.protocol(request.getProtocol()); + + // method + logBuilder.method(request.getMethod()); + + // url + String query = request.getQueryString(); + String url = request.getRequestURL().toString(); + logBuilder.url(query == null ? url : url + "?" + query); + + // headers + Enumeration headers = request.getHeaderNames(); + List requestHeaders = new ArrayList<>(); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + requestHeaders.add(header + ": " + request.getHeader(header)); + } + logBuilder.headers(requestHeaders); + + // body + if (!HttpHeaderUtil.isMultipartRequest(requestHeaders)) { + String body = IOUtils.toString(request.getInputStream(), UTF_8); + if (StringUtils.hasText(body)) { + logBuilder.body(body); + } + } + log.info(API_MARKER, formatter.format(logBuilder.build())); + return true; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/InterceptorRegistry.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/InterceptorRegistry.java new file mode 100644 index 0000000..bd2bdd8 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/InterceptorRegistry.java @@ -0,0 +1,15 @@ +package cn.axzo.framework.web.servlet.interceptor; + +import java.util.List; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:09 + **/ +public class InterceptorRegistry extends org.springframework.web.servlet.config.annotation.InterceptorRegistry { + @Override + public List getInterceptors() { + return super.getInterceptors(); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/config/InterceptorProperties.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/config/InterceptorProperties.java new file mode 100644 index 0000000..e044b74 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/interceptor/config/InterceptorProperties.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.web.servlet.interceptor.config; + +import lombok.Data; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:08 + **/ +@Data +public class InterceptorProperties { + + /** + * Define uris that not applied to Interceptor + */ + private String[] denyPatterns = {}; +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodParameter.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodParameter.java new file mode 100644 index 0000000..80068a2 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodParameter.java @@ -0,0 +1,53 @@ +package cn.axzo.framework.web.servlet.method; + +import cn.axzo.framework.web.http.ApiCoreEntity; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; + +import java.lang.reflect.Type; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:13 + **/ +public class ApiCoreEntityMethodParameter extends MethodParameter { + + private final Object returnValue; + + private final ResolvableType returnType; + + public ApiCoreEntityMethodParameter(MethodParameter parameter, ApiCoreEntity returnValue) { + super(parameter); + this.returnValue = returnValue.getBody(); + ResolvableType generic = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0); + this.returnType = ResolvableType.forClassWithGenerics(returnValue.getBody().getClass(), generic); + } + + public ApiCoreEntityMethodParameter(ApiCoreEntityMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + this.returnType = original.returnType; + } + + @Override + public Class getParameterType() { + if (this.returnValue != null) { + return this.returnValue.getClass(); + } + if (!ResolvableType.NONE.equals(this.returnType)) { + return this.returnType.resolve(Object.class); + } + return super.getParameterType(); + } + + @Override + public Type getGenericParameterType() { + return this.returnType.getType(); + } + + @Override + public MethodParameter clone() { + return new ApiCoreEntityMethodParameter(this); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodProcessor.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodProcessor.java new file mode 100644 index 0000000..4f9c2fd --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/method/ApiCoreEntityMethodProcessor.java @@ -0,0 +1,47 @@ +package cn.axzo.framework.web.servlet.method; + +import cn.axzo.framework.web.http.ApiCoreEntity; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor; + +import javax.servlet.http.HttpServletResponse; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:16 + **/ +public class ApiCoreEntityMethodProcessor implements HandlerMethodReturnValueHandler { + + private final HttpEntityMethodProcessor httpEntityMethodProcessor; + + public ApiCoreEntityMethodProcessor(HttpEntityMethodProcessor httpEntityMethodProcessor) { + this.httpEntityMethodProcessor = httpEntityMethodProcessor; + } + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return ApiCoreEntity.class.isAssignableFrom(returnType.getParameterType()); + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + if (returnValue == null) { + return; + } + + // 设置http status + Assert.isInstanceOf(ApiCoreEntity.class, returnValue); + ApiCoreEntity apiCoreEntity = (ApiCoreEntity) returnValue; + webRequest.getNativeResponse(HttpServletResponse.class).setStatus(apiCoreEntity.getStatusCodeValue()); + + //重写返回类型 + returnType = new ApiCoreEntityMethodParameter(returnType, apiCoreEntity); + httpEntityMethodProcessor.handleReturnValue(returnValue, returnType, mavContainer, webRequest); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/rest/PaginationUtil.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/rest/PaginationUtil.java new file mode 100644 index 0000000..8422658 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/rest/PaginationUtil.java @@ -0,0 +1,47 @@ +package cn.axzo.framework.web.servlet.rest; + +import cn.axzo.framework.domain.page.Page; +import cn.axzo.framework.domain.page.Pageable; +import lombok.experimental.UtilityClass; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Utility class for handling pagination. + *

+ *

+ * Pagination uses the same principles as the GitHub API, + * and follow RFC 5988 (Link header). + */ +@UtilityClass +public final class PaginationUtil { + + public HttpHeaders generatePaginationHttpHeaders(Page page, String baseUrl) { + Pageable current = page.current(); + HttpHeaders headers = new HttpHeaders(); + if (page.getTotal() != null) { + headers.add("X-Total-Count", Long.toString(page.getTotal())); + } + String link = ""; + // next link + if (page.getLastPageNumber() != null && current.getPageNumber() < page.getLastPageNumber()) { + link = "<" + generateUri(baseUrl, current.getPageNumber() + 1, current.getPageSize()) + ">; rel=\"next\","; + } + // prev link + if ((current.getPageNumber()) > 1) { + link += "<" + generateUri(baseUrl, current.getPageNumber() - 1, current.getPageSize()) + ">; rel=\"prev\","; + } + // last link + if (page.getLastPageNumber() != null) { + link += "<" + generateUri(baseUrl, page.getLastPageNumber(), current.getPageSize()) + ">; rel=\"last\","; + } + // first link + link += "<" + generateUri(baseUrl, 1, current.getPageSize()) + ">; rel=\"first\""; + headers.add(HttpHeaders.LINK, link); + return headers; + } + + private String generateUri(String baseUrl, int page, int size) { + return UriComponentsBuilder.fromUriString(baseUrl).queryParam("page", page).queryParam("size", size).toUriString(); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/NoneRequestMatcher.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/NoneRequestMatcher.java new file mode 100644 index 0000000..6457d0f --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/NoneRequestMatcher.java @@ -0,0 +1,20 @@ +package cn.axzo.framework.web.servlet.security; + +import org.springframework.security.web.util.matcher.RequestMatcher; + +import javax.servlet.http.HttpServletRequest; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:24 + **/ +public enum NoneRequestMatcher implements RequestMatcher { + + INSTANCE; + + @Override + public boolean matches(HttpServletRequest httpServletRequest) { + return false; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/CustomOAuth2ExceptionRenderer.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/CustomOAuth2ExceptionRenderer.java new file mode 100644 index 0000000..4c2d4b6 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/CustomOAuth2ExceptionRenderer.java @@ -0,0 +1,184 @@ +package cn.axzo.framework.web.servlet.security.oauth; + +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.web.http.ApiCoreEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.oauth2.provider.error.OAuth2ExceptionRenderer; +import org.springframework.util.CollectionUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.*; + +import static org.springframework.http.MediaType.SPECIFICITY_COMPARATOR; +import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + +/** + * 自定义OAuth2异常渲染 + * + * @author liyong.tian + * @since 2019/4/16 15:24 + */ +@Slf4j +public class CustomOAuth2ExceptionRenderer implements OAuth2ExceptionRenderer { + + private final List> messageConverters; + + private final List allSupportedMediaTypes; + + public CustomOAuth2ExceptionRenderer(List> messageConverters) { + this.messageConverters = messageConverters; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters); + } + + @Override + public void handleHttpEntityResponse(HttpEntity httpEntity, ServletWebRequest webRequest) throws Exception { + if (httpEntity != null) { + return; + } + HttpServletRequest request = webRequest.getRequest(); + HttpInputMessage inputMessage = createHttpInputMessage(webRequest); + HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest); + if (outputMessage instanceof ServerHttpResponse) { + if (httpEntity instanceof ResponseEntity) { + ((ServerHttpResponse) outputMessage).setStatusCode(((ResponseEntity) httpEntity).getStatusCode()); + } else if (httpEntity instanceof ApiCoreEntity) { + ((ServerHttpResponse) outputMessage).setStatusCode(((ApiCoreEntity) httpEntity).getStatusCode()); + } + } + HttpHeaders entityHeaders = httpEntity.getHeaders(); + if (!entityHeaders.isEmpty()) { + outputMessage.getHeaders().putAll(entityHeaders); + } + Object body = httpEntity.getBody(); + if (body != null) { + writeWithMessageConverters(body, request, inputMessage, outputMessage); + } else { + outputMessage.getBody(); + } + } + + private void writeWithMessageConverters(@NotNull Object value, + HttpServletRequest request, + HttpInputMessage inputMessage, + HttpOutputMessage outputMessage) throws IOException, HttpMediaTypeNotAcceptableException { + Class valueType; + Type declaredType; + if (value instanceof CharSequence) { + valueType = String.class; + declaredType = String.class; + } else { + valueType = value.getClass(); + declaredType = value.getClass(); + } + List requestedMediaTypes = getAcceptableMediaTypes(inputMessage); + List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); + + if (producibleMediaTypes.isEmpty()) { + throw new IllegalArgumentException("No converter found for return value of type: " + valueType); + } + + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType requestedType : requestedMediaTypes) { + for (MediaType producibleType : producibleMediaTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + if (compatibleMediaTypes.isEmpty()) { + throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); + } + + List mediaTypes = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(mediaTypes); + for (MediaType mediaType : mediaTypes) { + mediaType = mediaType.removeQualityValue(); + for (HttpMessageConverter messageConverter : messageConverters) { + if (messageConverter.canWrite(valueType, mediaType)) { + messageConverter.write(ClassUtil.cast(value), mediaType, outputMessage); + if (log.isDebugEnabled()) { + MediaType contentType = outputMessage.getHeaders().getContentType(); + if (contentType == null) { + contentType = mediaType; + } + log.debug("Written [" + value + "] as \"" + contentType + "\" using [" + messageConverter + "]"); + } + return; + } + } + } + throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes); + } + + private List getAcceptableMediaTypes(HttpInputMessage inputMessage) { + List mediaTypes = inputMessage.getHeaders().getAccept(); + return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); + } + + /** + * Return the more specific of the acceptable and the producible media types + * with the q-value of the former. + */ + private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { + MediaType produceTypeToUse = produceType.copyQualityValue(acceptType); + return (SPECIFICITY_COMPARATOR.compare(acceptType, produceTypeToUse) <= 0 ? acceptType : produceTypeToUse); + } + + @SuppressWarnings("unchecked") + private List getProducibleMediaTypes(HttpServletRequest request, Class valueClass, Type declaredType) { + Set mediaTypes = (Set) request.getAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } else if (!this.allSupportedMediaTypes.isEmpty()) { + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && declaredType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(declaredType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes()); + } + } else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes()); + } + } + return result; + } else { + return Collections.singletonList(MediaType.ALL); + } + } + /** + * Return the media types supported by all provided message converters sorted + * by specificity via {@link MediaType#sortBySpecificity(List)}. + */ + private static List getAllSupportedMediaTypes(List> messageConverters) { + Set allSupportedMediaTypes = new LinkedHashSet<>(); + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + } + List result = new ArrayList<>(allSupportedMediaTypes); + MediaType.sortBySpecificity(result); + return Collections.unmodifiableList(result); + } + + private HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + return new ServletServerHttpRequest(servletRequest); + } + + private HttpOutputMessage createHttpOutputMessage(NativeWebRequest webRequest) { + HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse(); + return new ServletServerHttpResponse(servletResponse); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/OAuth2Util.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/OAuth2Util.java new file mode 100644 index 0000000..f1f7612 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/OAuth2Util.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.web.servlet.security.oauth; + +import lombok.experimental.UtilityClass; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 17:25 + **/ +@UtilityClass +public class OAuth2Util { + + public String getMessage(OAuth2Exception exception) { + return exception.getOAuth2ErrorCode() + " : " + exception.getMessage(); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/SpringOAuth2AuthenticationEntryPoint.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/SpringOAuth2AuthenticationEntryPoint.java new file mode 100644 index 0000000..63f1d05 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/SpringOAuth2AuthenticationEntryPoint.java @@ -0,0 +1,73 @@ +package cn.axzo.framework.web.servlet.security.oauth; + +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.web.http.ApiEntity; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; +import org.springframework.security.oauth2.provider.error.OAuth2ExceptionRenderer; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @author liyong.tian + * @since 2019/4/16 16:39 + */ +public class SpringOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint { + + private final WebResponseExceptionTranslator exceptionTranslator; + + private final OAuth2ExceptionRenderer exceptionRenderer; + + // This is from Spring MVC. + private final HandlerExceptionResolver handlerExceptionResolver; + + public SpringOAuth2AuthenticationEntryPoint(List> messageConverters, + WebResponseExceptionTranslator exceptionTranslator, + HandlerExceptionResolver handlerExceptionResolver) { + this.exceptionRenderer = new CustomOAuth2ExceptionRenderer(messageConverters); + this.exceptionTranslator = exceptionTranslator; + this.handlerExceptionResolver = handlerExceptionResolver; + } + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + try { + ResponseEntity result = exceptionTranslator.translate(authException); + result = (ResponseEntity) enhanceResponse(result, authException); + HttpEntity httpEntity = beforeBodyWrite(result); + exceptionRenderer.handleHttpEntityResponse(httpEntity, new ServletWebRequest(request, response)); + response.flushBuffer(); + } catch (ServletException e) { + // Re-use some of the default Spring dispatcher behaviour - the exception came from the filter chain and + // not from an MVC handler so it won't be caught by the dispatcher (even if there is one) + if (handlerExceptionResolver.resolveException(request, response, this, e) == null) { + throw e; + } + } catch (IOException | RuntimeException e) { + throw e; + } catch (Exception e) { + // Wrap other Exceptions. These are not expected to happen + throw new RuntimeException(e); + } + } + + private HttpEntity beforeBodyWrite(ResponseEntity responseEntity) { + OAuth2Exception e = responseEntity.getBody(); + return ApiEntity.status(responseEntity.getStatusCode()) + .headers(responseEntity.getHeaders()) + .err(BaseCode.parse(e.getHttpErrorCode()), OAuth2Util.getMessage(e)); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/resource/ResourceSecurityCustomConfigurer.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/resource/ResourceSecurityCustomConfigurer.java new file mode 100644 index 0000000..d1bdbd6 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/security/oauth/resource/ResourceSecurityCustomConfigurer.java @@ -0,0 +1,40 @@ +package cn.axzo.framework.web.servlet.security.oauth.resource; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.Ordered; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.web.AuthenticationEntryPoint; + +/** + * @author liyong.tian + * @since 2019/4/16 16:40 + */ +@RequiredArgsConstructor +public class ResourceSecurityCustomConfigurer extends ResourceServerConfigurerAdapter implements Ordered { + + private final String resourceId; + + private final AuthenticationEntryPoint authenticationEntryPoint; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.authenticationEntryPoint(authenticationEntryPoint) + .resourceId(resourceId); + } + + @Override + @SuppressWarnings("unchecked") + public void configure(HttpSecurity http) throws Exception { + if (http.getConfigurer(ExpressionUrlAuthorizationConfigurer.class) == null) { + http.authorizeRequests().antMatchers().authenticated(); + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDataTypeReader.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDataTypeReader.java new file mode 100644 index 0000000..4ea6114 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDataTypeReader.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.web.servlet.swagger; + +import com.fasterxml.classmate.ResolvedType; +import javax.servlet.http.Part; +import org.springframework.core.Ordered; +import org.springframework.web.bind.annotation.RequestPart; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.ParameterBuilderPlugin; +import springfox.documentation.spi.service.contexts.ParameterContext; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/9 下午2:41 + */ +public class CustomParameterDataTypeReader implements ParameterBuilderPlugin, Ordered { + @Override + public void apply(ParameterContext context) { + ResolvedMethodParameter methodParameter = context.resolvedMethodParameter(); + + if (methodParameter.hasParameterAnnotation(RequestPart.class)) { + ResolvedType parameterType = methodParameter.getParameterType(); + parameterType = context.alternateFor(parameterType); + if (parameterType.isInstanceOf(Part.class) || parameterType.isInstanceOf(byte[].class)) { + context.parameterBuilder().modelRef(new ModelRef("__file")); + } + // TODO: 2018/3/13 part数组 + } + } + + @Override + public boolean supports(DocumentationType delimiter) { + return true; + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDefaultReader.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDefaultReader.java new file mode 100644 index 0000000..a5c5b2f --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/CustomParameterDefaultReader.java @@ -0,0 +1,41 @@ +package cn.axzo.framework.web.servlet.swagger; + +import lombok.val; +import org.springframework.core.Ordered; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.ParameterContext; +import springfox.documentation.spring.web.DescriptionResolver; +import springfox.documentation.spring.web.readers.parameter.ParameterDefaultReader; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/17 下午8:55 + */ +public class CustomParameterDefaultReader extends ParameterDefaultReader implements Ordered { + + public CustomParameterDefaultReader(DescriptionResolver descriptions) { + super(descriptions); + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE + 1; + } + + @Override + public void apply(ParameterContext context) { + val resolvedMethodParameter = context.resolvedMethodParameter(); + Class erasedType = resolvedMethodParameter.getParameterType().getErasedType(); + if (erasedType.equals(Boolean.class) || erasedType.equals(Boolean.TYPE)) { + context.parameterBuilder().defaultValue("false"); + } + super.apply(context); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return true; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumExpandedParameterBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumExpandedParameterBuilderPlugin.java new file mode 100644 index 0000000..327c5ef --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumExpandedParameterBuilderPlugin.java @@ -0,0 +1,117 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static com.google.common.base.Optional.fromNullable; +import static java.lang.String.format; +import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; +import static springfox.documentation.schema.Types.typeNameFor; +import static springfox.documentation.swagger.annotations.Annotations.findApiParamAnnotation; +import static springfox.documentation.swagger.common.SwaggerPluginSupport.pluginDoesApply; +import static springfox.documentation.swagger.schema.ApiModelProperties.findApiModePropertyAnnotation; + +import cn.axzo.framework.core.enums.Enums; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.enums.ReservedEnum; +import com.google.common.collect.Lists; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.val; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.ExpandedParameterBuilderPlugin; +import springfox.documentation.spi.service.contexts.ParameterExpansionContext; + +/** + * 枚举参数格式化 - 对key=value类型的有效 + * Created by liyong.tian on 16/6/5. + */ +@SuppressWarnings("all") +public class EnumExpandedParameterBuilderPlugin implements ExpandedParameterBuilderPlugin { + + @Override + public void apply(ParameterExpansionContext context) { + boolean isArray = false; + Field field = context.getField().getRawMember(); + Class fieldType = field.getType(); + if (List.class.isAssignableFrom(fieldType)) { + ParameterizedType type = (ParameterizedType) field.getGenericType(); + fieldType = ((Class) type.getActualTypeArguments()[0]); + isArray = true; + } + if (fieldType.isArray()) { + fieldType = fieldType.getComponentType(); + isArray = true; + } + ReservedEnum reservedEnum = findReservedEnumAnnotation(field); + boolean reserved = reservedEnum != null; + if (reserved) { + Class using = reservedEnum.using(); + if (ICode.class.isAssignableFrom(using) && Integer.class.isAssignableFrom(fieldType)) { + fieldType = using; + } else if (IStringCode.class.isAssignableFrom(using) && String.class.isAssignableFrom(fieldType)) { + fieldType = using; + } + } + if (!fieldType.isEnum()) { + return; + } + if (!(ICode.class.isAssignableFrom(fieldType) || IStringCode.class.isAssignableFrom(fieldType))) { + return; + } + + Map, Object> enumCodeMap = Enums.getEnumAndValue(fieldType, "code"); + Map, Object> enumNameMap = Enums.getEnumAndValue(fieldType, "name"); + List descValues = Lists.newArrayList(); + enumCodeMap.forEach((anEnum, o) -> descValues.add(format("%s-%s", o.toString(), enumNameMap.get(anEnum)))); + + ModelRef modelRef; + if (ICode.class.isAssignableFrom(fieldType)) { + if (isArray) { + modelRef = new ModelRef("array", new ModelRef(typeNameFor(Integer.class))); + } else { + modelRef = new ModelRef(typeNameFor(Integer.class)); + } + } else { + if (isArray) { + modelRef = new ModelRef("array", new ModelRef(typeNameFor(String.class))); + } else { + modelRef = new ModelRef(typeNameFor(String.class)); + } + } + + val builder = context.getParameterBuilder(); + builder.modelRef(modelRef) + .allowableValues(reserved ? null : EnumStdPluginUtil.getAllowableValues(fieldType)); + if (isArray) { + builder.allowMultiple(true); + } + + String fieldDesc = ""; + Optional apiModelPropertyOptional = findApiModePropertyAnnotation(field); + if (apiModelPropertyOptional.isPresent()) { + fieldDesc = apiModelPropertyOptional.get().value(); + } + Optional apiParamOptional = findApiParamAnnotation(field); + if (apiParamOptional.isPresent()) { + fieldDesc = apiParamOptional.get().value(); + } + builder.description(EnumStdPluginUtil.getFullDesc(fieldDesc, fieldType, reserved)); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return pluginDoesApply(delimiter); + } + + @Nullable + public static ReservedEnum findReservedEnumAnnotation(AnnotatedElement annotated) { + return fromNullable(getAnnotation(annotated, ReservedEnum.class)).orNull(); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumFormatModelBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumFormatModelBuilderPlugin.java new file mode 100644 index 0000000..284c8da --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumFormatModelBuilderPlugin.java @@ -0,0 +1,91 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static springfox.documentation.swagger.common.SwaggerPluginSupport.pluginDoesApply; + +import cn.axzo.framework.core.IName; +import cn.axzo.framework.core.util.ClassUtil; +import cn.axzo.framework.jackson.datatype.enumstd.EnumFormat; +import com.fasterxml.classmate.TypeResolver; +import com.fasterxml.classmate.members.RawField; +import com.google.common.collect.ImmutableMap; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.util.ClassUtils; +import springfox.documentation.builders.ModelPropertyBuilder; +import springfox.documentation.schema.Model; +import springfox.documentation.schema.ResolvedTypes; +import springfox.documentation.schema.TypeNameExtractor; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.schema.ModelBuilderPlugin; +import springfox.documentation.spi.schema.contexts.ModelContext; + +/** + * @author liyong.tian + * @since 2018/2/1 上午1:46 + */ +@RequiredArgsConstructor +public class EnumFormatModelBuilderPlugin implements ModelBuilderPlugin, BeanClassLoaderAware { + + private final TypeResolver typeResolver; + + private final TypeNameExtractor typeNameExtractor; + + private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + + @Override + public void apply(ModelContext context) { + if (!context.isReturnType()) { + return; + } + Model model = context.getBuilder().build(); + List fields = model.getType().getMemberFields() + .stream() + .map(RawField::getRawMember) + .collect(Collectors.toList()); + model.getProperties().values().stream() + .map(property -> { + val enumClsOptional = ClassUtil.loadIfPresent(property.getQualifiedType(), classLoader) + .filter(Enum.class::isAssignableFrom) + .filter(IName.class::isAssignableFrom); + return enumClsOptional.map(enumCls -> { + val enumFormatOptional = fields.stream() + .filter(f -> f.getName().equals(property.getName())) + .filter(f -> f.isAnnotationPresent(EnumFormat.class)) + .map(f -> f.getAnnotation(EnumFormat.class)) + .filter(enumFormat -> !enumFormat.nested()) + .findFirst(); + return enumFormatOptional.map(enumFormat -> new ModelPropertyBuilder() + .example(property.getExample()) + .extensions(property.getVendorExtensions()) + .isHidden(property.isHidden()) + .name(property.getName() + enumFormat.nameSuffix()) + .pattern(property.getPattern()) + .position(property.getPosition()) + .qualifiedType(String.class.getCanonicalName()) + .readOnly(property.isReadOnly()) + .required(property.isRequired()) + .type(typeResolver.resolve(String.class)) + .build() + .updateModelRef(ResolvedTypes.modelRefFactory(context, null, typeNameExtractor))).orElse(null); + }).orElse(null); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + .forEach(property -> context.getBuilder().properties(ImmutableMap.of(property.getName(), property))); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return pluginDoesApply(delimiter); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumModelPropertyBuilder.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumModelPropertyBuilder.java new file mode 100644 index 0000000..b0ddd0a --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumModelPropertyBuilder.java @@ -0,0 +1,85 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static springfox.documentation.schema.Annotations.findPropertyAnnotation; + +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.enums.ReservedEnum; +import cn.axzo.framework.core.enums.meta.IntCode; +import cn.axzo.framework.core.enums.meta.StringCode; +import cn.axzo.framework.jackson.datatype.enumstd.EnumFormat; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import io.swagger.annotations.ApiModelProperty; +import java.util.Optional; +import springfox.documentation.builders.ModelPropertyBuilder; +import springfox.documentation.spi.schema.contexts.ModelPropertyContext; +import springfox.documentation.spring.web.DescriptionResolver; +import springfox.documentation.swagger.schema.ApiModelPropertyPropertyBuilder; + +@SuppressWarnings("all") +public class EnumModelPropertyBuilder extends ApiModelPropertyPropertyBuilder { + + public EnumModelPropertyBuilder(DescriptionResolver descriptions) { + super(descriptions, null); + } + + @Override + public void apply(ModelPropertyContext context) { + Optional beanPropertyDefinitionOptional = context.getBeanPropertyDefinition(); + if (beanPropertyDefinitionOptional.isPresent()) { + BeanPropertyDefinition beanPropertyDefinition = beanPropertyDefinitionOptional.get(); + Class fieldType = beanPropertyDefinition.getRawPrimaryType(); + if (fieldType == null) { + return; + } + ReservedEnum reservedEnum = findPropertyAnnotation(beanPropertyDefinition, ReservedEnum.class).orElse(null); + final boolean reserved = reservedEnum != null; + if (reserved) { + Class using = reservedEnum.using(); + if (ICode.class.isAssignableFrom(using) && Integer.class.isAssignableFrom(fieldType)) { + fieldType = using; + } else if (IStringCode.class.isAssignableFrom(using) && String.class.isAssignableFrom(fieldType)) { + fieldType = using; + } + } + if (!fieldType.isEnum()) { + return; + } + if (!(ICode.class.isAssignableFrom(fieldType) || IStringCode.class.isAssignableFrom(fieldType))) { + return; + } + + Optional annotation = findPropertyAnnotation(beanPropertyDefinition, ApiModelProperty.class); + Optional enumFormatOptional = findPropertyAnnotation(beanPropertyDefinition, EnumFormat.class); + + // 重写Swagger属性 + ModelPropertyBuilder builder = context.getBuilder(); + if (enumFormatOptional.isPresent() && enumFormatOptional.get().nested()) { + if (ICode.class.isAssignableFrom(fieldType)) { + builder.type(context.getResolver().resolve(IntCode.class)); + } else { + builder.type(context.getResolver().resolve(StringCode.class)); + } + } else { + builder.allowableValues(reserved ? null : EnumStdPluginUtil.getAllowableValues(fieldType)); + if (ICode.class.isAssignableFrom(fieldType)) { + builder.type(context.getResolver().resolve(Integer.class)); + } else { + builder.type(context.getResolver().resolve(String.class)); + } + } + + String description; + if (annotation.isPresent()) { + description = EnumStdPluginUtil.getFullDesc(annotation.get().value(), fieldType, reserved); + } else { + description = EnumStdPluginUtil.getFullDesc("", fieldType, reserved); + } + builder.description(description); + + if (reserved) { + EnumStdPluginUtil.getExampleValue(fieldType).ifPresent(value -> builder.example(value)); + } + } + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdOperationBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdOperationBuilderPlugin.java new file mode 100644 index 0000000..9ff1171 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdOperationBuilderPlugin.java @@ -0,0 +1,214 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static java.lang.String.format; +import static springfox.documentation.schema.Types.typeNameFor; + +import cn.axzo.framework.core.InternalException; +import cn.axzo.framework.core.enums.Enums; +import cn.axzo.framework.core.enums.ICode; +import cn.axzo.framework.core.enums.IStringCode; +import cn.axzo.framework.core.enums.ReservedEnum; +import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.classmate.TypeResolver; +import com.google.common.collect.Lists; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiParam; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import jodd.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.core.Ordered; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ValueConstants; +import springfox.documentation.builders.OperationBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.Parameter; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.spi.service.contexts.ParameterContext; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +/** + * @author liyong.tian + * @since 2017/11/24 下午3:36 + */ +@Slf4j +public class EnumStdOperationBuilderPlugin implements OperationBuilderPlugin, Ordered { + + private final TypeResolver typeResolver; + + public EnumStdOperationBuilderPlugin(TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + @Override + public void apply(OperationContext context) { + List parameters = getExistingParameters(context); + AtomicBoolean changed = new AtomicBoolean(false); + for (ResolvedMethodParameter methodParameter : context.getParameters()) { + try { + rewriteParameter(context, methodParameter).ifPresent(parameter -> { + int index = methodParameter.getParameterIndex(); + parameters.set(index, parameter); + changed.compareAndSet(false, true); + }); + } catch (Exception e) { + log.error("Enum resolved error", e); + throw new InternalException(e); + } + } + if (changed.get() && !parameters.isEmpty()) { + context.operationBuilder().parameters(parameters); + } + } + + private Optional rewriteParameter(OperationContext context, ResolvedMethodParameter methodParameter) { + ResolvedType resolvedType = methodParameter.getParameterType(); + if (resolvedType == null) { + return Optional.empty(); + } + + boolean isArray = false; + Class fieldType = resolvedType.getErasedType(); + if (List.class.isAssignableFrom(fieldType) && !resolvedType.getTypeParameters().isEmpty()) { + fieldType = resolvedType.getTypeParameters().get(0).getErasedType(); + isArray = true; + } + if (fieldType.isArray()) { + fieldType = resolvedType.getArrayElementType().getErasedType(); + isArray = true; + } + ReservedEnum reservedEnum = methodParameter.findAnnotation(ReservedEnum.class).orElse(null); + boolean reserved = reservedEnum != null; + if (reserved) { + Class using = reservedEnum.using(); + if (ICode.class.isAssignableFrom(using) && Integer.class.isAssignableFrom(fieldType)) { + fieldType = using; + } else if (IStringCode.class.isAssignableFrom(using) && String.class.isAssignableFrom(fieldType)) { + fieldType = using; + } + } + if (!fieldType.isEnum()) { + return Optional.empty(); + } + if (!(ICode.class.isAssignableFrom(fieldType) || IStringCode.class.isAssignableFrom(fieldType))) { + return Optional.empty(); + } + + Map, Object> enumCodeMap = Enums.getEnumAndValue(fieldType, "code"); + Map, Object> enumNameMap = Enums.getEnumAndValue(fieldType, "name"); + List descValues = Lists.newArrayList(); + enumCodeMap.forEach((anEnum, o) -> descValues.add(format("%s-%s", o.toString(), enumNameMap.get(anEnum)))); + + ModelRef modelRef; + ResolvedType type; + if (ICode.class.isAssignableFrom(fieldType)) { + if (isArray) { + modelRef = new ModelRef("array", new ModelRef(typeNameFor(Integer.class))); + } else { + modelRef = new ModelRef(typeNameFor(Integer.class)); + } + type = typeResolver.resolve(Integer.class); + } else { + if (isArray) { + modelRef = new ModelRef("array", new ModelRef(typeNameFor(String.class))); + } else { + modelRef = new ModelRef(typeNameFor(String.class)); + } + type = typeResolver.resolve(String.class); + } + + ApiModelProperty apiModelProperty = methodParameter.findAnnotation(ApiModelProperty.class).orElse(null); + ApiParam apiParam = methodParameter.findAnnotation(ApiParam.class).orElse(null); + RequestParam requestParam = methodParameter.findAnnotation(RequestParam.class).orElse(null); + PathVariable pathVariable = methodParameter.findAnnotation(PathVariable.class).orElse(null); + RequestPart requestPart = methodParameter.findAnnotation(RequestPart.class).orElse(null); + + String fieldDesc = ""; + if (apiModelProperty != null) { + fieldDesc = apiModelProperty.value(); + } + if (apiParam != null) { + fieldDesc = apiParam.value(); + } + String description = EnumStdPluginUtil.getFullDesc(fieldDesc, fieldType, reserved); + + String name = null; + if (requestParam != null && StringUtil.isNotBlank(requestParam.value())) { + name = requestParam.value(); + } + if (requestPart != null && StringUtil.isNotBlank(requestPart.value())) { + name = requestPart.value(); + } + if (name == null) { + name = methodParameter.defaultName().orElse(null); + } + + String defaultValue = null; + if (requestParam != null && !Objects.equals(requestParam.defaultValue(), ValueConstants.DEFAULT_NONE)) { + defaultValue = requestParam.defaultValue(); + } + + ParameterContext parameterContext = new ParameterContext(methodParameter, + context.getDocumentationContext(), + context.getGenericsNamingStrategy(), + context, + 0); + String parameterType = FixParameterTypeReader.findParameterType(parameterContext); + + boolean required = false; + if (requestParam != null && requestParam.required()) { + required = true; + } + if (pathVariable != null && pathVariable.required()) { + required = true; + } + if (requestPart != null && requestPart.required()) { + required = true; + } + + val parameter = new ParameterBuilder() + .parameterType(parameterType) + .name(name) + .defaultValue(defaultValue) + .required(required) + .type(type) + .allowableValues(reserved ? null : EnumStdPluginUtil.getAllowableValues(fieldType)) + .modelRef(modelRef) + .description(description) + .allowMultiple(isArray) + .build(); + return Optional.of(parameter); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return SwaggerPluginSupport.pluginDoesApply(delimiter); + } + + @Override + public int getOrder() { + return SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1; + } + + @SuppressWarnings("unchecked") + private List getExistingParameters(OperationContext context) { + try { + Field parametersField = OperationBuilder.class.getDeclaredField("parameters"); + parametersField.setAccessible(true); + return (List) parametersField.get(context.operationBuilder()); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new InternalException(e); + } + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdPluginUtil.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdPluginUtil.java new file mode 100644 index 0000000..c676dad --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/EnumStdPluginUtil.java @@ -0,0 +1,56 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; +import static org.jooq.lambda.Seq.seq; + +import cn.axzo.framework.core.enums.Enums; +import com.google.common.collect.Lists; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; +import springfox.documentation.service.AllowableListValues; + +/** + * @Description + * @Author liyong.tian + * @Date 2020/9/9 11:44 + **/ +@UtilityClass +public class EnumStdPluginUtil { + + public String getFullDesc(String propertyDesc, Class enumCls, boolean reserved) { + Map, Object> enumCodeMap = Enums.getEnumAndValue(enumCls, "code"); + Map, Object> enumNameMap = Enums.getEnumAndValue(enumCls, "name"); + + List descValues = Lists.newArrayList(); + enumCodeMap.forEach((anEnum, o) -> descValues.add(format("%s-%s", o.toString(), enumNameMap.get(anEnum)))); + + String whiteSpace = "        "; + String enumsDesc = seq(descValues).toString("
" + whiteSpace); + return propertyDesc + + (reserved ? StringUtils.repeat(" ", 100) : "") + + " " + + (reserved ? "<系统预设>" : "") + + "[
" + + whiteSpace + + enumsDesc + + "
]"; + } + + public AllowableListValues getAllowableValues(Class enumCls) { + Map, Object> enumCodeMap = Enums.getEnumAndValue(enumCls, "code"); + List codeValues = enumCodeMap.values().stream().map(Object::toString).collect(toList()); + return new AllowableListValues(codeValues, "LIST"); + } + + public Optional getExampleValue(Class enumCls) { + AllowableListValues values = getAllowableValues(enumCls); + if (values.getValues() != null && !values.getValues().isEmpty()) { + return Optional.of(values.getValues().get(0)); + } + return Optional.empty(); + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FieldsParameterOperationBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FieldsParameterOperationBuilderPlugin.java new file mode 100644 index 0000000..fbd1687 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FieldsParameterOperationBuilderPlugin.java @@ -0,0 +1,53 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static springfox.documentation.schema.Types.typeNameFor; + +import com.google.common.collect.Lists; +import org.springframework.core.Ordered; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/10 下午4:19 + */ +public class FieldsParameterOperationBuilderPlugin implements OperationBuilderPlugin, Ordered { + + private final boolean enabled; + + public FieldsParameterOperationBuilderPlugin(boolean enabled) { + this.enabled = enabled; + } + + @Override + public void apply(OperationContext context) { + if (!enabled) { + return; + } + Parameter traceParameter = new ParameterBuilder() + .parameterType("query") + .name("fields") + .modelRef(new ModelRef("LIST", new ModelRef(typeNameFor(String.class)))) + .allowMultiple(false) + .description("指定data里的响应字段(针对内嵌对象或数组里的字段用.隔开)") + .build(); + context.operationBuilder().parameters(Lists.newArrayList(traceParameter)); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return SwaggerPluginSupport.pluginDoesApply(delimiter); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixApiModelPropertyPropertyBuilder.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixApiModelPropertyPropertyBuilder.java new file mode 100644 index 0000000..6040378 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixApiModelPropertyPropertyBuilder.java @@ -0,0 +1,31 @@ +package cn.axzo.framework.web.servlet.swagger; + +import org.springframework.core.Ordered; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin; +import springfox.documentation.spi.schema.contexts.ModelPropertyContext; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/12 下午5:28 + */ +public class FixApiModelPropertyPropertyBuilder implements ModelPropertyBuilderPlugin, Ordered { + + @Override + public void apply(ModelPropertyContext context) { + context.getBuilder().allowEmptyValue(null); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return SwaggerPluginSupport.pluginDoesApply(delimiter); + } + + @Override + public int getOrder() { + return SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixParameterTypeReader.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixParameterTypeReader.java new file mode 100644 index 0000000..8721a4d --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixParameterTypeReader.java @@ -0,0 +1,43 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static springfox.documentation.swagger.common.SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER; + +import org.springframework.core.Ordered; +import org.springframework.web.bind.annotation.RequestPart; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.ParameterBuilderPlugin; +import springfox.documentation.spi.service.contexts.ParameterContext; +import springfox.documentation.spring.web.readers.parameter.ParameterTypeReader; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/17 下午8:17 + */ +public class FixParameterTypeReader implements ParameterBuilderPlugin, Ordered { + + @Override + public void apply(ParameterContext context) { + context.parameterBuilder().parameterType(findParameterType(context)); + } + + public static String findParameterType(ParameterContext context) { + ResolvedMethodParameter methodParameter = context.resolvedMethodParameter(); + if (methodParameter.hasParameterAnnotation(RequestPart.class)) { + return "form"; + } + return ParameterTypeReader.findParameterType(context); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return true; + } + + @Override + public int getOrder() { + return SWAGGER_PLUGIN_ORDER + 1000; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixedParameterRequiredReader.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixedParameterRequiredReader.java new file mode 100644 index 0000000..5b5b622 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/FixedParameterRequiredReader.java @@ -0,0 +1,17 @@ +package cn.axzo.framework.web.servlet.swagger; + +import org.springframework.core.Ordered; +import springfox.documentation.spring.web.DescriptionResolver; +import springfox.documentation.spring.web.readers.parameter.ParameterRequiredReader; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +public class FixedParameterRequiredReader extends ParameterRequiredReader implements Ordered { + public FixedParameterRequiredReader(DescriptionResolver descriptions) { + super(descriptions); + } + + @Override + public int getOrder() { + return SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/PageableOperationBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/PageableOperationBuilderPlugin.java new file mode 100644 index 0000000..f079223 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/PageableOperationBuilderPlugin.java @@ -0,0 +1,254 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static cn.axzo.framework.domain.page.PageUtil.getFirstPageNumber; +import static com.google.common.base.Joiner.on; +import static jodd.util.StringUtil.isNotBlank; +import static springfox.documentation.schema.Types.typeNameFor; + +import cn.axzo.framework.domain.page.PageDefaults; +import cn.axzo.framework.domain.page.Pageable; +import cn.axzo.framework.domain.page.PageableVerbose; +import cn.axzo.framework.domain.sort.SortLimit; +import cn.axzo.framework.web.page.RestPageProperties; +import cn.axzo.framework.web.servlet.swagger.annotations.ApiPageableParam; +import cn.axzo.framework.web.servlet.swagger.annotations.ApiSortParam; +import cn.axzo.framework.web.servlet.swagger.annotations.ApiSortParams; +import com.fasterxml.classmate.ResolvedType; +import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import jodd.util.ArraysUtil; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.core.Ordered; +import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.data.web.SortDefault.SortDefaults; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.AllowableListValues; +import springfox.documentation.service.Parameter; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +/** + * @author liyong.tian + * @since 2017/10/4 下午3:39 + */ +@RequiredArgsConstructor +public class PageableOperationBuilderPlugin implements OperationBuilderPlugin, Ordered { + + private final RestPageProperties properties; + + @Override + public void apply(OperationContext context) { + for (ResolvedMethodParameter parameter : context.getParameters()) { + ResolvedType resolvedType = parameter.getParameterType(); + if (!Pageable.class.isAssignableFrom(resolvedType.getErasedType())) { + continue; + } + + List parameters = Lists.newArrayList(); + + // page + PageableDefault pageableDefault = parameter.findAnnotation(PageableDefault.class).orElse(null); + int defaultPage = getFirstPageNumber(properties.isOneIndexedParameters()); + if (pageableDefault != null) { + defaultPage = Math.max(defaultPage, pageableDefault.page()); + } + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getPageParameterName()) + .defaultValue(defaultPage + "") + .modelRef(new ModelRef(typeNameFor(Integer.TYPE))) + .description("页码(从" + (properties.isOneIndexedParameters() ? 1 : 0) + "开始)") + .build()); + + // size + int defaultSize = properties.getDefaultPageSize(); + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getSizeParameterName()) + .defaultValue((pageableDefault == null ? defaultSize : pageableDefault.size()) + "") + .modelRef(new ModelRef(typeNameFor(Integer.TYPE))) + .description("每页数量") + .build()); + + // sort + SortDefault sortDefault = parameter.findAnnotation(SortDefault.class).orElse(null); + SortDefaults sortDefaults = parameter.findAnnotation(SortDefaults.class).orElse(null); + SortLimit sortLimit = parameter.findAnnotation(SortLimit.class).orElse(null); + val sortValues = new AllowableListValues(_getAllowableValues(sortLimit), "LIST"); + final ModelRef sortItemModel; + final String defaultSort; + if (sortValues.getValues().isEmpty()) { + sortItemModel = new ModelRef(typeNameFor(String.class)); + defaultSort = null; + } else { + sortItemModel = new ModelRef(typeNameFor(String.class), sortValues); + String sort = _getDefaultSort(pageableDefault, sortDefault, sortDefaults); + if (sort.contains("&")) { + defaultSort = null; + } else { + defaultSort = sort; + } + } + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getSortParameterName()) + .defaultValue(defaultSort) + .modelRef(new ModelRef("LIST", sortItemModel)) + .required(sortLimit != null && sortLimit.required()) + .allowMultiple(false) + .description(_getSortDescription(context)) + .build()); + + // needTotal & needContent & fixEdge & verbose + ApiPageableParam apiPageableParam = context.findAnnotation(ApiPageableParam.class).orElse(null); + if (apiPageableParam != null) { + // needTotal + if (apiPageableParam.needTotalParam() == ShowStatus.VISIBLE) { + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getNeedTotalParameterName()) + .defaultValue(Boolean.toString(PageDefaults.NEED_TOTAL)) + .modelRef(new ModelRef(typeNameFor(Boolean.TYPE))) + .description("是否需要查询总记录数") + .build()); + } + + // needContent + if (apiPageableParam.needContentParam() == ShowStatus.VISIBLE) { + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getNeedContentParameterName()) + .defaultValue(Boolean.toString(PageDefaults.NEED_CONTENT)) + .modelRef(new ModelRef(typeNameFor(Boolean.TYPE))) + .description("是否需要查询记录列表") + .build()); + } + + // fixEdge + if (apiPageableParam.fixEdgeParam() == ShowStatus.VISIBLE) { + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getFixEdgeParameterName()) + .defaultValue(Boolean.toString(properties.isFixEdge())) + .modelRef(new ModelRef(typeNameFor(Boolean.TYPE))) + .description("是否自动矫正分页边界") + .build()); + } + + // verbose + val verboseValues = EnumStdPluginUtil.getAllowableValues(PageableVerbose.class); + val description = EnumStdPluginUtil.getFullDesc("分页冗余选项", PageableVerbose.class, false); + if (apiPageableParam.verboseParam() == ShowStatus.VISIBLE) { + parameters.add(new ParameterBuilder() + .parameterType("query") + .name(properties.getVerboseParameterName()) + .defaultValue(PageDefaults.PAGEABLE_VERBOSE.getCode()) + .modelRef(new ModelRef(typeNameFor(String.class), verboseValues)) + .allowableValues(verboseValues) + .allowMultiple(false) + .description(description) + .build()); + } + } + + context.operationBuilder().parameters(parameters); + } + } + + private String _getSortDescription(OperationContext context) { + List params = Lists.newArrayList(); + context.findAnnotation(ApiSortParams.class).ifPresent(apiSortParams -> Arrays.stream(apiSortParams.value()) + .filter(param -> isNotBlank(param.name())) + .forEach(params::add)); + context.findAnnotation(ApiSortParam.class) + .filter(param -> isNotBlank(param.name())) + .ifPresent(params::add); + return _concatSortDescription(params); + } + + private String _concatSortDescription(List params) { + StringBuilder sb = new StringBuilder(); + sb.append("排序标准:
property\\[,property\\](,asc\\|desc)
"); + sb.append("默认升序,支持多级排序
"); + if (params.size() == 0) { + return sb.toString(); + } + sb.append("
排序字段描述:"); + for (ApiSortParam param : params) { + sb.append("
").append(param.name()).append("  ").append(param.value()); + } + return sb.toString(); + } + + private List _getAllowableValues(@Nullable SortLimit sortLimit) { + List allowableValues = Lists.newArrayList(); + if (sortLimit != null) { + for (String limit : sortLimit.value()) { + if (!limit.endsWith(",desc") && !limit.endsWith(",asc")) { + allowableValues.add(limit + ",asc"); + allowableValues.add(limit + ",desc"); + } else { + allowableValues.add(limit); + } + } + } + return allowableValues; + } + + private String _getDefaultSort(@Nullable PageableDefault pageableDefault, + @Nullable SortDefault sortDefault, + @Nullable SortDefaults sortDefaults) { + List sortQueries = Lists.newArrayList(); + if (pageableDefault != null) { + String[] sort = pageableDefault.sort(); + if (sort.length > 0) { + sortQueries.add(new SortQuery(sort, pageableDefault.direction().toString())); + } + } + if (sortDefault != null) { + String[] sort = ArraysUtil.join(sortDefault.value(), sortDefault.sort()); + if (sort.length > 0) { + sortQueries.add(new SortQuery(sort, sortDefault.direction().toString())); + } + } + if (sortDefaults != null) { + for (SortDefault moreSortDefault : sortDefaults.value()) { + String[] sort = ArraysUtil.join(moreSortDefault.value(), moreSortDefault.sort()); + if (sort.length > 0) { + sortQueries.add(new SortQuery(sort, moreSortDefault.direction().toString())); + } + } + } + return on("&").join(sortQueries); + } + + @Override + public boolean supports(DocumentationType documentationType) { + return SwaggerPluginSupport.pluginDoesApply(documentationType); + } + + @Override + public int getOrder() { + return SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 100; + } + + @RequiredArgsConstructor + private static class SortQuery { + private final String[] sort; + private final String direction; + + @Override + public String toString() { + return on(",").join(sort) + "," + direction.toLowerCase(); + } + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/RequestEntityPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/RequestEntityPlugin.java new file mode 100644 index 0000000..36f80e8 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/RequestEntityPlugin.java @@ -0,0 +1,90 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static com.google.common.collect.Lists.newArrayList; + +import com.fasterxml.classmate.ResolvedType; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.core.Ordered; +import org.springframework.http.RequestEntity; +import springfox.documentation.common.Compatibility; +import springfox.documentation.service.Parameter; +import springfox.documentation.service.RequestParameter; +import springfox.documentation.service.ResolvedMethodParameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.OperationModelsProviderPlugin; +import springfox.documentation.spi.service.ParameterBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.spi.service.contexts.ParameterContext; +import springfox.documentation.spi.service.contexts.RequestMappingContext; +import springfox.documentation.spring.web.plugins.DocumentationPluginsManager; + +/** + * @author liyong.tian + * @since 2018/7/19 + */ +@RequiredArgsConstructor +public class RequestEntityPlugin implements ParameterBuilderPlugin, OperationBuilderPlugin, + OperationModelsProviderPlugin, Ordered { + + private final DocumentationPluginsManager pluginsManager; + + @Override + public void apply(ParameterContext context) { + ResolvedMethodParameter resolvedMethodParameter = context.resolvedMethodParameter(); + ResolvedType parameterType = resolvedMethodParameter.getParameterType(); + if (parameterType.getErasedType().equals(RequestEntity.class)) { + context.parameterBuilder() + .parameterType("body") + .required(true); + } + } + + @Override + public void apply(RequestMappingContext context) { + List parameterTypes = context.getParameters(); + for (ResolvedMethodParameter parameterType : parameterTypes) { + if (parameterType.getParameterType().getErasedType().equals(RequestEntity.class)) { + ResolvedType modelType = context.alternateFor(parameterType.getParameterType()); + context.operationModelsBuilder().addInputParam(modelType); + } + } + } + + @Override + public void apply(OperationContext context) { + List methodParameters = context.getParameters(); + List> parameters = newArrayList(); + + for (ResolvedMethodParameter methodParameter : methodParameters) { + if (methodParameter.getParameterType().getErasedType().equals(RequestEntity.class)) { + ParameterContext parameterContext = new ParameterContext(methodParameter, + context.getDocumentationContext(), + context.getGenericsNamingStrategy(), + context, + 0); + parameters.add(pluginsManager.parameter(parameterContext)); + } + } + List collect = new ArrayList<>(); + for (Compatibility compatibility : parameters) { + if (compatibility.getLegacy().isPresent()) + collect.add(compatibility.getLegacy().get()); + } + context.operationBuilder().parameters(collect); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return true; + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/ShowStatus.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/ShowStatus.java new file mode 100644 index 0000000..1c89337 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/ShowStatus.java @@ -0,0 +1,9 @@ +package cn.axzo.framework.web.servlet.swagger; + +/** + * @author liyong.tian + * @since 2018/1/25 下午1:40 + */ +public enum ShowStatus { + VISIBLE, HIDDEN +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/TraceParameterOperationBuilderPlugin.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/TraceParameterOperationBuilderPlugin.java new file mode 100644 index 0000000..80541b8 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/TraceParameterOperationBuilderPlugin.java @@ -0,0 +1,58 @@ +package cn.axzo.framework.web.servlet.swagger; + +import static java.lang.Boolean.TYPE; +import static springfox.documentation.schema.Types.typeNameFor; + +import com.google.common.collect.Lists; +import org.springframework.core.Ordered; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.OperationBuilderPlugin; +import springfox.documentation.spi.service.contexts.OperationContext; +import springfox.documentation.swagger.common.SwaggerPluginSupport; + +/** + * Add some description about this class. + * + * @author liyong.tian + * @since 2018/3/10 下午4:19 + */ +public class TraceParameterOperationBuilderPlugin implements OperationBuilderPlugin, Ordered { + + private final boolean enabled; + + private final boolean required; + + public TraceParameterOperationBuilderPlugin(boolean enabled, boolean required) { + this.enabled = enabled; + this.required = required; + } + + @Override + public void apply(OperationContext context) { + if (!enabled) { + return; + } + Parameter traceParameter = new ParameterBuilder() + .parameterType("query") + .name("trace") + .defaultValue("true") + .required(required) + .modelRef(new ModelRef(typeNameFor(TYPE))) + .description("异常跟踪(只限非生产环境)") + .build(); + context.operationBuilder().parameters(Lists.newArrayList(traceParameter)); + } + + @Override + public boolean supports(DocumentationType delimiter) { + return SwaggerPluginSupport.pluginDoesApply(delimiter); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiPageableParam.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiPageableParam.java new file mode 100644 index 0000000..0c730b6 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiPageableParam.java @@ -0,0 +1,38 @@ +package cn.axzo.framework.web.servlet.swagger.annotations; + +import cn.axzo.framework.web.servlet.swagger.ShowStatus; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author liyong.tian + * @since 2018/1/25 下午1:38 + */ +@Documented +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiPageableParam { + + /** + * 是否显示'needTotal'参数 + */ + ShowStatus needTotalParam() default ShowStatus.VISIBLE; + + /** + * 是否显示'needContent'参数 + */ + ShowStatus needContentParam() default ShowStatus.VISIBLE; + + /** + * 是否显示'fixEdge'参数 + */ + ShowStatus fixEdgeParam() default ShowStatus.HIDDEN; + + /** + * 是否显示'verbose'参数 + */ + ShowStatus verboseParam() default ShowStatus.VISIBLE; +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParam.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParam.java new file mode 100644 index 0000000..b6581b1 --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParam.java @@ -0,0 +1,29 @@ +package cn.axzo.framework.web.servlet.swagger.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author liyong.tian + * @since 2017/10/4 下午6:33 + */ +@Documented +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(value = ApiSortParams.class) +public @interface ApiSortParam { + + /** + * Name of the parameter. + */ + String name() default ""; + + /** + * A brief description of the parameter. + */ + String value() default ""; +} diff --git a/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParams.java b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParams.java new file mode 100644 index 0000000..3a5a24f --- /dev/null +++ b/axzo-common-webmvc/src/main/java/cn.axzo.framework.web.servlet/swagger/annotations/ApiSortParams.java @@ -0,0 +1,21 @@ +package cn.axzo.framework.web.servlet.swagger.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author liyong.tian + * @since 2017/10/4 下午6:32 + *

+ * 注:该注解只在Java7或更低版本中使用 + */ +@Documented +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiSortParams { + + ApiSortParam[] value() default {}; +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8db86c1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + cn.axzo.infra + axzo-parent + 2.4.13.5-SNAPSHOT + + + + cn.axzo.framework + axzo-framework-commons + 1.0.0-SNAPSHOT + pom + + Axzo Framework Commons + Axzo Common Parent + http://www.example.com + + + axzo-common-dependencies + axzo-common-core + axzo-common-math + axzo-common-validator + axzo-common-domain + axzo-common-framework + axzo-common-boot + axzo-common-clients + axzo-common-loggings + axzo-common-web + axzo-common-webmvc + axzo-common-autoconfigure + axzo-common-jackson + + + + + + + + + org.projectlombok + lombok + provided + + + junit + junit + test + + + + + + + cn.axzo.infra + axzo-common-dependencies + 1.0.0-SNAPSHOT + pom + import + + + + + + + + nexus + + false + + + + nexus + my-nexus-repository + https://nexus.axzo.cn/content/groups/public/ + + + + + nexus + my-nexus-repository + https://nexus.axzo.cn/content/groups/public/ + + + + +