diff --git a/.gitignore b/.gitignore index a1c2a238a965f004ff76978ac1086aa6fe95caea..fd85fe3290223fa6211b6dacbf57a4fae6d11530 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,26 @@ -# Compiled class file +**/nb-* +/target/ +bin +bak +.pmd +.project +.settings +.classpath +.idea.xml +.idea *.class - -# Log file +*.bak +*.iml +*.ipr +*.iws +.DS_Store +nb-configuration.xml +coverage-report +logs *.log +/.metadata/ -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +/.metadata/ +/.idea/ +**/target/ +**/*.iml \ No newline at end of file diff --git a/itools-core/itools-common/pom.xml b/itools-core/itools-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9f9b8ec917328bd0db2aa73d0019d075da4b1cc3 --- /dev/null +++ b/itools-core/itools-common/pom.xml @@ -0,0 +1,93 @@ + + + + itools-core + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + jar + itools-common + + + com.itools.core + itools-model + 1.0-SNAPSHOT + + + com.itools.core + itools-utils + 1.0-SNAPSHOT + + + com.baomidou + mybatis-plus-boot-starter + + + + + + + com.baomidou + mybatis-plus-generator + + + org.apache.commons + commons-lang3 + + + org.aspectj + aspectjweaver + + + io.github.openfeign + feign-core + + + org.hibernate.validator + hibernate-validator + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + + + + io.springfox + springfox-swagger2 + + + io.springfox + springfox-swagger-ui + + + com.github.xiaoymin + swagger-bootstrap-ui + 1.9.6 + + + com.google.guava + guava + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-databind + 2.8.3 + + + + \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimit.java b/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimit.java new file mode 100644 index 0000000000000000000000000000000000000000..701033ddb77e0af7bb0b58e64d8e9dc3b2cd4d5b --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimit.java @@ -0,0 +1,34 @@ +package com.itools.core.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/10 15:33 + */ +@Inherited +@Documented +@Target({ElementType.TYPE,ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RateLimit { + /** + * 每秒发放许可数 + * @return + */ + double permitsPerSecond() default 100.0; + + /** + * 超时时间,即能否在指定内得到令牌,如果不能则立即返回,不进入目标方法/类 + * 默认为0,即不等待,获取不到令牌立即返回 + * @return + */ + long timeout() default 0; + + /** + * 超时时间单位,默认取毫秒 + * @return + */ + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimitAspect.java b/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimitAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..ed06f7ea37bd2f348a214b0e9244c288fad81422 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/annotation/RateLimitAspect.java @@ -0,0 +1,97 @@ +package com.itools.core.annotation; + +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.RateLimiter; +import com.itools.core.em.SystemCodeBean; +import com.itools.core.exception.AppException; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 使用Guava的RateLimiter实现限流,设置每秒最大的请求数,仅适用于单体 + * 不保证公平访问 + * 允许先消费,后付款,就是它可以来一个请求的时候一次性取走几个或者是剩下所有的令牌甚至多取 + * 但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌 + * 所以实际上每秒能够通过的数量会比设置的permitsPerSecond大 + * 在设置permitsPerSecond的时候应比实际估计的流量要小 + * 限制能处理的总数 + * @author xuchang + */ +@Aspect +@Component +public class RateLimitAspect { + /** + * com.google.common.collect.Maps 别导错包了 + * 存放RateLimiter,一个url对应一个令牌桶 + */ + private Map limitMap = Maps.newConcurrentMap(); + @Autowired + private Environment environment; + + private static final Logger logger = LoggerFactory.getLogger(RateLimitAspect.class); + + @Pointcut("@annotation(com.itools.core.annotation.RateLimit)") + private void pointcut() { + } + + @Around(value = "pointcut()") + public Object around(ProceedingJoinPoint joinPoint) { + Object obj = null; + // 获取注解 + RateLimit rateLimit = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RateLimit.class); + // 获取request,然后获取请求的url,存入map中 + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String url = request.getRequestURI(); + // 若获取注解不为空 + if (rateLimit != null) { + // 获取注解的permitsPerSecond与timeout + double permitsPerSecond = rateLimit.permitsPerSecond(); + if (environment.containsProperty("permitsPerSecond")){ + permitsPerSecond = environment.getProperty("permitsPerSecond",Double.class); + } + long timeout = rateLimit.timeout(); + TimeUnit timeUnit = rateLimit.timeUnit(); + RateLimiter rateLimiter = null; + // 判断map集合中是否有创建有创建好的令牌桶 + // 若是第一次请求该url,则创建新的令牌桶 + if (!limitMap.containsKey(url)) { + // 创建令牌桶 + rateLimiter = RateLimiter.create(permitsPerSecond); + limitMap.put(url, rateLimiter); + logger.info("请求URL为{},创建令牌桶,容量为:{}",url,permitsPerSecond); + }else { + // 否则从已经保存的map中取 + rateLimiter = limitMap.get(url); + } + // 若得到令牌 + if (rateLimiter.tryAcquire(timeout, timeUnit)) { + // 开始执行目标controller + try { + obj = joinPoint.proceed(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } else { + // 否则直接返回错误信息 + logger.error("请求URL为{},请求频繁,请稍后重试!",url); + throw new AppException(SystemCodeBean.SystemCode.RETE_EXCEPTION.getCode()); + } + } + return obj; + } + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageInfo.java b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..2c32dc79b3d9bb431d61562320be0fe5cc27490f --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageInfo.java @@ -0,0 +1,51 @@ +package com.itools.core.base; + +/** + * @description: 分页参数 + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public class CommonPageInfo { + + public CommonPageInfo(int currentPage, int pageSize) { + if (currentPage < 1 || pageSize < 1) { + throw new IllegalArgumentException("currentPage and pageSize must more than 0."); + } + + this.pageNum = currentPage; + this.pageSize = pageSize; + } + + /** + * 当前页号 + */ + private int pageNum; + /** + * 页面大小 + */ + private int pageSize; + + public int getPageNum() { + return pageNum; + } + + public void setPageNum(int pageNum) { + this.pageNum = pageNum; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + /** + * 计算查询的开始行数 + * @return + */ + public int getStartRow(){ + return (pageNum - 1) * pageSize; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageResult.java b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageResult.java new file mode 100644 index 0000000000000000000000000000000000000000..2196ae7fd8268b1671962df2399b2755ec05d26e --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonPageResult.java @@ -0,0 +1,130 @@ +package com.itools.core.base; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @description: + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public class CommonPageResult implements java.io.Serializable { + private static final long serialVersionUID = -3628865867907230918L; + + /** + * 总记录数 + */ + private long total; + + /** + * 总页数 + */ + private long pageCount; + /** + * 当前页号 + */ + private long pageNum; + /** + * 页面大小 + */ + private long pageSize; + + /** + * 数据列表 + */ + private List datas; + + private CommonPageResult(){} + + public CommonPageResult(List list, CommonPageInfo pageQueryRequest, long total){ + CommonPageResult result = build(list, pageQueryRequest, total); + this.total = result.getTotal(); + this.pageCount = result.getPageCount(); + this.pageNum = result.getPageNum(); + this.pageSize = result.getPageSize(); + this.datas = list; + } + + /** + * 构建分页返回的结果集 + * @param list 分页的数据 + * @param pageQueryRequest 分页参数 + * @param total 总数 + * @param + * @return + */ + public static CommonPageResult build(List list, CommonPageInfo pageQueryRequest, long total) { + + int pageSize = pageQueryRequest.getPageSize(); + int pageNum = pageQueryRequest.getPageNum(); + if (total < 0 || pageSize <= 0 || pageNum < 0) { + throw new IllegalArgumentException("total must more than 0"); + } + + CommonPageResult result = new CommonPageResult<>(); + //判断结果集是否分页的数据,是的话,重新构建分页 + if(list.size() == total || list.size() > pageSize){ + List temp = list.stream().skip((pageNum-1) * pageSize).limit(pageSize).collect(Collectors.toList()); + result.setDatas(temp); + }else { + result.setDatas(list); + } + result.setTotal(total); + + result.setPageSize(pageSize); + result.setPageNum(pageNum); + //判断分页的页数 + if (total == 0) { + result.setPageCount(0); + } else { + if (total % pageSize > 0) { + result.setPageCount(total / pageSize + 1); + } else { + result.setPageCount(total / pageSize); + } + } + + return result; + } + + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public List getDatas() { + return datas; + } + + public void setDatas(List datas) { + this.datas = datas; + } + + public long getPageCount() { + return pageCount; + } + + public void setPageCount(long pageCount) { + this.pageCount = pageCount; + } + + public long getPageNum() { + return pageNum; + } + + public void setPageNum(long pageNum) { + this.pageNum = pageNum; + } + + public long getPageSize() { + return pageSize; + } + + public void setPageSize(long pageSize) { + this.pageSize = pageSize; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/base/CommonResult.java b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonResult.java new file mode 100644 index 0000000000000000000000000000000000000000..298434e6c030749b261cff2245241bc61f997348 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/base/CommonResult.java @@ -0,0 +1,131 @@ +package com.itools.core.base; + +import com.alibaba.fastjson.JSON; + +import java.io.Serializable; +import java.util.UUID; + +/** + * @description: 返回的结果集 + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public class CommonResult implements Serializable { + + private static final long serialVersionUID = 9191892693219217387L; + + private static final String RESP_CODE_SUCCESS = "0000"; + private static final String RESP_MSG_SUCCESS = "Success"; + + /** + * 0000表示成功,其他表示失败 + */ + private String returnCode = RESP_CODE_SUCCESS; + + /** + * 如果result非0000,则 errorMsg 为错误信息, result为0000,errorMsg为空 + */ + private String returnMsg = RESP_MSG_SUCCESS; + + private String nonceStr = UUID.randomUUID().toString().replaceAll("-", ""); + + private boolean success; + + private T data; + + @Override + public String toString() { + return JSON.toJSONString(this); + } + + public String getReturnCode() { + return returnCode; + } + + + public void setReturnCode(String returnCode) { + this.returnCode = returnCode; + } + + + public String getReturnMsg() { + return returnMsg; + } + + + public void setReturnMsg(String returnMsg) { + this.returnMsg = returnMsg; + } + + + public String getNonceStr() { + return nonceStr; + } + + + public void setNonceStr(String nonceStr) { + this.nonceStr = nonceStr; + } + + public boolean isSuccess() + { + return success; + } + + public void setSuccess(boolean success){ + this.success = success; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public CommonResult(){} + + /** + * 构造返回成功对象结果 + * @param data 结果参数 + * @return result + */ + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.setSuccess(true); + result.setData(data); + return result; + } + + /** + * 构造失败对象 + * @param code 编码 + * @param message 消息 + * @return result + */ + public static CommonResult fail(String code, String message, T data) { + CommonResult result = new CommonResult<>(); + result.setReturnCode(code); + result.setData(data); + result.setReturnMsg(message); + result.setSuccess(false); + return result; + } + + /** + * 构造失败对象 + * @param code 编码 + * @param message 消息 + * @return result + */ + public static CommonResult fail(String code, String message) { + + CommonResult result = new CommonResult<>(); + result.setReturnCode(code); + result.setReturnMsg(message); + result.setSuccess(false); + result.setData(null); + return result; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/base/package-info.java b/itools-core/itools-common/src/main/java/com/itools/core/base/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..7ef7f3209a4692cb474cfcfa8d03fca0fb1f40f5 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 集成请求返回的结果集 + */ +package com.itools.core.base; \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/bean/BeanCopyUtils.java b/itools-core/itools-common/src/main/java/com/itools/core/bean/BeanCopyUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..7fd97f3c00ebffc013bdadecfac0818dbc9a6d06 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/bean/BeanCopyUtils.java @@ -0,0 +1,108 @@ +package com.itools.core.bean; + +import com.itools.core.context.CommonCallContext; +import org.springframework.beans.BeanUtils; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @description: + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public class BeanCopyUtils { + + /** + * 将对象属性拷贝到目标类型的同名属性字段中 + * @param + * @param source + * @param targetClazz + * @return + */ + public static T copyProperties(Object source, Class targetClazz) { + + T target = null; + try { + target = targetClazz.newInstance(); + BeanUtils.copyProperties(source, target); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return target; + } + + /** + * 将对象属性拷贝到目标类型的同名属性字段中 + * @param source + * @param target + * @return + */ + public static T copyProperties(Object source, T target) { + BeanUtils.copyProperties(source, target); + return target; + } + + /** + * 将对象属性拷贝给目标对象 + * @param source + * @param context + * @param target + * @return + */ + public static T copyProperties(Object source, CommonCallContext context, T target) { + + BeanUtils.copyProperties(source, target); + if (CommonBaseTimes.class.isAssignableFrom(target.getClass())) { + BeanCopyUtils.copyProperties((CommonBaseTimes) target, context); + } + + return target; + } + + /** + * 拷贝通用属性 + * @param dto + * @param context + */ + public static void copyProperties(CommonBaseTimes dto, CommonCallContext context) { + if (context == null || dto == null) { + return; + } + + dto.setCreateTime(context.getCallTime().getTime()); + dto.setCreateUserId(context.getUser().getCaller()); + + dto.setLastUpdateTime(context.getCallTime().getTime()); + dto.setLastUpdateUserId(context.getUser().getCaller()); + } + + /** + * 将list的对象拷贝到目标类型对象中 + * @param list + * @param clazz + * @return + */ + public static List copy(Collection list, Class clazz) { + List result = new ArrayList<>(12); + + if (!CollectionUtils.isEmpty(list)) { + for (V source : list) { + E target = null; + try { + target = (E) clazz.newInstance(); + BeanUtils.copyProperties(source, target); + } catch (Exception e) { + throw new RuntimeException(e); + } + + result.add(target); + } + } + + return result; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseIdentify.java b/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseIdentify.java new file mode 100644 index 0000000000000000000000000000000000000000..dde2b652febc9cd0496fd9f143e4251f2907e528 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseIdentify.java @@ -0,0 +1,52 @@ +package com.itools.core.bean; + +import java.io.Serializable; + +/** + * 公用id + * @author xuchang + */ +public class CommonBaseIdentify implements Serializable { + + private static final long serialVersionUID = -2923446725609856732L; + + /** + * 主键编号 + */ + private String id; + + /** + * 创建用户ID + */ + private String createUserId; + + /** + * 最后更新用户ID + */ + private String lastUpdateUserId; + + public String getId() { + return id; + } + + public String getCreateUserId() { + return createUserId; + } + + public String getLastUpdateUserId() { + return lastUpdateUserId; + } + + public void setId(String id) { + this.id = id; + } + + public void setCreateUserId(String createUserId) { + this.createUserId = createUserId; + } + + public void setLastUpdateUserId(String lastUpdateUserId) { + this.lastUpdateUserId = lastUpdateUserId; + } +} + diff --git a/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseTimes.java b/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseTimes.java new file mode 100644 index 0000000000000000000000000000000000000000..b5950f88997fe7877ad9cdb1cac7d378342c26db --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/bean/CommonBaseTimes.java @@ -0,0 +1,37 @@ +package com.itools.core.bean; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class CommonBaseTimes extends CommonBaseIdentify { + private static final long serialVersionUID = -1915432938815132522L; + + /** + * 创建时间 + */ + private Long createTime; + + /** + * 最后更新用户时间 + */ + private Long lastUpdateTime; + + public Long getCreateTime() { + return createTime; + } + + public Long getLastUpdateTime() { + return lastUpdateTime; + } + + public void setCreateTime(Long createTime) { + this.createTime = createTime; + } + + public void setLastUpdateTime(Long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/bean/package-info.java b/itools-core/itools-common/src/main/java/com/itools/core/bean/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a3910232337ce144dd6c993eeec1e9286ff3fe8a --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/bean/package-info.java @@ -0,0 +1,4 @@ +/** + * bean属性拷贝 + */ +package com.itools.core.bean; \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCode.java b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCode.java new file mode 100644 index 0000000000000000000000000000000000000000..f6f658257ef463a72fcf3d4558dd1c60eefbfb61 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCode.java @@ -0,0 +1,23 @@ +package com.itools.core.code; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 该注解用于将系统返回码加载到redis中 + * @author xuchang + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface SystemCode { + /** + * 这个参数填写的是枚举的全限定名 + * @return + */ + /*public String qualifiedName();*/ +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeController.java b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeController.java new file mode 100644 index 0000000000000000000000000000000000000000..4f4c5bca3a94a314753c8f3a085dc1e4bc2a3221 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeController.java @@ -0,0 +1,41 @@ +package com.itools.core.code; + +import com.itools.core.base.CommonResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@RestController +@RequestMapping("system") +public class SystemCodeController { + + private final SystemCodeService systemCodeService; + + public SystemCodeController(@Autowired SystemCodeService systemCodeService) { + this.systemCodeService = systemCodeService; + } + + @RequestMapping(value = "translate/{code}", method = RequestMethod.GET) + public CommonResult> translate(@PathVariable(value = "code") String code){ + Map result = new HashMap<>(2); + result.put("code", code); + result.put("msg", systemCodeService.getMessageOptional(code).orElse("")); + return CommonResult.success(result); + } + + @RequestMapping(value = "translate/codes", method = RequestMethod.GET) + public CommonResult> codes(){ + return CommonResult.success(systemCodeService.getSystemCodeMap()); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeService.java b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeService.java new file mode 100644 index 0000000000000000000000000000000000000000..a636e407aa401265f58cfd26a193ea6a4416a5bc --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/code/SystemCodeService.java @@ -0,0 +1,87 @@ +package com.itools.core.code; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Component +public class SystemCodeService implements BeanPostProcessor { + + private static final Logger log = LoggerFactory.getLogger(SystemCodeService.class); + + private static final Map SYSTEM_CODE_MAP = new HashMap<>(); + +// @Deprecated + public String getMessage(String code) { + return SYSTEM_CODE_MAP.get(code); + } + + public Optional getMessageOptional(String code) { + return Optional.ofNullable(getMessage(code)); + } + + public String getMessage(String code, Map params){ + String message = getMessage(code); + if(params != null){ + StringBuilder key; + for(String param : params.keySet()){ + key = new StringBuilder("{" + param + "}"); + message = StringUtils.replace(message, key.toString(), (String) params.get(param)); + } + } + return message; + } + + public Map getSystemCodeMap(){ + return Collections.unmodifiableMap(SYSTEM_CODE_MAP); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + try { + Class clazz = bean.getClass(); + Annotation systemCode = clazz.getAnnotation(SystemCode.class); + if (systemCode != null) { + Class[] classes = clazz.getClasses(); + if(null != classes) { + for (Class clazz0 : classes) { + Method getCode = clazz0.getMethod("getCode"); + Method getMessage = clazz0.getMethod("getMessage"); + Object[] objects = clazz0.getEnumConstants(); + if (getCode != null && getMessage != null && objects != null) { + for (Object object : objects) { + String code = (String) getCode.invoke(object); + String message = (String) getMessage.invoke(object); + log.info("将" + clazz0.getCanonicalName() + "的枚举放入内存,code:" + code + ",message:" + message); + SYSTEM_CODE_MAP.put(code, message); + } + } + } + } + } + }catch (Exception e){ + log.error("将Exception编码缓存至内存处理异常", e); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/config/Swagger2Config.java b/itools-core/itools-common/src/main/java/com/itools/core/config/Swagger2Config.java new file mode 100644 index 0000000000000000000000000000000000000000..449ebf806e2e1f677d0557f096fe2e2e74acfedc --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/config/Swagger2Config.java @@ -0,0 +1,40 @@ +package com.itools.core.config; + +import io.swagger.annotations.ApiOperation; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@EnableSwagger2 +@Configuration +public class Swagger2Config { + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() +// .apis(RequestHandlerSelectors.basePackage("com.xuchang.itools")) +// .paths(PathSelectors.any()) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("iTools") + .version("1.0") + .build(); + } +} \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContext.java b/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContext.java new file mode 100644 index 0000000000000000000000000000000000000000..34847b6db34099d350589d9b979d8cf5b08eb425 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContext.java @@ -0,0 +1,91 @@ +package com.itools.core.context; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public final class CommonCallContext implements java.io.Serializable { + private static final long serialVersionUID = -6192221637110412715L; + + /** + * 应用编码 + */ + private String applicationId; + + /** + * 设备编号 + */ + private String deviceId; + + /** + * logId + */ + private String logId; + + /** + * 调用时间 + */ + private Date callTime; + + /** + * 用户信息 + */ + private UserContext user; + + /** + * 企业信息 + */ + private CompanyContext company; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getLogId() { + return logId; + } + + public void setLogId(String logId) { + this.logId = logId; + } + + public Date getCallTime() { + return callTime; + } + + public void setCallTime(Date callTime) { + this.callTime = callTime; + } + + public UserContext getUser() { + return user; + } + + public void setUser(UserContext user) { + this.user = user; + } + + public CompanyContext getCompany() { + return company; + } + + public void setCompany(CompanyContext company) { + this.company = company; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContextBuilder.java b/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContextBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..b7d663f1f68dbd5cb0bb1c9abb26524fb01f7cc1 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/context/CommonCallContextBuilder.java @@ -0,0 +1,41 @@ +package com.itools.core.context; + + +import com.itools.core.session.BusinessSessionObject; +import com.itools.core.session.BusinessSessionContextHolder; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public final class CommonCallContextBuilder { + /** + * 构造请求上下文 + * + * @param sessionContextHolder session会话信息 + * @return 会话信息 + */ + public static CommonCallContext buildContext(BusinessSessionContextHolder sessionContextHolder) { + + BusinessSessionObject session = sessionContextHolder.getSession(); + CommonCallContext context = new CommonCallContext(); + + context.setUser(session.getUser()); + context.setCompany(session.getCompany()); + + context.setCallTime(session.getCallTime()); + context.setApplicationId(session.getApplicationId()); + + return context; + } + + /** + * 清除 session会话信息 + * @param sessionContextHolder session会话信息 + */ + public static void cleanContext(BusinessSessionContextHolder sessionContextHolder) { + sessionContextHolder.clearSession(); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/context/CompanyContext.java b/itools-core/itools-common/src/main/java/com/itools/core/context/CompanyContext.java new file mode 100644 index 0000000000000000000000000000000000000000..653e3806ff0436c32805589b116c846aaa86c8d1 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/context/CompanyContext.java @@ -0,0 +1,63 @@ +package com.itools.core.context; + +import java.io.Serializable; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class CompanyContext implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 7852895769123192530L; + + /** + * 企业编号 + */ + private String companyId; + + /** + * 企业名称 + */ + private String companyName; + + /** + * 获取 企业编号 + * + * @return 企业编号 + */ + public String getCompanyId() { + return companyId; + } + + /** + * 设置 企业编号 + * + * @param companyId 企业编号 + */ + public void setCompanyId(String companyId) { + this.companyId = companyId; + } + + /** + * 企业名称 + * + * @return 企业名称 + */ + public String getCompanyName() { + return companyName; + } + + /** + * 企业名称 + * + * @param companyName 企业名称 + */ + public void setCompanyName(String companyName) { + this.companyName = companyName; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/context/UserContext.java b/itools-core/itools-common/src/main/java/com/itools/core/context/UserContext.java new file mode 100644 index 0000000000000000000000000000000000000000..3ba22de291005bbb67fad25b95bff138ac04027e --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/context/UserContext.java @@ -0,0 +1,59 @@ +package com.itools.core.context; + + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class UserContext implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 1937298343860044405L; + + /** + * 调用者信息 + */ + @NotNull(message = "10000013") + private String caller; + + /** + * 姓名 + */ + private String callerName; + + /** + * 用户角色 + */ + private Set roles; + + public String getCaller() { + return caller; + } + + public void setCaller(String caller) { + this.caller = caller; + } + + public String getCallerName() { + return callerName; + } + + public void setCallerName(String callerName) { + this.callerName = callerName; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java b/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java new file mode 100644 index 0000000000000000000000000000000000000000..5889dec17192e2a30079210de0e1388082013040 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java @@ -0,0 +1,42 @@ +package com.itools.core.em; + + +import com.itools.core.code.SystemCode; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@SystemCode +public class SystemCodeBean { + + public enum SystemCode { + /** + * 需要在配置文件指定 + * system: + * errorCode: Code001 + */ + SYSTEM_EXCEPTION("Code001" , "系统内部错误"), + RETE_EXCEPTION("Code002" , "接口请求频繁,请稍后重试!"), + + + ; + private final String code; + private final String message; + + SystemCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/AppException.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/AppException.java new file mode 100644 index 0000000000000000000000000000000000000000..318d3d05a9cdc6d7a4d145374d9c5d79b51673c7 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/AppException.java @@ -0,0 +1,91 @@ +package com.itools.core.exception; + + +import java.util.HashMap; +import java.util.Map; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class AppException extends RuntimeException { + private static final long serialVersionUID = -8610734771461037783L; + + private final String errorCode; + + private String errorMsg; + + private ExceptionParamBuilder builder; + + public AppException(ISystemCode sysCode) + { + super(sysCode.getCode());//这里不可以修改,因为依赖FeignException来传输错误码 + this.errorCode=sysCode.getCode(); + this.errorMsg = sysCode.getMessage(); + if(errorMsg.length()>256) { + errorMsg=errorMsg.substring(0, 255); + } + } + + public AppException(String errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + public AppException(Throwable cause) { + super(cause); + this.errorCode = ""; + } + + public AppException(String errorCode, String errorMsg) { + super(errorCode); + this.errorCode = errorCode; + if(errorMsg.length()>256) { + errorMsg=errorMsg.substring(0, 255); + } + this.errorMsg = errorMsg; + } + + public AppException(String errorCode, ExceptionParamBuilder builder) { + super(errorCode); + this.errorCode = errorCode; + if(builder == null){ + throw new IllegalArgumentException("builder must not be null"); + } + this.builder = builder; + } + + public AppException(String errorCode, Throwable cause) { + super(errorCode, cause); + this.errorCode = errorCode; + } + + public AppException(String errorCode, String errorMsg, Throwable cause) { + super(errorCode, cause); + this.errorCode = errorCode; + if(errorMsg.length()>256) { + errorMsg=errorMsg.substring(0, 255); + } + this.errorMsg = errorMsg; + } + + public Map getAll(){ + if(builder == null) { + return new HashMap<>(); + } + return builder.getAll(); + } + + public String getErrorCode() { + return errorCode; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/DataAccessException.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/DataAccessException.java new file mode 100644 index 0000000000000000000000000000000000000000..e597c94b98d42252370635da97103f55b3b183f1 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/DataAccessException.java @@ -0,0 +1,37 @@ +package com.itools.core.exception; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class DataAccessException extends Exception { + private static final long serialVersionUID = -1219262335729891920L; + + /** + * 构造方法 + * @param message + */ + public DataAccessException(final String message) { + super(message); + } + + /** + * 构造方法 + * @param cause + */ + public DataAccessException(final Throwable cause) { + super(cause); + } + + /** + * 构造方法 + * @param message + * @param cause + */ + public DataAccessException(final String message, final Throwable cause) { + super(message, cause); + } +} + diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java new file mode 100644 index 0000000000000000000000000000000000000000..873098faf55d81b4af086f75ee7285938d37a6d5 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java @@ -0,0 +1,112 @@ +package com.itools.core.exception; + +import com.itools.core.base.CommonResult; +import com.itools.core.code.SystemCodeService; +import feign.codec.DecodeException; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Component +public class ExceptionHandle implements HandlerExceptionResolver, Ordered { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class); + + @Value("${system.errorCode}") + private String systemErrorCode; + + @Value("${system.errorMsg:系统处理异常,请联系管理员!}") + private String DEFAULT_ERR_MSG; + + @Autowired + private SystemCodeService systemCodeService; + + @Override + public int getOrder() { + return -2147483648; + } + + @Override + public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + return resolveException0(request, response, handler, ex); + } + + private ModelAndView resolveException0(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){ + String errorCode = systemErrorCode, errorMsg = DEFAULT_ERR_MSG; + if(ex instanceof DecodeException) { + DecodeException decodeException = (DecodeException) ex; + Throwable throwable = decodeException.getCause(); + if(throwable instanceof AppException) { + AppException appException = (AppException) throwable; + errorCode = appException.getErrorCode(); + errorMsg = appException.getErrorMsg(); + if(StringUtils.isEmpty(errorMsg)){ + errorMsg = systemCodeService.getMessage(errorCode, appException.getAll()); + } + LOGGER.error(errorMsg + " : "+ errorCode, appException.getCause()); + } + } + else if (ex instanceof AppException) { + AppException appException = (AppException) ex; + errorCode = appException.getErrorCode(); + errorMsg = appException.getErrorMsg(); + if(StringUtils.isEmpty(errorMsg)){ + errorMsg = systemCodeService.getMessage(errorCode, appException.getAll()); + } + LOGGER.error(errorMsg + " : "+ errorCode, appException.getCause()); + } + else if(ex instanceof ParamException) { + ParamException paramException = (ParamException) ex; + errorCode = paramException.getErrorCode(); + errorMsg = paramException.getErrorMsg(); + LOGGER.error(errorMsg); + } + else if(ex instanceof ConstraintViolationException) { + ConstraintViolationException exs = (ConstraintViolationException) ex; + Set> violations = exs.getConstraintViolations(); + for (ConstraintViolation item : violations) { + errorCode = item.getMessage(); + break; + } + errorMsg = systemCodeService.getMessageOptional(errorCode).orElse("Invalid Parameter"); + LOGGER.error(errorMsg); + } else { +// errorMsg = ex.getMessage(); + LOGGER.error("Uncaught exception", ex); + } + + if(DEFAULT_ERR_MSG.equals(errorMsg)){ + errorMsg = systemCodeService.getMessageOptional(errorCode).orElse(DEFAULT_ERR_MSG); + } + CommonResult commonOuterResponse = new CommonResult(); + commonOuterResponse.setReturnCode(errorCode); + commonOuterResponse.setReturnMsg(errorMsg); + + try { + response.setContentType("application/json;charset=UTF-8"); + response.getOutputStream().write(commonOuterResponse.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Exception ee) { + LOGGER.error("resolveException", ee); + } + + return new ModelAndView(); + } +} \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionParamBuilder.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionParamBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..fa852305efb1b8f33a211d6f7a1c1c2bac0b3c25 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionParamBuilder.java @@ -0,0 +1,93 @@ +package com.itools.core.exception; + +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public final class ExceptionParamBuilder { + + private static ExceptionParamBuilder builder; + private ThreadLocal> keys = new ThreadLocal<>(); + private ThreadLocal> values = new ThreadLocal<>(); + + private ExceptionParamBuilder() { + } + + public static ExceptionParamBuilder getInstance() { + if (builder == null) { + builder = new ExceptionParamBuilder(); + } + + return builder; + } + + + public ExceptionParamBuilder put(String key, Object value) { + if (this.keys.get() == null) { + this.keys.set(new LinkedList<>()); + } + + if (this.values.get() == null) { + this.values.set(new LinkedList<>()); + } + + if ((this.keys.get()).contains(key)) { + throw new IllegalArgumentException("param redefine:" + key); + } + + (this.keys.get()).add(key); + (this.values.get()).add(value); + + return this; + } + + private String[] getKeys() { + LinkedList keys = this.keys.get(); + String[] result; + if (keys != null && keys.size() > 0) { + result = keys.toArray(new String[0]); + } else { + result = new String[0]; + } + + this.keys.remove(); + return result; + } + + private Object[] getValues() { + LinkedList values = this.values.get(); + Object[] result; + if (values != null && values.size() > 0) { + result = values.toArray(); + } else { + result = new Object[0]; + } + + this.values.remove(); + return result; + } + + public Map getAll() { + Map all = new LinkedHashMap<>(); + String[] keys = this.getKeys(); + Object[] values = this.getValues(); + + for (int i = 0; i < keys.length; ++i) { + all.put(keys[i], values[i]); + } + + return all; + } + + public ExceptionParamBuilder empty() { + this.keys.set(new LinkedList<>()); + this.values.set(new LinkedList<>()); + return this; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionDTO.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..6b7c844767fa3ea1e5e1b7356734893fadbad36c --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionDTO.java @@ -0,0 +1,29 @@ +package com.itools.core.exception; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class FeignExceptionDTO { + + private String code; + private String cause; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getCause() { + return cause; + } + + public void setCause(String cause) { + this.cause = cause; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionUtils.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d5d4d74de9225d372c1f8660cb6aa5a928df3cc7 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/FeignExceptionUtils.java @@ -0,0 +1,71 @@ +package com.itools.core.exception; + +import com.alibaba.fastjson.JSONObject; +import feign.FeignException; +import org.apache.commons.lang3.StringUtils; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class FeignExceptionUtils { + + /** + * 解析FeignException异常信息 + * + * @param e + * @return + */ + public static FeignExceptionDTO parseFeignException(Throwable e) { + if (!(e instanceof FeignException)) { + return null; + } + // 如果是 FeignExceptionDTO 异常,则是有上游服务返回的异常,需要解析该异常内容 + String subStr = StringUtils.substringBetween(e.getMessage(), "", ""); + if (StringUtils.isBlank(subStr)) { + return null; + } + + JSONObject jsonObject = JSONObject.parseObject(subStr.replaceAll(""", "\""), JSONObject.class); + if (jsonObject == null) { + return null; + } + + FeignExceptionDTO feignExceptionDTO = new FeignExceptionDTO(); + feignExceptionDTO.setCode(jsonObject.get("code").toString()); + if (jsonObject.get("cause") != null) { + feignExceptionDTO.setCause(jsonObject.get("cause").toString()); + } + return feignExceptionDTO; + } + + + /** + * 解析Exception: + * 如果是AppException,则返回其错误码; + * 如果是FeignException,则试图从其中解析出AppException的错误码,成功则返回该错误码 + * 否则返回入参指定的默认错误码 + * 此处添加@Logable便于记录异常信息 + * @param e + * @return + */ +// @Logable(businessTag="parseException") + public static String parseException(Exception e, String defaultErrorCode) { + if(e instanceof AppException) + { + return ((AppException)e).getErrorCode(); + } + FeignExceptionDTO dto = FeignExceptionUtils.parseFeignException(e); + if (dto == null) { + return defaultErrorCode; + } else { + if(dto.getCode().length()>5) { + return defaultErrorCode; + } else { + return dto.getCode(); + } + } + } +} \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/GlobleException.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/GlobleException.java new file mode 100644 index 0000000000000000000000000000000000000000..9ed4def4f9b33337bd9178e9d26c9bd2460d99ce --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/GlobleException.java @@ -0,0 +1,48 @@ +package com.itools.core.exception; + +import com.alibaba.fastjson.JSONObject; +import org.springframework.context.annotation.Configuration; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +//@Configuration +//@ControllerAdvice +public class GlobleException { + + /** + * 跳转自定义异常页面 + * + * @param response + * @param e + * @param model + * @return + * @throws Exception + */ + @ExceptionHandler(value = Throwable.class) + public String gbExceptionHandler(HttpServletResponse response, Throwable e, Model model) throws Exception { + // response错误码 + response.setStatus(400); + JSONObject jsonObject = new JSONObject(); + // 错误编码 + jsonObject.put("code", e.getMessage()); + // 错误原因 + Throwable cause = e.getCause(); + if (cause != null) { + // 错误信息 + jsonObject.put("cause", cause.getMessage()); + } + // 异常信息 + model.addAttribute("errorMsg", jsonObject.toJSONString()); + // 自定义错误页面 + return "400"; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/ISystemCode.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/ISystemCode.java new file mode 100644 index 0000000000000000000000000000000000000000..04a0ed6d8ba8bdab7da6fb6f86e728443543c2b0 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/ISystemCode.java @@ -0,0 +1,15 @@ +package com.itools.core.exception; + +/** + * @project: itools-backend + * @description: 所有AppException使用的各种异常枚举定义均应该实现此接口 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public interface ISystemCode { + + String getCode(); + + String getMessage(); + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/ParamException.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/ParamException.java new file mode 100644 index 0000000000000000000000000000000000000000..a70ed00d9cf2c68e057769835115451499ab068a --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/ParamException.java @@ -0,0 +1,34 @@ +package com.itools.core.exception; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class ParamException extends RuntimeException { + private static final long serialVersionUID = -1L; + + private String errorCode; + + private String errorMsg; + + public ParamException(String errorCode, String errorMsg) { + super(errorCode); + this.errorCode = errorCode; + if (errorMsg.length() > 256) { + errorMsg = errorMsg.substring(0, 255); + } + this.errorMsg = errorMsg; + } + + public String getErrorCode() { + return errorCode; + } + + public String getErrorMsg() { + return errorMsg; + } + +} + diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbService.java b/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbService.java new file mode 100644 index 0000000000000000000000000000000000000000..d9850b09b26012e0094096e48cc5cf1eba976dac --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbService.java @@ -0,0 +1,20 @@ +package com.itools.core.init; + + +import com.itools.core.rbac.param.initDB.InitDBInsertParam; +import com.itools.core.rbac.param.initDB.InitDBQueryParam; +import com.itools.core.rbac.param.initDB.InitDBResult; + +import java.util.List; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:56 + */ +public interface InitDbService { + + List query(InitDBQueryParam dbQuery); + + Integer multipartInsert(List dbInsertParams); +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbServiceAware.java b/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbServiceAware.java new file mode 100644 index 0000000000000000000000000000000000000000..5ffc23b7954b0af87191d93f188bc7cf9fcb3f6e --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/InitDbServiceAware.java @@ -0,0 +1,30 @@ +package com.itools.core.init; + +import com.itools.core.rbac.param.initDB.InitDBInsertParam; +import com.itools.core.rbac.param.initDB.InitDBQueryParam; +import com.itools.core.rbac.param.initDB.InitDBResult; +import com.itools.core.system.AbstractService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * @project: iTools + * @description: + * @author: XUCHANG + * @create: 2020-09-28 22:52 + */ +@Service +public class InitDbServiceAware extends AbstractService implements InitDbService { + + @Override + public List query(InitDBQueryParam dbQuery) { + return new ArrayList<>(); + } + + @Override + public Integer multipartInsert(List dbInsertParams) { + return null; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/MyDdlApplicationRunner.java b/itools-core/itools-common/src/main/java/com/itools/core/init/MyDdlApplicationRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..8676dfb2b786450774dcd0d81c8eedad25f2b343 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/MyDdlApplicationRunner.java @@ -0,0 +1,116 @@ +package com.itools.core.init; + +import com.itools.core.rbac.param.initDB.InitDBInsertParam; +import com.itools.core.rbac.param.initDB.InitDBQueryParam; +import com.itools.core.rbac.param.initDB.InitDBResult; +import com.itools.core.utils.StringUtils; +import org.apache.ibatis.io.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.Reader; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/10 15:33 + */ +@Component +@Order(value = 0) +public class MyDdlApplicationRunner implements ApplicationRunner { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Autowired + private ApplicationContext applicationContext; + @Autowired + private Environment environment; + @Override + public void run(ApplicationArguments args) throws Exception { + + if (!environment.containsProperty("initDB.is-use")){ + return; + } + Boolean isUse = environment.getProperty("initDB.is-use",Boolean.class); + if (!isUse){ + return; + } + + DataSource dataSource = applicationContext.getBean(DataSource.class); + Connection connection = null; + InitDbService initDBService = applicationContext.getBean(InitDbService.class); + String ddlProperty = environment.getProperty("initDB.ddl"); + String[] files = ddlProperty.split(","); + try { + connection = dataSource.getConnection(); + } catch (SQLException e) { + logger.info("数据源连接失败",e); + } + MyScriptRunner runner = new MyScriptRunner(connection,getInitDBMap(initDBService)); + try { + List dbInsertParams = new ArrayList<>(); + for (String file : files){ + logger.info("开始执行{}脚本文件",file); + Reader ddlResourceAsReader = Resources.getResourceAsReader(file); + runner.setDelimiter(environment.getProperty("initDB.delimiter")); + runner.setImplemented(true); + runner.setThrowWarning(true); + List result = runner.runLineScript(ddlResourceAsReader); + + for (String sql : result){ + if (StringUtils.isEmpty(sql)){ + continue; + } + InitDBInsertParam initDBInsertParam = new InitDBInsertParam(); + initDBInsertParam.setType("ddl"); + initDBInsertParam.setContent(sql); + logger.info("DDL:执行了"+sql); + initDBInsertParam.setEnv(environment.getProperty("initDB.env")); + initDBInsertParam.setVersion(environment.getProperty("initDB.version")); + dbInsertParams.add(initDBInsertParam); + } + logger.info("脚本{}执行成功",file); + } + initDBService.multipartInsert(dbInsertParams); + } catch (Exception e) { + logger.error("Resources获取失败",e); + } + logger.info("DDL脚本执行完成"); + } + private Map getInitDBMap(InitDbService initDBService){ + Map result = new HashMap<>(); + InitDBQueryParam initDBQueryParam = new InitDBQueryParam(); + initDBQueryParam.setEnv(environment.getProperty("initDB.env")); + initDBQueryParam.setType("ddl"); +// initDBQueryParam.setVersion(environment.getProperty("initDB.version")); + List resultList = initDBService.query(initDBQueryParam); + for (InitDBResult initDBResult : resultList){ + result.put(initDBResult.getId(),replaceBlank(initDBResult.getContent())); + } + return result; + } + private static String replaceBlank(String str) { + String dest = ""; + if (str!=null) { + Pattern p = Pattern.compile("\\s*|\t|\r|\n"); + Matcher m = p.matcher(str); + dest = m.replaceAll(""); + } + return dest; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/MyDmlApplicationRunner.java b/itools-core/itools-common/src/main/java/com/itools/core/init/MyDmlApplicationRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..bd2374e3b32d0ab138335e7e4abea2fd481e4ac6 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/MyDmlApplicationRunner.java @@ -0,0 +1,115 @@ +package com.itools.core.init; + +import com.itools.core.rbac.param.initDB.InitDBInsertParam; +import com.itools.core.rbac.param.initDB.InitDBQueryParam; +import com.itools.core.rbac.param.initDB.InitDBResult; +import com.itools.core.utils.StringUtils; +import org.apache.ibatis.io.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.IOException; +import java.io.Reader; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/10 15:33 + */ +@Component +@Order(value = 1) +public class MyDmlApplicationRunner implements ApplicationRunner { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Autowired + private ApplicationContext applicationContext; + @Autowired + private Environment environment; + @Override + public void run(ApplicationArguments args) throws Exception { + if (!environment.containsProperty("initDB.is-use")){ + return; + } + Boolean isUse = environment.getProperty("initDB.is-use",Boolean.class); + if (!isUse){ + return; + } + DataSource dataSource = applicationContext.getBean(DataSource.class); + Connection connection = null; + try { + connection = dataSource.getConnection(); + } catch (SQLException e) { + logger.info("数据源连接失败",e); + } + String dmlProperty = environment.getProperty("initDB.dml"); + String[] files = dmlProperty.split(","); + InitDbService initDBService = applicationContext.getBean(InitDbService.class); + MyScriptRunner runner = new MyScriptRunner(connection,getInitDBMap(initDBService)); + try { + List dbInsertParams = new ArrayList<>(); + for (String file : files){ + logger.info("开始执行{}脚本文件",file); + Reader dmlResourceAsReader = Resources.getResourceAsReader(file); + runner.setImplemented(true); + runner.setDelimiter(environment.getProperty("initDB.delimiter")); + runner.setThrowWarning(true); + List result = runner.runLineScript(dmlResourceAsReader); + + for (String sql : result){ + if (StringUtils.isEmpty(sql)){ + continue; + } + InitDBInsertParam initDBInsertParam = new InitDBInsertParam(); + initDBInsertParam.setContent(sql); + initDBInsertParam.setEnv(environment.getProperty("initDB.env")); + initDBInsertParam.setType("dml"); + logger.info("DML:执行了"+sql); + initDBInsertParam.setVersion(environment.getProperty("initDB.version")); + dbInsertParams.add(initDBInsertParam); + } + logger.info("脚本{}执行成功",file); + } + initDBService.multipartInsert(dbInsertParams); + } catch (IOException e) { + logger.error("Resources获取失败",e); + } + logger.info("DML脚本执行完成"); + } + private Map getInitDBMap(InitDbService initDBService){ + Map result = new HashMap<>(); + InitDBQueryParam initDBQueryParam = new InitDBQueryParam(); + initDBQueryParam.setEnv(environment.getProperty("initDB.env")); +// initDBQueryParam.setVersion(environment.getProperty("initDB.version")); + initDBQueryParam.setType("dml"); + List resultList = initDBService.query(initDBQueryParam); + for (InitDBResult initDBResult : resultList){ + result.put(initDBResult.getId(),replaceBlank(initDBResult.getContent())); + } + return result; + } + private static String replaceBlank(String str) { + String dest = ""; + if (str!=null) { + Pattern p = Pattern.compile("\\s*|\t|\r|\n"); + Matcher m = p.matcher(str); + dest = m.replaceAll(""); + } + return dest; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/MyFunctionApplicationRunner.java b/itools-core/itools-common/src/main/java/com/itools/core/init/MyFunctionApplicationRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..82b233f61d39aa7e3dea69e617a45e84e1e66764 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/MyFunctionApplicationRunner.java @@ -0,0 +1,115 @@ +package com.itools.core.init; + +import com.itools.core.rbac.param.initDB.InitDBInsertParam; +import com.itools.core.rbac.param.initDB.InitDBQueryParam; +import com.itools.core.rbac.param.initDB.InitDBResult; +import com.itools.core.utils.StringUtils; +import org.apache.ibatis.io.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.IOException; +import java.io.Reader; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/10 15:33 + */ +@Component +@Order(value = 0) +public class MyFunctionApplicationRunner implements ApplicationRunner { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Autowired + private ApplicationContext applicationContext; + @Autowired + private Environment environment; + @Override + public void run(ApplicationArguments args) throws Exception { + if (!environment.containsProperty("initDB.is-use")){ + return; + } + Boolean isUse = environment.getProperty("initDB.is-use",Boolean.class); + if (!isUse){ + return; + } + DataSource dataSource = applicationContext.getBean(DataSource.class); + Connection connection = null; + try { + connection = dataSource.getConnection(); + } catch (SQLException e) { + logger.info("数据源连接失败",e); + } + InitDbService initDBService = applicationContext.getBean(InitDbService.class); + String functionProperty = environment.getProperty("initDB.function"); + String[] files = functionProperty.split(","); + MyScriptRunner runner = new MyScriptRunner(connection,getInitDBMap(initDBService)); + try { + List dbInsertParams = new ArrayList<>(); + for (String file : files){ + logger.info("开始执行{}脚本文件",file); + Reader functionResourceAsReader = Resources.getResourceAsReader(file); + runner.setDelimiter(environment.getProperty("initDB.delimiter")); + runner.setImplemented(true); + runner.setThrowWarning(true); + List result = runner.runLineScript(functionResourceAsReader); + + for (String sql : result){ + if (StringUtils.isEmpty(sql)){ + continue; + } + InitDBInsertParam initDBInsertParam = new InitDBInsertParam(); + initDBInsertParam.setContent(sql); + initDBInsertParam.setType("function"); + logger.info("function:执行了"+sql); + initDBInsertParam.setEnv(environment.getProperty("initDB.env")); + initDBInsertParam.setVersion(environment.getProperty("initDB.version")); + dbInsertParams.add(initDBInsertParam); + } + logger.info("脚本{}执行成功",file); + } + initDBService.multipartInsert(dbInsertParams); + } catch (IOException e) { + logger.info("Resources获取失败",e); + } + logger.info("FUNCTION脚本执行完成"); + } + private Map getInitDBMap(InitDbService initDBService){ + Map result = new HashMap<>(); + InitDBQueryParam initDBQueryParam = new InitDBQueryParam(); + initDBQueryParam.setEnv(environment.getProperty("initDB.env")); +// initDBQueryParam.setVersion(environment.getProperty("initDB.version")); + initDBQueryParam.setType("function"); + List resultList = initDBService.query(initDBQueryParam); + for (InitDBResult initDBResult : resultList){ + result.put(initDBResult.getId(),replaceBlank(initDBResult.getContent())); + } + return result; + } + private static String replaceBlank(String str) { + String dest = ""; + if (str!=null) { + Pattern p = Pattern.compile("\\s*|\t|\r|\n"); + Matcher m = p.matcher(str); + dest = m.replaceAll(""); + } + return dest; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/MyScriptRunner.java b/itools-core/itools-common/src/main/java/com/itools/core/init/MyScriptRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..5c5130acf30d648f5c4c8a919076c2a381774549 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/MyScriptRunner.java @@ -0,0 +1,361 @@ +package com.itools.core.init; + +import org.apache.ibatis.jdbc.RuntimeSqlException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/10 16:36 + */ +public class MyScriptRunner { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); + private static final String DEFAULT_DELIMITER = ";"; + private final Connection connection; + private boolean stopOnError; + private boolean throwWarning; + private boolean autoCommit; + private boolean sendFullScript; + private boolean removeCRs; + private boolean escapeProcessing = true; + private PrintWriter logWriter; + private PrintWriter errorLogWriter; + private String delimiter; + private boolean fullLineDelimiter; + private boolean isImplemented; + private Map sqlMap; + + public MyScriptRunner(Connection connection,Map sqlMap) { + this.logWriter = new PrintWriter(System.out); + this.errorLogWriter = new PrintWriter(System.err); + this.delimiter = ";"; + this.connection = connection; + this.sqlMap = sqlMap; + } + + public boolean isImplemented() { + return isImplemented; + } + + public void setImplemented(boolean implemented) { + isImplemented = implemented; + } + + public void setStopOnError(boolean stopOnError) { + this.stopOnError = stopOnError; + } + + public void setThrowWarning(boolean throwWarning) { + this.throwWarning = throwWarning; + } + + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } + + public void setSendFullScript(boolean sendFullScript) { + this.sendFullScript = sendFullScript; + } + + public void setRemoveCRs(boolean removeCRs) { + this.removeCRs = removeCRs; + } + + public void setEscapeProcessing(boolean escapeProcessing) { + this.escapeProcessing = escapeProcessing; + } + + public void setLogWriter(PrintWriter logWriter) { + this.logWriter = logWriter; + } + + public void setErrorLogWriter(PrintWriter errorLogWriter) { + this.errorLogWriter = errorLogWriter; + } + + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + public void setFullLineDelimiter(boolean fullLineDelimiter) { + this.fullLineDelimiter = fullLineDelimiter; + } + + public void runScript(Reader reader) { + this.setAutoCommit(); + + try { + if (this.sendFullScript) { + this.executeFullScript(reader); + } else { + this.executeLineByLine(reader); + } + } finally { + this.rollbackConnection(); + } + + } + public List runLineScript(Reader reader) { + this.setAutoCommit(); + + try { + return this.executeLineByLine(reader); + } finally { + this.rollbackConnection(); + } + + } + + private void executeFullScript(Reader reader) { + StringBuilder script = new StringBuilder(); + + String line; + try { + BufferedReader lineReader = new BufferedReader(reader); + + while((line = lineReader.readLine()) != null) { + script.append(line); + script.append(LINE_SEPARATOR); + } + + String command = script.toString(); + this.println(command); + this.executeStatement(command); + this.commitConnection(); + } catch (Exception var6) { + line = "Error executing: " + script + ". Cause: " + var6; + this.printlnError(line); + throw new RuntimeSqlException(line, var6); + } + } + + private List executeLineByLine(Reader reader) { + StringBuilder command = new StringBuilder(); + List sql = new ArrayList<>(); + String line; + try { +// for(BufferedReader lineReader = new BufferedReader(reader); (line = lineReader.readLine()) != null; command = this.handleLine(command, line,sql)) { +// } + BufferedReader lineReader = new BufferedReader(reader); + while ((line = lineReader.readLine()) != null){ + command = this.handleLine(command, line,sql); + } + this.commitConnection(); + this.checkForMissingLineTerminator(command); + return sql; + } catch (Exception var5) { + line = "Error executing: " + command + ". Cause: " + var5; + this.printlnError(line); + throw new RuntimeSqlException(line, var5); + } + } + + private boolean isRun(String s) { + + if (sqlMap.containsValue(s) && isImplemented){ + return true; + } + return false; + } + + public void closeConnection() { + try { + this.connection.close(); + } catch (Exception var2) { + } + + } + + private void setAutoCommit() { + try { + if (this.autoCommit != this.connection.getAutoCommit()) { + this.connection.setAutoCommit(this.autoCommit); + } + + } catch (Throwable var2) { + throw new RuntimeSqlException("Could not set AutoCommit to " + this.autoCommit + ". Cause: " + var2, var2); + } + } + + private void commitConnection() { + try { + if (!this.connection.getAutoCommit()) { + this.connection.commit(); + } + + } catch (Throwable var2) { + throw new RuntimeSqlException("Could not commit transaction. Cause: " + var2, var2); + } + } + + private void rollbackConnection() { + try { + if (!this.connection.getAutoCommit()) { + this.connection.rollback(); + } + } catch (Throwable var2) { + } + + } + + private void checkForMissingLineTerminator(StringBuilder command) { + if (command != null && command.toString().trim().length() > 0) { + throw new RuntimeSqlException("Line missing end-of-line terminator (" + this.delimiter + ") => " + command); + } + } + + private StringBuilder handleLine(StringBuilder command, String line, List sql) throws SQLException, UnsupportedEncodingException { + String trimmedLine = line.trim(); + + if (this.lineIsComment(trimmedLine)) { + String cleanedString = trimmedLine.substring(2).trim().replaceFirst("//", ""); + if (cleanedString.toUpperCase().startsWith("@DELIMITER")) { + this.delimiter = cleanedString.substring(11, 12); + return command; + } + + this.println(trimmedLine); + } else if (this.commandReadyToExecute(trimmedLine)) { + command.append(line.substring(0, line.lastIndexOf(this.delimiter))); + command.append(LINE_SEPARATOR); + this.println(command); + String sqlLine = replaceBlank(command.toString()); + if (this.isRun(sqlLine)){ + command.setLength(0); + return command; + } + if (!sql.contains(sqlLine) && !this.isRun(sqlLine)){ + sql.add(command.toString()); + sqlMap.put(String.valueOf(System.currentTimeMillis()),sqlLine); + } + this.executeStatement(command.toString()); + command.setLength(0); + } else if (trimmedLine.length() > 0) { + command.append(line); + command.append(LINE_SEPARATOR); + } + + return command; + } + private String replaceBlank(String str) { + String dest = ""; + if (str!=null) { + Pattern p = Pattern.compile("\\s*|\t|\r|\n"); + Matcher m = p.matcher(str); + dest = m.replaceAll(""); + } + return dest; + } + private boolean lineIsComment(String trimmedLine) { + return trimmedLine.startsWith("//") || trimmedLine.startsWith("--"); + } + + private boolean commandReadyToExecute(String trimmedLine) { + return !this.fullLineDelimiter && trimmedLine.contains(this.delimiter) || this.fullLineDelimiter && trimmedLine.equals(this.delimiter); + } + + private void executeStatement(String command) throws SQLException { + boolean hasResults = false; + Statement statement = this.connection.createStatement(); + statement.setEscapeProcessing(this.escapeProcessing); + String sql = command; + if (this.removeCRs) { + sql = command.replaceAll("\r\n", "\n"); + } + + if (this.stopOnError) { + hasResults = statement.execute(sql); + if (this.throwWarning) { + SQLWarning warning = statement.getWarnings(); + if (warning != null) { + throw warning; + } + } + } else { + try { + hasResults = statement.execute(sql); + } catch (SQLException var8) { + String message = "Error executing: " + command + ". Cause: " + var8; + this.printlnError(message); + } + } + + this.printResults(statement, hasResults); + + try { + statement.close(); + } catch (Exception var7) { + } + + } + + private void printResults(Statement statement, boolean hasResults) { + try { + if (hasResults) { + ResultSet rs = statement.getResultSet(); + if (rs != null) { + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + int i; + String value; + for(i = 0; i < cols; ++i) { + value = md.getColumnLabel(i + 1); + this.print(value + "\t"); + } + + this.println(""); + + while(rs.next()) { + for(i = 0; i < cols; ++i) { + value = rs.getString(i + 1); + this.print(value + "\t"); + } + + this.println(""); + } + } + } + } catch (SQLException var8) { + this.printlnError("Error printing results: " + var8.getMessage()); + } + + } + + private void print(Object o) { + if (this.logWriter != null) { + this.logWriter.print(o); + this.logWriter.flush(); + } + + } + + private void println(Object o) { + if (this.logWriter != null) { + this.logWriter.println(o); + this.logWriter.flush(); + } + + } + + private void printlnError(Object o) { + if (this.errorLogWriter != null) { + this.errorLogWriter.println(o); + this.errorLogWriter.flush(); + } + + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/init/package-info.java b/itools-core/itools-common/src/main/java/com/itools/core/init/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a43fc1a4accb624364dd7645e40fa6f0ca5b5e4d --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/init/package-info.java @@ -0,0 +1,4 @@ +/** + * 自动初始化脚本,需要执行 + */ +package com.itools.core.init; \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ControllerInterceptor.java b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ControllerInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..274c6eacd4c27dfdc25e17eae05a536a65cd7c0a --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ControllerInterceptor.java @@ -0,0 +1,47 @@ +package com.itools.core.interceptor; + +import com.itools.core.utils.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; + + +@Component +@Aspect +public class ControllerInterceptor{ + private static final Logger logger = LoggerFactory.getLogger(ControllerInterceptor.class); + private static final int PARAM_LENGTH_MAX = 100; + + @Pointcut("execution(* com.itools..controller..*(..))") + public void pointcut(){ + + } + + @Around("pointcut()") + public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + ":" + method.getName(); + Object[] objects = proceedingJoinPoint.getArgs(); + String paramString = Arrays.toString(objects); + if(StringUtils.hasText(paramString) && paramString.length() > PARAM_LENGTH_MAX){ + paramString = paramString.substring(0, PARAM_LENGTH_MAX); + } + logger.info("调用控制层服务:" + methodName + ". param=" + paramString + ";"); + Object returnVal; + try { + returnVal = proceedingJoinPoint.proceed(); + } catch (Exception e) { + logger.error("服务{}调用异常,", methodName, e); + throw e; + } + return returnVal; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/interceptor/DaoInterceptor.java b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/DaoInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..2212a567089f9e5b937cbc4283da95780b5dd662 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/DaoInterceptor.java @@ -0,0 +1,47 @@ +package com.itools.core.interceptor; + +import com.itools.core.utils.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; + + +@Component +@Aspect +public class DaoInterceptor { + private static final Logger logger = LoggerFactory.getLogger(DaoInterceptor.class); + private static final int PARAM_LENGTH_MAX = 100; + + @Pointcut("execution(* com.itools..data..*(..))") + public void pointcut(){ + + } + + @Around("pointcut()") + public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + ":" + method.getName(); + Object[] objects = proceedingJoinPoint.getArgs(); + String paramString = Arrays.toString(objects); + if(StringUtils.hasText(paramString) && paramString.length() > PARAM_LENGTH_MAX){ + paramString = paramString.substring(0, PARAM_LENGTH_MAX); + } + logger.info("调用数据服务:" + methodName + ". param=" + paramString + ";"); + Object returnVal; + try { + returnVal = proceedingJoinPoint.proceed(); + } catch (Exception e) { + logger.error("服务{}调用异常,", methodName, e); + throw e; + } + return returnVal; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ManagerInterceptor.java b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ManagerInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..99b52275ade088bf9ecda128b2379954e42d2c37 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ManagerInterceptor.java @@ -0,0 +1,47 @@ +package com.itools.core.interceptor; + +import com.itools.core.utils.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; + + +@Component +@Aspect +public class ManagerInterceptor { + private static final Logger logger = LoggerFactory.getLogger(ManagerInterceptor.class); + private static final int PARAM_LENGTH_MAX = 100; + + @Pointcut("execution(* com.itools..manager..*(..))") + public void pointcut(){ + + } + + @Around("pointcut()") + public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + ":" + method.getName(); + Object[] objects = proceedingJoinPoint.getArgs(); + String paramString = Arrays.toString(objects); + if(StringUtils.hasText(paramString) && paramString.length() > PARAM_LENGTH_MAX){ + paramString = paramString.substring(0, PARAM_LENGTH_MAX); + } + logger.info("调用业务服务:" + methodName + ". param=" + paramString + ";"); + Object returnVal; + try { + returnVal = proceedingJoinPoint.proceed(); + } catch (Exception e) { + logger.error("服务{}调用异常,", methodName, e); + throw e; + } + return returnVal; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceInterceptor.java b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..940cc586be53735a2fa21b0dae0636e1862056c4 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceInterceptor.java @@ -0,0 +1,47 @@ +package com.itools.core.interceptor; + +import com.itools.core.utils.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; + + +@Component +@Aspect +public class ServiceInterceptor{ + private static final Logger logger = LoggerFactory.getLogger(ServiceInterceptor.class); + private static final int PARAM_LENGTH_MAX = 100; + + @Pointcut("execution(* com.itools..service..*(..))") + public void pointcut(){ + + } + + @Around("pointcut()") + public Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + ":" + method.getName(); + Object[] objects = proceedingJoinPoint.getArgs(); + String paramString = Arrays.toString(objects); + if(!StringUtils.isEmpty(paramString) && paramString.length() > PARAM_LENGTH_MAX){ + paramString = paramString.substring(0, PARAM_LENGTH_MAX); + } + logger.info("调用接口服务:" + methodName + ". param=" + paramString + ";"); + Object returnVal; + try { + returnVal = proceedingJoinPoint.proceed(); + } catch (Exception e) { + logger.error("服务{}调用异常,", methodName, e); + throw e; + } + return returnVal; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceThreadManager.java b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceThreadManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4a9f791f6a18f0256dfae528f78737c20423296a --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/interceptor/ServiceThreadManager.java @@ -0,0 +1,35 @@ +package com.itools.core.interceptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + + +@Component +public class ServiceThreadManager { + + private AtomicInteger threadCount = new AtomicInteger(0); + private ExecutorService service; + private static final Logger logger = LoggerFactory.getLogger(ServiceThreadManager.class); + + public ServiceThreadManager() { + this.service = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), r -> { + Thread thread = new Thread(r); + thread.setName("Service Thread ->" + ServiceThreadManager.this.threadCount.getAndIncrement()); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler((t, e) -> logger.error("ServiceThreadManager exception ,", e)); + return thread; + }); + } + + public void execute(Runnable task) { + this.service.execute(task); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/Constants.java b/itools-core/itools-common/src/main/java/com/itools/core/log/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..c336820870b94eac31c868abf846366b051b4903 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/Constants.java @@ -0,0 +1,367 @@ +package com.itools.core.log; + +import java.util.HashMap; +import java.util.Map; +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public interface Constants { + public final static String SUCCESS = ReturnCode.SUCCESS.code; + public final static String FAIL = ReturnCode.FAIL.code; + public final static String INVALID_PARAM = ReturnCode.INVALID_PARAM.code; + + enum ReturnCode { + SUCCESS("0", "处理成功"), + FAIL("1", "系统内部错误"), + INVALID_PARAM("2", "参数错误"), + REMOTE_ERROR("3", "远程调用失败"), + DB_ERROR("4", "数据库操作失败"), + CACHE_ERROR("5", "缓存操作失败"), + MQ_ERROR("6", "消息调用失败"), + FILE_ERROR("7", "读写文件失败"), + UNSUPPORT_MQ_MSG_TYPE("8", "不支持的消息类型"), + REPEATED_REQUEST("0103", "重复请求"); + + public final String code; + public final String comment; + + ReturnCode(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + enum HeadKey { + TOKEN("x-token", "令牌"), + USERID("x-userid", "用户ID"), + CUSTOMER_CODE("x-customer-code", "客户编码"), + USERTYPE("x-user-type", "用户类型,参考Constants.UserType定义"), + BUSINESS_TYPE("x-business-type", "业务类型"), + EXTRA("x-extra", "扩展信息,在令牌中保存,不做其他处理"), + IP("x-user-ip", "用户IP"), + UA("x-user-ua", "用户UA"); + + public final String code; + public final String comment; + + HeadKey(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + enum ActionType { + COMMAND("CMD", "命令型,Method只返回String,表示成败"), + QUERY("QRY", "查询型,Method返回值对象或者值对象集合"); + + public final String code; + public final String comment; + + ActionType(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + public enum PayState { + + SUCCESS("SUCCESS", "成功"), + FAIL("FAIL", "失败"), + INIT("INIT", "初始化"), + PROCESSING("IN_PROCESS", "处理中"); + + public final String code; + public final String message; + + PayState(String code, String message) { + this.code = code; + this.message = message; + } + + private static final Map CODE_MAP = new HashMap(); + + static { + for (PayState typeEnum : PayState.values()) { + CODE_MAP.put(typeEnum.code, typeEnum); + } + } + + public static PayState getEnum(String typeName) { + return CODE_MAP.get(typeName); + } + } + + /** + * 费率模式 + * + */ + enum RateMode { + FIX_RATE((short) 1, "固定费率"), + PERCENT_RATE((short) 2, "百分比费率"); + public final Short code; + public final String comment; + + RateMode(Short code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 系统预置的业务编码 + */ + enum BusinessCode { + + YMF("YMF", "一码付业务"), + API_REFUND("API_REFUND", "API退款业务"), + FZ("FZ", "分账,将某条支付订单的金额分给其他客户"), + BFZ("BFZ", "被分账,从其他客户的支付订单分得金额"), + WITHDRAW("WITHDRAW", "提现业务"), + DL("DL", "代理业务"), + TK("TK", "退款业务"); + + public final String code; + public final String comment; + + BusinessCode(String code, String comment) { + this.code = code; + this.comment = comment; + } + + private static final Map CODE_MAP = new HashMap(); + + static { + for (BusinessCode typeEnum : BusinessCode.values()) { + CODE_MAP.put(typeEnum.code, typeEnum); + } + } + + public static BusinessCode getEnum(String code) { + return CODE_MAP.get(code); + } + } + + /** + * 业务参数,这里还是需要的,否则一些业务逻辑的判断就不好做了 + */ + enum BusinessParamCode { + + AGENT_CUSTOMER_CODE("agentCustomerCode", "代理商客户编码"), + MAX_TXS_AMOUNT("maxTxsAmount", "最大交易限额"), + MIN_TXS_AMOUNT("minTxsAmount", "最小交易限额"), + /** + * 分账客户的客户编码,只允许该商户给对应的子商户分账 + */ + FZ_CUSTOMER_CODE("fzCustomerCode", "分账客户编码"), + SUBSCRIPTION_RATIO("subscriptionRatio", "兑换比例"); + public final String code; + public final String comment; + + BusinessParamCode(String code, String comment) { + this.code = code; + this.comment = comment; + } + + } + + /** + * 业务类别 + */ + enum BusinessCategory { + DSP_BASIC_PAY_SERVICE("dspBasicPayService", "基础支付类业务"), + DSP_FZ_SERVICE("dspFzService", "分账类业务(分账)"), + DSP_BFZ_SERVICE("dspBfzService", "分账类业务(被分账)"), + DSP_FZ_PAY_SERVICE("dspFzPayService", "分账支付类业务"), + DSP_ACCOUNT_SERVICE("dspAccountService", "账务服务"), + DSP_SPECIAL_SERVICE("dspSpecialService", "系统专用业务"), + DSP_MEMBER_RECHARGE_SERVICE("dspMemberRechargeService", "充值服务"); + public final String code; + public final String comment; + + BusinessCategory(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 基础支付类业务 + */ + enum BasicPayService { + YMF("YMF", "一码付"), + AlIJSAPI("AliJSAPI", "支付宝生活号"), + AlIMICRO("AliMicro", "支付宝被扫"), + WXMICRO("WxMicro", "微信被扫"), + AlINATIVE("AliNative", "支付宝主扫"), + QUICKPAY("QuickPay", "快捷支付"), + WXMWEB("WxMWEB", "微信H5支付"), + WXJSAPI("WxJSAPI", "微信公众号支付"), + WXNATIVE("WxNative", "微信主扫"), + SAVINGCARDPAY("SavingCardPay", "网银储蓄卡"), + CREDITCARDPAY("CreditCardPay", "网银信用卡"), + REFUND_USING_FLOAT("RefundUsingFloat", "支付_在途金额退款"); + public final String code; + public final String comment; + + BasicPayService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 分账类业务(分账) 注:记账计入簿记账户 + */ + enum FzService { + FZ("FZ", "分账"), + FZ_REFUND_USING_FLOAT("FzRefundUsingFloat", "分账_在途金额退款"); + public final String code; + public final String comment; + + FzService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 分账类业务(被分账) 注:记账计入分账账户 + */ + enum BfzService { + BFZ("BFZ", "被分账"), + BFZ_REFUND_USING_FLOAT("BfzRefundUsingFloat", "被分账_在途金额退款"); + public final String code; + public final String comment; + + BfzService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 分账支付类业务 + */ + enum FzPayService { + FZ_ALIJSAPI("FZ-AliJSAPI", "分账-支付宝生活号"), + FZ_ALIMICRO("FZ-AliMicro", "分账-支付宝被扫"), + FZ_WXMICRO("FZ-WxMicro", "分账-微信被扫"), + FZ_ALINATIVE("FZ-AliNative", "分账-支付宝主扫"), + FZ_QUICKPAY("FZ-QuickPay", "分账-快捷支付"), + FZ_WXMWEB("FZ-WxMWEB", "分账-微信H5支付"), + FZ_WXJSAPI("FZ-WxJSAPI", "分账-微信公众号支付"), + FZ_WXNATVIE("FZ-WxNative", "分账-微信主扫"), + FZ_SAVINGARDPAY("FZ-SavingCardPay", "分账网银支付"), + FZ_ENTERPRISEUNION("FZ-SavingCardPay", "分账网银支付"); + public final String code; + public final String comment; + + FzPayService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * DSP账户服务(实时结算) + */ + enum AccountService { + REFUND_USING_AVAILABLE("RefundUsingAvailable", "支付_可用金额退款"), + FZ_REFUND_USING_AVAILABLE("FzRefundUsingAvailable", "分账_可用金额退款"), + BFZ_REFUND_USING_AVAILABLE("BfzRefundUsingAvailable", "被分账_可用金额退款"), + FZ_PAY_REFUND_USING_AVAILABLE("FzPayRefundUsingAvailable", "分账支付_可用金额退款"), + WITHDRAW("Withdraw", "代付"), + DL("DL", "代理"), + RECHARGE("Recharge", "充值"), + MEMBER_INSIDE_PAY("MemberInsidePay", "会员内转"), + MEMBER_EXCHANGE_IN("MemberExchangeIn", "会员兑换转入业务"), + MEMBER_EXCHANGE_OUT("MemberExchangeOut", "会员兑换转出业务"), + MEMBER_OPPOSITE_EXCHANGE_IN("MemberOppositeExchangeIn", "会员反兑换转入业务"), + MEMBER_OPPOSITE_EXCHANGE_OUT("MemberOppositeExchangeOut", "会员反兑换转出业务"), + MEMBER_INSIDE_PAY_CENT("MemberInsidePayCent", "会员内转分成业务"), + D0_QUICKPAY("D0-QuickPay", "D0快捷支付"), + D0_WITHDRAW_AUTO("D0-Withdraw-Auto", "D0代付-自动"); + + public final String code; + public final String comment; + + AccountService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * DSP账户服务 + */ + enum MemberRechargeService { + MEMBER_RECHARGE_ALIJSAPI("MemberRecharge-AliJSAPI", "会员充值-支付宝生活号"), + MEMBER_RECHARGE_ALIMICRO("MemberRecharge-AliMicro", "会员充值-支付宝被扫"), + MEMBER_RECHARGE_WXMICRO("MemberRecharge-WxMicro", "会员充值-微信被扫"), + MEMBER_RECHARGE_ALINATIVE("MemberRecharge-AliNative", "会员充值-支付宝主扫"), + MEMBER_RECHARGE_QUICKPAY("MemberRecharge-QuickPay", "会员充值-快捷支付"), + MEMBER_RECHARGE_WXMWEB("MemberRecharge-WxMWEB", "会员充值-微信H5支付"), + MEMBER_RECHARGE_WXJSAPI("MemberRecharge-WxJSAPI", "会员充值-企业网银"), + MEMBER_RECHARGE_WXNATIVE("MemberRecharge-WxNative", "会员充值-微信主扫"), + MEMBER_RECHARGE_UNION("MemberRecharge-Union", "会员充值-企业网银"); + public final String code; + public final String comment; + + MemberRechargeService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 系统专用业务 + */ + enum SpecialService { + FR("FR", "分润"); + public final String code; + public final String comment; + + SpecialService(String code, String comment) { + this.code = code; + this.comment = comment; + } + } + + /** + * 结算粒度 + * + */ + enum settGrained { + + BUSINESS_CATEGORY("BusinessCategory", "业务类型"), + BUSINESS("business", "业务"); + + public final String code; + public final String comment; + + settGrained(String code, String comment) { + this.code = code; + this.comment = comment; + } + + } + + enum CustomerCategory { + + CUSTOMER("CUSTOMER", "客户"), /* 要营业执照或身份证全局唯一的类型 */ + CUSTOMER_MEMBER("CUSTOMER_MEMBER", "客户(的)会员");/* 营业执照或身份证只需要在所属客户下唯一即可 */ + + public final String code; + public final String comment; + + CustomerCategory(String code, String comment) { + this.code = code; + this.comment = comment; + } + + } + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/LogRecorder.java b/itools-core/itools-common/src/main/java/com/itools/core/log/LogRecorder.java new file mode 100644 index 0000000000000000000000000000000000000000..f52914f923d04fc2011ea182a4c03ea65fab120c --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/LogRecorder.java @@ -0,0 +1,256 @@ +package com.itools.core.log; + +import com.alibaba.fastjson.JSONObject; +import com.itools.core.exception.AppException; +import com.itools.core.exception.FeignExceptionDTO; +import com.itools.core.exception.FeignExceptionUtils; +import com.itools.core.utils.StringUtils; +import feign.FeignException; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Aspect +@Component +public class LogRecorder { + + private final static DateTimeFormatter DF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + private final static AtomicLong COUNTER = new AtomicLong(1000000); + private final static String LOG_POSITION_POST = "POST"; + private final static String LOG_POSITION_PRE = "PRE"; + + private final static String SPLIT = "|"; + + private final static String EXCEPTION_SPLIT = "******"; + + @Value("${spring.application.name}") + private String applicationName; + @Value("${encryKey:''}") + private String encryKey; + + @Autowired + private ServerEnv serverEnv; + + public static final String LOG_LEVEL_DEBUG = "DEBUG"; + + public static final String LOG_LEVEL_INFO = "INFO"; + + @Around("@annotation(com.itools.core.log.Logable)") + public Object log(ProceedingJoinPoint pjp) throws Throwable { + final long start = System.currentTimeMillis(); + final long end; + //final long count = COUNTER.incrementAndGet(); + Throwable t = null; + Object result = null; + Class classTarget = pjp.getTarget().getClass(); + Method currentMethod = LogUtil.getCurrentMethod(pjp); + Object[] args = pjp.getArgs(); + Logable log = currentMethod.getAnnotation(Logable.class); + Logger detailLogger = LogUtil.getLogger(log.loggerName() + "-detail"); + String beginLogTexs = this.buildSimpleLogText(log, classTarget, currentMethod, t, null, LOG_POSITION_PRE) + buildBeginLogText(log, args); + if (detailLogger.isInfoEnabled() && log.level().name().equals(LOG_LEVEL_INFO)) { + detailLogger.info(beginLogTexs); + } else if (detailLogger.isDebugEnabled() && log.level().name().equals(LOG_LEVEL_DEBUG)) { + detailLogger.debug(beginLogTexs); + } + try { + result = pjp.proceed(); + } catch (Throwable t1) { + t = t1; + } finally { + end = System.currentTimeMillis(); + } + String endLogTexs = this.buildSimpleLogText(log, classTarget, currentMethod, t, (end - start), LOG_POSITION_POST) + this.buildEndLogText(log, result, t); + if (t == null) { + if (detailLogger.isInfoEnabled() && log.level().name().equals(LOG_LEVEL_INFO)) { + detailLogger.info(endLogTexs); + } else if (detailLogger.isDebugEnabled() && log.level().name().equals(LOG_LEVEL_DEBUG)) { + detailLogger.debug(endLogTexs); + } + } else { + detailLogger.error(endLogTexs); + } + + if (t == null) { + return result; + } else { + throw t; + } + } + + + /** + * @param log + * @param clazz + * @param method + * @param exception + * @param time + * @return + */ + protected final String buildSimpleLogText(Logable log, Class clazz, Method method, Throwable exception, Long time, String position) { + final StringBuilder sb = new StringBuilder(DF.format(ZonedDateTime.now())); + sb.append(SPLIT).append(this.serverEnv.getServerIp()).append(":").append(this.serverEnv.getServerPort()); + sb.append(SPLIT).append(applicationName); + sb.append(SPLIT).append(clazz.getName()); + sb.append(SPLIT).append(method.getName()); + sb.append(SPLIT).append(LOG_POSITION_POST.equals(position)? (time+"ms"): (0L+"ms")); + sb.append(SPLIT).append(LOG_POSITION_POST.equals(position)? ("errorCode:"+getReturnCode(exception)): "-"); + sb.append(SPLIT).append(LOG_POSITION_POST.equals(position)? (exception == null ? "SUCCESS" : "FAIL"): "-"); + sb.append(SPLIT).append(position); + sb.append(SPLIT).append(LogUtil.getTransaction(log)); + return sb.toString(); + } + + /** + * 开始日志 + * + * @param log + * @param args + * @return + */ + protected final String buildBeginLogText(Logable log, Object[] args) { + final StringBuilder sb = new StringBuilder(); + List objects = LogUtil.args(args); + int[] encryptArgsIndex = log.encryptArgsIndex(); + int[] ignoreOutputArgsIndex = log.ignoreOutputArgsIndex(); + sb.append(SPLIT + "Args: ["); + if(objects != null && !objects.isEmpty()){ + for(Object argSource : objects) { + boolean encrypt = false; + boolean output = true; + for (int index : encryptArgsIndex) { + Object arg = objects.get(index); + if(argSource.equals(arg)){ + encrypt = true; + break; + } + } + for (int index : ignoreOutputArgsIndex) { + Object arg = objects.get(index); + if(argSource.equals(arg)){ + output = false; + break; + } + } + String jsonObjects = JSONObject.toJSONString(argSource); + if(encrypt && log.encrypt() && StringUtils.hasText(encryKey)){ + jsonObjects = LogUtil.encrypt(jsonObjects, encryKey); + } + if (log.outputArgs() && output) { + sb.append(jsonObjects); + sb.append(','); + } + } + sb.deleteCharAt(sb.length() - 1); + } + sb.append(']'); + return sb.toString(); + } + + /** + * 方法执行完毕后的日志 + * + * @param log + * @param result + * @param exception + * @return + */ + protected final String buildEndLogText(Logable log, Object result, Throwable exception) { + final StringBuilder sb = new StringBuilder(); + sb.append(SPLIT + "Result: "); + if (log.outputResult() && result != null) { + if(result instanceof Collection) {//返回集合,减少日志起见,仅打印类型和大小 + sb.append(result.getClass()+"@Size:"+((Collection)result).size()); + } + else { + sb.append(result instanceof String ? result : JSONObject.toJSONString(result)); + } + } + sb.append(SPLIT + "Error: "); + if (log.outputError() && exception != null) { + sb.append(exception.getMessage() + EXCEPTION_SPLIT); + StackTraceElement[] stackTrace = exception.getStackTrace(); + for (StackTraceElement stackTraceElement : stackTrace) { + String str = StringUtils.toString(stackTraceElement); + if (str.contains("com.hashtech")) { + sb.append(str + EXCEPTION_SPLIT); + } else { + continue; + } + } + + Throwable cause = exception.getCause(); + if (cause != null) { + sb.append(cause.getMessage() + EXCEPTION_SPLIT); + StackTraceElement[] stackTrace1 = cause.getStackTrace(); + for (StackTraceElement stackTraceElement : stackTrace1) { + String str = StringUtils.toString(stackTraceElement); + if (str.contains("com.hashtech")) { + sb.append(str + EXCEPTION_SPLIT); + } else { + continue; + } + } + } + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + + + public static AppException getBusinessCause(final Throwable t) { + if (t == null) { + return null; + } + Throwable cause = t; + + while (cause != null) { + if (cause instanceof AppException) { + return (AppException) cause; + } else { + cause = cause.getCause(); + } + } + + return null; + } + + public static String getReturnCode(final Throwable t) { + if(t == null) { + return Constants.SUCCESS; + } + if(t instanceof AppException) { + return ((AppException)t).getErrorCode(); + } else if(t instanceof FeignException) { + FeignExceptionDTO fed = FeignExceptionUtils.parseFeignException(t); + if(fed == null) { + return Constants.ReturnCode.REMOTE_ERROR.code; + } else { + return fed.getCode(); + } + } + if(t.getCause() != null) { + return getReturnCode(t.getCause()); + } else { + return Constants.FAIL; + } + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/LogUtil.java b/itools-core/itools-common/src/main/java/com/itools/core/log/LogUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..2e769b1a6a06ff0382342dc173bc99fa5172baf1 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/LogUtil.java @@ -0,0 +1,199 @@ +package com.itools.core.log; + +import com.itools.core.utils.AesUtils; +import com.itools.core.utils.StringUtils; +import com.itools.core.utils.UUIDUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequestWrapper; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public class LogUtil { + private final static ThreadLocal transactionNoInThread = new ThreadLocal(); + private final static Map LOGGERS = new HashMap(); + + /** + * 获取 transaction + * + * @return + */ + public static String getTransaction(Logable log) { + String transactionNo = null; + //from attrs + try { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + ServletRequestAttributes attrs = null ; + if (requestAttributes != null && requestAttributes instanceof ServletRequestAttributes) { + attrs = (ServletRequestAttributes) requestAttributes; + } + if(attrs != null) { + transactionNo = (String) attrs.getRequest().getAttribute("transactionNo"); + if(StringUtils.isEmpty(transactionNo)) { + transactionNo = UUIDUtils.uuid(); + attrs.setAttribute("transactionNo", transactionNo, RequestAttributes.SCOPE_REQUEST); + } + } + } catch (Exception e) { } + //from thread context + if(StringUtils.isEmpty(transactionNo) && log != null) { + transactionNo = transactionNoInThread.get(); + if(transactionNo==null) { + transactionNo = UUIDUtils.uuid(); + transactionNoInThread.set(transactionNo); + } + } + return transactionNo; + } + + /** + * 加密字段 + * + * @param str + * @param key + * @param encryKey + * @return + */ + public static String encrypt(String str, String key, String encryKey) { + if (org.apache.commons.lang3.StringUtils.isBlank(str) || org.apache.commons.lang3.StringUtils.isBlank(key)) { + return null; + } + if (!org.apache.commons.lang3.StringUtils.contains(str, key)) { + return null; + } + String subString = org.apache.commons.lang3.StringUtils.substringAfter(str, key); + if (org.apache.commons.lang3.StringUtils.isBlank(subString)) { + return null; + } + String[] strings = org.apache.commons.lang3.StringUtils.substringsBetween(subString, ":\"", "\""); + if (strings == null || strings.length == 0) { + return null; + } + String str0 = strings[0]; + if (org.apache.commons.lang3.StringUtils.isBlank(str0)) { + return null; + } + String encryptStr = null; + try { + encryptStr = AesUtils.encrypt(str0, encryKey); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + if (org.apache.commons.lang3.StringUtils.isBlank(encryptStr)) { + return null; + } + return org.apache.commons.lang3.StringUtils.replace(str, str0, encryptStr); + } + + /** + * 获取参数 + * + * @param args + * @return + */ + public static List args(Object[] args) { + if (args == null) { + return null; + } + List objects = new ArrayList<>(); + for (int i = 0; i < args.length; i++) { + if (args[i] == null) { + objects.add(null); + } else { + String packageName; + if (args[i] instanceof Object[]) { + packageName = args[i].getClass().getClass().getPackage().getName(); + } else { + packageName = args[i].getClass().getPackage().getName(); + } + boolean flag = (packageName.startsWith("java.lang") || packageName.startsWith("java.util") || packageName.startsWith("com.hashtech")) + && !(args[i] instanceof HttpServletRequestWrapper); + if (flag) { + objects.add(args[i]); + } else { + objects.add(args[i] != null ? args[i].getClass().getName() : "NULL"); + } + } + } + return objects; + } + + /** + * 加密字符串 + * + * @param jsonString + * @param encrypts + * @return + */ + public static String encrypt(String jsonString, String[] encrypts, String encryKey) { + for (int i = 0; i < encrypts.length; i++) { + String encrypt = encrypt(jsonString, encrypts[i], encryKey); + if (org.apache.commons.lang3.StringUtils.isNotBlank(encrypt)) { + return encrypt; + } + } + return jsonString; + } + + /** + * 获取注解的方法 + * + * @param pjp + * @return + * @throws NoSuchMethodException + * @throws SecurityException + */ + public static Method getCurrentMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException, SecurityException { + MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); + return methodSignature.getMethod(); + } + + + /** + * 根据loggerName,获取Logger对象 + * + * @param loggerName + * @return + */ + public static Logger getLogger(String loggerName) { + if (LOGGERS.containsKey(loggerName)) { + return LOGGERS.get(loggerName); + } else { + synchronized (loggerName) { + Logger log = LoggerFactory.getLogger(loggerName); + LOGGERS.put(loggerName, log); + return log; + } + } + } + + public static String encrypt(String str, String encryKey) { + String encryptStr = null; + try { + encryptStr = AesUtils.encrypt(str, encryKey); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + return encryptStr; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/Logable.java b/itools-core/itools-common/src/main/java/com/itools/core/log/Logable.java new file mode 100644 index 0000000000000000000000000000000000000000..8a6f202230af4e642c4fe327a7de8cb82b06f185 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/Logable.java @@ -0,0 +1,68 @@ +package com.itools.core.log; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface Logable { + String loggerName() default "monitor"; + + String businessTag() default ""; + + Format format() default Format.TEXT; + + Level level() default Level.INFO; + + String errorCode() default ""; + + boolean outputArgs() default true; + + int[] ignoreOutputArgsIndex() default {}; + + boolean outputResult() default true; + + boolean outputError() default true; + + boolean encrypt() default false; + + int[] encryptArgsIndex() default {}; + + boolean showAll() default false; + /* + * 该方法是否是入口,对于Request=null时,如果指定该方法为入口,则在该方法处生成transactionNo,多用在JOB中 + * transacionNo是用于跟踪一个调用栈的 + */ + boolean isEntrance() default false; + + /** + * 如果是查询类方法,不需要输出响应值,可设置为QUERY,则日志记录中不会记录其响应内容,避免大规模查询(例如界面的查询批量订单) + * 输出过多无用日志 + * @return + */ + MethodType methodType() default MethodType.ACTION; + + enum Format { + JSON, + TEXT + } + + enum MethodType { + QUERY, + ACTION + } + + enum Level { + INFO, + DEBUG + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/ServerEnv.java b/itools-core/itools-common/src/main/java/com/itools/core/log/ServerEnv.java new file mode 100644 index 0000000000000000000000000000000000000000..96e13ea72b3a32bbdcde82afc80cf2268fee9876 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/ServerEnv.java @@ -0,0 +1,34 @@ +package com.itools.core.log; + +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Component +public class ServerEnv implements ApplicationListener { + private int serverPort; + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + this.serverPort = event.getWebServer().getPort(); + } + + public int getServerPort() { + return this.serverPort; + } + + public String getServerIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (Exception e) { + return "0.0.0.0"; + } + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java b/itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..9ab11d5fd3222983ff4eedcc76790ae0a0b6cab4 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java @@ -0,0 +1,57 @@ +package com.itools.core.log; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * 描述 :用户信息 + * + * @author lidab + * @date 2017/11/15. + */ +public class UserInfo { + String userId; + String userType; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserType() { + return userType; + } + + public void setUserType(String userType) { + this.userType = userType; + } + + /** + * 获取用户信息 + * + * @return + */ + public static UserInfo getUserInfo() { + String userId = "NONE"; + String userType = "NONE"; + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes != null) { + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + if (request != null) { + userId = request.getHeader(Constants.HeadKey.USERID.code) == null ? "NONE" : request.getHeader(Constants.HeadKey.USERID.code); + userType = request.getHeader(Constants.HeadKey.USERTYPE.code) == null ? "NONE" : request.getHeader(Constants.HeadKey.USERTYPE.code); + } + } + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + userInfo.setUserType(userType); + return userInfo; + } + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionContextHolder.java b/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionContextHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..2ad2f5b6ee5b0d933ac0d42b3d628fc9cad9ee45 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionContextHolder.java @@ -0,0 +1,39 @@ +package com.itools.core.session; + +/** + * @description: + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public final class BusinessSessionContextHolder { + + /** + * 线程本地变量缓存session + */ + private ThreadLocal sessionObject = new ThreadLocal<>(); + + /** + * 获取session信息 + * + * @return session会话信息 + */ + public BusinessSessionObject getSession() { + return sessionObject.get(); + } + + /** + * 设置session信息 + * + * @param businessSessionObject session会话信息 + */ + public void putSession(BusinessSessionObject businessSessionObject) { + this.sessionObject.set(businessSessionObject); + } + + /** + * 清空session信息 + */ + public void clearSession() { + this.sessionObject.remove(); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionObject.java b/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionObject.java new file mode 100644 index 0000000000000000000000000000000000000000..913b7c3160f75ce18da296b50b62ee8eef52887c --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/session/BusinessSessionObject.java @@ -0,0 +1,92 @@ +package com.itools.core.session; + +import com.itools.core.context.CompanyContext; +import com.itools.core.context.UserContext; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.Date; + +/** + * @description: + * @author: XUCHANG + * @time: 2019/12/4 10:51 + */ +public final class BusinessSessionObject implements Serializable { + private static final long serialVersionUID = -5960069764782340892L; + + /** + * 应用编号 + */ + private String applicationId; + + /** + * 请求时间 + */ + private Date callTime; + + /** + * 用户信息 + */ + private UserContext user; + + /** + * 企业信息 + */ + private CompanyContext company; + + /** + * 构造函数 + * + * @param values 会话数据 + */ + public BusinessSessionObject(String[] values) { + + Assert.notNull(values, "values is not null"); + if (values.length != 4) { + throw new IllegalArgumentException("values is not vaild"); + } + + this.user = new UserContext(); + this.company = new CompanyContext(); + + this.user.setCaller(values[0]); + this.user.setCallerName(values[2]); + this.company.setCompanyId(values[1]); + + this.setApplicationId(values[3]); + this.setCallTime(new Date()); + } + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public Date getCallTime() { + return callTime; + } + + public void setCallTime(Date callTime) { + this.callTime = callTime; + } + + public UserContext getUser() { + return user; + } + + public void setUser(UserContext user) { + this.user = user; + } + + public CompanyContext getCompany() { + return company; + } + + public void setCompany(CompanyContext company) { + this.company = company; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/SequenceService.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/SequenceService.java new file mode 100644 index 0000000000000000000000000000000000000000..f855b83c974709c5c599c84ca394a16c62f6d11a --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/SequenceService.java @@ -0,0 +1,25 @@ +package com.itools.core.snowflake; + +/** + * 描述 :序列服务类 + * @author xuchang + */ +public interface SequenceService { + + /** + * 根据分类,获取序列 + * + * @param category 分类 + * @return Sequence对象 + */ + public Long nextValue(String category); + + /** + * 如果大于maxValue,则会取整除maxValue后的余数 + * @param category + * @param maxValue + * @return + */ + public Long nextValue(String category, Long maxValue); + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/EnableSequenceService.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/EnableSequenceService.java new file mode 100644 index 0000000000000000000000000000000000000000..5f4c0cfb321d546aa855bd3e3d5a437e3905f760 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/EnableSequenceService.java @@ -0,0 +1,19 @@ +package com.itools.core.snowflake.config; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(SequenceAutoConfiguration.class) +public @interface EnableSequenceService { + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..27e9a224039e19ab96f873708b9f60f9daa12a65 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java @@ -0,0 +1,81 @@ +package com.itools.core.snowflake.config; + + +import com.itools.core.snowflake.impl.*; +import com.itools.core.snowflake.SequenceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@EnableConfigurationProperties({SequenceProperties.class}) +//@AutoConfigureAfter(PageHelperAutoConfiguration.class) +public class SequenceAutoConfiguration { + + /** + * simple获取节点 + */ + private static final String GENERATE_TYPE_SIMPLE = "simple"; + + /** + * 随机获取节点 + */ + private static final String GENERATE_TYPE_RANDOM = "random"; + + /** + * mac地址获取节点 + */ + private static final String GENERATE_TYPE_MAC = "mac"; + + @Autowired + private SequenceProperties properties; + + @Lazy + @Autowired + private RedisTemplate redisTemplate; + + @Primary + @Bean + @ConditionalOnProperty( + prefix = "sequence", + name = "type", + havingValue = "snowflake") + public SequenceService snowFlakeService(WorkNodeGenerate workNodeGenerate) { + if(redisTemplate == null){ + throw new IllegalStateException("Snowflake sequence service need RedisTemplate bean."); + } + SnowflakeSequenceService snowflakeSequenceService = new SnowflakeSequenceService(); + snowflakeSequenceService.setRedisTemplate(redisTemplate); + snowflakeSequenceService.setWorkNodeGenerate(workNodeGenerate); + return snowflakeSequenceService; + } + + + @Bean + @ConditionalOnProperty( + prefix = "sequence", + name = "type", + havingValue = "snowflake") + public WorkNodeGenerate workNodeGenerate() { + String generate = properties.getGenerate(); + if (GENERATE_TYPE_RANDOM.equals(generate)) { + return new RandomNodeGenerate(); + } else if (GENERATE_TYPE_SIMPLE.equals(generate)) { + return new SimpleNodeGenerate(); + } else if (GENERATE_TYPE_MAC.equals(generate)) { + return new MacNodeGenerate(); + } + return new RandomNodeGenerate(); + } + + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceProperties.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..11528ef50e91d6f1adfe5a1bf86b445f0e95cf34 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceProperties.java @@ -0,0 +1,51 @@ +package com.itools.core.snowflake.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @project: itools-backend + * @description: Logable注解的切面实现 + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@ConfigurationProperties( + prefix = "sequence" +) +public class SequenceProperties { + + private boolean enable = true; + + /** + * default, snowflake + */ + private String type; + + /** + * mac, random, simple + */ + private String generate; + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getGenerate() { + return generate; + } + + public void setGenerate(String generate) { + this.generate = generate; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/ClusterNode.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/ClusterNode.java new file mode 100644 index 0000000000000000000000000000000000000000..43dcfb255e71f40078d2ba5fc33d30f045430fef --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/ClusterNode.java @@ -0,0 +1,42 @@ +package com.itools.core.snowflake.impl; + +/** + * ClassName: ClusterNode
+ * Description: sequence服务分布式节点 + * Date: 2019/03/08 11:07 + * + * @author xuchang + */ +public class ClusterNode { + + /** + * 机房id + */ + private int centerId; + + /** + * 机器id + */ + private int workId; + + public ClusterNode(int centerId, int workId) { + this.centerId = centerId; + this.workId = workId; + } + + public int getCenterId() { + return centerId; + } + + public void setCenterId(int centerId) { + this.centerId = centerId; + } + + public int getWorkId() { + return workId; + } + + public void setWorkId(int workId) { + this.workId = workId; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/MacNodeGenerate.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/MacNodeGenerate.java new file mode 100644 index 0000000000000000000000000000000000000000..d5945287bc789ea55653f91d7a72394d816ff2a6 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/MacNodeGenerate.java @@ -0,0 +1,153 @@ +package com.itools.core.snowflake.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.itools.core.utils.NetworkUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.StringUtils; + +import java.net.NetworkInterface; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * ClassName: MacNodeGenerate
+ * Description: 分布式节点唯一编号按照机器man地址获取 + * 每个应用单独部署一台机器时使用,可保证获取到的workid不重复 + */ +public class MacNodeGenerate implements WorkNodeGenerate { + + /** redis work id hash key*/ + private static final String SNOW_FLAKE_WORK_ID_KEY = "SNOW_FLAKE_WORK::ID::KEY"; + + /** redis lock time out 5 seconds*/ + private static final int LOCK_TIMEOUT = 5; + + /** + * 机器id所占的位数 + */ + private final int workerIdBits = 5; + + /** 最大机器节点数 31 理论支持31*31台机器*/ + private final Integer MAX_NODE_ID = ~(-1 << workerIdBits); + + /** 最小节点编号 */ + private final Integer MIN_NODE_ID = 0; + + /** redis lock hash key*/ + private static String SNOW_FLAKE_KEY_LOCK; + + static { + SNOW_FLAKE_KEY_LOCK = SNOW_FLAKE_WORK_ID_KEY + "::LOCK"; + } + + @Override + public ClusterNode generate(RedisTemplate redisTemplate) { + return getClusterNode(getServer(), redisTemplate); + } + + @Override + public boolean release(RedisTemplate redisTemplate, ClusterNode node) { + return true; + } + + private ClusterNode getClusterNode(String server, RedisTemplate redisTemplate) { + if (!StringUtils.hasText(server)) { + throw new IllegalArgumentException("server can not be null."); + } else if (redisTemplate.opsForHash().hasKey(SNOW_FLAKE_WORK_ID_KEY, server)) { + Object value = redisTemplate.opsForHash().get(SNOW_FLAKE_WORK_ID_KEY, server); + return JSONObject.parseObject(value.toString(), ClusterNode.class); + } else { + return getNewClusterNode(server, redisTemplate); + } + } + + private ClusterNode getMaxClusterNode(Map entries) { + ClusterNode maxNode = null; + for (Map.Entry entry : entries.entrySet()) { + ClusterNode its = JSONObject.parseObject(entry.getValue().toString(), ClusterNode.class); + if (null == maxNode) { + maxNode = its; + } else if (maxNode.getCenterId() < its.getCenterId()) { + maxNode = its; + } else if (maxNode.getCenterId() == its.getCenterId() && maxNode.getWorkId() < its.getWorkId()) { + maxNode = its; + } + } + + return maxNode; + } + + private ClusterNode getNextClusterNode(Map entries) { + ClusterNode maxNode = getMaxClusterNode(entries); + int centerId = maxNode.getCenterId(); + int workId = maxNode.getWorkId() + 1; + if (workId > MAX_NODE_ID) { + ++centerId; + if (centerId > MAX_NODE_ID) { + throw new IllegalStateException("CenterId max."); + } + + workId = MIN_NODE_ID; + } + + return new ClusterNode(centerId, workId); + } + + private ClusterNode getNewClusterNode(String server, RedisTemplate redisTemplate) { + if (!lock(redisTemplate, server, LOCK_TIMEOUT)) { + throw new IllegalStateException("lock timeout from redis server."); + } else { + Map entries = redisTemplate.opsForHash().entries(SNOW_FLAKE_WORK_ID_KEY); + ClusterNode nextNode; + if (entries != null && !entries.isEmpty()) { + nextNode = getNextClusterNode(entries); + } else { + nextNode = new ClusterNode(MIN_NODE_ID, MIN_NODE_ID); + } + + redisTemplate.opsForHash().put(SNOW_FLAKE_WORK_ID_KEY, server, JSON.toJSONString(nextNode)); + return nextNode; + } + } + + private boolean lock(RedisTemplate redisTemplate, String server, Integer maxTimeout) { + boolean ret; + while (true) { + ret = redisTemplate.opsForHash().putIfAbsent(SNOW_FLAKE_KEY_LOCK, server, Long.toString(System.currentTimeMillis())); + if (ret) { + redisTemplate.expire(SNOW_FLAKE_KEY_LOCK, LOCK_TIMEOUT, TimeUnit.SECONDS); + return true; + } + + try { + TimeUnit.SECONDS.sleep(LOCK_TIMEOUT); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + Integer waitTime = maxTimeout; + maxTimeout = maxTimeout - 1; + if (waitTime > 0) { + continue; + } + + return false; + } + } + + private String getServer() { + Set networkInterfaceSet = NetworkUtils.getNICs(NetworkUtils.Filter.PHYSICAL_ONLY, NetworkUtils.Filter.UP); + Iterator it = networkInterfaceSet.iterator(); + String mac = ""; + while (it.hasNext()) { + mac = NetworkUtils.getMacAddress(it.next(), "-"); + if (StringUtils.hasText(mac)) { + break; + } + } + return mac.toUpperCase(); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/RandomNodeGenerate.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/RandomNodeGenerate.java new file mode 100644 index 0000000000000000000000000000000000000000..af44815d561a88deca18c633e7d34c01709364d9 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/RandomNodeGenerate.java @@ -0,0 +1,139 @@ +package com.itools.core.snowflake.impl; + +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * ClassName: RandomNodeGenerate
+ * Description:使用一个set维护datacenter, 使用set维护每一个datacenter的work list + *

+ * 每个机器多个应用时使用,可用于容器,不能根据id反查workid,因为workid会变 + */ +public class RandomNodeGenerate implements WorkNodeGenerate { + + /** + * 机器id所占的位数 + */ + private final int workerIdBits = 5; + + /** + * 最大机器节点数 31 理论支持31*31台机器 + */ + private final Integer MAX_NODE_ID = ~(-1 << workerIdBits); + + /** + * 最小节点编号 + */ + private final Integer MIN_NODE_ID = 0; + + private static String DATA_CENTER_KEY = "DATA_CENTER_KEY"; + + private static String WORK_ID_KEY_PREFIXX = "WORK_ID_KEY::"; + + private static String SNOW_FLAKE_KEY_LOCK; + + private static String SNOW_FLAKE_KEY_LOCK_VALUE; + + /** redis lock time out 5 seconds*/ + private static final int LOCK_TIMEOUT = 5; + + private Integer dataCenterId; + + static { + SNOW_FLAKE_KEY_LOCK = DATA_CENTER_KEY + "::LOCK"; + SNOW_FLAKE_KEY_LOCK_VALUE = SNOW_FLAKE_KEY_LOCK + "::VALUE"; + } + + /** + * 1.先找到可以使用的datacenter(满足work list大小不大于最大节点数即可) + * 2.取到已经找到的datacenter的 work list ,并占位 + * 3.datacenter id , workid + * + * @param redisTemplate + * @return + */ + @Override + public ClusterNode generate(RedisTemplate redisTemplate) { + if (!lock(redisTemplate, LOCK_TIMEOUT)) { + throw new IllegalStateException("lock timeout from redis server."); + }else { + this.dataCenterId = getDatacenterId(redisTemplate); + int workId = getWorkId(redisTemplate, dataCenterId); + return new ClusterNode(dataCenterId, workId); + } + } + + @Override + public boolean release(RedisTemplate redisTemplate, ClusterNode node) { + int datacenterId = node.getCenterId(); + int workId = node.getWorkId(); + return redisTemplate.opsForSet().remove(WORK_ID_KEY_PREFIXX + datacenterId, workId) == workId; + } + + private int getDatacenterId(RedisTemplate redisTemplate) { + Set set = redisTemplate.opsForSet().members(DATA_CENTER_KEY); + if (set == null || set.isEmpty()) { + redisTemplate.opsForSet().add(DATA_CENTER_KEY, MIN_NODE_ID); + return MIN_NODE_ID; + } + + for (int i = 0; i <= MAX_NODE_ID; i++) { + if (!set.contains(i)) { + redisTemplate.opsForSet().add(DATA_CENTER_KEY, i); + return i; + } else { + Set workSet = redisTemplate.opsForSet().members(WORK_ID_KEY_PREFIXX + i); + if (workSet == null || workSet.isEmpty() || workSet.size() <= MAX_NODE_ID) { + return i; + } + } + } + + throw new IllegalStateException("have no left datacenter."); + } + + private int getWorkId(RedisTemplate redisTemplate, int datacenterId) { + Set workSet = redisTemplate.opsForSet().members(WORK_ID_KEY_PREFIXX + datacenterId); + + if (workSet == null || workSet.isEmpty()) { + redisTemplate.opsForSet().add(WORK_ID_KEY_PREFIXX + datacenterId, MIN_NODE_ID); + return MIN_NODE_ID; + } + + for (int i = 0; i <= MAX_NODE_ID; i++) { + if (!workSet.contains(i)) { + redisTemplate.opsForSet().add(WORK_ID_KEY_PREFIXX + datacenterId, i); + return i; + } + } + this.dataCenterId = getDatacenterId(redisTemplate); + return getWorkId(redisTemplate, dataCenterId); + } + + private boolean lock(RedisTemplate redisTemplate , Integer maxTimeout) { + boolean ret; + while (true) { + ret = redisTemplate.opsForHash().putIfAbsent(SNOW_FLAKE_KEY_LOCK, SNOW_FLAKE_KEY_LOCK_VALUE, Long.toString(System.currentTimeMillis())); + if (ret) { + redisTemplate.expire(SNOW_FLAKE_KEY_LOCK, LOCK_TIMEOUT, TimeUnit.SECONDS); + return true; + } + + try { + TimeUnit.SECONDS.sleep(LOCK_TIMEOUT); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + Integer waitTime = maxTimeout; + maxTimeout = maxTimeout - 1; + if (waitTime > 0) { + continue; + } + + return false; + } + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SimpleNodeGenerate.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SimpleNodeGenerate.java new file mode 100644 index 0000000000000000000000000000000000000000..c0377b6b3f41d216e0926e4da8556199553e024c --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SimpleNodeGenerate.java @@ -0,0 +1,22 @@ +package com.itools.core.snowflake.impl; + +import org.springframework.data.redis.core.RedisTemplate; + +/** + * ClassName: SimpleNodeGenerate
+ * Description:非分布式环境使用 + * Date: 2019/03/08 11:07 + * @author xuchang + */ +public class SimpleNodeGenerate implements WorkNodeGenerate { + + @Override + public ClusterNode generate(RedisTemplate redisTemplate) { + return new ClusterNode(0, 0); + } + + @Override + public boolean release(RedisTemplate redisTemplate, ClusterNode node) { + return true; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowFlakeIdWorker.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowFlakeIdWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..e5d3c38837c24f33a3e08f3c97a650bea288cd1d --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowFlakeIdWorker.java @@ -0,0 +1,146 @@ +package com.itools.core.snowflake.impl; + +/** + * ClassName: SnowFlakeIdWorker
+ * Description: snowFlake算法 + * @author xuchang + */ +public class SnowFlakeIdWorker { + + /** + * 开始时间截 (2019-01-01) + */ + private final long twepoch = 1420041600000L; + + /** + * 机器id所占的位数 + */ + private final long workerIdBits = 5L; + + /** + * 数据标识id所占的位数 + */ + private final long datacenterIdBits = 5L; + + /** + * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) + */ + private final long maxWorkerId = -1L ^ (-1L << workerIdBits); + + /** + * 支持的最大数据标识id,结果是31 + */ + private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + + /** + * 序列在id中占的位数 + */ + private final long sequenceBits = 12L; + + /** + * 机器ID向左移12位 + */ + private final long workerIdShift = sequenceBits; + + /** + * 数据标识id向左移17位(12+5) + */ + private final long datacenterIdShift = sequenceBits + workerIdBits; + + /** + * 时间截向左移22位(5+5+12) + */ + private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + + /** + * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) + */ + private final long sequenceMask = -1L ^ (-1L << sequenceBits); + + /** + * 工作机器ID(0~31) + */ + private long workerId; + + /** + * 数据中心ID(0~31) + */ + private long datacenterId; + + /** + * 毫秒内序列(0~4095) + */ + private long sequence = 0L; + + /** + * 上次生成ID的时间截 + */ + private long lastTimestamp = -1L; + + public SnowFlakeIdWorker(long workerId, long datacenterId) { + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + + public synchronized Long nextValue() { + long timestamp = timeGen(); + + //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候 + //等待时间 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + //如果是同一时间生成的,则进行毫秒内序列 + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + //毫秒内序列溢出 + if (sequence == 0) { + //阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } + //时间戳改变,毫秒内序列重置 + else { + sequence = 0L; + } + + //上次生成ID的时间截 + lastTimestamp = timestamp; + + //移位并通过或运算拼到一起组成64位的ID + return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; + } + +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowflakeSequenceService.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowflakeSequenceService.java new file mode 100644 index 0000000000000000000000000000000000000000..8cacf14a54f32afc9a10f285236149aedb7dc079 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/SnowflakeSequenceService.java @@ -0,0 +1,65 @@ +package com.itools.core.snowflake.impl; + +import com.itools.core.snowflake.SequenceService; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +/** + * ClassName: SnowflakeSequenceService
+ * Description: 基于snowFlake算法实现序列号生成 + * @author xuchang + */ +@Service +public class SnowflakeSequenceService implements SequenceService { + + /** 最小节点编号 */ + private final Integer MIN_NODE_ID = 0; + + /** id worker*/ + private SnowFlakeIdWorker idWorker; + + /** redis client*/ + private RedisTemplate redisTemplate; + + private WorkNodeGenerate workNodeGenerate; + + private ClusterNode node; + + public void setRedisTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void setWorkNodeGenerate(WorkNodeGenerate workNodeGenerate) { + this.workNodeGenerate = workNodeGenerate; + } + + @PostConstruct + public void init() { + if (null == redisTemplate) { + this.idWorker = new SnowFlakeIdWorker(MIN_NODE_ID, MIN_NODE_ID); + } else { + node = workNodeGenerate.generate(redisTemplate); + this.idWorker = new SnowFlakeIdWorker(node.getWorkId(), node.getCenterId()); + } + } + + @Override + public Long nextValue(String category) { + return idWorker.nextValue(); + } + + @Override + public Long nextValue(String category, Long maxValue) { + return idWorker.nextValue(); + } + + @PreDestroy + public void destroy(){ + if(node != null){ + workNodeGenerate.release(redisTemplate, node); + } + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/WorkNodeGenerate.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/WorkNodeGenerate.java new file mode 100644 index 0000000000000000000000000000000000000000..1335129c514998ebf06d4ba70a9f03b4abe732e4 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/impl/WorkNodeGenerate.java @@ -0,0 +1,31 @@ +package com.itools.core.snowflake.impl; + +import org.springframework.data.redis.core.RedisTemplate; + +/** + * ClassName: WorkNodeGenerate
+ * Description: 分布式环境获取节点 + * Date: 2019/03/08 11:07 + * + * @author xuchang + */ +public interface WorkNodeGenerate { + + /** + * 生成节点 + * + * @param redisTemplate + * @return + */ + ClusterNode generate(RedisTemplate redisTemplate); + + /** + * 释放节点 + *

+ * 根据datacenter id 取到 worklist, 并从list removce workid + * + * @param node + * @return + */ + boolean release(RedisTemplate redisTemplate, ClusterNode node); +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/package-info.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..e7b7a227b6441fb46fb441cb924019737619922c --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/package-info.java @@ -0,0 +1,12 @@ +/** + * 集成分布式雪花算法 + * 容器启动加入@EnableSequenceService + * 使用结合SequenceService + * + * ###sequence 服务 ### + * sequence: + * enable: true + * type: snowflake + * generate: simple + */ +package com.itools.core.snowflake; \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/system/AbstractService.java b/itools-core/itools-common/src/main/java/com/itools/core/system/AbstractService.java new file mode 100644 index 0000000000000000000000000000000000000000..42f13d294efe691fd10c257546a56f5968c96688 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/system/AbstractService.java @@ -0,0 +1,30 @@ +package com.itools.core.system; + +import com.itools.core.code.SystemCodeService; +import com.itools.core.snowflake.SequenceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @project: iTools + * @description: + * @author: XUCHANG + * @create: 2020-09-28 22:56 + */ +public class AbstractService { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + @Autowired + private SystemCodeService systemCodeService; + @Autowired + protected SequenceService sequenceService; + + + public String getMessage(String code) { + return this.systemCodeService.getMessage(code); + } + + public String createGeneralCode() { + return String.valueOf(this.sequenceService.nextValue(null)); + } +} \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/validate/EnableValidator.java b/itools-core/itools-common/src/main/java/com/itools/core/validate/EnableValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..fa6a9b1070480dfd5fe7e6191705b3528c473239 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/validate/EnableValidator.java @@ -0,0 +1,16 @@ +package com.itools.core.validate; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + + +/** + * @author xuchang + */ +@Target(ElementType.TYPE) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Import(ValidatorConfig.class) +public @interface EnableValidator { +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidate.java b/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidate.java new file mode 100644 index 0000000000000000000000000000000000000000..50e3636aa73ba463fd9523a569325713f1a84f96 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidate.java @@ -0,0 +1,18 @@ +package com.itools.core.validate; + +import javax.validation.groups.Default; +import java.lang.annotation.*; + + +/** + * @author xuchang + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ParamsValidate { + + int[] argsIndexs() default {0}; + + Class[] groups() default {Default.class}; +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidateAspect.java b/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidateAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..52e1a5c68037e338abc094b3ec4f128d04980b02 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/validate/ParamsValidateAspect.java @@ -0,0 +1,101 @@ +package com.itools.core.validate; + + +import com.itools.core.exception.ParamException; +import com.itools.core.code.SystemCodeService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.lang.reflect.Method; +import java.util.Set; + + +@Aspect +@Component +public class ParamsValidateAspect { + + /** + * 日志 + */ + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * 参数校验对象 + */ + @Autowired + private Validator validator; + + /** + * 消息源资源对象 + */ + @Autowired + private SystemCodeService systemCodeService; + + /** + * 获取对应编码的信息描述 + * @param code 编码 + * @return + */ + public String getMessage(String code) { + return this.systemCodeService.getMessageOptional(code).orElse("Invalid Parameter"); + } + + /** + * 定义切面,扫描所有service的实现类 + */ + @Pointcut("@annotation(com.itools.core.validate.ParamsValidate)") + public void validatePointcut() {} + + /** + * 使用环绕通知对service实现类的参数进行检查 + * @param joinPoint + * @throws Throwable + */ + @Around("validatePointcut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method targetMethod = joinPoint.getTarget().getClass().getMethod( + methodSignature.getName(), methodSignature.getParameterTypes()); + + logger.debug("正在对类{}中方法{}进行参数检查", joinPoint.getTarget().getClass().getName(), targetMethod.getName()); + + // 查找带有参数校验的注解 + ParamsValidate paramsValidate = targetMethod.getAnnotation(ParamsValidate.class); + if (paramsValidate == null) { + return joinPoint.proceed(); + } + + // 遍历方法的所有参数,通过注解指定需要检查的索引,逐个检查 + int[] needCheckParamIndexes = paramsValidate.argsIndexs(); + + // 校验分组,如果未设置,使用默认的分组 + Class[] groupsClazz = paramsValidate.groups(); + + Object[] args = joinPoint.getArgs(); + for (int index : needCheckParamIndexes) { + Object arg = args[index]; + + Set> constraintViolationSet = validator.validate(arg, groupsClazz); + if (constraintViolationSet != null && constraintViolationSet.size() > 0) { + for (ConstraintViolation constraintViolation : constraintViolationSet) { + String code = constraintViolation.getMessage(); + throw new ParamException(code, getMessage(code)); + } + } + } + + logger.debug("类{}中方法{}参数检查通过", joinPoint.getTarget().getClass().getName(), targetMethod.getName()); + // 参数检测通过 + return joinPoint.proceed(); + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/validate/ValidatorConfig.java b/itools-core/itools-common/src/main/java/com/itools/core/validate/ValidatorConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..462d939c73e737ebb2e0c5a088c97a7e14c127d1 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/validate/ValidatorConfig.java @@ -0,0 +1,39 @@ +package com.itools.core.validate; + +import org.hibernate.validator.HibernateValidator; +import org.springframework.context.annotation.Bean; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + + +public class ValidatorConfig { + + /** + * 实例化HibernateValidator + * @return Validator + */ + @Bean + public Validator validator() { + ValidatorFactory validatorFactory = Validation + .byProvider(HibernateValidator.class) + .configure().failFast(true) + .buildValidatorFactory(); + + return validatorFactory.getValidator(); + } + + /** + * 默认是普通模式,会返回所有的验证不通过信息集合 + * @return + */ + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidator(validator()); + return postProcessor; + } +} + diff --git a/itools-core/itools-common/src/main/java/com/itools/core/validate/package-info.java b/itools-core/itools-common/src/main/java/com/itools/core/validate/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..ae8ff880f0d643269b2039b6a6aec66181c437db --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/validate/package-info.java @@ -0,0 +1,4 @@ +/** + * 容器启动需要加注解 + */ +package com.itools.core.validate; \ No newline at end of file diff --git a/itools-core/itools-common/src/main/java/com/itools/core/web/BaseController.java b/itools-core/itools-common/src/main/java/com/itools/core/web/BaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..2d9927febcda0574ff7b52acd35006ab8ea23b15 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/web/BaseController.java @@ -0,0 +1,27 @@ +package com.itools.core.web; + +import org.springframework.web.bind.annotation.ModelAttribute; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + + +public class BaseController { + protected HttpServletRequest request; + + protected HttpServletResponse response; + + protected HttpSession session; + + @ModelAttribute + public void setReqAndRes(HttpServletRequest request, HttpServletResponse response) { + + this.request = request; + + this.response = response; + + this.session = request.getSession(); + + } +} diff --git a/itools-core/itools-common/src/main/resources/META-INF/spring-configuration-metadata.json b/itools-core/itools-common/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..3cd08cd76e72978eb3b977d61dcd91680caef884 --- /dev/null +++ b/itools-core/itools-common/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,26 @@ +{ + "hints": [], + "groups": [ + { + "sourceType": "com.itools.core.snowflake.config.SequenceProperties", + "name": "sequence", + "type": "com.itools.core.snowflake.config.SequenceProperties" + } + ], + "properties": [ + { + "sourceType": "com.itools.core.snowflake.config.SequenceProperties", + "name": "sequence.type", + "type": "java.lang.String", + "defaultValue": "snowflake", + "description": "sequence type property, have two value with snowflake and default. default is snowflake" + }, + { + "sourceType": "com.itools.core.snowflake.config.SequenceProperties", + "name": "sequence.generate", + "type": "java.lang.String", + "defaultValue": "random", + "description": "this property worked with sequence.type=snowflake. mac = mac address, random = redis random value , simple = local value" + } + ] +} \ No newline at end of file diff --git a/itools-core/itools-common/src/main/resources/spring.factories b/itools-core/itools-common/src/main/resources/spring.factories new file mode 100644 index 0000000000000000000000000000000000000000..721b830029d3d54f408525662682f1574eb45e2b --- /dev/null +++ b/itools-core/itools-common/src/main/resources/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.itools.core.config.Swagger2Config \ No newline at end of file diff --git a/itools-core/itools-model/pom.xml b/itools-core/itools-model/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ead66b357b393e777e31906d581fdfacebbae5e1 --- /dev/null +++ b/itools-core/itools-model/pom.xml @@ -0,0 +1,20 @@ + + + + itools-core + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + jar + itools-model + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/itools-core/itools-model/src/main/java/com/itools/core/package-info.java b/itools-core/itools-model/src/main/java/com/itools/core/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d4c6d6e3cfd0afe26b20e56e970b0e7817e40579 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/package-info.java @@ -0,0 +1,4 @@ +/** + * model的实体类对象 + */ +package com.itools.core; \ No newline at end of file diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBInsertDTO.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBInsertDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..18b82bac20a41ef04cbe9b82f99370320b9fbd73 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBInsertDTO.java @@ -0,0 +1,38 @@ +package com.itools.core.rbac.dto.initDB; + +import lombok.Data; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBInsertDTO { + /** + * id + */ + private String id; + /** + * 执行的sql + */ + private String content; + /** + * 类型,ddl,dml,function + */ + private String type; + /** + * 版本 + */ + private String version; + /** + * 创建时间 + */ + private Date createTime; + /** + * 分支,环境 + */ + private String env; +} diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBQueryDTO.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBQueryDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..a26adcb9754d117a7b921a75ae3379d6f297ff44 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBQueryDTO.java @@ -0,0 +1,38 @@ +package com.itools.core.rbac.dto.initDB; + +import lombok.Data; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBQueryDTO { + /** + * id + */ + private String id; + /** + * 执行的sql + */ + private String content; + /** + * 类型,ddl,dml,function + */ + private String type; + /** + * 版本 + */ + private String version; + /** + * 创建时间 + */ + private Date createTime; + /** + * 分支,环境 + */ + private String env; +} diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBResultDTO.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBResultDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..7042ab9bc2131b1b380c9847026c921a951263e7 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/dto/initDB/InitDBResultDTO.java @@ -0,0 +1,38 @@ +package com.itools.core.rbac.dto.initDB; + +import lombok.Data; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBResultDTO { + /** + * id + */ + private String id; + /** + * 执行的sql + */ + private String content; + /** + * 类型,ddl,dml,function + */ + private String type; + /** + * 版本 + */ + private String version; + /** + * 创建时间 + */ + private Date createTime; + /** + * 分支,环境 + */ + private String env; +} diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBInsertParam.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBInsertParam.java new file mode 100644 index 0000000000000000000000000000000000000000..42b36d9cf637f0446142a1e88b8f168c647f32a2 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBInsertParam.java @@ -0,0 +1,16 @@ +package com.itools.core.rbac.param.initDB; + +import lombok.Data; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBInsertParam { + private String content; + private String type; + private String version; + private String env; +} diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBQueryParam.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBQueryParam.java new file mode 100644 index 0000000000000000000000000000000000000000..f7d17d7a1cf07dad7f0cb348dd95b3c3a327251b --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBQueryParam.java @@ -0,0 +1,20 @@ +package com.itools.core.rbac.param.initDB; + +import lombok.Data; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBQueryParam { + private String id; + private String content; + private String type; + private String version; + private Date createTime; + private String env; +} diff --git a/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBResult.java b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBResult.java new file mode 100644 index 0000000000000000000000000000000000000000..760a6311772e5e70f95c738a1be75dc3d483d319 --- /dev/null +++ b/itools-core/itools-model/src/main/java/com/itools/core/rbac/param/initDB/InitDBResult.java @@ -0,0 +1,20 @@ +package com.itools.core.rbac.param.initDB; + +import lombok.Data; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/9/11 9:57 + */ +@Data +public class InitDBResult { + private String id; + private String content; + private String type; + private String version; + private Date createTime; + private String env; +} diff --git a/itools-core/itools-utils/pom.xml b/itools-core/itools-utils/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..2808f4e53199184d3f5c733a6deb51c82d0ed316 --- /dev/null +++ b/itools-core/itools-utils/pom.xml @@ -0,0 +1,60 @@ + + + + itools-core + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + jar + itools-utils + + + javax.servlet + javax.servlet-api + + + org.springframework.boot + spring-boot-starter-logging + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + net.lingala.zip4j + zip4j + 1.3.1 + + + org.springframework.security + spring-security-crypto + 4.2.3.RELEASE + + + org.springframework.security + spring-security-jwt + 1.0.8.RELEASE + + + com.alibaba + fastjson + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/AesUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/AesUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..85707ead1291edde880ea90846f343df471bcc09 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/AesUtils.java @@ -0,0 +1,81 @@ +package com.itools.core.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + + +public class AesUtils { + + public final static String CHARSET = "utf-8"; + + /** + * 密钥算法 + */ + public final static String KEY_ALGORITHM = "AES"; + + /** + * 加密算法/工作模式/填充方式 + */ + public final static String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; + + + /** + * 转换密钥 + * + * @param key + * @return + * @throws Exception + */ + private static Key toKey(byte[] key) throws Exception { + // 生成秘密密钥 + SecretKey secretKey = new SecretKeySpec(key, KEY_ALGORITHM); + return secretKey; + } + + /** + * 解密 + * + * @param data + * @param key + * @return + * @throws Exception + */ + public static String decrypt(String data, String key) throws Exception { + // 还原密钥 + Key k = toKey(key.getBytes()); + // 实例化 + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + // 初始化, 设置为解密模式 + cipher.init(Cipher.DECRYPT_MODE, k); + // 执行操作 + return new String(cipher.doFinal(org.apache.commons.codec.binary.Base64.decodeBase64(data))); + } + + /** + * 加密 + * + * @param data + * @param key 16位 + * @return + * @throws Exception + */ + public static String encrypt(String data, String key) throws Exception { + // 还原密钥 + Key k = toKey(key.getBytes()); + // 实例化 + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + // 初始化, 设置为加密模式 + cipher.init(Cipher.ENCRYPT_MODE, k); + // 执行操作 + return new String(org.apache.commons.codec.binary.Base64.encodeBase64(cipher.doFinal(data.getBytes())), CHARSET); + + } + + public static void main(String[] args) throws Exception { + //高级加密标准,是下一代的加密算法标准,速度快,安全级别高, key的长度为16位 + String encrypt = AesUtils.encrypt("11", "hashtechiright00"); + System.out.println(encrypt); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/BCryptUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/BCryptUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..052f9fc6970279a423624a0cc1d01e9f750ddd77 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/BCryptUtil.java @@ -0,0 +1,21 @@ +package com.itools.core.utils; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @description: + * @author: XUCHANG + */ +public class BCryptUtil { + public static String encode(String password){ + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String hashPass = passwordEncoder.encode(password); + return hashPass; + } + public static boolean matches(String password,String hashPass){ + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + boolean f = passwordEncoder.matches(password, hashPass); + return f; + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64.java new file mode 100644 index 0000000000000000000000000000000000000000..1fbfccb24d59c87aac21c9570019514b3fce0b62 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64.java @@ -0,0 +1,224 @@ +package com.itools.core.utils; + +public class Base64 { + private static final byte[] encodingTable = new byte[]{65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47}; + private static final byte[] decodingTable = new byte[128]; + + public Base64() { + } + + public static byte[] encode(byte[] data) { + int modulus = data.length % 3; + byte[] bytes; + if (modulus == 0) { + bytes = new byte[4 * data.length / 3]; + } else { + bytes = new byte[4 * (data.length / 3 + 1)]; + } + + int dataLength = data.length - modulus; + int b1 = 0; + + int b2; + for(b2 = 0; b1 < dataLength; b2 += 4) { + int a1 = data[b1] & 255; + int a2 = data[b1 + 1] & 255; + int a3 = data[b1 + 2] & 255; + bytes[b2] = encodingTable[a1 >>> 2 & 63]; + bytes[b2 + 1] = encodingTable[(a1 << 4 | a2 >>> 4) & 63]; + bytes[b2 + 2] = encodingTable[(a2 << 2 | a3 >>> 6) & 63]; + bytes[b2 + 3] = encodingTable[a3 & 63]; + b1 += 3; + } + + int d1; + switch(modulus) { + case 0: + default: + break; + case 1: + d1 = data[data.length - 1] & 255; + b1 = d1 >>> 2 & 63; + b2 = d1 << 4 & 63; + bytes[bytes.length - 4] = encodingTable[b1]; + bytes[bytes.length - 3] = encodingTable[b2]; + bytes[bytes.length - 2] = 61; + bytes[bytes.length - 1] = 61; + break; + case 2: + d1 = data[data.length - 2] & 255; + int d2 = data[data.length - 1] & 255; + b1 = d1 >>> 2 & 63; + b2 = (d1 << 4 | d2 >>> 4) & 63; + int b3 = d2 << 2 & 63; + bytes[bytes.length - 4] = encodingTable[b1]; + bytes[bytes.length - 3] = encodingTable[b2]; + bytes[bytes.length - 2] = encodingTable[b3]; + bytes[bytes.length - 1] = 61; + } + + return bytes; + } + + public static byte[] decode(byte[] data) { + data = discardNonBase64Bytes(data); + byte[] bytes; + if (data[data.length - 2] == 61) { + bytes = new byte[(data.length / 4 - 1) * 3 + 1]; + } else if (data[data.length - 1] == 61) { + bytes = new byte[(data.length / 4 - 1) * 3 + 2]; + } else { + bytes = new byte[data.length / 4 * 3]; + } + + int i = 0; + + byte b1; + byte b2; + byte b3; + byte b4; + for(int j = 0; i < data.length - 4; j += 3) { + b1 = decodingTable[data[i]]; + b2 = decodingTable[data[i + 1]]; + b3 = decodingTable[data[i + 2]]; + b4 = decodingTable[data[i + 3]]; + bytes[j] = (byte)(b1 << 2 | b2 >> 4); + bytes[j + 1] = (byte)(b2 << 4 | b3 >> 2); + bytes[j + 2] = (byte)(b3 << 6 | b4); + i += 4; + } + + if (data[data.length - 2] == 61) { + b1 = decodingTable[data[data.length - 4]]; + b2 = decodingTable[data[data.length - 3]]; + bytes[bytes.length - 1] = (byte)(b1 << 2 | b2 >> 4); + } else if (data[data.length - 1] == 61) { + b1 = decodingTable[data[data.length - 4]]; + b2 = decodingTable[data[data.length - 3]]; + b3 = decodingTable[data[data.length - 2]]; + bytes[bytes.length - 2] = (byte)(b1 << 2 | b2 >> 4); + bytes[bytes.length - 1] = (byte)(b2 << 4 | b3 >> 2); + } else { + b1 = decodingTable[data[data.length - 4]]; + b2 = decodingTable[data[data.length - 3]]; + b3 = decodingTable[data[data.length - 2]]; + b4 = decodingTable[data[data.length - 1]]; + bytes[bytes.length - 3] = (byte)(b1 << 2 | b2 >> 4); + bytes[bytes.length - 2] = (byte)(b2 << 4 | b3 >> 2); + bytes[bytes.length - 1] = (byte)(b3 << 6 | b4); + } + + return bytes; + } + + public static byte[] decode(String data) { + data = discardNonBase64Chars(data); + byte[] bytes; + if (data.charAt(data.length() - 2) == '=') { + bytes = new byte[(data.length() / 4 - 1) * 3 + 1]; + } else if (data.charAt(data.length() - 1) == '=') { + bytes = new byte[(data.length() / 4 - 1) * 3 + 2]; + } else { + bytes = new byte[data.length() / 4 * 3]; + } + + int i = 0; + + byte b1; + byte b2; + byte b3; + byte b4; + for(int j = 0; i < data.length() - 4; j += 3) { + b1 = decodingTable[data.charAt(i)]; + b2 = decodingTable[data.charAt(i + 1)]; + b3 = decodingTable[data.charAt(i + 2)]; + b4 = decodingTable[data.charAt(i + 3)]; + bytes[j] = (byte)(b1 << 2 | b2 >> 4); + bytes[j + 1] = (byte)(b2 << 4 | b3 >> 2); + bytes[j + 2] = (byte)(b3 << 6 | b4); + i += 4; + } + + if (data.charAt(data.length() - 2) == '=') { + b1 = decodingTable[data.charAt(data.length() - 4)]; + b2 = decodingTable[data.charAt(data.length() - 3)]; + bytes[bytes.length - 1] = (byte)(b1 << 2 | b2 >> 4); + } else if (data.charAt(data.length() - 1) == '=') { + b1 = decodingTable[data.charAt(data.length() - 4)]; + b2 = decodingTable[data.charAt(data.length() - 3)]; + b3 = decodingTable[data.charAt(data.length() - 2)]; + bytes[bytes.length - 2] = (byte)(b1 << 2 | b2 >> 4); + bytes[bytes.length - 1] = (byte)(b2 << 4 | b3 >> 2); + } else { + b1 = decodingTable[data.charAt(data.length() - 4)]; + b2 = decodingTable[data.charAt(data.length() - 3)]; + b3 = decodingTable[data.charAt(data.length() - 2)]; + b4 = decodingTable[data.charAt(data.length() - 1)]; + bytes[bytes.length - 3] = (byte)(b1 << 2 | b2 >> 4); + bytes[bytes.length - 2] = (byte)(b2 << 4 | b3 >> 2); + bytes[bytes.length - 1] = (byte)(b3 << 6 | b4); + } + + return bytes; + } + + private static byte[] discardNonBase64Bytes(byte[] data) { + byte[] temp = new byte[data.length]; + int bytesCopied = 0; + + for(int i = 0; i < data.length; ++i) { + if (isValidBase64Byte(data[i])) { + temp[bytesCopied++] = data[i]; + } + } + + byte[] newData = new byte[bytesCopied]; + System.arraycopy(temp, 0, newData, 0, bytesCopied); + return newData; + } + + private static String discardNonBase64Chars(String data) { + StringBuffer sb = new StringBuffer(); + int length = data.length(); + + for(int i = 0; i < length; ++i) { + if (isValidBase64Byte((byte)data.charAt(i))) { + sb.append(data.charAt(i)); + } + } + + return sb.toString(); + } + + private static boolean isValidBase64Byte(byte b) { + if (b == 61) { + return true; + } else if (b >= 0 && b < 128) { + return decodingTable[b] != -1; + } else { + return false; + } + } + + static { + int i; + for(i = 0; i < 128; ++i) { + decodingTable[i] = -1; + } + + for(i = 65; i <= 90; ++i) { + decodingTable[i] = (byte)(i - 65); + } + + for(i = 97; i <= 122; ++i) { + decodingTable[i] = (byte)(i - 97 + 26); + } + + for(i = 48; i <= 57; ++i) { + decodingTable[i] = (byte)(i - 48 + 52); + } + + decodingTable[43] = 62; + decodingTable[47] = 63; + } +} \ No newline at end of file diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64Utils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..c033222e1687b569c8dea3c0b3925767536f2157 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Base64Utils.java @@ -0,0 +1,80 @@ +package com.itools.core.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Base64Utils { + private static final int CACHE_SIZE = 1024; + + public Base64Utils() { + } + + public static byte[] decode(String base64) { + return Base64.decode(base64.getBytes()); + } + + public static String encode(byte[] bytes) { + return new String(Base64.encode(bytes)); + } + + public static String encodeFile(String filePath) throws IOException { + byte[] bytes = fileToByte(filePath); + return encode(bytes); + } + + public static void decodeToFile(String filePath, String base64) throws IOException { + byte[] bytes = decode(base64); + byteArrayToFile(bytes, filePath); + } + + public static byte[] fileToByte(String filePath) throws IOException { + byte[] data = new byte[0]; + File file = new File(filePath); + if (file.exists()) { + FileInputStream in = new FileInputStream(file); + ByteArrayOutputStream out = new ByteArrayOutputStream(2048); + byte[] cache = new byte[1024]; + boolean var6 = false; + + int nRead; + while((nRead = in.read(cache)) != -1) { + out.write(cache, 0, nRead); + out.flush(); + } + + out.close(); + in.close(); + data = out.toByteArray(); + } + + return data; + } + + public static void byteArrayToFile(byte[] bytes, String filePath) throws IOException { + InputStream in = new ByteArrayInputStream(bytes); + File destFile = new File(filePath); + if (!destFile.getParentFile().exists()) { + destFile.getParentFile().mkdirs(); + } + + destFile.createNewFile(); + OutputStream out = new FileOutputStream(destFile); + byte[] cache = new byte[1024]; + boolean var6 = false; + + int nRead; + while((nRead = in.read(cache)) != -1) { + out.write(cache, 0, nRead); + out.flush(); + } + + out.close(); + in.close(); + } +} \ No newline at end of file diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/CharCheckUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CharCheckUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..0bc461e999b2fed77401bd127173ea077a8147fb --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CharCheckUtils.java @@ -0,0 +1,62 @@ +package com.itools.core.utils; + +import java.util.List; + +/** + * @description: + * @author: XUCHANG + */ +public class CharCheckUtils { + + /** + * 手机号码前三后四脱敏 + * @param mobile + * @return + */ + public static String mobileEncrypt(String mobile) { + if (StringUtils.isEmpty(mobile) || (mobile.length() != 11)) { + return mobile; + } + return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + + /** + * 身份证前三后四脱敏 + * @param id + * @return + */ + public static String idEncrypt(String id) { + if (StringUtils.isEmpty(id) || (id.length() < 8)) { + return id; + } + return id.replaceAll("(?<=\\w{3})\\w(?=\\w{4})", "*"); + } + + /** + * 护照前2后3位脱敏,护照一般为8或9位 + * @param id + * @return + */ + public static String idPassport(String id) { + if (StringUtils.isEmpty(id) || (id.length() < 8)) { + return id; + } + return id.substring(0, 2) + new String(new char[id.length() - 5]).replace("\0", "*") + id.substring(id.length() - 3); + } + + /** + * 判断list是否相同 + * @param list1 + * @param list2 + * @return + */ + public static boolean isEquals(List list1, List list2){ + if(null != list1 && null != list2){ + if(list1.containsAll(list2) && list2.containsAll(list1)){ + return true; + } + return false; + } + return true; + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/CollectionUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CollectionUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9a7726949cf8557b479a1f161f1052d42c42f61e --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CollectionUtils.java @@ -0,0 +1,368 @@ +/* + * Copyright 2002-2010 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 com.itools.core.utils; + +import java.util.*; + +/** + * @description: + * @author: XUCHANG + */ +public abstract class CollectionUtils { + + /** + * Return true if the supplied Collection is null + * or empty. Otherwise, return false. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return true if the supplied Map is null + * or empty. Otherwise, return false. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Convert the supplied array into a List. A primitive array gets + * converted into a List of the appropriate wrapper type. + *

A null source value will be converted to an + * empty List. + * @param source the (potentially primitive) array + * @return the converted List result + * @see ObjectUtils#toObjectArray(Object) + */ + public static List arrayToList(Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + public static List arrayToList(T[] source) { + List list = Utils.newArrayList(); + if(source != null) { + for(T item: source) { + list.add(item); + } + } + return list; + } + + /** + * Merge the given array into the given Collection. + * @param array the array to merge (may be null) + * @param collection the target Collection to merge the array into + */ + @SuppressWarnings("unchecked") + public static void mergeArrayIntoCollection(Object array, Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + Object[] arr = ObjectUtils.toObjectArray(array); + for (Object elem : arr) { + collection.add(elem); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + *

Uses Properties.propertyNames() to even catch + * default properties linked into the original Properties instance. + * @param props the Properties instance to merge (may be null) + * @param map the target Map to merge the properties into + */ + @SuppressWarnings("unchecked") + public static void mergePropertiesIntoMap(Properties props, Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.getProperty(key); + if (value == null) { + // Potentially a non-String value... + value = props.get(key); + } + map.put(key, value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + *

Enforces the given instance to be present, rather than returning + * true for an equal element as well. + * @param collection the Collection to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean containsInstance(Collection collection, Object element) { + if (collection != null) { + for (Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return true if any element in 'candidates' is + * contained in 'source'; otherwise returns false. + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in 'candidates' that is contained in + * 'source'. If no element in 'candidates' is present in + * 'source' returns null. Iteration order is + * {@link Collection} implementation specific. + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or null if not found + */ + public static Object findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or null if none or more than one such value found + */ + @SuppressWarnings("unchecked") + public static T findValueOfType(Collection collection, Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or null if none or more than one such value found + */ + public static Object findValueOfType(Collection collection, Class[] types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (Class type : types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * @param collection the Collection to check + * @return true if the collection contains a single reference or + * multiple references to the same instance, false else + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem : collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Find the common element type of the given Collection, if any. + * @param collection the Collection to check + * @return the common element type, or null if no clear + * common type has been found (or the collection was empty) + */ + public static Class findCommonElementType(Collection collection) { + if (isEmpty(collection)) { + return null; + } + Class candidate = null; + for (Object val : collection) { + if (val != null) { + if (candidate == null) { + candidate = val.getClass(); + } + else if (candidate != val.getClass()) { + return null; + } + } + } + return candidate; + } + + /** + * Adapts an enumeration to an iterator. + * @param enumeration the enumeration + * @return the iterator + */ + public static Iterator toIterator(Enumeration enumeration) { + return new EnumerationIterator(enumeration); + } + + /** + * @return item's property in oneCollection but not in otherCollection,ignore null values + */ + public static Collection> notExistsItems(Collection> oneCollection, + Collection> otherCollection, String oneProperty, String otherProperty) { + + Collection> result = Utils.newArrayList(); + for(Map oneItem: oneCollection) { + if(oneItem.get(oneProperty) == null) { + continue; + } + + String oneItemPropertyValue = oneItem.get(oneProperty).toString(); + boolean found = false; + for(Map otherItem: otherCollection) { + String otherItemPropertyValue = otherItem.get(otherProperty) == null? null:otherItem.get(otherProperty).toString(); + if(StringUtils.equals(oneItemPropertyValue, otherItemPropertyValue)) { + found = true; + break; + } + } + // + if(found) { + result.add(oneItem); + } + } + return result; + } + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + @Override + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + @Override + public E next() { + return this.enumeration.nextElement(); + } + + @Override + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/CookieUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CookieUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a941934eb66d1a298b6ec6e3cd8a37d38294ffbc --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/CookieUtil.java @@ -0,0 +1,109 @@ +package com.itools.core.utils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * @description: + * @author: XUCHANG + */ +public class CookieUtil { + + /** + * 设置cookie + * + * @param response + * @param name cookie名字 + * @param value cookie值 + * @param maxAge cookie生命周期 以秒为单位 + */ + public static void addCookie(HttpServletResponse response,String domain,String path, String name, + String value, int maxAge,boolean httpOnly) { + Cookie cookie = new Cookie(name, value); + cookie.setDomain(domain); + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(httpOnly); + response.addCookie(cookie); + } + + public static void setCookie(HttpServletResponse response,String path, String name, + String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath(path); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void updateCookie(HttpServletRequest request,HttpServletResponse response,String path, String name, + String value, int maxAge) { + deleteCookie(request.getCookies(),name); + setCookie(response,path,name,value,maxAge); + } + + public static void updateCookie(HttpServletRequest request,HttpServletResponse response,String domain,String path, String name, + String value, int maxAge,boolean httpOnly) { + deleteCookie(request.getCookies(),name); + addCookie(response,domain,path,name,value,maxAge,httpOnly); + } + + + + /** + * 根据cookie名称读取cookie + * @param request + * @return map + */ + + public static Map readCookie(HttpServletRequest request,String ... cookieNames) { + Map cookieMap = new HashMap(); + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + String cookieName = cookie.getName(); + String cookieValue = cookie.getValue(); + for(int i=0;i0) { + for (Cookie cookie : cookies) { + String cookieName = cookie.getName(); + if (StringUtils.equals(cookieName,name)){ + token = cookie.getValue(); + } + } + } + return token; + } + public static void deleteCookie(Cookie[] cookies,String name) { + + if (cookies != null && cookies.length>0) { + for (Cookie cookie : cookies) { + String cookieName = cookie.getName(); + if (StringUtils.equals(cookieName,name)){ + cookie.setMaxAge(-1); + } + } + } + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/DateUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..aeef54f21615e5f8c550d03b784eb355b5806bed --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/DateUtils.java @@ -0,0 +1,756 @@ +package com.itools.core.utils; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.SimpleTimeZone; + +/** + * @description: + * @author: XUCHANG + * @time: 2019/12/12 14:17 + */ +public final class DateUtils { + + private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + + private static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private static final String DATETIME_FORMAT = "yyyyMMddHHmmss"; + + private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; + + private static SimpleDateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT); + + private static SimpleDateFormat dateTimeFormat = new SimpleDateFormat(DEFAULT_DATETIME_FORMAT); + + private static SimpleDateFormat datePattern = new SimpleDateFormat(DATETIME_FORMAT); + + + private DateUtils() { + // + } + + /** + * 当前日期转为String + * + * @param date + * 日期 + * @param pattern + * 格式 + * @return + */ + public static String dateToString(Date date, String pattern) { + SimpleDateFormat dateFormat = new SimpleDateFormat(pattern); + return dateFormat.format(date); + } + + /** + * 获得日期相差天数 + * + * @param date1 + * @param date2 + * @return 相差天数 + */ + public static int getDateDifferent(Date date1, Date date2) { + if (date1 == null || date2 == null) { + return -1; + } + long intervalMilli = date1.getTime() - date2.getTime(); + return (int) (intervalMilli / (24 * 60 * 60 * 1000)); + } + + /** + * 获得往后推迟amount天的时间 如果填入负数,则往前推 + * + * @param date + * @param amount + * @return + */ + public static Date addDate(Date date, int amount) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + calendar.add(Calendar.DATE, amount); + date = calendar.getTime(); + return date; + } + + public static Date addMonth(Date date, int monthCnt) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + calendar.add(Calendar.MONTH, monthCnt); + date = calendar.getTime(); + return date; + } + + /** + * 获得往后推迟amount的时间 + * + * @param date + * @param amount + * 分钟数 + * @return + */ + public static Date addDateWithMin(Date date, int amount) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + calendar.add(Calendar.MINUTE, amount); + date = calendar.getTime(); + return date; + } + + /** + * 以入参构造一个新的Date,时、分、秒、毫秒均设置为0 + * + * @param date + * @return + */ + public static Date getFirstSecondOfDate(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + public static Date getLastSecondOfDate(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + + public static Timestamp getNow() { + return new Timestamp(System.currentTimeMillis()); + } + + /** + * 使用指定的格式将字符串解析成日期 + * + * @modify Dec 26, 2006 11:06:48 AM + * @param dateString + * @param format + * @return + */ + public static Date parseDate(String dateString, String format) { + if (StringUtils.isEmpty(dateString)) { + return null; + } + try { + return new SimpleDateFormat(format).parse(dateString); + } catch (Exception e) { + return null; + } + } + + /** + * 将字符串解析成日期 + * + * @modify Dec 26, 2006 11:06:27 AM + * @param dateString + * @return + */ + public static Date parseDate(String dateString) { + if (StringUtils.isEmpty(dateString)) { + return null; + } + try { + if (dateString.length() <= 10) { + return dateFormat.parse(dateString); + } else { + if(dateString.length() == 14){ + return dateTimeFormat.parse(dateString); + } + return dateTimeFormat.parse(dateString); + } + } catch (Exception e) { + return null; + } + } + + /** + * 解析日期时间 + * + * @modify Dec 28, 2006 9:31:26 AM + * @param dateString + * @return + */ + public static Timestamp parseDatetime(String dateString) { + if (StringUtils.isEmpty(dateString)) { + return null; + } + Date date = null; + try { + date = dateTimeFormat.parse(dateString); + } catch (Exception e) { + date = parseDate(dateString); + } + return date == null ? null : new Timestamp(date.getTime()); + } + + public static int get(Date date, Field field) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Field.toInt(field)); + } + + /** + * 获取日期中的年份 + * + * @modify Dec 26, 2006 11:05:34 AM + * @param date + * @return + */ + public static int getYear(Date date) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.YEAR); + } + + /** + * 获取日期所在的季度 + * + * @modify Dec 29, 2006 11:13:26 AM + * @param date + * @return + */ + public static Season getSeason(Date date) { + Month m = getMonth(date); + int mi = Month.toInt(m); + return Season.valueOf(mi / 3); + } + + /** + * 获取日期中的月份 + * + * @modify Dec 26, 2006 11:05:50 AM + * @param date + * @return + */ + public static Month getMonth(Date date) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return Month.valueOf(cal.get(Calendar.MONTH)); + } + + /** + * 获取日期中的天 从1开始 + * + * @modify Dec 26, 2006 11:06:04 AM + * @param date + * @return + */ + public static int getDay(Date date) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.DATE); + } + + /** + * 计算获取每年的第几天 + * + * @modify Dec 25, 2006 2:40:39 PM + * @param year + * @param days + * 1表示年的第一天 + * @return 那一天的日期类型 + */ + public static Date getDate(int year, int days) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, 0); + cal.add(Calendar.DATE, days); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + /** + * 根据年月日获取日期 + * + * @modify Dec 26, 2006 1:04:12 PM + * @param year + * @param month + * 0表示一月 + * @param day + * 1表示月的第一天 + * @return + */ + public static Date getDate(int year, int month, int day) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + cal.set(Calendar.DATE, day); + return getDate(cal.getTime()); + } + + /** + * 同上 + * + * @modify Dec 29, 2006 10:17:36 AM + * @param year + * @param month + * @param day + * @return + */ + public static Date getDate(int year, Month month, int day) { + return getDate(year, Month.toInt(month), day); + } + + /** + * 取得季度的的一个月 + * + * @modify Dec 29, 2006 10:27:28 AM + * @param season + * @return + */ + public static Month getFirstMonthOfSeason(Season season) { + return Month.valueOf(Season.toInt(season) * 3); + } + + /** + * 获取那个季度的第几天 + * + * @modify Dec 29, 2006 10:24:39 AM + * @param year + * @param season + * @param days + * @return + */ + public static Date getDate(final int year, final Season season, final int days) { + Month firstMonth = getFirstMonthOfSeason(season); + Date firstDate = getDate(year, firstMonth, 1); + Date date = add(firstDate, Field.DATE, days - 1); + int y = getYear(date); + Season s = getSeason(date); + if (y != year || s != season) { + throw new IllegalArgumentException("day is too large!"); + } + return date; + } + + /** + * 获取日期部分 + * + * @modify Dec 29, 2006 9:18:08 AM + * @param date + * @return + */ + public static Date getDate(final Date date) { + return setFields(date, new Field[] { Field.HOUR, Field.MINUTE, + Field.SECOND, Field.MILLISECOND }, new int[] { 0, 0, 0, 0 }); + } + + /** + * 根据给定值获取日期时间 + * + * @modify Dec 29, 2006 11:13:02 AM + * @param year + * @param month + * @param day + * @param hour + * @param min + * @param seconds + * @return + */ + public static Date getDate(int year, int month, int day, int hour, int min, int seconds) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + cal.set(Calendar.DATE, day); + cal.set(Calendar.HOUR, hour); + cal.set(Calendar.MINUTE, min); + cal.set(Calendar.SECOND, seconds); + return cal.getTime(); + } + + /** + * 计算年度中一月的天数 0:是一月 + * + * @modify Dec 26, 2006 11:03:46 AM + * @param year + * @param month + * @return + */ + public static int getMonthDays(int year, Month month) { + Calendar cal1 = Calendar.getInstance(); + cal1.set(Calendar.YEAR, year); + cal1.set(Calendar.MONTH, Month.toInt(month)); + Calendar cal2 = Calendar.getInstance(); + cal2.set(Calendar.YEAR, year); + cal2.set(Calendar.MONTH, Month.toInt(month) + 1); + long time = cal2.getTimeInMillis() - cal1.getTimeInMillis(); + time = time / (24 * 3600 * 1000); + return (int) time; + } + + /** + * 一个星期的第几天 1:周日,2:周一 + * + * @modify Dec 26, 2006 1:01:08 PM + * @param date + * @return + */ + public static Weekday getWeekDay(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return Weekday.valueOf(cal.get(Calendar.DAY_OF_WEEK)); + } + + /** + * 获取一个月第几周的所有日期 + * + * @modify Jan 4, 2007 10:01:10 PM + * @param year + * @param month + * @param index + * 从0开始计数 + * @return + */ + public static Date[] getWeekDays(int year, Month month, int index) { + Date[] result = new Date[7]; + Date date = getDate(year, month, index * 7); + setFields(date, new Field[] { Field.HOUR, Field.MINUTE, Field.SECOND }, new int[] { 0, 0, 0 }); + date = add(date, Field.DATE, Weekday.toInt(getWeekDay(date))); + for (int i = 0; i < 7; i++) { + result[i] = add(date, Field.DATE, i); + } + return result; + } + + /** + * 找到最近的一个周天,这一天与给定的天在同一周 + * + * @modify Dec 28, 2006 5:30:30 PM + * @param date + * @param week + * 0~6,分别取周日到周六 + * @return + */ + public static Date getNearestWeekDay(Date date, Weekday week) { + Weekday weekday = getWeekDay(date); + return add(date, Field.DATE, Weekday.toInt(week) - Weekday.toInt(weekday)); + } + + /** + * 获取当月的第几天 + * + * @modify Dec 28, 2006 11:08:50 PM + * @param date + * @param day + * @return + */ + public static Date getNearestMonthDay(Date date, int day) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH); + return getDate(year, month, day); + } + + /** + * 比较两个日期的大小 + * + * @modify Nov 7, 2006 10:37:49 PM + * @param date1 + * @param date2 + * @return + */ + public static int compareTo(Date date1, Date date2) { + if (date1 == null && date2 == null) { + return 0; + } + if (date1 == null && date2 != null) { + return -1; + } + if (date1 != null && date2 == null) { + return 1; + } + return (int) (date1.compareTo(date2)); + } + + /** + * 判断指定日期是否在指定范围内 + * + * @modify Dec 28, 2006 3:12:21 PM + * @param input + * @param from + * @param to + * @return + */ + public static boolean between(Date input, Date from, Date to) { + return compareTo(input, from) >= 0 && compareTo(input, to) <= 0; + } + + /** + * 使用缺省的格式格式化日期 + * + * @modify Dec 25, 2006 2:41:25 PM + * @param date + * @return + */ + public static String formatDate(Date date) { + if (date == null) { + return ""; + } + if (date instanceof java.sql.Date) { + return dateFormat.format(date); + } else { + return dateTimeFormat.format(date); + } + } + + /** + * 使用指定的格式将日期格式化 + * + * @modify Dec 26, 2006 11:07:16 AM + * @param date + * @param format + * @return + */ + public static String formatDate(Date date, String format) { + if (date == null) { + return ""; + } + SimpleDateFormat sdf = new SimpleDateFormat(format); + return sdf.format(date); + } + + /** + * 向日期增加若干部分 + * + * @modify Dec 28, 2006 4:00:42 PM + * @param date + * @param field + * @param amount + * @return + */ + public static Date add(final Date date, final Field field, final int amount) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + Field type = field; + if (type == null) { + type = Field.DATE; + } + // + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + + int calType = Field.toInt(field); + if (calType != -1) { + cal.add(calType, amount); + } + return cal.getTime(); + } + + /** + * 设置一个字段的值 + * + * @modify Dec 28, 2006 5:14:00 PM + * @param date + * @param field + * @param amount + * @return + */ + public static Date setField(final Date date, final Field field, final int amount) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + // + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + + int calType = Field.toInt(field); + if (calType != -1) { + cal.set(calType, amount); + } + return cal.getTime(); + } + + public static Date setFields(final Date date, final Field[] fields, final int[] amounts) { + if (date == null) { + throw new IllegalArgumentException("Date is null!"); + } + // + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + + for (int i = 0; i < fields.length; i++) { + int calType = Field.toInt(fields[i]); + if (calType != -1) { + cal.set(calType, amounts[i]); + } + } + return cal.getTime(); + } + + /** + * 时间段类型 + * + * @author + * @create Dec 28, 2006 3:59:27 PM + * + */ + public static enum Field { + YEAR, MONTH, DATE, HOUR, MINUTE, SECOND, MILLISECOND; + + public static int toInt(final Field type) { + int calType = -1; + if (type == Field.YEAR) { + calType = Calendar.YEAR; + } else if (type == Field.MONTH) { + calType = Calendar.MONTH; + } else if (type == Field.DATE) { + calType = Calendar.DATE; + } else if (type == Field.HOUR) { + calType = Calendar.HOUR_OF_DAY; + } else if (type == Field.MINUTE) { + calType = Calendar.MINUTE; + } else if (type == Field.SECOND) { + calType = Calendar.SECOND; + } else if (type == Field.MILLISECOND) { + calType = Calendar.MILLISECOND; + } + return calType; + } + } + + /** + * 季度枚举 + * + * @author + * @create Dec 29, 2006 9:24:04 AM + * + */ + public static enum Season { + SPRING, SUMMER, AUTUMN, WINTER; + + public static Season valueOf(final int i) { + if (i >= 0 && i <= 3) { + return Season.values()[i]; + } else { + return null; + } + } + + public static int toInt(final Season season) { + Season[] seasons = Season.values(); + for (int i = 0; i < seasons.length; i++) { + if (seasons[i] == season) { + return i; + } + } + return -1; + } + } + + /** + * 枚举月 0:一月; 11:12月 + * + * @author + * @create Dec 29, 2006 9:28:06 AM + * + */ + public static enum Month { + JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC; + + /** + * 取值0~11 + * + * @modify Dec 29, 2006 11:09:33 AM + * @param i + * @return + */ + public static Month valueOf(final int i) { + if (i >= 0 && i <= 11) { + return Month.values()[i]; + } else { + return null; + } + } + + /** + * 返回值0~11 + * + * @modify Dec 29, 2006 11:09:16 AM + * @param month + * @return + */ + public static int toInt(final Month month) { + Month[] months = Month.values(); + for (int i = 0; i < months.length; i++) { + if (months[i] == month) { + return i; + } + } + return -1; + } + } + + /** + * 枚举周 1:周日, 7:周六 + * + * @author + * @create Dec 29, 2006 9:29:07 AM + * + */ + public static enum Weekday { + SUN, MON, TUE, WEN, THR, FRI, SAT; + + /** + * 从1开始获取 + * + * @modify Dec 29, 2006 10:05:20 AM + * @param i + * @return + */ + public static Weekday valueOf(final int i) { + if (i >= 1 && i <= 7) { + return Weekday.values()[i - 1]; + } else { + return null; + } + } + + public static int toInt(final Weekday week) { + Weekday[] weeks = Weekday.values(); + for (int i = 0; i < weeks.length; i++) { + if (weeks[i] == week) { + return i; + } + } + return -1; + } + } + + protected final static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static String toGMT(Date date) { + Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); + format.setCalendar(cal); + return format.format(date); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/FileOperateUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/FileOperateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..ba2dcfc2e8b66a8c6709ca0ad44909afb643ea3a --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/FileOperateUtils.java @@ -0,0 +1,265 @@ +package com.itools.core.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * @description:复制、删除、剪切文件封装的工具类 + * @author: XUCHANG + */ +public class FileOperateUtils { + /** + * 复制文件或文件夹 + * + * @param srcPath + * @param destDir + * 目标文件所在的目录 + * @return + */ + public static boolean copyGeneralFile(String srcPath, String destDir) { + boolean flag = false; + File file = new File(srcPath); + if (!file.exists()) { + System.out.println("源文件或源文件夹不存在!"); + return false; + } + if (file.isFile()) { // 源文件 + System.out.println("下面进行文件复制!"); + flag = copyFile(srcPath, destDir); + } else if (file.isDirectory()) { + System.out.println("下面进行文件夹复制!"); + flag = copyDirectory(srcPath, destDir); + } + + return flag; + } + + /** + * 复制文件 + * + * @param srcPath + * 源文件绝对路径 + * @param destDir + * 目标文件所在目录 + * @return boolean + */ + private static boolean copyFile(String srcPath, String destDir) { + boolean flag = false; + + File srcFile = new File(srcPath); + if (!srcFile.exists()) { // 源文件不存在 + System.out.println("源文件不存在"); + return false; + } + // 获取待复制文件的文件名 + File file = new File(srcPath); + String fileName = file.getName(); + // String fileName = srcPath.substring(srcPath.lastIndexOf("//")+2, + // srcPath.length()); + // String fileName = srcPath + // .substring(srcPath.lastIndexOf(File.separator)); + String destPath = destDir + fileName; + if (destPath.equals(srcPath)) { // 源文件路径和目标文件路径重复 + System.out.println("源文件路径和目标文件路径重复!"); + return false; + } + File destFile = new File(destPath); + if (destFile.exists() && destFile.isFile()) { // 该路径下已经有一个同名文件 + System.out.println("目标目录下已有同名文件!"); + return false; + } + + File destFileDir = new File(destDir); + destFileDir.mkdirs(); + try { + FileInputStream fis = new FileInputStream(srcPath); + FileOutputStream fos = new FileOutputStream(destFile); + byte[] buf = new byte[1024]; + int c; + while ((c = fis.read(buf)) != -1) { + fos.write(buf, 0, c); + } + fis.close(); + fos.close(); + + flag = true; + } catch (IOException e) { + // + } + + if (flag) { + System.out.println("复制文件成功!"); + } + + return flag; + } + + /** + * + * @param srcPath + * 源文件夹路径 + * @param destDir + * 目标文件夹所在目录 + * @return + */ + private static boolean copyDirectory(String srcPath, String destDir) { + System.out.println("复制文件夹开始!"); + boolean flag = false; + + File srcFile = new File(srcPath); + if (!srcFile.exists()) { // 源文件夹不存在 + System.out.println("源文件夹不存在"); + return false; + } + // 获得待复制的文件夹的名字,比如待复制的文件夹为"E://dir"则获取的名字为"dir" + String dirName = getDirName(srcPath); + // 目标文件夹的完整路径 + String destPath = destDir + File.separator + dirName; + // System.out.println("目标文件夹的完整路径为:" + destPath); + + if (destPath.equals(srcPath)) { + System.out.println("目标文件夹与源文件夹重复"); + return false; + } + File destDirFile = new File(destPath); + if (destDirFile.exists()) { // 目标位置有一个同名文件夹 + System.out.println("目标位置已有同名文件夹!"); + return false; + } + destDirFile.mkdirs(); // 生成目录 + + File[] fileList = srcFile.listFiles(); // 获取源文件夹下的子文件和子文件夹 + if (fileList.length == 0) { // 如果源文件夹为空目录则直接设置flag为true,这一步非常隐蔽,debug了很久 + flag = true; + } else { + for (File temp : fileList) { + if (temp.isFile()) { // 文件 + flag = copyFile(temp.getAbsolutePath(), destPath); + } else if (temp.isDirectory()) { // 文件夹 + flag = copyDirectory(temp.getAbsolutePath(), destPath); + } + if (!flag) { + break; + } + } + } + + if (flag) { + System.out.println("复制文件夹成功!"); + } + + return flag; + } + + /** + * 获取待复制文件夹的文件夹名 + * + * @param dir + * @return String + */ + private static String getDirName(String dir) { + if (dir.endsWith(File.separator)) { // 如果文件夹路径以"//"结尾,则先去除末尾的"//" + dir = dir.substring(0, dir.lastIndexOf(File.separator)); + } + return dir.substring(dir.lastIndexOf(File.separator) + 1); + } + + /** + * 删除文件或文件夹 + * + * @param path + * 待删除的文件的绝对路径 + * @return boolean + */ + public static boolean deleteGeneralFile(String path) { + boolean flag = false; + + File file = new File(path); + if (!file.exists()) { // 文件不存在 + System.out.println("要删除的文件不存在!"); + } + + if (file.isDirectory()) { // 如果是目录,则单独处理 + flag = deleteDirectory(file.getAbsolutePath()); + } else if (file.isFile()) { + flag = deleteFile(file); + } + + if (flag) { + System.out.println("删除文件或文件夹成功!"); + } + + return flag; + } + + /** + * 删除文件 + * + * @param file + * @return boolean + */ + private static boolean deleteFile(File file) { + return file.delete(); + } + + /** + * 删除目录及其下面的所有子文件和子文件夹,注意一个目录下如果还有其他文件或文件夹 + * 则直接调用delete方法是不行的,必须待其子文件和子文件夹完全删除了才能够调用delete + * + * @param path + * path为该目录的路径 + */ + private static boolean deleteDirectory(String path) { + boolean flag = true; + File dirFile = new File(path); + if (!dirFile.isDirectory()) { + return flag; + } + File[] files = dirFile.listFiles(); + for (File file : files) { // 删除该文件夹下的文件和文件夹 + // Delete file. + if (file.isFile()) { + flag = deleteFile(file); + } else if (file.isDirectory()) {// Delete folder + flag = deleteDirectory(file.getAbsolutePath()); + } + if (!flag) { // 只要有一个失败就立刻不再继续 + break; + } + } + flag = dirFile.delete(); // 删除空目录 + return flag; + } + + /** + * 由上面方法延伸出剪切方法:复制+删除 + * + * @param destDir + * 同上 + */ + public static boolean cutGeneralFile(String srcPath, String destDir) { + if (!copyGeneralFile(srcPath, destDir)) { + System.out.println("复制失败导致剪切失败!"); + return false; + } + if (!deleteGeneralFile(srcPath)) { + System.out.println("删除源文件(文件夹)失败导致剪切失败!"); + return false; + } + + System.out.println("剪切成功!"); + return true; + } + + public static void main(String[] args) { + copyGeneralFile("E://Assemble.txt", "E://New.txt"); // 复制文件 + copyGeneralFile("E://hello", "E://world"); // 复制文件夹 + deleteGeneralFile("E://onlinestockdb.sql"); // 删除文件 + deleteGeneralFile("E://woman"); // 删除文件夹 + cutGeneralFile("E://hello", "E://world"); // 剪切文件夹 + cutGeneralFile("E://Difficult.java", "E://Cow//"); // 剪切文件 + } +} + diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/GenerateOrderNum.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/GenerateOrderNum.java new file mode 100644 index 0000000000000000000000000000000000000000..151dc4cc591b21de292e249148771134f9305404 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/GenerateOrderNum.java @@ -0,0 +1,82 @@ +package com.itools.core.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @description: + * @author: XUCHANG + */ +public class GenerateOrderNum { + /** + * 锁对象,可以为任意对象 + */ + private static Object lockObj = "lockerOrder"; + /** + * 订单号生成计数器 + */ + private static long orderNumCount = 0L; + /** + * 每毫秒生成订单号数量最大值 + */ + private int maxPerMSECSize=1000; + + /** + * + */ + + /** + * 生成非重复订单号,理论上限1毫秒1000个,可扩展 + * @param tname 测试用 + */ + public synchronized void generate(String tname) { + try { + // 最终生成的订单号 + String finOrderNum = ""; + synchronized (lockObj) { + // 取系统当前时间作为订单号变量前半部分,精确到毫秒 + long nowLong = Long.parseLong(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())); + // 计数器到最大值归零,可扩展更大,目前1毫秒处理峰值1000个,1秒100万 + if (orderNumCount >= maxPerMSECSize) { + orderNumCount = 0L; + } + //组装订单号 + String countStr=maxPerMSECSize +orderNumCount+""; + finOrderNum=nowLong+countStr.substring(1); + orderNumCount++; + System.out.println(finOrderNum + "--" + Thread.currentThread().getName() + "::" + tname ); + // Thread.sleep(1000); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + // 测试多线程调用订单号生成工具 + try { + for (int i = 0; i < 200; i++) { + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + GenerateOrderNum generateOrderNum = new GenerateOrderNum(); + generateOrderNum.generate("a"); + } + }, "at" + i); + t1.start(); + + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + GenerateOrderNum generateOrderNum = new GenerateOrderNum(); + generateOrderNum.generate("b"); + } + }, "bt" + i); + t2.start(); + } + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println(System.currentTimeMillis()); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/HlsVideoUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/HlsVideoUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e2fa86dde87d50c73fca99f585f58e23a2ec9d31 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/HlsVideoUtil.java @@ -0,0 +1,158 @@ +package com.itools.core.utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * * 此文件用于视频文件处理,步骤如下: + * 1、生成mp4 + * 2、生成m3u8 + * @author: XUCHANG + */ +public class HlsVideoUtil extends VideoUtil { + + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + String video_path = "D:\\BaiduNetdiskDownload\\test1.avi"; + String m3u8_name = "test1.m3u8"; + String m3u8folder_path = "D:/BaiduNetdiskDownload/Movies/test1/"; + public HlsVideoUtil(String ffmpeg_path, String video_path, String m3u8_name,String m3u8folder_path){ + super(ffmpeg_path); + this.ffmpeg_path = ffmpeg_path; + this.video_path = video_path; + this.m3u8_name = m3u8_name; + this.m3u8folder_path = m3u8folder_path; + } + + private void clear_m3u8(String m3u8_path){ + //删除原来已经生成的m3u8及ts文件 + File m3u8dir = new File(m3u8_path); + if(!m3u8dir.exists()){ + m3u8dir.mkdirs(); + } + /* if(m3u8dir.exists()&&m3u8_path.indexOf("/hls/")>=0){//在hls目录方可删除,以免错误删除 + String[] children = m3u8dir.list(); + //删除目录中的文件 + for (int i = 0; i < children.length; i++) { + File file = new File(m3u8_path, children[i]); + file.delete(); + } + }else{ + m3u8dir.mkdirs(); + }*/ + } + + /** + * 生成m3u8文件 + * @return 成功则返回success,失败返回控制台日志 + */ + public String generateM3u8(){ + //清理m3u8文件目录 + clear_m3u8(m3u8folder_path); + /* + ffmpeg -i lucene.mp4 -hls_time 10 -hls_list_size 0 -hls_segment_filename ./hls/lucene_%05d.ts ./hls/lucene.m3u8 + */ +// String m3u8_name = video_name.substring(0, video_name.lastIndexOf("."))+".m3u8"; + List commend = new ArrayList(); + commend.add(ffmpeg_path); + commend.add("-i"); + commend.add(video_path); + commend.add("-hls_time"); + commend.add("10"); + commend.add("-hls_list_size"); + commend.add("0"); + commend.add("-hls_segment_filename"); +// commend.add("D:/BaiduNetdiskDownload/Movies/test1/test1_%05d.ts"); + commend.add(m3u8folder_path + m3u8_name.substring(0,m3u8_name.lastIndexOf(".")) + "_%05d.ts"); +// commend.add("D:/BaiduNetdiskDownload/Movies/test1/test1.m3u8"); + commend.add(m3u8folder_path + m3u8_name ); + String outstring = null; + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(commend); + //将标准输入流和错误输入流合并,通过标准输入流程读取信息 + builder.redirectErrorStream(true); + Process p = builder.start(); + outstring = waitFor(p); + + } catch (Exception ex) { + + ex.printStackTrace(); + + } + //通过查看视频时长判断是否成功 + Boolean check_video_time = check_video_time(video_path, m3u8folder_path + m3u8_name); + if(!check_video_time){ + return outstring; + } + //通过查看m3u8列表判断是否成功 + List ts_list = get_ts_list(); + if(ts_list == null){ + return outstring; + } + return "success"; + + + } + + + + /** + * 检查视频处理是否完成 + * @return ts列表 + */ + public List get_ts_list() { +// String m3u8_name = video_name.substring(0, video_name.lastIndexOf("."))+".m3u8"; + List fileList = new ArrayList(); + List tsList = new ArrayList(); + String m3u8file_path =m3u8folder_path + m3u8_name; + BufferedReader br = null; + String str = null; + String bottomline = ""; + try { + br = new BufferedReader(new FileReader(m3u8file_path)); + while ((str = br.readLine()) != null) { + bottomline = str; + if(bottomline.endsWith(".ts")){ + tsList.add(bottomline); + } + //System.out.println(str); + } + } catch (IOException e) { + e.printStackTrace(); + }finally { + if(br!=null){ + try { + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + if (bottomline.contains("#EXT-X-ENDLIST")) { +// fileList.add(hls_relativepath+m3u8_name); + fileList.addAll(tsList); + return fileList; + } + return null; + + } + + + + + public static void main(String[] args) throws IOException { + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + String video_path = "E:\\ffmpeg_test\\1.mp4"; + String m3u8_name = "1.m3u8"; + String m3u8_path = "E:\\ffmpeg_test\\1\\"; + HlsVideoUtil videoUtil = new HlsVideoUtil(ffmpeg_path,video_path,m3u8_name,m3u8_path); + String s = videoUtil.generateM3u8(); + System.out.println(s); + System.out.println(videoUtil.get_ts_list()); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/HttpClient.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/HttpClient.java new file mode 100644 index 0000000000000000000000000000000000000000..950a5c6d711303c9d1f97c99d49919beaf997f51 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/HttpClient.java @@ -0,0 +1,172 @@ +package com.itools.core.utils; + +import org.apache.http.Consts; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.*; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @description: + * @author: XUCHANG + */ +public class HttpClient { + private String url; + private Map param; + private int statusCode; + private String content; + private String xmlParam; + private boolean isHttps; + + public boolean isHttps() { + return isHttps; + } + + public void setHttps(boolean isHttps) { + this.isHttps = isHttps; + } + + public String getXmlParam() { + return xmlParam; + } + + public void setXmlParam(String xmlParam) { + this.xmlParam = xmlParam; + } + + public HttpClient(String url, Map param) { + this.url = url; + this.param = param; + } + + public HttpClient(String url) { + this.url = url; + } + + public void setParameter(Map map) { + param = map; + } + + public void addParameter(String key, String value) { + if (param == null) { + param = new HashMap(); + } + param.put(key, value); + } + + public void post() throws ClientProtocolException, IOException { + HttpPost http = new HttpPost(url); + setEntity(http); + execute(http); + } + + public void put() throws ClientProtocolException, IOException { + HttpPut http = new HttpPut(url); + setEntity(http); + execute(http); + } + + public void get() throws ClientProtocolException, IOException { + if (param != null) { + StringBuilder url = new StringBuilder(this.url); + boolean isFirst = true; + for (String key : param.keySet()) { + if (isFirst) { + url.append("?"); + } else { + url.append("&"); + } + url.append(key).append("=").append(param.get(key)); + } + this.url = url.toString(); + } + HttpGet http = new HttpGet(url); + execute(http); + } + + /** + * set http post,put param + */ + private void setEntity(HttpEntityEnclosingRequestBase http) { + if (param != null) { + List nvps = new LinkedList(); + for (String key : param.keySet()) { + nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数 + } + http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数 + } + if (xmlParam != null) { + http.setEntity(new StringEntity(xmlParam, Consts.UTF_8)); + } + } + + private void execute(HttpUriRequest http) throws ClientProtocolException, + IOException { + CloseableHttpClient httpClient = null; + try { + if (isHttps) { + SSLContext sslContext = new SSLContextBuilder() + .loadTrustMaterial(null, new TrustStrategy() { + // 信任所有 + @Override + public boolean isTrusted(X509Certificate[] chain, + String authType) + throws CertificateException { + return true; + } + }).build(); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + sslContext); + httpClient = HttpClients.custom().setSSLSocketFactory(sslsf) + .build(); + } else { + httpClient = HttpClients.createDefault(); + } + CloseableHttpResponse response = httpClient.execute(http); + try { + if (response != null) { + if (response.getStatusLine() != null) { + statusCode = response.getStatusLine().getStatusCode(); + } + HttpEntity entity = response.getEntity(); + // 响应内容 + content = EntityUtils.toString(entity, Consts.UTF_8); + } + } finally { + response.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + httpClient.close(); + } + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() throws ParseException, IOException { + return content; + } + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/IDUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/IDUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..a4ae61dcf1992461466de13e50115855574d4d82 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/IDUtils.java @@ -0,0 +1,48 @@ +package com.itools.core.utils; + +import java.util.Random; + +/** + * @description: + * @author: XUCHANG + */ +public class IDUtils { + + /** + * 图片名生成 + */ + public static String genImageName() { + //取当前时间的长整形值包含毫秒 + long millis = System.currentTimeMillis(); + //long millis = System.nanoTime(); + //加上三位随机数 + Random random = new Random(); + int end3 = random.nextInt(999); + //如果不足三位前面补0 + String str = millis + String.format("%03d", end3); + + return str; + } + + /** + * 商品id生成 + */ + public static long genItemId() { + //取当前时间的长整形值包含毫秒 + long millis = System.currentTimeMillis(); + //long millis = System.nanoTime(); + //加上两位随机数 + Random random = new Random(); + int end2 = random.nextInt(99); + //如果不足两位前面补0 + String str = millis + String.format("%02d", end2); + long id = new Long(str); + return id; + } + + public static void main(String[] args) { + for(int i=0;i< 100;i++) { + System.out.println(genItemId()); + } + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/IOUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/IOUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..b246fc72ef984abb8b30779ef3fa5fadf5a74cf7 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/IOUtils.java @@ -0,0 +1,128 @@ +package com.itools.core.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @description: + * @author: XUCHANG + */ +public class IOUtils { + private final static int CRYPTED_BYTES = 1024; + private final static String HEAD = "V1"; + + protected static String encrypt2Str(byte[] buff) { + // Hex处理 + String result = bytes2HexString(buff); + // 加密服务,demo,往字符串后面加"000"; + result = result + "000"; + // + return result; + } + + protected static byte[] decrypt2Bytes(String sEncryptedData) { + // 解密服务,demo:减去尾巴上:000 + sEncryptedData = sEncryptedData.substring(0, sEncryptedData.length() - 3); + // Hex处理 + return hexString2Bytes(sEncryptedData); + } + + public static void encrypt(InputStream is, OutputStream os) throws IOException { + final int sbytes = is.available() > CRYPTED_BYTES ? CRYPTED_BYTES : is.available(); + byte[] buff = new byte[sbytes]; + int len = is.read(buff, 0, sbytes); + String encryptedData = encrypt2Str(buff); + + os.write(HEAD.getBytes()); + os.write(intToBytes(encryptedData.getBytes().length)); + os.write(encryptedData.getBytes()); + // + byte[] buff2 = new byte[1024]; + len = is.read(buff2); + while (len > 0) { + os.write(buff2, 0, len); + len = is.read(buff2); + } + } + + public static void decrypt(InputStream is, OutputStream os) throws IOException { + byte headerBytes[] = new byte[HEAD.length()]; + int len = is.read(headerBytes); + boolean isEncryptedData = false; + if (len < 2 || !HEAD.equals(new String(headerBytes))) { // 判断是否加密数据 + isEncryptedData = false; + } else { + isEncryptedData = true; + } + if (!isEncryptedData) { + os.write(headerBytes); + + byte[] buff2 = new byte[1024]; + len = is.read(buff2); + while (len > 0) { + os.write(buff2, 0, len); + len = is.read(buff2); + } + return; + } else { + byte[] lenBytes = new byte[4]; + is.read(lenBytes); + int slen = bytesToInt(lenBytes); + byte[] encryptedData = new byte[slen]; + len = is.read(encryptedData); + String sEncryptedData = new String(encryptedData); + + byte[] sEncryptedData2 = decrypt2Bytes(sEncryptedData); + os.write(sEncryptedData2); + // + byte[] buff2 = new byte[1024]; + len = is.read(buff2); + while (len > 0) { + os.write(buff2, 0, len); + len = is.read(buff2); + } + return; + } + + } + + protected static byte[] intToBytes(int n) { + byte[] b = new byte[4]; + b[0] = (byte) (n & 0xff); + b[1] = (byte) (n >> 8 & 0xff); + b[2] = (byte) (n >> 16 & 0xff); + b[3] = (byte) (n >> 24 & 0xff); + return b; + } + + // 将低字节在前转为int,高字节在后的byte数组(与IntToByteArray1想对应) + protected static int bytesToInt(byte[] bArr) { + if (bArr.length != 4) { + return -1; + } + return (int) ((((bArr[3] & 0xff) << 24) | ((bArr[2] & 0xff) << 16) | ((bArr[1] & 0xff) << 8) + | ((bArr[0] & 0xff) << 0))); + } + + protected static String bytes2HexString(byte[] buffer) { + StringBuffer result = new StringBuffer(); + for (int i = 0; i < buffer.length; i++) { + String hex = Integer.toHexString(buffer[i] & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + result.append(hex.toUpperCase()); + } + return result.toString(); + } + + protected static byte[] hexString2Bytes(String src) { + int l = src.length() / 2; + byte[] ret = new byte[l]; + for (int i = 0; i < l; i++) { + ret[i] = Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue(); + } + return ret; + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Long64.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Long64.java new file mode 100644 index 0000000000000000000000000000000000000000..398a2e549449b23e2ab832a0cbf61848d5274fcd --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Long64.java @@ -0,0 +1,94 @@ +package com.itools.core.utils; + +/** + * @description:长整数与64进制数据互换 + * @author: XUCHANG + */ +public class Long64 { + private final static char[] CHARS64 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z', '*', '_' }; + + public static int charAt(char c) { + if (c >= '0' && c <= '9') { + return (int) c - 48; + } + if (c >= 'A' && c <= 'Z') { + return (int) c - 65 + 10; + } + if (c >= 'a' && c <= 'z') { + return (int) c - 97 + 10 + 26; + } + if (c == '@') { + return 62; + } + if (c == '!') { + return 63; + } + return -1; + } + + public static char[] generatChar64() { + char[] char64 = new char[64]; + int c = 0; + for (int i = '0'; i <= '9'; i++) { + char64[c] = (char) i; + c++; + } + for (int i = 'A'; i <= 'Z'; i++) { + char64[c] = (char) i; + c++; + } + for (int i = 'a'; i <= 'z'; i++) { + char64[c] = (char) i; + c++; + } + char64[c] = '@'; + char64[c++] = '!'; + return char64; + } + + public static String encode(final long x) { + int mr = 0; + for (long tmp = Math.abs(x) >>> 6; tmp > 0;) { + tmp = tmp >>> 6; + mr++; + } + // + long tmp = Math.abs(x); + StringBuffer res = new StringBuffer(); + if (x < 0) { + res.append("-"); + } + for (int i = mr; i >= 0; i--) { + if (i > 0) { + long ll = tmp >>> (6 * i); + res.append(CHARS64[(int) ll]); + + tmp -= ll << (6 * i); + } else { + res.append(CHARS64[(int) tmp]); + } + } + return res.toString(); + } + + public static long decode(final String x) { + long res = 0; + boolean b = false; + for (int i = 0; i < x.length(); i++) { + final char c = x.charAt(i); + if (c == '-' && i == 0) { + b = true; + continue; + } + res += charAt(c) * (1 << (6 * (x.length() - 1 - i))); + } + if (b) { + res *= -1; + } + return res; + } + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/MD5Util.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/MD5Util.java new file mode 100644 index 0000000000000000000000000000000000000000..b5e5e96c77cada44f0ae61925c8ffff008090e5c --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/MD5Util.java @@ -0,0 +1,162 @@ +package com.itools.core.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @description: + * @author: XUCHANG + */ +public class MD5Util { + + /** + * The M d5. + */ + static MessageDigest MD5 = null; + + /** + * The Constant HEX_DIGITS. + */ + private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + static { + try { + MD5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ne) { + ne.printStackTrace(); + } + } + + /** + * 获取文件md5值. + * + * @param file the file + * @return md5串 + * @throws IOException + */ + public static String getFileMD5String(File file) throws IOException { + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[8192]; + int length; + while ((length = fileInputStream.read(buffer)) != -1) { + MD5.update(buffer, 0, length); + } + + return new String(encodeHex(MD5.digest())); + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + throw e; + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + throw e; + } + } + } + + /** + * 获取文件md5值. + * + * @param data the byte[] data + * @return md5串 + * @throws IOException + */ + public static String getFileMD5String(byte[] data) throws IOException { + MD5.update(data); + return new String(encodeHex(MD5.digest())); + } + + /** + * Encode hex. + * + * @param bytes the bytes + * @return the string + */ + public static String encodeHex(byte bytes[]) { + return bytesToHex(bytes, 0, bytes.length); + + } + + /** + * Bytes to hex. + * + * @param bytes the bytes + * @param start the start + * @param end the end + * @return the string + */ + public static String bytesToHex(byte bytes[], int start, int end) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < start + end; i++) { + sb.append(byteToHex(bytes[i])); + } + return sb.toString(); + + } + + /** + * Byte to hex. + * + * @param bt the bt + * @return the string + */ + public static String byteToHex(byte bt) { + return HEX_DIGITS[(bt & 0xf0) >> 4] + "" + HEX_DIGITS[bt & 0xf]; + + } + + /** + * 获取md5值. + * + * @param str the string + * @return md5串 + * @throws IOException + */ + public static String getStringMD5(String str) { + StringBuilder sb = new StringBuilder(); + try { + byte[] data = str.getBytes("utf-8"); + MessageDigest MD5 = MessageDigest.getInstance("MD5"); + MD5.update(data); + data = MD5.digest(); + for (int i = 0; i < data.length; i++) { + sb.append(HEX_DIGITS[(data[i] & 0xf0) >> 4] + "" + HEX_DIGITS[data[i] & 0xf]); + } + } catch (Exception e) { + } + return sb.toString(); + } + + /** + * The main method. + * + * @param args the arguments + */ + public static void main(String[] args) { + + long beginTime = System.currentTimeMillis(); + File fileZIP = new File("D:\\BaiduNetdiskDownload\\test1.avi"); + + String md5 = ""; + try { + md5 = getFileMD5String(fileZIP); + } catch (IOException e) { + e.printStackTrace(); + } + long endTime = System.currentTimeMillis(); + System.out.println("MD5:" + md5 + "\n time:" + ((endTime - beginTime)) + "ms"); + + System.out.println(getStringMD5("440923201801010001")); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Mp4VideoUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Mp4VideoUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..34add4d14c3649bf00be73c33afac23a41c857fa --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Mp4VideoUtil.java @@ -0,0 +1,94 @@ +package com.itools.core.utils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: XUCHANG + */ +public class Mp4VideoUtil extends VideoUtil { + + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + String video_path = "D:\\BaiduNetdiskDownload\\test1.avi"; + String mp4_name = "test1.mp4"; + String mp4folder_path = "D:/BaiduNetdiskDownload/Movies/test1/"; + public Mp4VideoUtil(String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path){ + super(ffmpeg_path); + this.ffmpeg_path = ffmpeg_path; + this.video_path = video_path; + this.mp4_name = mp4_name; + this.mp4folder_path = mp4folder_path; + } + //清除已生成的mp4 + private void clear_mp4(String mp4_path){ + //删除原来已经生成的m3u8及ts文件 + File mp4File = new File(mp4_path); + if(mp4File.exists() && mp4File.isFile()){ + mp4File.delete(); + } + } + /** + * 视频编码,生成mp4文件 + * @return 成功返回success,失败返回控制台日志 + */ + public String generateMp4(){ + //清除已生成的mp4 + clear_mp4(mp4folder_path+mp4_name); + /* + ffmpeg.exe -i lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .\lucene.mp4 + */ + List commend = new ArrayList(); + //commend.add("D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe"); + commend.add(ffmpeg_path); + commend.add("-i"); +// commend.add("D:\\BaiduNetdiskDownload\\test1.avi"); + commend.add(video_path); + commend.add("-c:v"); + commend.add("libx264"); + commend.add("-y");//覆盖输出文件 + commend.add("-s"); + commend.add("1280x720"); + commend.add("-pix_fmt"); + commend.add("yuv420p"); + commend.add("-b:a"); + commend.add("63k"); + commend.add("-b:v"); + commend.add("753k"); + commend.add("-r"); + commend.add("18"); + commend.add(mp4folder_path + mp4_name ); + String outstring = null; + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(commend); + //将标准输入流和错误输入流合并,通过标准输入流程读取信息 + builder.redirectErrorStream(true); + Process p = builder.start(); + outstring = waitFor(p); + + } catch (Exception ex) { + + ex.printStackTrace(); + + } + Boolean check_video_time = this.check_video_time(video_path, mp4folder_path + mp4_name); + if(!check_video_time){ + return outstring; + }else{ + return "success"; + } + } + + public static void main(String[] args) throws IOException { + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + String video_path = "E:\\ffmpeg_test\\1.avi"; + String mp4_name = "809694a6a974c35e3a36f36850837d7c.mp4"; + String mp4_path = "F:/develop/upload/8/0/809694a6a974c35e3a36f36850837d7c/"; + Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path); + String s = videoUtil.generateMp4(); + System.out.println(s); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/NetworkUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/NetworkUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..534b5db62191c7d3fc027dd5c24e8e1ac4664c8c --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/NetworkUtils.java @@ -0,0 +1,203 @@ +package com.itools.core.utils; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +/** + * @description:网络工具类 + * @author: XUCHANG + */ +public class NetworkUtils { + + /** + * 网卡过滤 + */ + public enum Filter { + + /**全部*/ + ALL, + + /**在线*/ + UP, + + /**虚拟*/ + VIRTUAL, + + /**loopback*/ + LOOPBACK, + + /**物理*/ + PHYSICAL_ONLY, + ; + + public boolean apply(NetworkInterface input) { + + if (null == input) { + return false; + } + + try { + switch (this) { + case UP: + return input.isUp(); + case VIRTUAL: + return input.isVirtual(); + case LOOPBACK: + return input.isLoopback(); + case PHYSICAL_ONLY: + byte[] hardwareAddress = input.getHardwareAddress(); + return null != hardwareAddress && hardwareAddress.length > 0 && !input.isVirtual() && !isVmMac(hardwareAddress); + case ALL: + default: + return true; + } + } catch (SocketException ex) { + throw new IllegalStateException(ex); + } + } + } + + public enum Radix { + /**二进制*/ + BIN(2), + + /**10进制*/ + DEC(10), + + /** 16进制*/ + HEX(16), + ; + + final int value; + + Radix(int radix) { + this.value = radix; + } + } + + private static byte[][] invalidMacs = { + //VMWare + {0x00, 0x05, 0x69}, + {0x00, 0x1c, 0x14}, + {0x00, 0x0c, 0x29}, + {0x00, 0x50, 0x56}, + //VirtualBox + {0x08, 0x00, 0x27}, + {0x0a, 0x00, 0x27}, + //Virtual-PC + {0x00, 0x03, (byte) 0xff}, + //Hyper-V + {0x00, 0x15, 0x50} + }; + + private static boolean isVmMac(byte[] mac) { + if (null == mac) { + return false; + } + for (byte[] invalid : invalidMacs) { + if (invalid[0] == mac[0] && invalid[1] == mac[1] && invalid[2] == mac[2]) { + return true; + } + } + return false; + } + + public static Set getNICs(Filter... filters) { + if (null == filters) { + filters = new Filter[]{Filter.ALL}; + } + + Set ret = new HashSet<>(); + Enumeration networkInterfaceEnumeration; + try { + networkInterfaceEnumeration = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException ex) { + throw new IllegalStateException(ex); + } + while (networkInterfaceEnumeration.hasMoreElements()) { + boolean match = false; + NetworkInterface next = networkInterfaceEnumeration.nextElement(); + for (Filter filter : filters) { + if (!filter.apply(next)) { + continue; + } + match = true; + } + + if (match) { + ret.add(next); + } + } + + return ret; + } + + public static String getMacAddress(NetworkInterface networkInterface, String separator) { + try { + return format(networkInterface.getHardwareAddress(), separator, Radix.HEX); + } catch (SocketException ex) { + throw new IllegalStateException(ex); + } + } + + public static String format(byte[] source, String separator, final Radix radix) { + if (null == source) { + return ""; + } + + if (null == separator) { + separator = ""; + } + + StringBuilder sb = new StringBuilder(); + for (byte b : source) { + sb.append(separator).append(apply(b, radix)); + } + + return sb.length() > 0 ? sb.substring(separator.length()) : ""; + + } + + public final static String getIpAddress(HttpServletRequest request) throws IOException { + // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr();//这个是不可能拿不到值的 + } + if (ip.length() > 15) {//多个IP时,取第一个非unknow的IP + String[] ips = ip.split(","); + for (int index = 0; index < ips.length; index++) { + String strIp = (String) ips[index]; + if (!("unknown".equalsIgnoreCase(strIp))) { + ip = strIp; + break; + } + } + } + return ip; + } + + private static String apply(Byte input, final Radix radix) { + return String.copyValueOf(new char[]{Character.forDigit((input & 240) >> 4, radix.value), Character.forDigit(input & 15, radix.value)}); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Oauth2Util.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Oauth2Util.java new file mode 100644 index 0000000000000000000000000000000000000000..95c85855feb3d54a4f5970cf4a6ded7ab23e7664 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Oauth2Util.java @@ -0,0 +1,41 @@ +package com.itools.core.utils; + +import com.alibaba.fastjson.JSON; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + + +/** + * @description: + * @author: XUCHANG + */ +public class Oauth2Util { + + public static Map getJwtClaimsFromHeader(HttpServletRequest request) { + if (request == null) { + return null; + } + //取出头信息 + String authorization = request.getHeader("Authorization"); + if (StringUtils.isEmpty(authorization) || authorization.indexOf("Bearer") < 0) { + return null; + } + //从Bearer 后边开始取出token + String token = authorization.substring(7); + Map map = null; + try { + //解析jwt + Jwt decode = JwtHelper.decode(token); + //得到 jwt中的用户信息 + String claims = decode.getClaims(); + //将jwt转为Map + map = JSON.parseObject(claims, Map.class); + } catch (Exception e) { + e.printStackTrace(); + } + return map; + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/ObjectUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ObjectUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c55e80d1bb9ecaef1ac085ffa2fd7065032ce44c --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ObjectUtils.java @@ -0,0 +1,886 @@ +/* + * Copyright 2002-2009 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 com.itools.core.utils; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Miscellaneous object utility methods. + * + *

Mainly for internal use within the framework; consider + * Jakarta's Commons Lang + * for a more comprehensive suite of object utilities. + * + *

Thanks to Alex Ruiz for contributing several enhancements to this class! + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rod Johnson + * @author Rob Harrop + * @since 19.03.2004 + */ +public abstract class ObjectUtils { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see Exception + * @see RuntimeException + * @see Error + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the exceptions + * declared in a throws clause. + * @param ex the exception to checked + * @param declaredExceptions the exceptions declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class[] declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + int i = 0; + while (i < declaredExceptions.length) { + if (declaredExceptions[i].isAssignableFrom(ex.getClass())) { + return true; + } + i++; + } + } + return false; + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * @param obj the object to check + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: + * i.e. null or of zero length. + * @param array the array to check + */ + public static boolean isEmpty(Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Check whether the given array contains the given element. + * @param array the array to check (may be null, + * in which case the return value will always be false) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Append the given Object to the given array, returning a new array + * consisting of the input array contents plus the given Object. + * @param array the array to append to (can be null) + * @param obj the Object to append + * @return the new array (of the same component type; never null) + */ + public static Object[] addObjectToArray(Object[] array, Object obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } + else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + Object[] newArr = (Object[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an + * object array (if necessary of primitive wrapper objects). + *

A null source value will be converted to an + * empty Object array. + * @param source the (potentially primitive) array + * @return the corresponding object array (never null) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning true + * if both are null or false if only one is + * null. + *

Compares arrays with Arrays.equals, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see Arrays#equals + */ + public static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of + * {@link Object#hashCode()}. If the object is an array, + * this method will delegate to any of the nullSafeHashCode + * methods for arrays in this class. If the object is null, + * this method returns 0. + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + nullSafeHashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode()}. + * @see Boolean#hashCode() + */ + public static int hashCode(boolean bool) { + return bool ? 1231 : 1237; + } + + /** + * Return the same value as {@link Double#hashCode()}. + * @see Double#hashCode() + */ + public static int hashCode(double dbl) { + long bits = Double.doubleToLongBits(dbl); + return hashCode(bits); + } + + /** + * Return the same value as {@link Float#hashCode()}. + * @see Float#hashCode() + */ + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}. + * @see Long#hashCode() + */ + public static int hashCode(long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * @param obj the object (may be null) + * @return the object's identity as String representation, + * or an empty String if the object was null + */ + public static String identityToString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if obj is + * not null; otherwise returns an empty String. + *

Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a null value. + * @param obj the object to build a display String for + * @return a display String representation of obj + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + *

Returns "null" if obj is null. + * @param obj the object to introspect (may be null) + * @return the corresponding class name + */ + public static String nullSafeClassName(Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + *

Builds a String representation of the contents in case of an array. + * Returns "null" if obj is null. + * @param obj the object to build a String representation for + * @return a String representation of obj + */ + public static String nullSafeToString(Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(short[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * 获取对象的所有属性值,返回一个对象数组 + * @author lidab + */ + public static Object[] getFiledValues(Object o) { + String[] fieldNames = getFiledName(o); + int len = fieldNames.length; + Object[] values = new Object[len]; + for (int i = 0; i < len; i++) { + values[i] = getFieldValueByName(fieldNames[i], o); + } + return values; + } + /** + * 根据属性名获取属性值 + */ + protected static Object getFieldValueByName(String fieldName, Object o) { + try { + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String getter = "get" + firstLetter + fieldName.substring(1); + Method method = o.getClass().getMethod(getter, new Class[] {}); + Object value = method.invoke(o, new Object[] {}); + return value; + } catch (Exception e) { + return null; + } + } + /** + * 获取属性名数组 + */ + protected static String[] getFiledName(Object o) { + Field[] fields = o.getClass().getDeclaredFields(); + int len = fields.length; + String[] fieldNames = new String[len]; + for (int i = 0; i < len; i++) { + fieldNames[i] = fields[i].getName(); + } + return fieldNames; + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/RandomUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/RandomUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1a2187c5948f742393c36080d05dd47cc5d5c852 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/RandomUtil.java @@ -0,0 +1,42 @@ +package com.itools.core.utils; + +import java.util.Random; + +/** + * @description:随机生成1到10位的随机数的工具类 + * @author: XUCHANG + * @time: 2020/1/11 14:18 + */ +public class RandomUtil { + + public static String getRandom(int num) { + int[] array = {0,1,2,3,4,5,6,7,8,9}; + //随机对象 + Random rand = new Random(); + //循环产生 + for (int i = 10; i > 1; i--) { + int index = rand.nextInt(i); + int tmp = array[index]; + array[index] = array[i - 1]; + array[i - 1] = tmp; + } + int result = 0; + for(int i = 0; i < num; i++) { + result = result * 10 + array[i]; + } + String code = Integer.toString(result); + if (code.length() == num-1) { + code = "0" + code; + } + return code; + + } + public static void main(String[] args) { + for (int i=0 ; i<100;i++){ + System.out.println(RandomUtil.getRandom(6)); + + } + } + + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/RegexUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/RegexUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..11f321f2f11e7b209dec81b8a31dc353a7fd5170 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/RegexUtil.java @@ -0,0 +1,97 @@ +package com.itools.core.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @description: + * @author: XUCHANG + */ +public class RegexUtil { + private final static String POSITIVE_NUMBER_PATTERN = "^[1-9]\\d*$"; + + private final static String POSIT_NUMBER_PATTERN = "^[0-9]\\d*$"; + + private final static String MOBILE_NUMBER_PATTERN = "^[1][3,4,5,7,8][0-9]{9}$"; + + private final static String EMAIL_NUMBER_PATTERN = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; + + private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + + + private static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private static final String DATETIME_FORMAT = "yyyyMMddHHmmss"; + /** + * 正整数 不包括0 + * @param testStr + * @return + */ + public static boolean checkPositiveNum(Integer testStr){ + Pattern pattern= Pattern.compile(POSITIVE_NUMBER_PATTERN); + Matcher matcher = pattern.matcher(testStr.toString()); + return matcher.matches(); + } + /** + * 正整数包括0 + * @param testStr + * @return + */ + public static boolean checkPositNum(Integer testStr){ + Pattern pattern= Pattern.compile(POSIT_NUMBER_PATTERN); + Matcher matcher = pattern.matcher(testStr.toString()); + return matcher.matches(); + } + /** + * 校验日期 + * @param date + * @return + */ + public static boolean checkDatePattern(String date){ + if (StringUtils.isEmpty(date)){ + return false; + } + String rexp = "^((\\d{2}(([02468][048])|([13579][26]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))"; + Pattern patternDate = Pattern.compile(rexp); + Matcher matcherDate = patternDate.matcher(date); + boolean dateType = matcherDate.matches(); + return dateType; + } + + /** + * 校验手机号码 + * @param mobile + * @return + */ + public static boolean checkMobilePattern(String mobile){ + if (StringUtils.isEmpty(mobile)){ + return false; + } + Pattern patternDate = Pattern.compile(MOBILE_NUMBER_PATTERN); + Matcher matcherDate = patternDate.matcher(mobile); + boolean dateType = matcherDate.matches(); + return dateType; + } + + /** + * 校验邮箱 + * @param email + * @return + */ + public static boolean checkEmailPattern(String email){ + if (StringUtils.isEmpty(email)){ + return false; + } + Pattern patternDate = Pattern.compile(EMAIL_NUMBER_PATTERN); + Matcher matcherDate = patternDate.matcher(email); + boolean dateType = matcherDate.matches(); + return dateType; + } + + + public static boolean checkPositNum(Long deptId) { + Pattern pattern= Pattern.compile(POSIT_NUMBER_PATTERN); + Matcher matcher = pattern.matcher(deptId.toString()); + return matcher.matches(); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowFlake.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowFlake.java new file mode 100644 index 0000000000000000000000000000000000000000..099c9c9aab11d0424d9d4c3a5f220988ba8d61b4 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowFlake.java @@ -0,0 +1,108 @@ +package com.itools.core.utils; + + +/** + * @description:Twitter的分布式自增ID雪花算法snowflake (Java版) + * @author: XUCHANG + */ +public class SnowFlake { + + /** + * 起始的时间戳 + */ + private final static long START_STMP = 1480166465631L; + + /** + * 每一部分占用的位数 + */ + private final static long SEQUENCE_BIT = 12; //序列号占用的位数 + private final static long MACHINE_BIT = 5; //机器标识占用的位数 + private final static long DATACENTER_BIT = 5;//数据中心占用的位数 + + /** + * 每一部分的最大值 + */ + private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); + private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); + private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); + + /** + * 每一部分向左的位移 + */ + private final static long MACHINE_LEFT = SEQUENCE_BIT; + private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; + private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; + + private long datacenterId; //数据中心 + private long machineId; //机器标识 + private long sequence = 0L; //序列号 + private long lastStmp = -1L;//上一次时间戳 + + public SnowFlake(long datacenterId, long machineId) { + if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { + throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); + } + if (machineId > MAX_MACHINE_NUM || machineId < 0) { + throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); + } + this.datacenterId = datacenterId; + this.machineId = machineId; + } + + /** + * 产生下一个ID + * + * @return + */ + public synchronized long nextId() { + long currStmp = getNewstmp(); + if (currStmp < lastStmp) { + throw new RuntimeException("Clock moved backwards. Refusing to generate id"); + } + + if (currStmp == lastStmp) { + //相同毫秒内,序列号自增 + sequence = (sequence + 1) & MAX_SEQUENCE; + //同一毫秒的序列数已经达到最大 + if (sequence == 0L) { + currStmp = getNextMill(); + } + } else { + //不同毫秒内,序列号置为0 + sequence = 0L; + } + + lastStmp = currStmp; + + return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分 + | datacenterId << DATACENTER_LEFT //数据中心部分 + | machineId << MACHINE_LEFT //机器标识部分 + | sequence; //序列号部分 + } + + private long getNextMill() { + long mill = getNewstmp(); + while (mill <= lastStmp) { + mill = getNewstmp(); + } + return mill; + } + + private long getNewstmp() { + return System.currentTimeMillis(); + } + + public static void main(String[] args) { + SnowFlake snowFlake = new SnowFlake(2, 3); + + long start = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + System.out.println(snowFlake.nextId()); + } + + System.out.println(System.currentTimeMillis() - start); + + + } +} + diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowflakeIdWorkerUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowflakeIdWorkerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..123117caa965cbf03ee8fb9ec1d4d946338d05dc --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/SnowflakeIdWorkerUtil.java @@ -0,0 +1,147 @@ +package com.itools.core.utils; + + +/** + * Twitter_Snowflake
+ * SnowFlake的结构如下(每部分用-分开):
+ * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
+ * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
+ * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) + * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
+ * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
+ * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
+ * 加起来刚好64位,为一个Long型。
+ * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 + */ +public class SnowflakeIdWorkerUtil { + + // ==============================Fields=========================================== + /** 开始时间截 (2015-01-01) */ + private final long twepoch = 1420041600000L; + + /** 机器id所占的位数 */ + private final long workerIdBits = 5L; + + /** 数据标识id所占的位数 */ + private final long datacenterIdBits = 5L; + + /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ + private final long maxWorkerId = -1L ^ (-1L << workerIdBits); + + /** 支持的最大数据标识id,结果是31 */ + private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + + /** 序列在id中占的位数 */ + private final long sequenceBits = 12L; + + /** 机器ID向左移12位 */ + private final long workerIdShift = sequenceBits; + + /** 数据标识id向左移17位(12+5) */ + private final long datacenterIdShift = sequenceBits + workerIdBits; + + /** 时间截向左移22位(5+5+12) */ + private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + + /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ + private final long sequenceMask = -1L ^ (-1L << sequenceBits); + + /** 工作机器ID(0~31) */ + private long workerId; + + /** 数据中心ID(0~31) */ + private long datacenterId; + + /** 毫秒内序列(0~4095) */ + private long sequence = 0L; + + /** 上次生成ID的时间截 */ + private long lastTimestamp = -1L; + + //==============================Constructors===================================== + /** + * 构造函数 + * @param workerId 工作ID (0~31) + * @param datacenterId 数据中心ID (0~31) + */ + public SnowflakeIdWorkerUtil(long workerId, long datacenterId) { + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + // ==============================Methods========================================== + /** + * 获得下一个ID (该方法是线程安全的) + * @return SnowflakeId + */ + public synchronized long nextId() { + long timestamp = timeGen(); + + //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + //如果是同一时间生成的,则进行毫秒内序列 + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + //毫秒内序列溢出 + if (sequence == 0) { + //阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } + //时间戳改变,毫秒内序列重置 + else { + sequence = 0L; + } + + //上次生成ID的时间截 + lastTimestamp = timestamp; + + //移位并通过或运算拼到一起组成64位的ID + return ((timestamp - twepoch) << timestampLeftShift) // + | (datacenterId << datacenterIdShift) // + | (workerId << workerIdShift) // + | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 返回以毫秒为单位的当前时间 + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + + + public static void main(String[] args) { +// System.out.println(Long.toBinaryString(5)); + SnowflakeIdWorkerUtil idWorker = new SnowflakeIdWorkerUtil(1, 1); + for (int i = 0; i < 10000; i++) { + long id = idWorker.nextId(); + System.out.println(Long.toBinaryString(id)); + System.out.println(id); + } + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/StringUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/StringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..49ced1706f12a68db10538a519a118c1233ddc10 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/StringUtils.java @@ -0,0 +1,1325 @@ +/* + * Copyright 2002-2010 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 com.itools.core.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.util.*; +import java.util.regex.Pattern; + +/** + * Miscellaneous {@link String} utility methods. + * + *

Mainly for internal use within the framework; consider + * Jakarta's Commons Lang + * for a more comprehensive suite of String utilities. + * + *

This class delivers some simple functionality that should really + * be provided by the core Java String and {@link StringBuilder} + * classes, such as the ability to {@link #replace} all occurrences of a given + * substring in a target string. It also provides easy-to-use methods to convert + * between delimited strings, such as CSV strings, and collections and arrays. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Rick Evans + * @author Arjen Poutsma + * @since 16 April 2001 + * @see org.apache.commons.lang.StringUtils + */ +public abstract class StringUtils { + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + private static Pattern NUMBER_PATTERN = Pattern.compile("^[-\\+]?[\\d]*$"); + + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check that the given CharSequence is neither null nor of length 0. + * Note: Will return true for a CharSequence that purely consists of whitespace. + *

+	 * StringUtils.hasLength(null) = false
+	 * StringUtils.hasLength("") = false
+	 * StringUtils.hasLength(" ") = true
+	 * StringUtils.hasLength("Hello") = true
+	 * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null and has length + * @see #hasText(String) + */ + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given String is neither null nor of length 0. + * Note: Will return true for a String that purely consists of whitespace. + * @param str the String to check (may be null) + * @return true if the String is not null and has length + * @see #hasLength(CharSequence) + */ + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + /** + * Check whether the given CharSequence has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + *

+	 * StringUtils.hasText(null) = false
+	 * StringUtils.hasText("") = false
+	 * StringUtils.hasText(" ") = false
+	 * StringUtils.hasText("12345") = true
+	 * StringUtils.hasText(" 12345 ") = true
+	 * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + * @param str the String to check (may be null) + * @return true if the String is not null, its length is + * greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + */ + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see Character#isWhitespace + */ + public static boolean containsWhitespace(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String contains any whitespace characters. + * @param str the String to check (may be null) + * @return true if the String is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see Character#isWhitespace + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all whitespace from the given String: + * leading, trailing, and inbetween characters. + * @param str the String to check + * @return the trimmed String + * @see Character#isWhitespace + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + int index = 0; + while (sb.length() > index) { + if (Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } + else { + index++; + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied leading character from the given String. + * @param str the String to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed String + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied trailing character from the given String. + * @param str the String to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed String + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + + /** + * Test if the given String starts with the specified prefix, + * ignoring upper/lower case. + * @param str the String to check + * @param prefix the prefix to look for + * @see String#startsWith + */ + public static boolean startsWithIgnoreCase(String str, String prefix) { + if (str == null || prefix == null) { + return false; + } + if (str.startsWith(prefix)) { + return true; + } + if (str.length() < prefix.length()) { + return false; + } + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + + /** + * Test if the given String ends with the specified suffix, + * ignoring upper/lower case. + * @param str the String to check + * @param suffix the suffix to look for + * @see String#endsWith + */ + public static boolean endsWithIgnoreCase(String str, String suffix) { + if (str == null || suffix == null) { + return false; + } + if (str.endsWith(suffix)) { + return true; + } + if (str.length() < suffix.length()) { + return false; + } + + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring in string s. + * @param str string to search in. Return 0 if this is null. + * @param sub string to search for. Return 0 if this is null. + */ + public static int countOccurrencesOf(String str, String sub) { + if (str == null || sub == null || str.length() == 0 || sub.length() == 0) { + return 0; + } + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurences of a substring within a string with + * another string. + * @param inString String to examine + * @param oldPattern String to replace + * @param newPattern String to insert + * @return a String with the replacements + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + StringBuilder sb = new StringBuilder(); + int pos = 0; // our position in the old string + int index = inString.indexOf(oldPattern); + // the index of an occurrence we've found, or -1 + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + sb.append(inString.substring(pos)); + // remember to append any characters to the right of a match + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original String + * @param pattern the pattern to delete all occurrences of + * @return the resulting String + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given String. + * @param inString the original String + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting String + */ + public static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given String with single quotes. + * @param str the input String (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or null if the input was null + */ + public static String quote(String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a String with single quotes + * if it is a String; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or the input object as-is if not a String + */ + public static Object quoteIfString(Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a String, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the String to capitalize, may be null + * @return the capitalized String, null if null + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a String, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, null if null + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + protected static String changeFirstCharacterCase(String str, boolean capitalize) { + if (str == null || str.length() == 0) { + return str; + } + StringBuilder sb = new StringBuilder(str.length()); + if (capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } + else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Extract the filename from the given path, + * e.g. "mypath/myfile.txt" -> "myfile.txt". + * @param path the file path (may be null) + * @return the extracted filename, or null if none + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be null) + * @return the extracted filename extension, or null if none + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(sepIndex + 1) : null); + } + + /** + * Strip the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path (may be null) + * @return the path with stripped filename extension, + * or null if none + */ + public static String stripFilenameExtension(String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(0, sepIndex) : path); + } + + /** + * Apply the given relative path to the given 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)) { + newPath += FOLDER_SEPARATOR; + } + 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 = replace(path, 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); + pathToUse = pathToUse.substring(prefixIndex + 1); + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, 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)) { + // Points to current directory - drop it. + } + else 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 + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given localeString into a {@link Locale}. + *

This is the inverse operation of {@link Locale#toString Locale's toString}. + * @param localeString the locale string, following Locale's + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores + * @return a corresponding Locale instance + */ + public static Locale parseLocaleString(String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + String variant = ""; + if (parts.length >= 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as String + */ + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Append the given String to the given String array, returning a new array + * consisting of the input array contents plus the given String. + * @param array the array to append to (can be null) + * @param str the String to append + * @return the new array (never null) + */ + public static String[] addStringToArray(String[] array, String str) { + if (ObjectUtils.isEmpty(array)) { + return new String[] {str}; + } + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given String arrays into one, + * with overlapping array elements included twice. + *

The order of elements in the original arrays is preserved. + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] concatenateStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given String arrays into one, with overlapping + * array elements only included once. + *

The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurence). + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] mergeStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + List result = new ArrayList(); + result.addAll(Arrays.asList(array1)); + for (String str : array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source String array into sorted array. + * @param array the source array + * @return the sorted array (never null) + */ + public static String[] sortStringArray(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + Arrays.sort(array); + return array; + } + + /** + * Copy the given Collection into a String array. + * The Collection must contain String elements only. + * @param collection the Collection to copy + * @return the String array (null if the passed-in + * Collection was null) + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + + public static String[] toStringArray(Object[] coll) { + if(coll == null) { + return null; + } + String[] res = new String[coll.length]; + for(int i=0; i < coll.length; i++) { + if(coll[i] == null) { + res[i] = null; + } else if(coll[i] instanceof String) { + res[i] = (String)coll[i]; + } else { + res[i] = coll[i].toString(); + } + } + return res; + } + + public static String[] toStringArray(Object obj) { + if(obj == null) { + return null; + } + if(obj.getClass().isArray()) { + return toStringArray((Object[])obj); + } + return null; + } + + /** + * Copy the given Enumeration into a String array. + * The Enumeration must contain String elements only. + * @param enumeration the Enumeration to copy + * @return the String array (null if the passed-in + * Enumeration was null) + */ + public static String[] toStringArray(Enumeration enumeration) { + if (enumeration == null) { + return null; + } + List list = Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static String toString(Object obj) { + if(obj == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + if(obj.getClass().isArray()) { + sb.append("["); + for(int i = 0; i< Array.getLength(obj); i++) { + if(i > 0) { + sb.append(", "); + } + sb.append(toString(Array.get(obj, i))); + } + sb.append("]"); + } else if(obj instanceof Collection) { + sb.append("["); + ((Collection)obj).stream().map(o -> toString(o) + ", ").peek(sb::append); + sb.append("]"); + } else if(obj instanceof Map) { + sb.append("{"); + final Map m = (Map)obj; + for(Object k: m.keySet()) { + sb.append(k).append(":").append(toString(m.get(k))).append(", "); + } + sb.append("}"); + } else if(obj instanceof Throwable) { + Throwable t = (Throwable)obj; + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + sb.append(sw.toString()); + } else { + sb.append(obj); + } + return sb.toString(); + } + /** + * Trim the elements of the given String array, + * calling String.trim() on each of them. + * @param array the original String array + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate Strings from the given array. + * Also sorts the array, as it uses a TreeSet. + * @param array the String array + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + Set set = new TreeSet(); + for (String element : array) { + set.add(element); + } + return toStringArray(set); + } + + /** + * Split a String at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split + * @param delimiter to split the string up with + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or null if the delimiter wasn't found in the given input String + */ + public static String[] split(String toSplit, String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return new String[]{toSplit}; + } + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties( + String[] array, String delimiter, String charsToDelete) { + + if (ObjectUtils.isEmpty(array)) { + return null; + } + Properties result = new Properties(); + for (String element : array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * Trims tokens and omits empty tokens. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter). + * @return an array of the tokens + * @see StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens (null if the input String + * was null) + * @see StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete. Useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + List result = new ArrayList(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a CSV list into an array of Strings. + * @param str the input String + * @return an array of Strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convenience method to convert a CSV string list to a set. + * Note that this will suppress duplicates. + * @param str the input String + * @return a Set of String entries in the list + */ + public static Set commaDelimitedListToSet(String str) { + Set set = new TreeSet(); + String[] tokens = commaDelimitedListToStringArray(str); + for (String token : tokens) { + set.add(token); + } + return set; + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @param prefix the String to start each element with + * @param suffix the String to end each element with + * @return the delimited String + */ + public static String collectionToDelimitedString(@SuppressWarnings("rawtypes") Collection coll, String delim, String prefix, String suffix) { + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + @SuppressWarnings("rawtypes") + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convenience method to return a Collection as a CSV String. + * E.g. useful for toString() implementations. + * @param coll the Collection to display + * @return the delimited String + */ + public static String collectionToCommaDelimitedString(@SuppressWarnings("rawtypes") Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String arrayToDelimitedString(Object[] arr, String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convenience method to return a String array as a CSV String. + * E.g. useful for toString() implementations. + * @param arr the array to display + * @return the delimited String + */ + public static String arrayToCommaDelimitedString(Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + + public static boolean isEmpty(String str) { + return str == null || "".equals(str); + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + public static boolean isBlank(String str) { + return str == null || "".equals(str.trim()); + } + + public static boolean equalsIgnoreCase(String str1, String str2) { + if(str1 == null && str2 == null) { + return true; + } + if(str1 == null || str2 == null) { + return false; + } + return str1.equalsIgnoreCase(str2); + } + + public static boolean equals(String str1, String str2) { + if(str1 == null && str2 == null) { + return true; + } + if(str1 == null || str2 == null) { + return false; + } + return str1.equals(str2); + } + + public static String join(String[] strs, String spliter) { + if(strs == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + int c = 0; + for(String s: strs) { + if(c > 0) { + sb.append(spliter); + } + sb.append(s); + c++; + } + return sb.toString(); + } + + public static String join(Object strs, String spliter) { + if(strs == null) { + return null; + } + if(!strs.getClass().isArray()) { + return null; + } + StringBuffer sb = new StringBuffer(); + int c = 0; + for(int i = 0; i< Array.getLength(strs); i ++) { + if(c > 0) { + sb.append(spliter); + } + sb.append(Array.get(strs, i)); + c++; + } + return sb.toString(); + } + + /** + * 将文件名中的汉字转为UTF8编码的串,以便下载时能正确显示另存的文件名. + * + * @param s 原文件名 + * @return 重新编码后的文件名 + */ + public static String toUtf8String(String s) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 0 && c <= 255) { + sb.append(c); + } else { + byte[] b; + try { + b = Character.toString(c).getBytes("utf-8"); + } catch (Exception ex) { + System.out.println(ex); + b = new byte[0]; + } + for (int j = 0; j < b.length; j++) { + int k = b[j]; + if (k < 0) { + k += 256; + } + sb.append("%" + Integer.toHexString(k).toUpperCase()); + } + } + } + return sb.toString(); + } + + + public static String toUTF_8(String str) { + if(str == null) { + return null; + } + try { + return new String(str.getBytes("iso-8859-1"),"UTF-8"); + } catch (UnsupportedEncodingException e) { + return str; + } + } + + + public static String toGBK(String str) { + if(str == null) { + return null; + } + try { + return new String(str.getBytes("iso-8859-1"),"GBK"); + } catch (UnsupportedEncodingException e) { + return str; + } + } + + /** + * 字符串转换为HTML代码
return ex: &id= + * + * @param arg0 + * a String + * @return a String + */ + public static String HTMLEncoding(String arg0) { + String encodeStr = arg0; + encodeStr = encodeStr.replaceAll("&","&"); + encodeStr = encodeStr.replaceAll("\"","""); + encodeStr = encodeStr.replaceAll("<","<"); + encodeStr = encodeStr.replaceAll(">",">"); + return encodeStr; + } + + /** + * 判断String中是否是数字 + * @param str + * @return + */ + public static boolean isInteger(String str) { + return NUMBER_PATTERN.matcher(str).matches(); + } + + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/UUIDUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/UUIDUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9becec1baf5b7f7bf00f551d9be728ba9213f291 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/UUIDUtils.java @@ -0,0 +1,13 @@ +package com.itools.core.utils; + +import java.util.UUID; + +/** + * 描述 :uuid生成工具类 + */ +public class UUIDUtils { + + public static String uuid() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/Utils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..e6e25b22a319ca29d0736fbcf010076cd554fda3 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/Utils.java @@ -0,0 +1,193 @@ +package com.itools.core.utils; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.Map.Entry; + +public class Utils { + + public static String toString(Object obj) { + if (obj == null) { + return null; + } + StringBuffer sb = new StringBuffer(obj.getClass().getName()).append("@").append(obj.hashCode()); + if (obj.getClass().isArray()) { + int len = Array.getLength(obj); + sb.append("["); + for (int i = 0; i < len; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(toString(Array.get(obj, i))); + } + sb.append("]"); + } else { + return obj.toString(); + } + return sb.toString(); + } + + public static String toString2(Object obj) { + if (obj == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + if (obj.getClass().isArray()) { + int len = Array.getLength(obj); + sb.append("["); + for (int i = 0; i < len; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(toString(Array.get(obj, i))); + } + sb.append("]"); + } else { + return obj.toString(); + } + return sb.toString(); + } + + public static Map newHashMap(int size) { + return new HashMap(size); + } + + public static Map newHashMap() { + return new HashMap(64); + } + + public static Map newHashMap(K[] keys, V[] values) { + Map m = new HashMap(64); + int i =0; + for(K k: keys) { + m.put(k, values[i]); + i++; + } + return m; + } + + public static List newArrayList(int size) { + return new ArrayList(size); + } + + public static List newArrayList() { + return new ArrayList(64); + } + + public static Set newHashSet(int size) { + return new HashSet(size); + } + + public static Set newHashSet() { + return new HashSet(64); + } + + public static Collection addAll(Collection coll, T[] arr) { + if ((arr == null) || (coll == null)) { + return coll; + } + for (T e : arr) { + coll.add(e); + } + return coll; + } + + @SuppressWarnings("unchecked") + public static Map>> group(List> rows, String group_field) { + Map>> result = Utils.newHashMap(); + for(Map item: rows) { + K itemKey = (K)item.get(group_field); + if(result.containsKey(itemKey)) { + List> list = result.get(itemKey); + list.add(item); + } else { + List> list = Utils.newArrayList(); + list.add(item); + result.put(itemKey, list); + } + } + return result; + } + + public static List> groupAsList(List> rows, String group_field, String list_field) { + Map>> grouped = group(rows, group_field); + + List> result = Utils.newArrayList(); + for(Entry>> e: grouped.entrySet()) { + K key = e.getKey(); + List> value = e.getValue(); + + Map item = new HashMap(); + item.put(group_field, key); + item.put(list_field, value); + result.add(item); + } + return result; + } + + public static Throwable getBusinessCause(final Throwable t) { + Throwable cause = t; + + while ((cause != null) + && (cause.getCause() != null) + && (cause.getClass().getName().startsWith("java.lang.") || cause.getCause().getClass().getName() + .startsWith("java.lang."))) { + cause = cause.getCause(); + } + + return cause; + } + + public static String toDataBaseField(String name){ + if(null == name) + { + return null; + } + char c, pc = (char) 0; + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < name.length(); i++) + { + c = name.charAt(i); + if(Character.isLowerCase(pc) && Character.isUpperCase(c)) + { + sb.append('_'); + } + pc = c; + sb.append(Character.toUpperCase(c)); + } + return sb.toString(); + } + /** + * 生成n位随机数字,返回值为int + * int的最大值是2147483647,因此当生成10位随机数容易出现 + * 数值一直是2147483647,因此只能生成0到9位长度的数值 + * @param n + * @return + */ + public static int randomNum(int n){ + if (n <= 0 || n > 9){ + throw new IllegalArgumentException("随机数长度必须大于0且小于9位"); + } + int temp = n - 1; + int b = 1; + while (temp > 0){ + b *= 10; + temp--; + } + return (int) ((Math.random() * 9 + 1) * b); + } + + /** + * 截取参数 + * @param param + * @param length + * @return + */ + public static String subLong(Long param, int length) { + return String.valueOf(param).substring(String.valueOf(param).length()-length, String.valueOf(param).length()); + } + + public static void main(String[] args) { + System.out.println(Utils.subLong(134564567899L, 3)); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/VideoUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/VideoUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..63b53775651380a68ea4da8f805d8c54dd32ba49 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/VideoUtil.java @@ -0,0 +1,135 @@ +package com.itools.core.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 此文件作为视频文件处理父类,提供: + * 1、查看视频时长 + * 2、校验两个视频的时长是否相等 + * + */ +public class VideoUtil { + + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + + public VideoUtil(String ffmpeg_path){ + this.ffmpeg_path = ffmpeg_path; + } + + + //检查视频时间是否一致 + public Boolean check_video_time(String source,String target) { + String source_time = get_video_time(source); + //取出时分秒 + source_time = source_time.substring(0,source_time.lastIndexOf(".")); + String target_time = get_video_time(target); + //取出时分秒 + target_time = target_time.substring(0,target_time.lastIndexOf(".")); + if(source_time == null || target_time == null){ + return false; + } + if(source_time.equals(target_time)){ + return true; + } + return false; + } + + //获取视频时间(时:分:秒:毫秒) + public String get_video_time(String video_path) { + /* + ffmpeg -i lucene.mp4 + */ + List commend = new ArrayList(); + commend.add(ffmpeg_path); + commend.add("-i"); + commend.add(video_path); + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(commend); + //将标准输入流和错误输入流合并,通过标准输入流程读取信息 + builder.redirectErrorStream(true); + Process p = builder.start(); + String outstring = waitFor(p); + System.out.println(outstring); + int start = outstring.trim().indexOf("Duration: "); + if(start>=0){ + int end = outstring.trim().indexOf(", start:"); + if(end>=0){ + String time = outstring.substring(start+10,end); + if(time!=null && !time.equals("")){ + return time.trim(); + } + } + } + + } catch (Exception ex) { + + ex.printStackTrace(); + + } + return null; + } + + public String waitFor(Process p) { + InputStream in = null; + InputStream error = null; + String result = "error"; + int exitValue = -1; + StringBuffer outputString = new StringBuffer(); + try { + in = p.getInputStream(); + error = p.getErrorStream(); + boolean finished = false; + int maxRetry = 600;//每次休眠1秒,最长执行时间10分种 + int retry = 0; + while (!finished) { + if (retry > maxRetry) { + return "error"; + } + try { + while (in.available() > 0) { + Character c = new Character((char) in.read()); + outputString.append(c); + System.out.print(c); + } + while (error.available() > 0) { + Character c = new Character((char) in.read()); + outputString.append(c); + System.out.print(c); + } + //进程未结束时调用exitValue将抛出异常 + exitValue = p.exitValue(); + finished = true; + + } catch (IllegalThreadStateException e) { + Thread.sleep(1000);//休眠1秒 + retry++; + } + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + } + } + return outputString.toString(); + + } + + + public static void main(String[] args) throws IOException { + String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置 + VideoUtil videoUtil = new VideoUtil(ffmpeg_path); + String video_time = videoUtil.get_video_time("E:\\ffmpeg_test\\1.avi"); + System.out.println(video_time); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/XcOauth2Util.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/XcOauth2Util.java new file mode 100644 index 0000000000000000000000000000000000000000..5500cfa565f5afb38ac66bd785beb930d790e061 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/XcOauth2Util.java @@ -0,0 +1,34 @@ +package com.itools.core.utils; + +import lombok.Data; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + + +public class XcOauth2Util { + + public UserJwt getUserJwtFromHeader(HttpServletRequest request){ + Map jwtClaims = Oauth2Util.getJwtClaimsFromHeader(request); + if(jwtClaims == null || StringUtils.isEmpty(jwtClaims.get("id"))){ + return null; + } + UserJwt userJwt = new UserJwt(); + userJwt.setId(jwtClaims.get("id")); + userJwt.setName(jwtClaims.get("name")); + userJwt.setCompanyId(jwtClaims.get("companyId")); + userJwt.setUtype(jwtClaims.get("utype")); + userJwt.setUserpic(jwtClaims.get("userpic")); + return userJwt; + } + + @Data + public class UserJwt{ + private String id; + private String name; + private String userpic; + private String utype; + private String companyId; + } + +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtil.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..234ca6fda1df6b70a20c3562efbf7d6b646da855 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtil.java @@ -0,0 +1,38 @@ +package com.itools.core.utils; + + +import net.lingala.zip4j.core.ZipFile; +import net.lingala.zip4j.exception.ZipException; + +public class ZipUtil { + + /** + * 解压zip文件 + * @param zipFilePath + * @param targetPath + * @throws ZipException + */ + public static void unzip(String zipFilePath,String targetPath) throws Exception{ + ZipFile zipFile = new ZipFile(zipFilePath); + zipFile.extractAll(targetPath); + } + + /** + * 解压zip文件(带密码) + * @param zipFilePath + * @param targetPath + * @param password + * @throws ZipException + */ + public static void unzip(String zipFilePath,String password,String targetPath) throws Exception{ + ZipFile zipFile = new ZipFile(zipFilePath); + if (zipFile.isEncrypted()) { + zipFile.setPassword(password); + } + zipFile.extractAll(targetPath); + } + + public static void main(String[] args) throws Exception { + ZipUtil.unzip("F:\\develop\\upload\\upload.zip","F:\\develop\\upload\\zip\\"); + } +} diff --git a/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtils.java b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8591326727d639c3e534a23f694e3171ae732f48 --- /dev/null +++ b/itools-core/itools-utils/src/main/java/com/itools/core/utils/ZipUtils.java @@ -0,0 +1,56 @@ +package com.itools.core.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ZipUtils { + private static int BUFFER_SIZE = 1024; + + public ZipUtils() { + } + + public static void toZip(List files, OutputStream outputStream) throws RuntimeException { + long start = System.currentTimeMillis(); + ZipOutputStream zipOutputStream = null; + + try { + zipOutputStream = new ZipOutputStream(outputStream); + Iterator var5 = files.iterator(); + + while(var5.hasNext()) { + File file = (File)var5.next(); + byte[] buf = new byte[BUFFER_SIZE]; + zipOutputStream.putNextEntry(new ZipEntry(file.getName())); + FileInputStream in = new FileInputStream(file); + + int len; + while((len = in.read(buf)) != -1) { + zipOutputStream.write(buf, 0, len); + } + + zipOutputStream.closeEntry(); + in.close(); + } + + long end = System.currentTimeMillis(); + System.out.println("压缩完成,耗时:" + (end - start) + " ms"); + } catch (Exception var17) { + throw new RuntimeException("zip error from ZipUtils", var17); + } finally { + if (zipOutputStream != null) { + try { + zipOutputStream.close(); + } catch (IOException var16) { + var16.printStackTrace(); + } + } + + } + } +} \ No newline at end of file diff --git a/itools-core/pom.xml b/itools-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5c7361966f6bfc32a125d106e893fd8240e4ce7 --- /dev/null +++ b/itools-core/pom.xml @@ -0,0 +1,205 @@ + + + + + itools-backend + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + itools-core + pom + 工具集成统一异常、分布式雪花算法、参数验证、日志、统一拦截等等 + + itools-common + itools-utils + itools-model + + + + 1.8 + 8.5.28 + 2.0.1.RELEASE + 5.0.5.RELEASE + 3.3.2 + 1.1.6 + 5.1.45 + 2.6 + 1.3.2 + 1.3.3 + 1.10 + 3.6 + 3.9.1 + 8.18.0 + 1.16.16 + 2.9.2 + 1.2.30 + 1.27.0.0 + 5.1.40 + 6.2.1 + 24.0-jre + 4.1.35.Final + 6.0.13.Final + + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + Finchley.SR1 + pom + import + + + org.hibernate.validator + hibernate-validator + ${org.hibernate.validator.version} + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.2.4 + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + com.netflix.feign + feign-okhttp + ${feign-okhttp.version} + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.commons + commons-io + ${org.apache.commons.io.version} + + + commons-fileupload + commons-fileupload + ${commons-fileupload.version} + + + commons-codec + commons-codec + ${commons-codec.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + org.projectlombok + lombok + ${lombok.version} + + + io.springfox + springfox-swagger2 + ${springfox-swagger.version} + + + io.springfox + springfox-swagger-ui + ${springfox-swagger.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + net.oschina.zcx7878 + fastdfs-client-java + ${fastdfs-client-java.version} + + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + ${elasticsearch.version} + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + + + com.google.guava + guava + ${guava.version} + + + io.netty + netty-all + ${netty.version} + + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + UTF-8 + + + + org.codehaus.mojo + versions-maven-plugin + 2.3 + + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604555758869.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604555758869.png" new file mode 100644 index 0000000000000000000000000000000000000000..0e32b4943355381dfec39417aa8afa398bbd8cd2 Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604555758869.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556120012.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556120012.png" new file mode 100644 index 0000000000000000000000000000000000000000..8d7f13ae7c733f6fa572a2c2943233b320256d1d Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556120012.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556346057.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556346057.png" new file mode 100644 index 0000000000000000000000000000000000000000..44f988f8da16c2abfc259d24ab693495ce0f2020 Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556346057.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556562914.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556562914.png" new file mode 100644 index 0000000000000000000000000000000000000000..dc611f6980a833853d8a2af425eef00397d18359 Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556562914.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556812426.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556812426.png" new file mode 100644 index 0000000000000000000000000000000000000000..d529403d3712a85c91b4b97a45c78f48767e8cba Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604556812426.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604558494167.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604558494167.png" new file mode 100644 index 0000000000000000000000000000000000000000..4f14c0a225654e62308ed6e39879f51689b026e8 Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604558494167.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604561499549.png" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604561499549.png" new file mode 100644 index 0000000000000000000000000000000000000000..62202ac704972029a71cf63423b359afbfa63e8f Binary files /dev/null and "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.assets/1604561499549.png" differ diff --git "a/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.md" "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..a55bd16b5fc2d6ec14e06d0f8c295a69cf011c99 --- /dev/null +++ "b/itools-fms/document/fms\346\216\245\345\217\243\346\226\207\346\241\243.md" @@ -0,0 +1,260 @@ +## fms接口文档 + +### 配置 + +#### 1.配置文件 + +```java +spring: + servlet: + multipart: + # 最大支持文件大小 + max-file-size: 20MB + # 最大支持请求大小 + max-request-size: 200MB + # 上传文件的临时目录 + location: /home/hash/AppSvr01/installedApps/fsnsh_backend-master/temp + + +fileStoreRootPath: /data/fsnsh/fssFile #文件存储路径,此路径应该是多个FSS服务的共享磁盘 +fileAccessUrlForOutter: http://192.168.0.97:8002/File/ +fileUploadLimitSize: 10 #批量上传限制的个数 +fileMaxRequestSize: 204800 #最大的请求数,单位KB,200MB +fileMaxFileSize: 20480 #单个文件最大大小,单位KB,20MB +``` + +#### 2.执行运行的DDL/DML + +DDL在`script`目录下,再执行DML脚本配置文件类型 + +```sql +INSERT INTO `fss_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (1, '01', '文件上传', '102400', 'multipart/form-data,application/octet-stream,text/plain', '2019-08-20 18:48:57.000000', '2019-08-20 18:48:59.000000'); +INSERT INTO `fss_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (2, '02', 'jar包上传', '102400', 'application/octet-stream,text/plain,multipart/form-data', '2019-08-28 15:24:34.000000', '2019-08-28 15:24:37.000000'); +INSERT INTO `fss_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (3, 'currency', '通用类型', '102400', 'currency', '2019-10-14 20:21:37.000000', '2019-10-14 20:21:40.000000'); +INSERT INTO `fss_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (4, '03', 'doc', '102400', 'multipart/form-data; boundary=----WebKitFormBoundaryV9UszO8Om8qv8nNM', '2020-07-30 16:05:31.000000', '2020-07-30 16:05:35.000000'); + +``` + + + +### 接口 + +#### 申请文件上传token + +接口url:`http://127.0.0.1:8002/UploadToken` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| ------------ | ------ | -------------------------------------------------------- | +| businessType | String | 文件类型 | +| fromSystemId | String | 上传的文件来自哪个子系统,系统编号 | +| needEncrypt | String | 是否需加密,后续扩展使用,本期不使用,默认否.0:否 1:是 | +| remark | String | 文件描述信息 | + +接口描述: + +上传文件,先获取token,token设置有效时间 + +businessType:文件类型; + +fromSystemId:上传的文件来自哪个子系统,系统编号; + +needEncrypt:是否需加密,后续扩展使用,本期不使用,默认否.0:否 1:是; + +remark:文件描述信息 + +![1604555758869](./fms接口文档.assets\1604555758869.png) + +#### 单个文件上传 + +接口url:`http://127.0.0.1:8002/File` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| ----------- | ------ | ---------------------------------- | +| file | String | 文件 | +| uploadToken | String | 上传的文件来自哪个子系统,系统编号 | +| fileName | String | 自定义文件名,为空选择默认名称 | + +接口描述: + +![1604561499549](./fms接口文档.assets\1604561499549.png) + +#### 批量上传 文件 + +接口url:`http://127.0.0.1:8002/FilesPath` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| ----------- | ------ | ---------------------------------- | +| files | String | 选取的多个文件 | +| uploadToken | String | 上传的文件来自哪个子系统,系统编号 | + +接口描述: + +返回的文件名称对应的文件标识id + +uniqueId:文件标识id + +fileName:文件名称 + +```json +{ + "returnCode": "0000", + "returnMsg": "Success", + "nonceStr": "07830f77001141d9bcbb3f42a90481b3", + "success": true, + "data": [ + { + "uniqueId": "8cbaea82593a417ab530b91ce1c26e74", + "fileName": "微信图片_20201105130745 - 副本 - 副本 (3).jpg" + }, + { + "uniqueId": "a18210fb568345c4ba883d20704dc87b", + "fileName": "微信图片_20201105130745 - 副本 - 副本 - 副本.jpg" + } + ] +} +``` + + + +![1604556346057](./fms接口文档.assets\1604556346057.png) + +#### 获取文件访问路径 + +接口url:`http://127.0.0.1:8002/FilePath` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| -------------- | ------ | --------------------------- | +| uniqueId | String | 文件的唯一ID | +| expiredTime | Long | 有效时长,单位分钟 | +| maxAccessCount | Long | 最大访问次数 | +| type | String | 文件下载 download/展示 show | + +接口描述: + +获取该文件的访问的路径,也是需要根据传入的过期时间,访问类型等等 + +![1604556562914](./fms接口文档.assets\1604556562914.png) + +#### 获取多个文件访问路径 + +接口url:`http://127.0.0.1:8002/FilesPath` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| -------------- | -------- | --------------------------- | +| uniqueIds | String[] | 文件的唯一IDS,数组 | +| expiredTime | Long | 有效时长,单位分钟 | +| maxAccessCount | Long | 最大访问次数 | +| type | String | 文件下载 download/展示 show | + +接口描述: + +获取该文件的访问的路径,也是需要根据传入的过期时间,访问类型等等 + +```json +{ + "returnCode": "0000", + "returnMsg": "Success", + "nonceStr": "22487d32278e463b9cc6f3989df230a9", + "success": true, + "data": [ + { + "uniqueId": "1f449166a6e141f9b3b2dce5aa4e0b6b", + "path": "http://192.168.0.97:8002/File/1f449166a6e141f9b3b2dce5aa4e0b6b?fileAccessToken=a58f0d6969a945a795dd8cfb3ea85d26" + }, + { + "uniqueId": "a18210fb568345c4ba883d20704dc87b", + "path": "http://192.168.0.97:8002/File/a18210fb568345c4ba883d20704dc87b?fileAccessToken=44e79a91414d4692ac0224452e0d2c3d" + } + ] +} +``` + + + +![1604558494167](./fms接口文档.assets\1604558494167.png) + +#### 下载、访问文件 + +接口url:`http://127.0.0.1:8002/File` + +请求方式:GET + +接口参数: + +| 字段名称 | 类型 | 描述 | +| --------------- | ------ | ------------- | +| uniqueId | String | 文件的唯一ID | +| fileAccessToken | String | 文件访问token | + +接口描述: + +访问文件 + +#### 获取base64 + +接口url:`http://127.0.0.1:8002/File/getFileBase64` + +请求方式:GET + +接口参数: + +| 字段名称 | 类型 | 描述 | +| -------- | ------ | ------------ | +| uniqueId | String | 文件的唯一ID | + +接口描述: + +根据文件标识id返回base64的文件 + +#### 获取文件记录信息 + +接口url:`http://127.0.0.1:8002/fssRecord` + +请求方式:GET + +接口参数: + +| 字段名称 | 类型 | 描述 | +| -------- | ------ | ------------ | +| uniqueId | String | 文件的唯一ID | + +接口描述: + +获取文件记录信息 (内部调用) + +#### 下载zip压缩文件接口 + +接口url:`http://127.0.0.1:8002/download/zip` + +请求方式:POST + +接口参数: + +| 字段名称 | 类型 | 描述 | +| --------- | -------- | ------------- | +| uniqueIds | String[] | 文件的唯一IDS | +| zipName | String | 压缩文件名称 | + +接口描述: + +根据文件标识id下载对应的标识的文件,返回zip的压缩文件 \ No newline at end of file diff --git a/itools-fms/itools-fms-core/pom.xml b/itools-fms/itools-fms-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c32b56eb36cf39f34b6111bdb270b496d517f9f4 --- /dev/null +++ b/itools-fms/itools-fms-core/pom.xml @@ -0,0 +1,50 @@ + + + + itools-fms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-fms-core + + + com.itools.core + itools-common + 1.0-SNAPSHOT + compile + + + com.itools.core + itools-fms-model + 1.0-SNAPSHOT + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + com.github.xiaoymin + swagger-bootstrap-ui + 1.9.6 + + + io.minio + minio + 7.0.2 + + + + + + \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/annotation/FileStrategy.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/annotation/FileStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..57665e555d4c4964703bbdf989cfbf674310ff61 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/annotation/FileStrategy.java @@ -0,0 +1,24 @@ +package com.itools.core.annotation; + +import com.itools.core.em.StrategyType; +import org.springframework.stereotype.Service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @project: itools-backend + * @description: 选择文件系统的策略注解 + * @author: XUCHANG + * @create: 2021-04-01 15:57 + */ +@Service +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FileStrategy { + + StrategyType value(); + +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MinioConfig.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MinioConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..cb3ebe48f4ff6e7330e198b6e77c00e3f84a6f51 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MinioConfig.java @@ -0,0 +1,49 @@ +package com.itools.core.config; + +import io.minio.MinioClient; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:06 + */ +@Data +@Component +@ConfigurationProperties(prefix = "fms.minio") +public class MinioConfig { + + @ApiModelProperty("endPoint是一个URL,域名,IPv4或者IPv6地址") + private String endpoint; + + @ApiModelProperty("TCP/IP端口号") + private int port; + + @ApiModelProperty("accessKey类似于用户ID,用于唯一标识你的账户") + private String accessKey; + + @ApiModelProperty("secretKey是你账户的密码") + private String secretKey; + + @ApiModelProperty("如果是true,则用的是https而不是http,默认值是true") + private Boolean secure; + + @ApiModelProperty("默认存储桶") + private String bucketName; + + @ApiModelProperty("配置目录") + private String configDir; + + @Bean + public MinioClient getMinioClient() throws InvalidEndpointException, InvalidPortException { + MinioClient minioClient = new MinioClient(endpoint, port, accessKey, secretKey,secure); + return minioClient; + } +} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..56a51101d22b2a1fbbb89ddb08bd311c054e7111 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java @@ -0,0 +1,32 @@ +package com.itools.core.config; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:06 + */ +@Configuration +@EnableTransactionManagement +@MapperScan("com.itools.core.mapper") +public class MybatisPlusConfig { + @Bean + public PaginationInterceptor paginationInterceptor() { + + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false + // paginationInterceptor.setOverflow(false); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + // paginationInterceptor.setLimit(500); + // 开启 count 的 join 优化,只针对部分 left join + paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); + return paginationInterceptor; + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerAutoConfiguration.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerAutoConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..ddd193ef8a2a939e7eadeb0227965e08f58185e6 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerAutoConfiguration.java @@ -0,0 +1,41 @@ +//package com.itools.core.config; +// +//import io.swagger.annotations.ApiOperation; +//import io.swagger.annotations.Contact; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import springfox.documentation.builders.ApiInfoBuilder; +//import springfox.documentation.builders.PathSelectors; +//import springfox.documentation.builders.RequestHandlerSelectors; +//import springfox.documentation.service.ApiInfo; +//import springfox.documentation.spi.DocumentationType; +//import springfox.documentation.spring.web.plugins.Docket; +//import springfox.documentation.swagger2.annotations.EnableSwagger2; +// +//@Configuration +//@EnableSwagger2 +//public class SwaggerAutoConfiguration{ +// +// @Bean +// public Docket createRestApi() { +// return new Docket(DocumentationType.SWAGGER_2) +// .groupName("") +// .apiInfo(apiInfo()) +// .select() +// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) +// .paths(PathSelectors.any()) +// .build(); +// } +// +// +// private ApiInfo apiInfo() { +// return new ApiInfoBuilder() +// .title("**平台对外接口") +// .description("1.提供**后台使用的接口 2.提供对其他服务调用的服务") +//// .contact(new Contact("xtj332", "https://blog.csdn.net/xtj332", "xtj332111@163.com")) +// .version("1.0") +// .build(); +// } +// +// +//} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerConfig.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..b19297eadf3b6f2ccc430e53829d9279364240af --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/SwaggerConfig.java @@ -0,0 +1,108 @@ +//package com.itools.core.config; +// +//import io.swagger.annotations.ApiOperation; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.Profile; +//import springfox.documentation.builders.ApiInfoBuilder; +//import springfox.documentation.builders.ParameterBuilder; +//import springfox.documentation.builders.PathSelectors; +//import springfox.documentation.builders.RequestHandlerSelectors; +//import springfox.documentation.schema.ModelRef; +//import springfox.documentation.service.*; +//import springfox.documentation.spi.DocumentationType; +//import springfox.documentation.spi.service.contexts.SecurityContext; +//import springfox.documentation.spring.web.plugins.Docket; +//import springfox.documentation.swagger2.annotations.EnableSwagger2; +// +//import java.time.LocalTime; +//import java.util.ArrayList; +//import java.util.List; +// +///** +// * SwaggerConfig api文档地址:/swagger-ui.html 或者 /doc.html +// */ +////指定在某些环境启用swagger的配置 +//@Profile({ +// "dev","test" +//}) +//// swagger暂不支持webflux +//@Configuration +//@EnableSwagger2 +//public class SwaggerConfig { +// @Bean +// public Docket createRestApi() { +// //定义swagger全局入参token +// ParameterBuilder tokenPar = new ParameterBuilder(); +// List pars = new ArrayList(); +// tokenPar.name("Authorization").description("用户令牌(无须鉴权的可以不传 格式:bearer 密文串").modelRef(new ModelRef("string")) +// .parameterType("header").required(false).build(); +// pars.add(tokenPar.build()); +// +// return new Docket(DocumentationType.SWAGGER_2) +// .directModelSubstitute(LocalTime.class, String.class) +// //.groupName("normal") //使用默认分组,便于网关层的swagger转发 +// .apiInfo(defaultTitleInfo()) //可设置默认分组api顶部说明文字 +// .useDefaultResponseMessages(false) +// .select() +// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) +// //只允许生成ApiOperation注解的方法,其他系统内部用的api可隐藏 +// .build(); +//// //.globalOperationParameters(pars); +//// //设置安全模式,swagger全局设置token +//// .securitySchemes(securitySchemes()) +//// .securityContexts(securityContexts()); +// +// } +// +// /** +// * 安全模式,这里指定token通过Authorization头请求头传递 +// */ +// private List securitySchemes() +// { +// List apiKeyList = new ArrayList<>(); +// apiKeyList.add(new ApiKey("Authorization", "Authorization", "header")); +// return apiKeyList; +// } +// +// /** +// * 安全上下文 +// */ +// private List securityContexts() +// { +// List securityContexts = new ArrayList<>(); +// securityContexts.add( +// SecurityContext.builder() +// .securityReferences(defaultAuth()) +// .forPaths(PathSelectors.regex("^(?!auth).*$")) +// .build()); +// return securityContexts; +// } +// +// /** +// * 默认的安全上引用 +// */ +// private List defaultAuth() +// { +// AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); +// AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; +// authorizationScopes[0] = authorizationScope; +// List securityReferences = new ArrayList<>(); +// securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); +// return securityReferences; +// } +// +// /** +// * 默认标题信息 +// * @return - +// */ +// private ApiInfo defaultTitleInfo() { +// return new ApiInfoBuilder() +// .title("接口地址使用Base URL")//大标题 +// .description("· 接口成功返回:{\"success\": true,\"code\": 20000,\"message\": \"成功\",\"data\": null} " +// + "
· 失败的返回:{\"success\": false,\"code\": 50002,\"message\": \"算术运算异常\",\"data\": null} " +// )//详细描述 +// .version("1.0")//版本 +// .build(); +// } +//} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java new file mode 100644 index 0000000000000000000000000000000000000000..b0a54381097ce8e213354431819b90382201aa26 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java @@ -0,0 +1,52 @@ +package com.itools.core.context; + +import com.itools.core.em.StrategyType; +import com.itools.core.service.FileManagerService; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:02 + */ +@Component +public class FileStrategyServiceContext { + @Autowired + private Environment environment; + @Getter + private final static Map fileServiceMap; + + static { + fileServiceMap = new HashMap<>(); + } + + public FileManagerService get(String type) { + return fileServiceMap.get(type); + } + + /** + * 获取默认或者配置的文件服务策略 + * @return + */ + public FileManagerService get(){ + boolean containsProperty = environment.containsProperty("fms.strategy"); + if (containsProperty){ + fileServiceMap.get(environment.getProperty("fms.strategy")); + } + return fileServiceMap.get(StrategyType.NIO.getType()); + } + + public void put(String type, FileManagerService calcService) { + fileServiceMap.put(type, calcService); + } + public Map getMap(){ + return fileServiceMap; + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsCodeBean.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsCodeBean.java new file mode 100644 index 0000000000000000000000000000000000000000..894387e03220965d7b715dcc12217d126190a9a5 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsCodeBean.java @@ -0,0 +1,81 @@ +package com.itools.core.em; + + +import com.itools.core.code.SystemCode; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SystemCode +public class FmsCodeBean { + + public enum FmsCode { + + FAIL_SYSTEM("FMSCODE00", "系统内部错误,请联系管理员!"), + /** + * 文件系统内部错误 + */ + FAIL("FMSCODE01", "文件系统内部错误"), + /** + * BUSINESS_TYPE不存在 + */ + ERROR_BUSINESS_TYPE_NOT_EXIST("FMSCODE02", "BUSINESS_TYPE不存在"), + /** + * 文件不能为空 + */ + ERROR_FILE_IS_NULL("FMSCODE03", "文件不能为空"), + /** + * 参数错误 + */ + ERROR_PARAMS("FMSCODE04", "参数错误"), + /** + * 文件上传时间限定超时 + */ + ERROR_TIMEOUT("FMSCODE05", "文件上传时间限定超时"), + /** + * 文件大小超过限制 + */ + ERROR_FILESIZE_OUT_OFF_LIMIT("FMSCODE06", "文件大小超过限制"), + /** + * 该文件类型不允许 + */ + ERROR_FILETYPE("FMSCODE07", "该文件类型不允许"), + /** + * 文件ID非法 + */ + INVALID_FS_UNIQUE_ID("FMSCODE08", "文件ID非法"), + ERROR_TRANS_BASE64("FMSCODE09", "文件转base64失败"), + /** + * 鉴权失败 + */ + INVALID_FS_TOKEN("FMSCODE10", "鉴权失败"), + /** + * 找不到该文件 + * 错误码直接放到response中,所以按照http的规范设置 + */ + NOT_EXIST_FILE("FMSCODE11", "找不到该文件"), + FILE_LIMIT_SIZE("FMSCODE12", "文件上传个数受限"), + FILE_MULTIPART_UPLOAD("FMSCODE13", "批量上传文件失败"), + FAIL_FILE_TOKEN("FMSCODE14", "上传申请的TOKEN无效,请重新申请!"), + FILE_TOKEN_NOT_NULL("FMSCODE15", "文件访问的TOKEN不能为空"), + ; + + public final String code; + public final String message; + + FmsCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsConstants.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..50badfb565f13397272dc5429824d3495087a49e --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/FmsConstants.java @@ -0,0 +1,23 @@ +package com.itools.core.em; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsConstants { + /** + * 文件记录的KEY + */ + public static final String FSS_RECORD_ID_SEQ_NAME="FSS:FssRecord:ID"; + /** + * 文件申请上传的token的名称空间 + */ + public static final String FILE_TOKEN_NAME_SPACE="itools:fms:file-token:"; + /** + * 文件通用类型 + */ + public static final String FILE_CURRENCY_TYPE="currency"; + + +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/StrategyType.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/StrategyType.java new file mode 100644 index 0000000000000000000000000000000000000000..85be616086b5a0f05c0475d1c42b7a22476079a8 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/em/StrategyType.java @@ -0,0 +1,37 @@ +package com.itools.core.em; + +import com.itools.core.utils.StringUtils; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:02 + */ +public enum StrategyType { + NIO("nio", "NIO","nioFileManagerServiceImpl"), + MINIO("minio", "miniio文件系统","minioFileManagerServiceImpl"), + FASTDFS("fastdfs", "fastdfs文件系统","fastDfsFileManagerServiceImpl"); + private String type; + private String desc; + private String beanName; + + StrategyType(String type, String desc,String beanName) { + this.type = type; + this.desc = desc; + this.beanName = beanName; + } + + public String getType() { + return type; + } + + public static String getType(String beanName) { + for (StrategyType strategyType : StrategyType.values()){ + if (StringUtils.equals(beanName,strategyType.beanName)){ + return strategyType.type; + } + } + return null; + } +} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java new file mode 100644 index 0000000000000000000000000000000000000000..06b83d88643b09bfa6cd5315fbd00b625e6ff2d0 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java @@ -0,0 +1,44 @@ +package com.itools.core.listener; + +import com.itools.core.annotation.FileStrategy; +import com.itools.core.context.FileStrategyServiceContext; +import com.itools.core.em.StrategyType; +import com.itools.core.service.FileManagerService; +import com.itools.core.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:06 + */ +@Component +public class FileStrategyListener implements ApplicationListener { + @Autowired + private FileStrategyServiceContext fileStrategyServiceContext; + + /** + * 监听类 + * @param contextRefreshedEvent + */ + @Override + public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { + + Map beans = contextRefreshedEvent.getApplicationContext().getBeansOfType(FileManagerService.class); + beans.forEach((k, service) -> { + String type = StrategyType.getType(k); + fileStrategyServiceContext.put(type, service); +// if (StringUtils.equals(k,)) +// Class clazz = service.getClass(); +// FileStrategy fileStrategy = (FileStrategy)clazz.getAnnotation(FileStrategy.class); +// fileStrategyServiceContext.put(fileStrategy.value().getType(), service); + }); + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java new file mode 100644 index 0000000000000000000000000000000000000000..8d50488a041e75292db05e150b9a97f440ef3336 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java @@ -0,0 +1,33 @@ +package com.itools.core.service; + +import com.itools.core.base.CommonResult; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.result.FmsUploadTokenResult; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-02 16:34 + */ +public interface FileHandleService { + /** + * 根据申请的文件token获取返回的上传文件的类型信息 + * @param fileToken 文件token + * @return + */ + CommonResult getFileDetailByToken(String fileToken); + + /** + * 文件策略为NIO + * 执行文件写入和上传 + * @param file 文件 + * @param param 上传参数 + * @param uploadToken 访问token + * @param originalFilename 原文件名称 + * @return + */ + CommonResult saveFileForNio(MultipartFile file, FmsUploadTokenParam param, String uploadToken, String originalFilename); +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java new file mode 100644 index 0000000000000000000000000000000000000000..42a99d39e86c4dc9378cbc3dc9ea8c05ee29e1cc --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java @@ -0,0 +1,39 @@ +package com.itools.core.service; + +import com.itools.core.base.CommonResult; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import org.springframework.web.multipart.MultipartFile; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 15:44 + */ +public interface FileManagerService { + /** + * 批量上传文件 + * @param files 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + CommonResult mutipartUploadFiles(MultipartFile files, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename); + /** + * 单个上传文件 + * @param file 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + CommonResult singletonUploadFiles(MultipartFile file, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename); +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8b59e436911a98b7edbcefcec4f11d120d28e2b5 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java @@ -0,0 +1,52 @@ +package com.itools.core.service.impl; + +import com.itools.core.annotation.FileStrategy; +import com.itools.core.base.CommonResult; +import com.itools.core.em.StrategyType; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.service.FileManagerService; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:17 + */ +@FileStrategy(value = StrategyType.FASTDFS) +public class FastDfsFileManagerServiceImpl implements FileManagerService { + /** + * 批量上传文件 + * @param files 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + public CommonResult mutipartUploadFiles(MultipartFile files, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + return null; + } + + /** + * 单个上传文件 + * + * @param file 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + public CommonResult singletonUploadFiles(MultipartFile file, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + return null; + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..7b8215ba5dc4ebd729f38799306155ccf1d3454b --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java @@ -0,0 +1,52 @@ +package com.itools.core.service.impl; + +import com.itools.core.annotation.FileStrategy; +import com.itools.core.base.CommonResult; +import com.itools.core.em.StrategyType; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.service.FileManagerService; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:17 + */ +@FileStrategy(value = StrategyType.MINIO) +public class MinioFileManagerServiceImpl implements FileManagerService { + /** + * 批量上传文件 + * @param files 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + public CommonResult mutipartUploadFiles(MultipartFile files, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + return null; + } + + /** + * 单个上传文件 + * + * @param file 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + public CommonResult singletonUploadFiles(MultipartFile file, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + return null; + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..46319136f891733c1a9f8f707f3c741283e6bee6 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java @@ -0,0 +1,66 @@ +package com.itools.core.service.impl; + +import com.itools.core.annotation.FileStrategy; +import com.itools.core.base.CommonResult; +import com.itools.core.em.StrategyType; +import com.itools.core.log.Logable; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.service.FileHandleService; +import com.itools.core.service.FileManagerService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 16:17 + */ +@FileStrategy(value = StrategyType.NIO) +public class NioFileManagerServiceImpl implements FileManagerService { + @Autowired + private FileHandleService fileHandleService; + /** + * 批量上传文件 + * @param files 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + public CommonResult mutipartUploadFiles(MultipartFile files, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + return null; + } + + /** + * 单个上传文件 + * + * @param file 文件 + * @param uploadToken 文件的token + * @param businessType 文件的类型,以及限制条件 + * @param originalFilename 原名称 + * @return + */ + @Override + @Logable + @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) + public CommonResult singletonUploadFiles(MultipartFile file, + String uploadToken, + FmsBusinessTypeResult businessType, + String originalFilename) { + FmsUploadTokenParam param = new FmsUploadTokenParam(); + BeanUtils.copyProperties(businessType , param); + param.setOriginalFilename(originalFilename); + CommonResult fmsFileUploadResultCommonResult = fileHandleService.saveFileForNio(file, param, uploadToken, originalFilename); + return fmsFileUploadResultCommonResult; + } +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/utils/MinioUtil.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/utils/MinioUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..561de3f73ae1bcef3ae8497f263204bc3fcfb982 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/utils/MinioUtil.java @@ -0,0 +1,430 @@ +package com.itools.core.utils; + +import io.minio.MinioClient; +import io.minio.ObjectStat; +import io.minio.PutObjectOptions; +import io.minio.Result; +import io.minio.errors.ErrorResponseException; +import io.minio.errors.InvalidExpiresRangeException; +import io.minio.messages.Bucket; +import io.minio.messages.DeleteError; +import io.minio.messages.Item; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: 存储桶命名要求 + * 存储桶的命名需符合以下一个或多个规则 + * + * 存储桶名称的长度介于 3 和 63 个字符之间,并且只能包含小写字母、数字、句点和短划线。 + * 存储桶名称中的每个标签必须以小写字母或数字开头。 + * 存储桶名称不能包含下划线、以短划线结束、包含连续句点或在句点旁边使用短划线。 + * 存储桶名称不能采用 IP 地址格式 (198.51.100.24)。 + * 存储桶用作可公开访问的 URL,因此您选择的存储桶名称必须具有全局唯一性。如果您选择的名称已被其他一些帐户用于创建存储桶,则必须使用其他名称。 + * @author: XUCHANG + * @create: 2021-04-01 16:06 + */ +@Component +public class MinioUtil { + + @Autowired + private MinioClient minioClient; + + private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600; + + /** + * 检查存储桶是否存在 + * + * @param bucketName 存储桶名称 + * @return + */ + @SneakyThrows + public boolean bucketExists(String bucketName) { + boolean flag = false; + flag = minioClient.bucketExists(bucketName); + if (flag) { + return true; + } + return false; + } + + /** + * 创建存储桶 + * + * @param bucketName 存储桶名称 + */ + @SneakyThrows + public boolean makeBucket(String bucketName) { + boolean flag = bucketExists(bucketName); + if (!flag) { + minioClient.makeBucket(bucketName); + return true; + } else { + return false; + } + } + + /** + * 列出所有存储桶名称 + * + * @return + */ + @SneakyThrows + public List listBucketNames() { + List bucketList = listBuckets(); + List bucketListName = new ArrayList<>(); + for (Bucket bucket : bucketList) { + bucketListName.add(bucket.name()); + } + return bucketListName; + } + + /** + * 列出所有存储桶 + * + * @return + */ + @SneakyThrows + public List listBuckets() { + return minioClient.listBuckets(); + } + + /** + * 删除存储桶 + * + * @param bucketName 存储桶名称 + * @return + */ + @SneakyThrows + public boolean removeBucket(String bucketName) { + boolean flag = bucketExists(bucketName); + if (flag) { + Iterable> myObjects = listObjects(bucketName); + for (Result result : myObjects) { + Item item = result.get(); + // 有对象文件,则删除失败 + if (item.size() > 0) { + return false; + } + } + // 删除存储桶,注意,只有存储桶为空时才能删除成功。 + minioClient.removeBucket(bucketName); + flag = bucketExists(bucketName); + if (!flag) { + return true; + } + + } + return false; + } + + /** + * 列出存储桶中的所有对象名称 + * + * @param bucketName 存储桶名称 + * @return + */ + @SneakyThrows + public List listObjectNames(String bucketName) { + List listObjectNames = new ArrayList<>(); + boolean flag = bucketExists(bucketName); + if (flag) { + Iterable> myObjects = listObjects(bucketName); + for (Result result : myObjects) { + Item item = result.get(); + listObjectNames.add(item.objectName()); + } + } + return listObjectNames; + } + + /** + * 列出存储桶中的所有对象 + * + * @param bucketName 存储桶名称 + * @return + */ + @SneakyThrows + public Iterable> listObjects(String bucketName) { + boolean flag = bucketExists(bucketName); + if (flag) { + return minioClient.listObjects(bucketName); + } + return null; + } + + /** + * 通过文件上传到对象 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param fileName File name + * @return + */ + @SneakyThrows + public boolean putObject(String bucketName, String objectName, String fileName) { + boolean flag = bucketExists(bucketName); + if (flag) { + minioClient.putObject(bucketName, objectName, fileName, null); + ObjectStat statObject = statObject(bucketName, objectName); + if (statObject != null && statObject.length() > 0) { + return true; + } + } + return false; + + } + + /** + * 文件上传 + * + * @param bucketName + * @param multipartFile + */ + @SneakyThrows + public void putObject(String bucketName, MultipartFile multipartFile, String filename) { + PutObjectOptions putObjectOptions = new PutObjectOptions(multipartFile.getSize(), PutObjectOptions.MIN_MULTIPART_SIZE); + putObjectOptions.setContentType(multipartFile.getContentType()); + minioClient.putObject(bucketName, filename, multipartFile.getInputStream(), putObjectOptions); + } + + /** + * 通过InputStream上传对象 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param stream 要上传的流 + * @return + */ + @SneakyThrows + public boolean putObject(String bucketName, String objectName, InputStream stream) { + boolean flag = bucketExists(bucketName); + if (flag) { + minioClient.putObject(bucketName, objectName, stream, new PutObjectOptions(stream.available(), -1)); + ObjectStat statObject = statObject(bucketName, objectName); + if (statObject != null && statObject.length() > 0) { + return true; + } + } + return false; + } + + /** + * 以流的形式获取一个文件对象 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @return + */ + @SneakyThrows + public InputStream getObject(String bucketName, String objectName) { + boolean flag = bucketExists(bucketName); + if (flag) { + ObjectStat statObject = statObject(bucketName, objectName); + if (statObject != null && statObject.length() > 0) { + InputStream stream = minioClient.getObject(bucketName, objectName); + return stream; + } + } + return null; + } + + /** + * 以流的形式获取一个文件对象(断点下载) + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param offset 起始字节的位置 + * @param length 要读取的长度 (可选,如果无值则代表读到文件结尾) + * @return + */ + @SneakyThrows + public InputStream getObject(String bucketName, String objectName, long offset, Long length) { + boolean flag = bucketExists(bucketName); + if (flag) { + ObjectStat statObject = statObject(bucketName, objectName); + if (statObject != null && statObject.length() > 0) { + InputStream stream = minioClient.getObject(bucketName, objectName, offset, length); + return stream; + } + } + return null; + } + + /** + * 下载并将文件保存到本地 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param fileName File name + * @return + */ + @SneakyThrows + public boolean getObject(String bucketName, String objectName, String fileName) { + boolean flag = bucketExists(bucketName); + if (flag) { + ObjectStat statObject = statObject(bucketName, objectName); + if (statObject != null && statObject.length() > 0) { + minioClient.getObject(bucketName, objectName, fileName); + return true; + } + } + return false; + } + + /** + * 删除一个对象 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + */ + @SneakyThrows + public boolean removeObject(String bucketName, String objectName) { + boolean flag = bucketExists(bucketName); + if (flag) { + minioClient.removeObject(bucketName, objectName); + return true; + } + return false; + } + + /** + * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表 + * + * @param bucketName 存储桶名称 + * @param objectNames 含有要删除的多个object名称的迭代器对象 + * @return + */ + @SneakyThrows + public List removeObject(String bucketName, List objectNames) { + List deleteErrorNames = new ArrayList<>(); + boolean flag = bucketExists(bucketName); + if (flag) { + Iterable> results = minioClient.removeObjects(bucketName, objectNames); + for (Result result : results) { + DeleteError error = result.get(); + deleteErrorNames.add(error.objectName()); + } + } + return deleteErrorNames; + } + + /** + * 生成一个给HTTP GET请求用的presigned URL。 + * 浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param expires 失效时间(以秒为单位),默认是7天,不得大于七天 + * @return + */ + @SneakyThrows + public String presignedGetObject(String bucketName, String objectName, Integer expires) { + boolean flag = bucketExists(bucketName); + String url = ""; + if (flag) { + if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) { + throw new InvalidExpiresRangeException(expires, + "expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME); + } + url = minioClient.presignedGetObject(bucketName, objectName, expires); + } + return url; + } + + /** + * 生成一个给HTTP PUT请求用的presigned URL。 + * 浏览器/移动端的客户端可以用这个URL进行上传,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param expires 失效时间(以秒为单位),默认是7天,不得大于七天 + * @return + */ + @SneakyThrows + public String presignedPutObject(String bucketName, String objectName, Integer expires) { + boolean flag = bucketExists(bucketName); + String url = ""; + if (flag) { + if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) { + throw new InvalidExpiresRangeException(expires, + "expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME); + } + url = minioClient.presignedPutObject(bucketName, objectName, expires); + } + return url; + } + + /** + * 获取对象的元数据 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @return + */ + @SneakyThrows + public ObjectStat statObject(String bucketName, String objectName) { + boolean flag = bucketExists(bucketName); + if (flag) { + ObjectStat statObject = minioClient.statObject(bucketName, objectName); + return statObject; + } + return null; + } + + /** + * 文件访问路径 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @return + */ + @SneakyThrows + public String getObjectUrl(String bucketName, String objectName) { + boolean flag = bucketExists(bucketName); + String url = ""; + if (flag) { + url = minioClient.getObjectUrl(bucketName, objectName); + } + return url; + } + + + + public void downloadFile(String bucketName, String fileName, String originalName, HttpServletResponse response) { + try { + + InputStream file = minioClient.getObject(bucketName, fileName); + String filename = new String(fileName.getBytes("ISO8859-1"), StandardCharsets.UTF_8); + if (StringUtils.isNotEmpty(originalName)) { + fileName = originalName; + } + response.setHeader("Content-Disposition", "attachment;filename=" + filename); + ServletOutputStream servletOutputStream = response.getOutputStream(); + int len; + byte[] buffer = new byte[1024]; + while ((len = file.read(buffer)) > 0) { + servletOutputStream.write(buffer, 0, len); + } + servletOutputStream.flush(); + file.close(); + servletOutputStream.close(); + } catch (ErrorResponseException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/pom.xml b/itools-fms/itools-fms-model/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f7be9ab8fa10a0a0e5ef12a223e814b10a793bf4 --- /dev/null +++ b/itools-fms/itools-fms-model/pom.xml @@ -0,0 +1,33 @@ + + + + itools-fms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-fms-model + + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.baomidou + mybatis-plus-generator + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/accessToken/FmsAccessTokenDTO.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/accessToken/FmsAccessTokenDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ada12f03f8a82c48a0993c79b8f1916431f1e800 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/accessToken/FmsAccessTokenDTO.java @@ -0,0 +1,28 @@ +package com.itools.core.dto.accessToken; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +@TableName("itools_fms_access_token") +public final class FmsAccessTokenDTO { + /** + * 文件访问token + */ + private String fileAccessToken; + /** + * 文件的唯一id + */ + private String uniqueId; + /** + * 访问类型:show或者download + */ + private String type; + +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/businessType/FmsBusinessTypeDTO.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/businessType/FmsBusinessTypeDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..9a0ff7923a590bf84a8d2546b727da6e7ffde612 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/businessType/FmsBusinessTypeDTO.java @@ -0,0 +1,52 @@ +package com.itools.core.dto.businessType; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +@TableName("itools_fms_business_type") +public class FmsBusinessTypeDTO { + /** + * 主键 + */ + private Long id; + + /** + * 业务类型 + */ + private String businessType; + + /** + * 说明 + */ + private String remark; + + /** + * 文件大小, 单位kb + */ + private String fsSize; + + /** + * 文件类型,逗号分隔符,例如: jpg,png + */ + private String fsType; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新时间 + */ + private Date updateDate; + +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/fileRecord/FmsRecordDTO.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/fileRecord/FmsRecordDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..cc3932d1ff5b478eeaea763f1eae0134cb0c211c --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/dto/fileRecord/FmsRecordDTO.java @@ -0,0 +1,77 @@ +package com.itools.core.dto.fileRecord; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; +import java.util.List; +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +@TableName("itools_fms_record") +public class FmsRecordDTO { + /** + * 主键 + */ + private Long id; + + /** + * HTTP协议定义的文件类型,文件上传从文件流中获取 + * 文件存取时使用该字段填入response的content-type字段 + */ + private String contentType; + + /** + * 文件所属系统,文件上传时指定 + */ + private String fromSystemId; + + /** + * 文件大小,单位为字节 + */ + private Long fileSize; + + /** + * 文件的全局唯一ID,后续访问均使用该ID + */ + private String uniqueId; + /** + * 文件的全局唯一IDs,后续访问均使用该IDs + */ + private List uniqueIds; + + /** + * 文件描述信息 + */ + private String remark; + + /** + * 原文件名,来自入参 + */ + private String origFileName; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 删除时间 + */ + private Date deleteTime; + + /** + * 是否已删除文件本身,也即是说,删除文件时数据库记录仅仅将该字段置位1,并不实际删除数据库记录 + * 0:没有删除 + * 1:已删除 + */ + private String deleted; +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsAccessTokenParam.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsAccessTokenParam.java new file mode 100644 index 0000000000000000000000000000000000000000..01f4a848076de84859efb24c4b66d120c8a574ba --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsAccessTokenParam.java @@ -0,0 +1,69 @@ +package com.itools.core.param; + + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsAccessTokenParam { + /** + * 有效时长 + */ + private int expiredTime; + + /** + * 最大访问次数 + */ + private int maxAccessCount; + + /** + * 文件访问唯一id + */ + private String uniqueId; + + private String type; + + public FmsAccessTokenParam(int expiredTime, int maxAccessCount, String uniqueId, String type) { + this.expiredTime = expiredTime; + this.maxAccessCount = maxAccessCount; + this.uniqueId = uniqueId; + this.type = type; + } + + public FmsAccessTokenParam() { + } + + public int getExpiredTime() { + return expiredTime; + } + + public void setExpiredTime(int expiredTime) { + this.expiredTime = expiredTime; + } + + public int getMaxAccessCount() { + return maxAccessCount; + } + + public void setMaxAccessCount(int maxAccessCount) { + this.maxAccessCount = maxAccessCount; + } + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsBusinessTypeParam.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsBusinessTypeParam.java new file mode 100644 index 0000000000000000000000000000000000000000..209597469c8edca3ad2d81a4e9017c5e48c6863c --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsBusinessTypeParam.java @@ -0,0 +1,102 @@ +package com.itools.core.param; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsBusinessTypeParam { + /** + * 主键 + */ + private Long id; + + /** + * 业务类型 + */ + private String businessType; + + /** + * 说明 + */ + private String remark; + + /** + * 文件大小, 单位kb + */ + private String fsSize; + + /** + * 文件类型,逗号分隔符,例如: jpg,png + */ + private String fsType; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新时间 + */ + private Date updateDate; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBusinessType() { + return businessType; + } + + public void setBusinessType(String businessType) { + this.businessType = businessType; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getFsSize() { + return fsSize; + } + + public void setFsSize(String fsSize) { + this.fsSize = fsSize; + } + + public String getFsType() { + return fsType; + } + + public void setFsType(String fsType) { + this.fsType = fsType; + } + + public Date getCreateDate() { + return createDate; + } + + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(Date updateDate) { + this.updateDate = updateDate; + } +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsUploadTokenParam.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsUploadTokenParam.java new file mode 100644 index 0000000000000000000000000000000000000000..fb5b2c4e418f53ef6f3164fecfe408df2c7cf120 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/param/FmsUploadTokenParam.java @@ -0,0 +1,58 @@ +package com.itools.core.param; + +import com.itools.core.result.FmsBusinessTypeResult; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsUploadTokenParam { + + private FmsBusinessTypeResult fsBusinessType; + private String fromSystemId; + private String needEncrypt; + private String remark; + private String originalFilename; + + public FmsBusinessTypeResult getFsBusinessType() { + return fsBusinessType; + } + + public void setFsBusinessType(FmsBusinessTypeResult fsBusinessType) { + this.fsBusinessType = fsBusinessType; + } + + public String getFromSystemId() { + return fromSystemId; + } + + public void setFromSystemId(String fromSystemId) { + this.fromSystemId = fromSystemId; + } + + public String getNeedEncrypt() { + return needEncrypt; + } + + public void setNeedEncrypt(String needEncrypt) { + this.needEncrypt = needEncrypt; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsBase64FileUploadRequest.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsBase64FileUploadRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..42ac3265b99d833fa5590324ca603f1e6b76dbd8 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsBase64FileUploadRequest.java @@ -0,0 +1,55 @@ +package com.itools.core.request; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsBase64FileUploadRequest { + + String fileBaseSixFour; + String fileName; + String businessType; + String uploadToken; + + public String getFileBaseSixFour() { + return fileBaseSixFour; + } + + + public void setFileBaseSixFour(String fileBaseSixFour) { + this.fileBaseSixFour = fileBaseSixFour; + } + + + public String getFileName() { + return fileName; + } + + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + + public String getBusinessType() { + return businessType; + } + + + public void setBusinessType(String businessType) { + this.businessType = businessType; + } + + + public String getUploadToken() { + return uploadToken; + } + + + public void setUploadToken(String uploadToken) { + this.uploadToken = uploadToken; + } + +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsFileAccessUrlRequest.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsFileAccessUrlRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..6f06b4fdd27b19ea2ab72374e9ef2d2e42cbf0f5 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/request/FmsFileAccessUrlRequest.java @@ -0,0 +1,51 @@ +package com.itools.core.request; + +import java.io.Serializable; +import java.util.List; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/5 13:46 + */ +public class FmsFileAccessUrlRequest implements Serializable { + private List uniqueIds; + private Integer expiredTime; + private Integer maxAccessCount; + private String type; + + public FmsFileAccessUrlRequest() { + } + + public List getUniqueIds() { + return uniqueIds; + } + + public void setUniqueIds(List uniqueIds) { + this.uniqueIds = uniqueIds; + } + + public Integer getExpiredTime() { + return expiredTime; + } + + public void setExpiredTime(Integer expiredTime) { + this.expiredTime = expiredTime; + } + + public Integer getMaxAccessCount() { + return maxAccessCount; + } + + public void setMaxAccessCount(Integer maxAccessCount) { + this.maxAccessCount = maxAccessCount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsAccessTokenResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsAccessTokenResult.java new file mode 100644 index 0000000000000000000000000000000000000000..0eb4450aa99264db05e17c0a8f3d497ad1e1fbe8 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsAccessTokenResult.java @@ -0,0 +1,67 @@ +package com.itools.core.result; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/5 14:35 + */ +public class FmsAccessTokenResult { + /** + * 有效时长 + */ + private int expiredTime; + + /** + * 最大访问次数 + */ + private int maxAccessCount; + + /** + * 文件访问唯一id + */ + private String uniqueId; + + private String type; + + public FmsAccessTokenResult(int expiredTime, int maxAccessCount, String uniqueId, String type) { + this.expiredTime = expiredTime; + this.maxAccessCount = maxAccessCount; + this.uniqueId = uniqueId; + this.type = type; + } + + public FmsAccessTokenResult() { + } + + public int getExpiredTime() { + return expiredTime; + } + + public void setExpiredTime(int expiredTime) { + this.expiredTime = expiredTime; + } + + public int getMaxAccessCount() { + return maxAccessCount; + } + + public void setMaxAccessCount(int maxAccessCount) { + this.maxAccessCount = maxAccessCount; + } + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsBusinessTypeResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsBusinessTypeResult.java new file mode 100644 index 0000000000000000000000000000000000000000..b1a65469b7e5410ea3bf396aad6aa4a19d919914 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsBusinessTypeResult.java @@ -0,0 +1,104 @@ +package com.itools.core.result; + +import java.io.Serializable; +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/5 14:35 + */ +public class FmsBusinessTypeResult implements Serializable { + + private static final long serialVersionUID = -5246507559164483529L; + /** + * 主键 + */ + private Long id; + + /** + * 业务类型 + */ + private String businessType; + + /** + * 说明 + */ + private String remark; + + /** + * 文件大小, 单位kb + */ + private String fsSize; + + /** + * 文件类型,逗号分隔符,例如: jpg,png + */ + private String fsType; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新时间 + */ + private Date updateDate; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBusinessType() { + return businessType; + } + + public void setBusinessType(String businessType) { + this.businessType = businessType; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getFsSize() { + return fsSize; + } + + public void setFsSize(String fsSize) { + this.fsSize = fsSize; + } + + public String getFsType() { + return fsType; + } + + public void setFsType(String fsType) { + this.fsType = fsType; + } + + public Date getCreateDate() { + return createDate; + } + + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(Date updateDate) { + this.updateDate = updateDate; + } +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsDetailRecordResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsDetailRecordResult.java new file mode 100644 index 0000000000000000000000000000000000000000..cb29afe06f246700fb5978f7584ccc718c7a6ca2 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsDetailRecordResult.java @@ -0,0 +1,156 @@ +package com.itools.core.result; + +import java.util.Date; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/4 18:01 + */ +public class FmsDetailRecordResult { + /** + * 主键 + */ + private Long id; + + /** + * HTTP协议定义的文件类型,文件上传从文件流中获取 + * 文件存取时使用该字段填入response的content-type字段 + */ + private String contentType; + + /** + * 文件所属系统,文件上传时指定 + */ + private String fromSystemId; + + /** + * 文件大小,单位为字节 + */ + private Long fileSize; + + /** + * 文件的全局唯一ID,后续访问均使用该ID + */ + private String uniqueId; + + /** + * 文件描述信息 + */ + private String remark; + + /** + * 原文件名,来自入参 + */ + private String origFileName; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 删除时间 + */ + private Date deleteTime; + + /** + * 是否已删除文件本身,也即是说,删除文件时数据库记录仅仅将该字段置位1,并不实际删除数据库记录 + * 0:没有删除 + * 1:已删除 + */ + private String deleted; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getFromSystemId() { + return fromSystemId; + } + + public void setFromSystemId(String fromSystemId) { + this.fromSystemId = fromSystemId; + } + + public Long getFileSize() { + return fileSize; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getOrigFileName() { + return origFileName; + } + + public void setOrigFileName(String origFileName) { + this.origFileName = origFileName; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public Date getDeleteTime() { + return deleteTime; + } + + public void setDeleteTime(Date deleteTime) { + this.deleteTime = deleteTime; + } + + public String getDeleted() { + return deleted; + } + + public void setDeleted(String deleted) { + this.deleted = deleted; + } +} \ No newline at end of file diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileRecordPathResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileRecordPathResult.java new file mode 100644 index 0000000000000000000000000000000000000000..18b04c685136c95e141c5d79dcc1f0d5166514d1 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileRecordPathResult.java @@ -0,0 +1,32 @@ +package com.itools.core.result; + +import java.io.Serializable; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/5 14:35 + */ +public class FmsFileRecordPathResult implements Serializable { + /** + * 文件访问唯一id + */ + private String uniqueId; + private String path; + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileUploadResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileUploadResult.java new file mode 100644 index 0000000000000000000000000000000000000000..675c01835bb920501e6322cc994eab0ef6dff9c5 --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsFileUploadResult.java @@ -0,0 +1,21 @@ +package com.itools.core.result; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/5 14:35 + */ +public class FmsFileUploadResult { + /** + * 成功时必填,该文件的唯一ID + */ + private String uniqueId; + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsMultipartFileResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsMultipartFileResult.java new file mode 100644 index 0000000000000000000000000000000000000000..9d6b560c4c8f4fa03e3c3c12c3db60411760265d --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsMultipartFileResult.java @@ -0,0 +1,35 @@ +package com.itools.core.result; + +import java.io.Serializable; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/4 18:01 + */ +public class FmsMultipartFileResult implements Serializable { + /** + * 文件访问唯一id + */ + private String uniqueId; + /** + * 文件名称 + */ + private String fileName; + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsRecordResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsRecordResult.java new file mode 100644 index 0000000000000000000000000000000000000000..470226e692f505c8578a45bd32df402e6b48af3d --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsRecordResult.java @@ -0,0 +1,20 @@ +package com.itools.core.result; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/4 18:01 + */ +public class FmsRecordResult { + + private FmsDetailRecordResult fssRecord; + + public FmsDetailRecordResult getFssRecord() { + return fssRecord; + } + + public void setFssRecord(FmsDetailRecordResult fssRecord) { + this.fssRecord = fssRecord; + } + +} diff --git a/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsUploadTokenResult.java b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsUploadTokenResult.java new file mode 100644 index 0000000000000000000000000000000000000000..673b4ce7d1358509b0d8a11e48c6f90f99bfba3e --- /dev/null +++ b/itools-fms/itools-fms-model/src/main/java/com/itools/core/result/FmsUploadTokenResult.java @@ -0,0 +1,57 @@ +package com.itools.core.result; + +import java.io.Serializable; + +/** + * @Author XUCHANG + * @description: + * @Date 2020/11/4 18:01 + */ +public class FmsUploadTokenResult implements Serializable { + + private FmsBusinessTypeResult fsBusinessType; + /** + * 来源系统 + */ + private String fromSystemId; + /** + *是否需加密,后续扩展使用,默认否.0:否 1:是 + */ + private String needEncrypt; + /** + * 说明、描述 + */ + private String remark; + + public FmsBusinessTypeResult getFsBusinessType() { + return fsBusinessType; + } + + public void setFsBusinessType(FmsBusinessTypeResult fsBusinessType) { + this.fsBusinessType = fsBusinessType; + } + + public String getFromSystemId() { + return fromSystemId; + } + + public void setFromSystemId(String fromSystemId) { + this.fromSystemId = fromSystemId; + } + + public String getNeedEncrypt() { + return needEncrypt; + } + + public void setNeedEncrypt(String needEncrypt) { + this.needEncrypt = needEncrypt; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/itools-fms/itools-fms-server/pom.xml b/itools-fms/itools-fms-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c509107e9d8af9492bce0cd16c691fa1af9790d --- /dev/null +++ b/itools-fms/itools-fms-server/pom.xml @@ -0,0 +1,90 @@ + + + + itools-fms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-fms-server + + + com.itools.core + itools-fms-core + 1.0-SNAPSHOT + + + com.itools.core + itools-fms-model + 1.0-SNAPSHOT + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/FileSystemApplication.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/FileSystemApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..e56211404eb5a8afcdbd9bdd8492761948d2694f --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/FileSystemApplication.java @@ -0,0 +1,56 @@ +package com.itools.core; + +import com.itools.core.snowflake.config.EnableSequenceService; +import com.itools.core.validate.EnableValidator; +import lombok.extern.log4j.Log4j2; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@EnableSequenceService +@EnableValidator +@MapperScan("com.itools.core.mapper") +@EnableAspectJAutoProxy(exposeProxy = true)//exposeProxy类内部可以获取到当前类的代理对象 +public class FileSystemApplication { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(FileSystemApplication.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + // 允许cookies跨域 + config.setAllowCredentials(true); + // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedOrigin("*"); + // 允许访问的头信息,*表示全部 + config.addAllowedHeader("*"); + // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.setMaxAge(18000L); + // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + config.addAllowedMethod("*"); + config.addAllowedMethod("HEAD"); + // 允许Get的请求方法 + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java new file mode 100644 index 0000000000000000000000000000000000000000..c37bc8cafffe964a187bd7f098b4440d6c98d30a --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java @@ -0,0 +1,57 @@ +package com.itools.core.controller; + +import com.itools.core.base.CommonResult; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.exception.AppException; +import com.itools.core.log.Logable; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.service.FmsFileHandleStrategyService; +import com.itools.core.utils.StringUtils; +import com.itools.core.validate.ParamsValidate; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +@RestController +@ApiOperation("FmsFileHandleStrategyController") +public class FmsFileHandleStrategyController { + @Autowired + private FmsFileHandleStrategyService fmsFileHandleStrategyService; + + /** + * 单个文件上传 + * 返回文件的唯一ID + * @param file + * @param uploadToken + * @return + */ + @RequestMapping(value = "/upload/file",method = RequestMethod.POST) + @Logable + @ApiOperation(value = "文件上传", notes = "文件上传", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uploadToken", value = "文件上传token", required = true, dataType = "String", paramType = "query") + }) + @ParamsValidate(argsIndexs = {0,1}) + public CommonResult singletonUploadFile(@NotNull(message = "FMSCODE03") MultipartFile file, + @NotBlank(message = "FMSCODE15") String uploadToken) { + + return fmsFileHandleStrategyService.singletonUploadFile(file,uploadToken); + } + +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileRecordController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileRecordController.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6405bded3208a709bb8816808385a177caa4b --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileRecordController.java @@ -0,0 +1,44 @@ +package com.itools.core.controller; + + +import com.itools.core.base.CommonResult; +import com.itools.core.log.Logable; +import com.itools.core.result.FmsRecordResult; +import com.itools.core.result.FmsDetailRecordResult; +import com.itools.core.service.FmsRecordService; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +@RestController +@ApiOperation("FmsFileRecordController") +public class FmsFileRecordController { + + @Autowired + private FmsRecordService fmsRecordService; + + /** + * 获取文件记录信息 (内部调用) + * @return + * @throws IOException + */ + @RequestMapping(value = "/fssRecord", method = RequestMethod.GET) + @Logable + @ApiOperation(value = "获取文件记录信息", notes = "获取文件记录信息", httpMethod = "GET") + public CommonResult queryFssRecordByUniqueId(@RequestParam("uniqueId") String uniqueId) { + FmsRecordResult response = new FmsRecordResult(); + FmsDetailRecordResult record = fmsRecordService.queryFssRecordByUniqueId(uniqueId); + response.setFssRecord(record); + return CommonResult.success(response); + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java new file mode 100644 index 0000000000000000000000000000000000000000..b41ece896be53c2909ede0a0af56640d983f8f36 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java @@ -0,0 +1,328 @@ +package com.itools.core.controller; + +import com.itools.core.base.CommonResult; +import com.itools.core.context.FileStrategyServiceContext; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.exception.AppException; +import com.itools.core.log.Logable; +import com.itools.core.param.FmsAccessTokenParam; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.param.FmsBusinessTypeParam; +import com.itools.core.request.FmsBase64FileUploadRequest; +import com.itools.core.result.*; +import com.itools.core.service.FileHandleService; +import com.itools.core.service.FmsAccessTokenService; +import com.itools.core.service.FmsBusinessTypeService; +import com.itools.core.service.FmsFileSystemService; +import com.itools.core.utils.Base64Utils; +import com.itools.core.utils.StringUtils; +import com.itools.core.utils.ZipUtils; +import com.itools.core.validate.ParamsValidate; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.hibernate.validator.constraints.NotBlank; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import sun.misc.BASE64Encoder; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; +import java.io.*; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +@RestController +@ApiOperation("FmsFileSystemController") +public class FmsFileSystemController { + + @Autowired + private FmsBusinessTypeService fmsBusinessTypeService; + + @Autowired + private FmsAccessTokenService fmsAccessTokenService; + + @Autowired + private FmsFileSystemService fmsFileSystemService; + @Autowired + private FileStrategyServiceContext fileStrategyServiceContext; + @Autowired + private FileHandleService fileHandleService; + + private static final String SHOW = "show"; + + private static final String DOWNLOAD = "download"; + + private static final Logger LOGGER = LoggerFactory.getLogger(FmsFileSystemController.class); + + /** + * 申请文件上传token + * 仅内部文件服务使用,不应该对外开放 + * @param businessType 文件类型 + * @param fromSystemId 上传的文件来自哪个子系统 + * @param needEncrypt 是否需加密,后续扩展使用,本期不使用,默认否.0:否 1:是 + * @param remark 描述 + * @return + */ + @PostMapping("/apply/token") + @Logable + @ApiOperation(value = "申请文件上传token", notes = "申请文件上传token", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "businessType", value = "文件类型", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "fromSystemId", value = "上传的文件来自哪个子系统,系统编号", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "needEncrypt", value = "是否需加密,后续扩展使用,本期不使用,默认否.0:否 1:是", required = false, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "remark", value = "文件描述信息", required = false, dataType = "String", paramType = "query") + }) + public CommonResult applyUploadToken(@RequestParam(required = true, value = "businessType")String businessType, + @RequestParam(required = true, value = "fromSystemId")String fromSystemId, + @RequestParam(required = false, value = "needEncrypt") String needEncrypt, + @RequestParam(required = false, value = "remark")String remark) { + return fmsFileSystemService.applyUploadToken(businessType, fromSystemId, needEncrypt, remark); + } + + + @PostMapping("FilePath") + @Logable + @ApiOperation(value = "获取文件访问路径", notes = "获取文件访问路径", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uniqueId", value = "文件的唯一ID", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "expiredTime", value = "有效时长,单位分钟", required = true, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "maxAccessCount", value = "最大访问次数", required = false, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "type", value = "文件下载 download/展示 show", required = true, dataType = "String", paramType = "query") + }) + public CommonResult getFileAccessUrl(String uniqueId, Integer expiredTime, @RequestParam(defaultValue = "-1", required = false) int maxAccessCount, String type) { + String fileUrlByFileId = fmsFileSystemService.getFileUrlByFileId(uniqueId, expiredTime, maxAccessCount, type); + return CommonResult.success(fileUrlByFileId); + } + + @PostMapping("FilesPath") + @Logable + @ApiOperation(value = "获取文件访问路径", notes = "获取文件访问路径", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uniqueIds", value = "文件的唯一IDS数组", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "expiredTime", value = "有效时长,单位分钟", required = true, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "maxAccessCount", value = "最大访问次数", required = false, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "type", value = "文件下载 download/展示 show", required = true, dataType = "String", paramType = "query") + }) + public CommonResult getFileAccessUrl(@RequestParam(value = "uniqueIds",required = false) List uniqueIds, + @RequestParam(value = "expiredTime",required = false) Integer expiredTime, + @RequestParam(value = "maxAccessCount",defaultValue = "-1", required = false) Integer maxAccessCount, + @RequestParam(value = "type",required = false) String type) { + return fmsFileSystemService.getFileUrlByFileIds(uniqueIds, expiredTime, maxAccessCount, type); + } + + @GetMapping("File/{uniqueId}") + @Logable + public void downloadFile(@PathVariable String uniqueId, @RequestParam("fileAccessToken") String fileAccessToken, HttpServletResponse response) { + try { + FmsAccessTokenResult fsAccessToken = fmsAccessTokenService.getFileAccessToken(fileAccessToken); + if (fsAccessToken == null) { + throw new AppException(FmsCodeBean.FmsCode.INVALID_FS_TOKEN.code); + } + if (!uniqueId.equals(fsAccessToken.getUniqueId())) { + throw new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code); + } + FmsAccessTokenParam param = new FmsAccessTokenParam(); + BeanUtils.copyProperties(fsAccessToken , param); + FmsDetailRecordResult fsUploadRecord = fmsFileSystemService.getFileUploadRecord(param, uniqueId, fileAccessToken); + String type = fsAccessToken.getType(); + if (SHOW.equals(type)) { + response.addHeader("Content-Length", "" + fsUploadRecord.getFileSize()); + } else if (DOWNLOAD.equals(type)) { + // 设置文件名, 可用于下载 + response.addHeader("Content-Length", "" + fsUploadRecord.getFileSize()); + response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fsUploadRecord.getOrigFileName(), "utf-8")); + } + response.setCharacterEncoding("UTF-8"); + response.setContentType(fsUploadRecord.getContentType()); + OutputStream outputStream = response.getOutputStream(); + FileCopyUtils.copy(new FileInputStream(fmsFileSystemService.getFileLocalPath(fsUploadRecord.getUniqueId() , fsUploadRecord.getOrigFileName())), outputStream); + } catch (AppException e) { + String errorCode = e.getErrorCode(); + response.setStatus(Integer.parseInt(errorCode)); + return; + } catch (IOException e) { + response.setStatus(500); + return; + } + } + + @GetMapping("File/getFileBase64") + @Logable + public CommonResult getFileBase64(@RequestParam("uniqueId")String uniqueId){ + FmsDetailRecordResult fsUploadRecord = fmsFileSystemService.getFileUploadRecord(uniqueId); + try { + String path = fmsFileSystemService.getFileLocalPath(fsUploadRecord.getUniqueId() , fsUploadRecord.getOrigFileName()); + File file = new File(path); + FileInputStream fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[(int)file.length()]; + fileInputStream.read(buffer); + fileInputStream.close(); + return CommonResult.success(new BASE64Encoder().encode(buffer)); + } catch (Exception e) { + LOGGER.error(FmsCodeBean.FmsCode.ERROR_TRANS_BASE64.code, e); + throw new AppException(FmsCodeBean.FmsCode.ERROR_TRANS_BASE64.code, FmsCodeBean.FmsCode.ERROR_TRANS_BASE64.message); + } + } + + /** + * 此接口不对外开放,仅供内部微服务使用 + * @param base64Req + * @return + */ + @PostMapping("File/BaseSixFour") + @Logable + @ApiOperation(value = "文件上传", notes = "文件上传", httpMethod = "POST") + public CommonResult uploadFileBase64(@RequestBody FmsBase64FileUploadRequest base64Req) { + FmsUploadTokenResult fsUploadToken = fmsBusinessTypeService.getFsBusinessTypeByUploadToken(base64Req.getUploadToken()); + if (fsUploadToken == null) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_TIMEOUT.code); + } + FmsBusinessTypeResult fsBusinessType = fsUploadToken.getFsBusinessType(); + byte[] fileBytes; + try { + //将字符串转换为byte数组 + fileBytes = Base64Utils.decode(base64Req.getFileBaseSixFour()); + } catch (Exception e) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code); + } + + String fsSize = fsBusinessType.getFsSize(); + Long limitSize = Long.parseLong(fsSize); + long fileSize = fileBytes.length; + int mult = 1024; + if (fileSize > (limitSize * mult)) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILESIZE_OUT_OFF_LIMIT.code); + } + if (!fsBusinessType.getBusinessType().equals(base64Req.getBusinessType())) { + // 文件类型不一致 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILETYPE.code); + } + FmsBusinessTypeParam fmsBusinessTypeParam = new FmsBusinessTypeParam(); + BeanUtils.copyProperties(fsBusinessType , fmsBusinessTypeParam); + FmsUploadTokenParam fmsUploadTokenParam = new FmsUploadTokenParam(); + BeanUtils.copyProperties(fsUploadToken , fmsUploadTokenParam); + String fileUploadResponse = fmsFileSystemService.saveFileFromBytes(fileBytes, base64Req.getFileName(), fmsBusinessTypeParam, fmsUploadTokenParam); + fmsBusinessTypeService.deleteFsBusinessTypeByUploadToken(base64Req.getUploadToken()); + return CommonResult.success(fileUploadResponse); + } + + /** + * 返回文件的唯一ID + * @param file + * @param uploadToken + * @return + */ + @PostMapping("File") + @Logable + @ApiOperation(value = "文件上传", notes = "文件上传", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uploadToken", value = "文件上传token", required = true, dataType = "String", paramType = "query") + }) + @ParamsValidate(argsIndexs = {0,1}) + public CommonResult fileUpload(@RequestParam(value = "file")@NotNull(message = "FMSCODE03") MultipartFile file, + @RequestParam(value = "uploadToken") @NotBlank(message = "FMSCODE15") String uploadToken, + @RequestParam(value = "fileName",required = false)String fileName) { + //压缩过后的文件名称前端无法获取,额外的字段标识 + String originalFilename = file.getOriginalFilename(); + if (!StringUtils.isBlank(fileName)){ + originalFilename = fileName; + } + FmsFileUploadResult response = new FmsFileUploadResult(); + if (file == null || StringUtils.isBlank(originalFilename)) { + // 文件不能为空 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILE_IS_NULL.code); + } + + FmsUploadTokenResult fsUploadToken = fmsBusinessTypeService.getFsBusinessTypeByUploadToken(uploadToken); + if (fsUploadToken == null) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_TIMEOUT.code); + } + FmsBusinessTypeResult fsBusinessType = fsUploadToken.getFsBusinessType(); + String fsSize = fsBusinessType.getFsSize(); + Long limitSize = Long.parseLong(fsSize); + long fileSize = file.getSize(); + int mult = 102400; + if (fileSize > (limitSize * mult)) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILESIZE_OUT_OFF_LIMIT.code); + } + String fsType = fsBusinessType.getFsType(); + if (StringUtils.equals(fsType, "currency")){ + //如果取值为currency,则不判断类型 + }else { + String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + String contentType = file.getContentType(); + if (contentType == null || !fsType.contains(suffix.trim().toLowerCase()) || !fsType.contains(contentType)) { + LOGGER.error(FmsCodeBean.FmsCode.ERROR_FILETYPE.code, "文件类型:" + suffix + " || " + contentType); + // 文件后缀错误 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILETYPE.code); + } + } + + FmsUploadTokenParam param = new FmsUploadTokenParam(); + BeanUtils.copyProperties(fsUploadToken , param); + param.setOriginalFilename(originalFilename); + String uniqueId = fmsFileSystemService.saveFile(file, param); + fmsBusinessTypeService.deleteFsBusinessTypeByUploadToken(uploadToken); + response.setUniqueId(uniqueId); + return CommonResult.success(response); + } + + /** + * 文件批量上传 + * @param files 文件 + * @param uploadToken 申请的token + * @return + */ + @PostMapping("/Files") + @Logable + @ApiOperation(value = "批量上传文件", notes = "批量上传文件", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uploadToken", value = "文件上传token", required = true, dataType = "String", paramType = "query") + }) + public CommonResult filesMultipartUpload(@RequestPart(name = "files", required = true) MultipartFile[] files, String uploadToken) { + + return fmsFileSystemService.filesMultipartUpload(files,uploadToken); + } + + @ApiOperation(value = "下载zip压缩文件接口", notes = "下载zip压缩文件接口", httpMethod = "GET") + @Logable + @RequestMapping(value = "/download/zip",method = RequestMethod.GET) + public void downloadZipByUniqueIds(@RequestParam("uniqueIds") List uniqueIds, + @RequestParam(value = "zipName", required = false) String zipName, + HttpServletResponse response) throws IOException { + if (StringUtils.isBlank(zipName)) { + zipName = "attachment"; + } + List paths = new ArrayList<>(); + List files = new ArrayList<>(); + for (String uniqueId : uniqueIds) { + FmsDetailRecordResult fsUploadRecord = fmsFileSystemService.getFileUploadRecord(uniqueId); + String path = fmsFileSystemService.getFileLocalPath(uniqueId, fsUploadRecord.getOrigFileName()); + paths.add(path); + File file = new File(path); + files.add(file); + } + try { + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment; fileName=" + URLEncoder.encode(zipName + ".zip", "utf-8")); + OutputStream outputStream = response.getOutputStream(); + ZipUtils.toZip(files, outputStream); + } catch (FileNotFoundException e) { + throw new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsAccessTokenMapper.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsAccessTokenMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e770dbd97ae9aeebf5f4537c6735abe1886b1114 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsAccessTokenMapper.java @@ -0,0 +1,34 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.dto.accessToken.FmsAccessTokenDTO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Repository +public interface FmsAccessTokenMapper extends BaseMapper { + + + /** + * 根据主键(fileAccessToken)查询 + * + * @param fileAccessToken + * @return + */ + FmsAccessTokenDTO selectByPrimaryKey(String fileAccessToken); + + /** + * 批量插入 + * @param records + * @return + */ + int multipartInsert(@Param("records") List records); +} \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsBusinessTypeMapper.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsBusinessTypeMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7fd866bf82157577ba2a9746da4123871cbdf413 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsBusinessTypeMapper.java @@ -0,0 +1,52 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.dto.businessType.FmsBusinessTypeDTO; +import org.springframework.stereotype.Repository; + +import java.util.List; +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Repository +public interface FmsBusinessTypeMapper extends BaseMapper { + /** + * 根据主键删除 + * @param id + * @return + */ + int deleteByPrimaryKey(Long id); + + /** + * 插入非空参数 + * @param record + * @return + */ + int insertSelective(FmsBusinessTypeDTO record); + /** + * 根据主键查询 + * @param id + * @return + */ + FmsBusinessTypeDTO selectByPrimaryKey(Long id); + /** + * 根据主键更新非空参数 + * @param record + * @return + */ + int updateByPrimaryKeySelective(FmsBusinessTypeDTO record); + /** + * 根据主键更新所有参数 + * @param record + * @return + */ + int updateByPrimaryKey(FmsBusinessTypeDTO record); + /** + * 根据入参条件查询 + * @param record + * @return + */ + List selectBySelective(FmsBusinessTypeDTO record); +} \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsFileRecordMapper.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsFileRecordMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..28a6eb7539fafa1b4ca8c897ef3c591bd218dd7a --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/mapper/FmsFileRecordMapper.java @@ -0,0 +1,54 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.dto.fileRecord.FmsRecordDTO; +import org.springframework.stereotype.Repository; + +import java.util.List; +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Repository +public interface FmsFileRecordMapper extends BaseMapper { + + /** + * 根据主键更新非空参数 + * @param record + * @return + */ + int updateByPrimaryKeySelective(FmsRecordDTO record); + + /** + * 根据入参条件查询 + * @param record + * @return + */ + List selectbySelective(FmsRecordDTO record); + /** + * 插入非空参数 + * @param record + * @return + */ + int insertSelective(FmsRecordDTO record); + /** + * 根据主键查询 + * @param id + * @return + */ + FmsRecordDTO selectByPrimaryKey(Long id); + /** + * 根据主键删除 + * @param id + * @return + */ + int deleteByPrimaryKey(Long id); + /** + * 根据主键更新所有参数 + * @param record + * @return + */ + int updateByPrimaryKey(FmsRecordDTO record); + +} \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsAccessTokenService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsAccessTokenService.java new file mode 100644 index 0000000000000000000000000000000000000000..0cd8e5fbd93873f140bc679593f99e32c7d47d9d --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsAccessTokenService.java @@ -0,0 +1,40 @@ +package com.itools.core.service; + + +import com.itools.core.result.FmsAccessTokenResult; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +public interface FmsAccessTokenService { + + /** + * 从缓存中获取FsAccessToken信息 + * + * @param fileAccessToken + * @return + */ + FmsAccessTokenResult getFileAccessToken(String fileAccessToken); + + /** + * 缓存FsAccessToken信息 + * + * @param fileAccessToken + * @param expiredTime + * @param maxAccessCount + * @param uniqueId + * @param type + * @return + */ + FmsAccessTokenResult cacheFileAccessToken(String fileAccessToken, int expiredTime, int maxAccessCount, String uniqueId, String type); + + + /** + * 删除缓存数据 + * + * @param fileAccessToken + */ + void deleteFileAccessToken(String fileAccessToken); +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsBusinessTypeService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsBusinessTypeService.java new file mode 100644 index 0000000000000000000000000000000000000000..488b149faaa71470f62cde0d109f030bab5a224d --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsBusinessTypeService.java @@ -0,0 +1,42 @@ +package com.itools.core.service; + + +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.result.FmsBusinessTypeResult; + +import java.util.List; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +public interface FmsBusinessTypeService { + + /** + * 查询FsBusinessType列表 + * + * @param businessType + * @return + */ + List queryFsBusinessTypeList(String businessType); + + /** + * 根据 uploadToken ,缓存FsBusinessType对象 + * + * @param uploadToken + * @return + */ + FmsUploadTokenResult cacheFsUploadToken(String uploadToken, FmsUploadTokenParam fsUploadToken); + + /** + * 根据 uploadToken ,获取FsBusinessType对象 + * + * @param uploadToken + * @return + */ + FmsUploadTokenResult getFsBusinessTypeByUploadToken(String uploadToken); + + void deleteFsBusinessTypeByUploadToken(String uploadToken); +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java new file mode 100644 index 0000000000000000000000000000000000000000..71637147f5d849243a611d88fad5aca6665dc078 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java @@ -0,0 +1,21 @@ +package com.itools.core.service; + +import com.itools.core.base.CommonResult; +import com.itools.core.result.FmsFileUploadResult; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:43 + */ +public interface FmsFileHandleStrategyService { + /** + * 单个文件上传 + * @param file 文件 + * @param uploadToken 上传文件token + * @return + */ + CommonResult singletonUploadFile(MultipartFile file, String uploadToken); +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java new file mode 100644 index 0000000000000000000000000000000000000000..58d30093eedc632f668124a85b4bc434151c1505 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java @@ -0,0 +1,128 @@ +package com.itools.core.service; + +import com.itools.core.base.CommonResult; +import com.itools.core.param.FmsAccessTokenParam; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.param.FmsBusinessTypeParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsDetailRecordResult; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +public interface FmsFileSystemService { + + /** + * 根据uniqeId和文件名得到本地文件存储路径 + * @param uniqueId + * @param fileName + * @return + */ + String getFileLocalPath(String uniqueId, String fileName); + + + + /** + * 存储文件 + * + * @param file + * @param fsUploadToken + * @return + */ + String saveFile(MultipartFile file, FmsUploadTokenParam fsUploadToken); + + + + /** + * 申请文件上传token + * @param businessType + * @param fromSystemId + * @param needEncrypt + * @param remark + * @return + */ + CommonResult applyUploadToken(String businessType, String fromSystemId, String needEncrypt, String remark); + + + /** + * 存储文件 + * 使用字节流输入文件 + * 返回文件的全局唯一ID + * @param fileBytes + * @param fileName + * @param fsBusinessType + * @param fsUploadToken + * @return + */ + String saveFileFromBytes(byte[] fileBytes, String fileName, FmsBusinessTypeParam fsBusinessType, FmsUploadTokenParam fsUploadToken); + + + + + /** + * 获取文件访问路径,如果超时时间设置为小于等于0,那么返回的有效路径可以永久访问 + * + * @param uniqueId + * @param expiredTime + * @param maxAccessCount + * @return + */ + String getFileUrlByFileId(String uniqueId, int expiredTime, int maxAccessCount, String type); + + /** + * 获得uniqueId对应的文件上传记录 + * 会检查 + * @param fsAccessToken + * @param uniqueId + * @param fileAccessToken + * @return + */ + FmsDetailRecordResult getFileUploadRecord(FmsAccessTokenParam fsAccessToken, String uniqueId, String fileAccessToken); + + + /** + * 获得uniqueId对应的文件上传记录 + * @param uniqueId + * @return + */ + FmsDetailRecordResult getFileUploadRecord(String uniqueId); + + /** + * 文件批量上传 + * @param files + * @param param + * @return + */ + CommonResult uploadFiles(MultipartFile[] files, FmsUploadTokenParam param); + + /** + * 验证文件 + * @param files + * @param fsBusinessType + */ + void verifyFiles(MultipartFile[] files, FmsBusinessTypeResult fsBusinessType); + + /** + * 文件批量上传的业务逻辑 + * @param files + * @param uploadToken + * @return + */ + CommonResult filesMultipartUpload(MultipartFile[] files, String uploadToken); + + /** + * 获取文件访问路径,如果超时时间设置为小于等于0,那么返回的有效路径可以永久访问 + * @param uniqueIds + * @param expiredTime + * @param maxAccessCount + * @param type + * @return + */ + CommonResult getFileUrlByFileIds(List uniqueIds, Integer expiredTime, int maxAccessCount, String type); +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsRecordService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsRecordService.java new file mode 100644 index 0000000000000000000000000000000000000000..0df6b921120361a7b198c3a63f045168b9e4ab9c --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsRecordService.java @@ -0,0 +1,16 @@ +package com.itools.core.service; + + +import com.itools.core.result.FmsDetailRecordResult; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-04-01 11:26 + */ +public interface FmsRecordService { + + + FmsDetailRecordResult queryFssRecordByUniqueId(String uniqueId); + +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..622d570e08a21cca5209e54c5a300832f4aec247 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java @@ -0,0 +1,89 @@ +package com.itools.core.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.itools.core.base.CommonResult; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.em.FmsConstants; +import com.itools.core.exception.AppException; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.service.FileHandleService; +import com.itools.core.service.FmsAccessTokenService; +import com.itools.core.service.FmsBusinessTypeService; +import com.itools.core.service.FmsFileSystemService; +import com.itools.core.system.AbstractService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-02 16:36 + */ +@Service +public class FileHandleServiceImpl extends AbstractService implements FileHandleService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private FmsBusinessTypeService fmsBusinessTypeService; + + @Autowired + private FmsAccessTokenService fmsAccessTokenService; + + @Autowired + private FmsFileSystemService fmsFileSystemService; + /** + * 根据申请的文件token获取返回的上传文件的类型信息 + * KEY的名称itools:fms:file-token:fileToken + * @param fileToken + * @return + */ + @Override + public CommonResult getFileDetailByToken(String fileToken) { + //判断token是否不存在或者是过期 + boolean hasFileTokenKey = stringRedisTemplate.hasKey(FmsConstants.FILE_TOKEN_NAME_SPACE + fileToken).booleanValue(); + + if (!hasFileTokenKey){ + throw new AppException(FmsCodeBean.FmsCode.FAIL_FILE_TOKEN.code); + } + String tokenParam = stringRedisTemplate.opsForValue().get(FmsConstants.FILE_TOKEN_NAME_SPACE + fileToken); + FmsUploadTokenResult fmsUploadTokenResult = null; + try { + fmsUploadTokenResult = objectMapper.readValue(tokenParam,FmsUploadTokenResult.class); + } catch (IOException e) { + logger.error("JacKJson序列化失败"); + } + return CommonResult.success(fmsUploadTokenResult); + } + + /** + * 文件策略为NIO + * 执行文件写入和上传 + * @param file 文件 + * @param param 上传参数 + * @param uploadToken 访问token + * @param originalFilename 原文件名称 + * @return + */ + @Override + public CommonResult saveFileForNio(MultipartFile file, FmsUploadTokenParam param, String uploadToken, String originalFilename) { + FmsFileUploadResult fmsFileUploadResult = new FmsFileUploadResult(); + param.setOriginalFilename(originalFilename); + String uniqueId = fmsFileSystemService.saveFile(file, param); + stringRedisTemplate.delete(FmsConstants.FILE_TOKEN_NAME_SPACE + uploadToken); + fmsFileUploadResult.setUniqueId(uniqueId); + return CommonResult.success(fmsFileUploadResult); + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsAccessTokenServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsAccessTokenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..be0b5dedfe30145eafaea2499d455b2c627c68eb --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsAccessTokenServiceImpl.java @@ -0,0 +1,126 @@ +package com.itools.core.service.impl; + +import com.alibaba.fastjson.JSON; +import com.itools.core.dto.accessToken.FmsAccessTokenDTO; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.exception.AppException; +import com.itools.core.log.Logable; +import com.itools.core.mapper.FmsAccessTokenMapper; +import com.itools.core.result.FmsAccessTokenResult; +import com.itools.core.service.FmsAccessTokenService; +import com.itools.core.utils.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Service +public class FmsAccessTokenServiceImpl implements FmsAccessTokenService { + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private FmsAccessTokenMapper fmsAccessTokenMapper; + /** + * 从缓存中或者数据库中获取FsAccessToken信息 + * + * @param fileAccessToken + * @return + */ + @Override + public FmsAccessTokenResult getFileAccessToken(String fileAccessToken) { + Object object = redisTemplate.opsForValue().get(fileAccessToken); + if (object != null){ + FmsAccessTokenResult result = JSON.parseObject((String) object, FmsAccessTokenResult.class); + return result; + }else { + FmsAccessTokenDTO fssFmsAccessTokenDto = fmsAccessTokenMapper.selectByPrimaryKey(fileAccessToken); + if (fssFmsAccessTokenDto == null){ + return null; + } + FmsAccessTokenResult result = new FmsAccessTokenResult(); + BeanUtils.copyProperties(fssFmsAccessTokenDto, result); + return result; + } + } + + /** + * 缓存FsAccessToken信息,如果过期时间小于等于0,则会将accessToken持久化(缓存到数据库) + * @param fileAccessToken + * @param expiredTime + * @param maxAccessCount + * @param uniqueId + * @param type + * @return + */ + @Override + public FmsAccessTokenResult cacheFileAccessToken(String fileAccessToken, int expiredTime, int maxAccessCount, String uniqueId, String type) { + if (StringUtils.isBlank(fileAccessToken)) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code); + } + FmsAccessTokenResult fmsAccessTokenResult = new FmsAccessTokenResult(expiredTime, maxAccessCount, uniqueId, type); + if (expiredTime > 0){ + redisTemplate.opsForValue().set(fileAccessToken , JSON.toJSONString(fmsAccessTokenResult) , expiredTime , TimeUnit.MINUTES); + }else { + FmsAccessTokenDTO fssFmsAccessTokenDto = new FmsAccessTokenDTO(); + fssFmsAccessTokenDto.setFileAccessToken(fileAccessToken); + fssFmsAccessTokenDto.setType(type); + fssFmsAccessTokenDto.setUniqueId(uniqueId); + fmsAccessTokenMapper.insert(fssFmsAccessTokenDto); + } + return fmsAccessTokenResult; + } + /** + * 缓存FsAccessToken信息,如果过期时间小于等于0,则会将accessToken持久化(缓存到数据库) + * @param fileAccessToken + * @param expiredTime + * @param maxAccessCount + * @param uniqueIds + * @param type + * @return + */ + public List cacheFileAccessToken(String fileAccessToken, int expiredTime, int maxAccessCount, List uniqueIds, String type) { + if (StringUtils.isBlank(fileAccessToken)) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code); + } + List fssAccessTokens = new ArrayList<>(); + for (String uniqueId : uniqueIds){ + FmsAccessTokenResult fmsAccessTokenResult = new FmsAccessTokenResult(expiredTime, maxAccessCount, uniqueId, type); + if (expiredTime > 0){ + redisTemplate.opsForValue().set(fileAccessToken , JSON.toJSONString(fmsAccessTokenResult) , expiredTime , TimeUnit.MINUTES); + }else { + FmsAccessTokenDTO fssFmsAccessTokenDto = new FmsAccessTokenDTO(); + fssFmsAccessTokenDto.setFileAccessToken(fileAccessToken); + fssFmsAccessTokenDto.setType(type); + fssFmsAccessTokenDto.setUniqueId(uniqueId); + fssAccessTokens.add(fssFmsAccessTokenDto); + } + } + if (!CollectionUtils.isEmpty(fssAccessTokens)){ + fmsAccessTokenMapper.multipartInsert(fssAccessTokens); + } + return fssAccessTokens; + } + + /** + * 删除缓存数据 + * + * @param fileAccessToken + */ + @Override + @CacheEvict(value = "fileAccessToken", key = "#fileAccessToken") + @Logable(businessTag = "FsAccessService") + public void deleteFileAccessToken(String fileAccessToken) { + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsBusinessTypeServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsBusinessTypeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b61c808b63027ca0ce928d3a3441fcaaee0375f5 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsBusinessTypeServiceImpl.java @@ -0,0 +1,88 @@ +package com.itools.core.service.impl; + +import com.itools.core.dto.businessType.FmsBusinessTypeDTO; +import com.itools.core.log.Logable; +import com.itools.core.mapper.FmsBusinessTypeMapper; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.service.FmsBusinessTypeService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Service +public class FmsBusinessTypeServiceImpl implements FmsBusinessTypeService { + @Autowired + private FmsBusinessTypeMapper fmsBusinessTypeMapper; + + /** + * 查询FsBusinessType列表 + * + * @param businessType + * @return + */ + @Override + @Cacheable(value = "fsBusinessTypes", key = "#businessType") + @Logable + public List queryFsBusinessTypeList(String businessType) { + if (StringUtils.isBlank(businessType)) { + return null; + } + FmsBusinessTypeDTO tmp = new FmsBusinessTypeDTO(); + tmp.setBusinessType(businessType); + List fssBusinessTypes = fmsBusinessTypeMapper.selectBySelective(tmp); + List fmsBusinessTypeResults = new ArrayList<>(); + for (FmsBusinessTypeDTO fssBusinessType : fssBusinessTypes){ + FmsBusinessTypeResult fmsBusinessTypeResult = new FmsBusinessTypeResult(); + BeanUtils.copyProperties(fssBusinessType , fmsBusinessTypeResult); + fmsBusinessTypeResults.add(fmsBusinessTypeResult); + } + return fmsBusinessTypeResults; + } + + /** + * 根据 uploadToken ,缓存FsBusinessType对象 + * + * @param uploadToken + * @return + */ + @Override + @Cacheable(value = "fsBusinessType", key = "#uploadToken") + @Logable + public FmsUploadTokenResult cacheFsUploadToken(String uploadToken, FmsUploadTokenParam fmsUploadTokenParam) { + FmsUploadTokenResult result = new FmsUploadTokenResult(); + BeanUtils.copyProperties(fmsUploadTokenParam, result); + return result; + } + + /** + * 根据 uploadToken ,获取FsBusinessType对象 + * + * @param uploadToken + * @return + */ + @Override + @Cacheable(value = "fsBusinessType", key = "#uploadToken") + @Logable + public FmsUploadTokenResult getFsBusinessTypeByUploadToken(String uploadToken) { + return null; + } + + @Override + @CacheEvict(value = "fsBusinessType", key = "#uploadToken") + @Logable + public void deleteFsBusinessTypeByUploadToken(String uploadToken) { + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a01bb3e6457c856093c6509cb4044c01f619a3f5 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java @@ -0,0 +1,78 @@ +package com.itools.core.service.impl; + +import com.itools.core.base.CommonResult; +import com.itools.core.context.FileStrategyServiceContext; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.em.FmsConstants; +import com.itools.core.exception.AppException; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileUploadResult; +import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.service.FileHandleService; +import com.itools.core.service.FmsFileHandleStrategyService; +import com.itools.core.system.AbstractService; +import com.itools.core.utils.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-05 14:44 + */ +@Service +public class FmsFileHandleStrategyServiceImpl extends AbstractService implements FmsFileHandleStrategyService { + @Autowired + private FileStrategyServiceContext fileStrategyServiceContext; + @Autowired + private FileHandleService fileHandleService; + + + /** + * 单个文件上传 + * + * @param file 文件 + * @param uploadToken 上传文件token + * @return + */ + @Override + @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) + public CommonResult singletonUploadFile(MultipartFile file, String uploadToken) { + //压缩过后的文件名称前端无法获取,额外的字段标识 + String originalFilename = file.getOriginalFilename(); + //返回 + FmsFileUploadResult response = new FmsFileUploadResult(); + //验证文件token + CommonResult fmsUploadTokenResultCommonResult = fileHandleService.getFileDetailByToken(uploadToken); + + FmsBusinessTypeResult businessType = fmsUploadTokenResultCommonResult.getData().getFsBusinessType(); + String fsSize = businessType.getFsSize(); + Long limitSize = Long.parseLong(fsSize); + long fileSize = file.getSize(); + int mult = 102400; + if (fileSize > (limitSize * mult)) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILESIZE_OUT_OFF_LIMIT.code); + } + String fsType = businessType.getFsType(); + if (!StringUtils.equals(fsType, FmsConstants.FILE_CURRENCY_TYPE)){ + String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + String contentType = file.getContentType(); + if (contentType == null || !fsType.contains(suffix.trim().toLowerCase()) || !fsType.contains(contentType)) { + logger.error(FmsCodeBean.FmsCode.ERROR_FILETYPE.code, "文件类型:" + suffix + " || " + contentType); + // 文件后缀错误 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILETYPE.code); + } + } + //执行文件上传,根据配置的不同策略处理文件上传 + CommonResult fmsFileUploadResultCommonResult = fileStrategyServiceContext.get().singletonUploadFiles(file, uploadToken, businessType, originalFilename); + response.setUniqueId(fmsFileUploadResultCommonResult.getData().getUniqueId()); + + return CommonResult.success(response); + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileRecordUploadTask.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileRecordUploadTask.java new file mode 100644 index 0000000000000000000000000000000000000000..2e5363a6075e759c48f2785290bd059f14ea1012 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileRecordUploadTask.java @@ -0,0 +1,48 @@ +package com.itools.core.service.impl; + +import com.itools.core.param.FmsUploadTokenParam; +import org.springframework.web.multipart.MultipartFile; + +import java.util.concurrent.Callable; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsFileRecordUploadTask implements Callable { + /** + * 自身实现 + */ + private FmsFileSystemServiceImpl self; + /** + * 上传的文件 + */ + private MultipartFile file; + /** + * 文件的访问的token获取的信息 + */ + private FmsUploadTokenParam param; + /** + * 生成的UUID + */ + private String uuid; + + public FmsFileRecordUploadTask(FmsFileSystemServiceImpl self, MultipartFile file, FmsUploadTokenParam param, String uuid) { + this.self = self; + this.file = file; + this.param = param; + this.uuid = uuid; + } + + @Override + public String call() { + try { + String file = self.saveFile(this.file, param, uuid); + return file; + }catch (Exception e){ + return null; + } + } + +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..27eec260c91c57f1294a666ee9a81ce1f3a66edf --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java @@ -0,0 +1,578 @@ +package com.itools.core.service.impl; + +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.itools.core.base.CommonResult; +import com.itools.core.dto.accessToken.FmsAccessTokenDTO; +import com.itools.core.dto.fileRecord.FmsRecordDTO; +import com.itools.core.em.FmsCodeBean; +import com.itools.core.em.FmsConstants; +import com.itools.core.exception.AppException; +import com.itools.core.log.Logable; +import com.itools.core.mapper.FmsAccessTokenMapper; +import com.itools.core.mapper.FmsFileRecordMapper; +import com.itools.core.param.FmsAccessTokenParam; +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.param.FmsBusinessTypeParam; +import com.itools.core.result.*; +import com.itools.core.service.FmsFileSystemService; +import com.itools.core.snowflake.SequenceService; +import com.itools.core.utils.CollectionUtils; +import com.itools.core.utils.UUIDUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Service +public class FmsFileSystemServiceImpl implements FmsFileSystemService { + + @Value("${fms.file.store-root-path}") + String fileStoreRootPath; + + @Value("${fms.file.access-url-for-outter}") + String fileAccessUrlForOutter; + + @Value("${fms.file.upload-limit-size}") + Integer fileUploadLimitSize; + + @Value("${fms.file.max-request-size}") + Integer fileMaxRequestSize; + + @Value("${fms.file.max-file-size}") + Integer fileMaxFileSize; + + @Value("${fms.token-expiration-time}") + Integer tokenExpirationTime; + + @Autowired + FmsFileSystemServiceImpl self; + + @Autowired + private SequenceService sequenceService; + + @Autowired + private FmsBusinessTypeServiceImpl fmsBusinessTypeService; + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private FmsFileRecordMapper fmsFileRecordMapper; + + @Autowired + private FmsAccessTokenServiceImpl fmsAccessTokenService; + + @Autowired + private FmsAccessTokenMapper fmsAccessTokenMapper; + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + //文件的type + private final static String CURRENCY_TYPE = "currency"; + //线程池:核心线程:10,非核心线程:40,阻塞队列:100 + private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 0, TimeUnit.SECONDS, new LinkedBlockingDeque(100)); + + /** + * 根据uniqeId和文件名得到本地文件存储路径 + * @param uniqueId + * @param fileName + * @return + */ + @Override + public String getFileLocalPath(String uniqueId , String fileName) { + if (StringUtils.isBlank(uniqueId)) { + throw new AppException(FmsCodeBean.FmsCode.INVALID_FS_UNIQUE_ID.code); + } + String path = fileStoreRootPath+File.separator; + int pathLength = 4; + for(int i=0;i applyUploadToken(String businessType, String fromSystemId, String needEncrypt, String remark) { + if (StringUtils.isBlank(businessType)) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_BUSINESS_TYPE_NOT_EXIST.code); + } + String uploadToken = UUIDUtils.uuid(); + List fsBusinessTypes = fmsBusinessTypeService.queryFsBusinessTypeList(businessType); + if (fsBusinessTypes == null || fsBusinessTypes.isEmpty()) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_BUSINESS_TYPE_NOT_EXIST.code); + } + FmsUploadTokenParam fsUploadToken = new FmsUploadTokenParam(); + fsUploadToken.setFromSystemId(fromSystemId); + fsUploadToken.setFsBusinessType(fsBusinessTypes.get(0)); + fsUploadToken.setNeedEncrypt(needEncrypt); + fsUploadToken.setRemark(remark); + //将文件申请的token写入到缓存 + try { + String tokenParam = objectMapper.writeValueAsString(fsUploadToken); + stringRedisTemplate.opsForValue().set((FmsConstants.FILE_TOKEN_NAME_SPACE+uploadToken),tokenParam,tokenExpirationTime,TimeUnit.MINUTES); + } catch (JsonProcessingException e) { + logger.error("JackJson序列化失败"); + } +// fmsBusinessTypeService.cacheFsUploadToken(uploadToken, fsUploadToken); + return CommonResult.success(uploadToken); + } + + + /** + * 存储文件 + * 使用字节流输入文件 + * 返回文件的全局唯一ID + * @param fileBytes + * @param fileName + * @param fsBusinessType + * @param fsUploadToken + * @return + */ + @Override + public String saveFileFromBytes(byte[] fileBytes, String fileName, FmsBusinessTypeParam fsBusinessType, FmsUploadTokenParam fsUploadToken) { + String uniqueId = UUIDUtils.uuid(); + saveFileToLocal(fileBytes, fileName, uniqueId); + + String originalFilename = StringUtils.isBlank(fileName) ? uniqueId : fileName; + String contentType = fsBusinessType.getFsType(); + + return createAndPersistDBRecord(originalFilename, contentType, (long) fileBytes.length, fsUploadToken, uniqueId); + } + + + + protected void saveFileToLocal(byte[] fileBytes, String fileName, String uuid) { + File outputFile = new File(getFileLocalPath(uuid , fileName)); + if (!outputFile.getParentFile().exists()) { + outputFile.getParentFile().mkdirs(); + } + try { + outputFile.createNewFile(); + } catch (IOException e) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code, e); + } + try (OutputStream out = new FileOutputStream(outputFile)) { + FileCopyUtils.copy(fileBytes, out); + } catch (IOException e) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code, e); + } + } + + /** + * 获取文件访问路径 + * + * @param uniqueId + * @param expiredTime + * @param maxAccessCount + * @return + */ + @Override + @Logable + public String getFileUrlByFileId(String uniqueId, int expiredTime, int maxAccessCount, String type) { + FmsRecordDTO tmp = new FmsRecordDTO(); + tmp.setUniqueId(uniqueId); + List fsUploadRecords = fmsFileRecordMapper.selectbySelective(tmp); + if (fsUploadRecords == null || fsUploadRecords.isEmpty()) { + logger.error(FmsCodeBean.FmsCode.NOT_EXIST_FILE.message,new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code, FmsCodeBean.FmsCode.NOT_EXIST_FILE.message)); + return ""; + } + String fileAccessToken = UUIDUtils.uuid(); + //String fileAccessUrlForOutter = "http://192.168.0.135:8150/fs/File/";//本地路径 + String path = fileAccessUrlForOutter + uniqueId + "?fileAccessToken=" + fileAccessToken; + // 缓存文件token + FmsAccessTokenResult fsAccessToken = fmsAccessTokenService.cacheFileAccessToken(fileAccessToken, expiredTime, maxAccessCount, uniqueId, type); + if (fsAccessToken == null) { + throw new AppException(FmsCodeBean.FmsCode.FAIL.code); + } + return path; + } + + /** + * 获得uniqueId对应的文件上传记录 + * 会检查 + * @param fsAccessToken + * @param uniqueId + * @param fileAccessToken + * @return + */ + @Override + @Logable + public FmsDetailRecordResult getFileUploadRecord(FmsAccessTokenParam fsAccessToken, String uniqueId, String fileAccessToken) { + FmsRecordDTO tmp = new FmsRecordDTO(); + tmp.setUniqueId(uniqueId); + List fsUploadRecords = fmsFileRecordMapper.selectbySelective(tmp); + if (fsUploadRecords == null || fsUploadRecords.isEmpty()) { + // 无该文件,返回404 + throw new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code); + } + int maxAccessCount = fsAccessToken.getMaxAccessCount(); + if (fsAccessToken.getExpiredTime() > 0){ + if (maxAccessCount != -1) { + // 次数减一 + maxAccessCount--; + if (maxAccessCount == 0) { + // 删除缓存 + fmsAccessTokenService.deleteFileAccessToken(fileAccessToken); + } else { + // 更新缓存 + int expiredTime = fsAccessToken.getExpiredTime(); + fmsAccessTokenService.cacheFileAccessToken(fileAccessToken, expiredTime, maxAccessCount, uniqueId, fsAccessToken.getType()); + // 更新缓存时间 + redisTemplate.expire(fileAccessToken, expiredTime, TimeUnit.MINUTES); + } + } + } + + // 返回该文件 + FmsRecordDTO fssRecord = fsUploadRecords.get(0); + FmsDetailRecordResult result = new FmsDetailRecordResult(); + BeanUtils.copyProperties(fssRecord , result); + return result; + } + + @Override + public FmsDetailRecordResult getFileUploadRecord(String uniqueId) { + FmsRecordDTO tmp = new FmsRecordDTO(); + tmp.setUniqueId(uniqueId); + List fsUploadRecords = fmsFileRecordMapper.selectbySelective(tmp); + if (fsUploadRecords == null || fsUploadRecords.isEmpty()) { + // 无该文件,返回404 + throw new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code); + } + FmsRecordDTO fssRecord = fsUploadRecords.get(0); + FmsDetailRecordResult result = new FmsDetailRecordResult(); + BeanUtils.copyProperties(fssRecord , result); + return result; + } + + /** + * 文件批量上传 + * @param files + * @param param + * @return + */ + @Override + public CommonResult uploadFiles(MultipartFile[] files, FmsUploadTokenParam param) { + + //收集文件数对应的结果 + List results = new ArrayList<>(); + List> taskList = new ArrayList<>(); + for (MultipartFile file : files){ + //执行多线程上传 执行上传的service,文件,参数 +// this.saveFile(file,param); + String uuid = UUIDUtils.uuid(); + FutureTask futureTask = new FutureTask<>(new FmsFileRecordUploadTask(self,file,param,uuid)); + taskList.add(futureTask); + //返回信息 + FmsMultipartFileResult multipartFileResult = new FmsMultipartFileResult(); + multipartFileResult.setFileName(file.getOriginalFilename()); + multipartFileResult.setUniqueId(String.valueOf(uuid)); + results.add(multipartFileResult); + try { + threadPoolExecutor.execute(futureTask); + }catch (Exception e){ + logger.error("批量上传文件失败"); + throw new AppException(FmsCodeBean.FmsCode.FILE_MULTIPART_UPLOAD.code); + } + } + //待所有线程任务完成,返回结果。其中一个上传失败,抛出异常 + for(FutureTask task: taskList){ + if (task == null){ + logger.error("批量上传文件失败"); + throw new AppException(FmsCodeBean.FmsCode.FILE_MULTIPART_UPLOAD.code); + } + } + + return CommonResult.success(results); + } + + /** + * 验证文件 + * @param files + * @param fsBusinessType + */ + @Override + public void verifyFiles(MultipartFile[] files, FmsBusinessTypeResult fsBusinessType) { + if (files.length == 0 || files == null){ + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILE_IS_NULL.code); + } + if (files.length > fileUploadLimitSize){ + throw new AppException(FmsCodeBean.FmsCode.FILE_LIMIT_SIZE.code); + } + //所有文件大小,单位字节 + long allFileSize = 0; + for (MultipartFile file : files){ + if (file == null || StringUtils.isBlank(file.getOriginalFilename())) { + // 文件不能为空 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILE_IS_NULL.code); + } + //通用类型的文件大小,单位kb + String fsSize = fsBusinessType.getFsSize(); + //通用类型的文件大小,单位kb + Long limitSize = Long.parseLong(fsSize); + //文件的大小,单位字节 + long fileSize = file.getSize(); + //总文件累计,单位字节 + allFileSize += fileSize; + //类型允许的文件大小,KB转字节 + long allowFileSize = limitSize * 1024; + //判断文件大小与限制的大小是否符合 + if (fileSize > allowFileSize || fileSize > (fileMaxFileSize * 1024)) { + logger.error("文件名称{}的大小{}字节的文件超过限制【类型限制大小{}KB,单个文件限制大小{}KB】",file.getOriginalFilename(),fileSize,fileSize,fileMaxFileSize); + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILESIZE_OUT_OFF_LIMIT.code); + } + String fsType = fsBusinessType.getFsType(); + if (!StringUtils.equals(fsType, CURRENCY_TYPE)){ + //如果取值不为currency,则判断类型 + String originFileName = file.getOriginalFilename(); + String suffix = originFileName.substring(originFileName.lastIndexOf(".") + 1); + String contentType = file.getContentType(); + if (contentType == null || !fsType.contains(suffix.trim().toLowerCase()) || !fsType.contains(contentType)) { + logger.error(FmsCodeBean.FmsCode.ERROR_FILETYPE.code, "文件类型:" + suffix + " || " + contentType); + // 文件后缀错误 + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILETYPE.code); + } + } + } + //请求的文件大小限制,KB转字节 + if (allFileSize > (fileMaxRequestSize * 1024)){ + logger.error("文件的总大小为{}字节,已经超过请求限制的{}字节大小",allFileSize,fileMaxRequestSize * 1024); + throw new AppException(FmsCodeBean.FmsCode.ERROR_FILESIZE_OUT_OFF_LIMIT.code); + } + } + + /** + * 文件批量上传的业务逻辑 + * @param files + * @param uploadToken + * @return + */ + @Override + @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) + public CommonResult filesMultipartUpload(MultipartFile[] files, String uploadToken) { + //验证token是否合理 + FmsUploadTokenResult fsUploadToken = fmsBusinessTypeService.getFsBusinessTypeByUploadToken(uploadToken); + if (fsUploadToken == null) { + throw new AppException(FmsCodeBean.FmsCode.ERROR_TIMEOUT.code); + } + //获取文件允许是类型 + FmsBusinessTypeResult fsBusinessType = fsUploadToken.getFsBusinessType(); + //验证文件 + self.verifyFiles(files,fsBusinessType); + FmsUploadTokenParam param = new FmsUploadTokenParam(); + BeanUtils.copyProperties(fsUploadToken , param); + //批量上传 + CommonResult fileResult = self.uploadFiles(files,param); +// String uniqueId = fsService.saveFile(file, param); + //删除token + fmsBusinessTypeService.deleteFsBusinessTypeByUploadToken(uploadToken); + return CommonResult.success(fileResult.getData()); + } + + /** + * 获取文件访问路径,如果超时时间设置为小于等于0,那么返回的有效路径可以永久访问 + * @param uniqueIds + * @param expiredTime + * @param maxAccessCount + * @param type + * @return + */ + @Override + @Transactional(propagation = Propagation.SUPPORTS,readOnly = true,rollbackFor = Exception.class) + public CommonResult getFileUrlByFileIds(List uniqueIds, Integer expiredTime, int maxAccessCount, String type) { + + if (CollectionUtils.isEmpty(uniqueIds)){ + logger.error(FmsCodeBean.FmsCode.NOT_EXIST_FILE.message,new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code, FmsCodeBean.FmsCode.NOT_EXIST_FILE.message)); + return CommonResult.success(new ArrayList<>()); + } + //根据uniqueIds查询任务记录 + FmsRecordDTO tmp = new FmsRecordDTO(); + tmp.setUniqueIds(uniqueIds); + List fsUploadRecords = fmsFileRecordMapper.selectbySelective(tmp); + if (CollectionUtils.isEmpty(fsUploadRecords)) { + logger.error(FmsCodeBean.FmsCode.NOT_EXIST_FILE.message,new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code, FmsCodeBean.FmsCode.NOT_EXIST_FILE.message)); + return CommonResult.success(new ArrayList<>()); + } + //返回 uniqueId--文件path + List recordPathResults = new ArrayList<>(); + //文件访问令牌的批量添加 + List fssAccessTokens = new ArrayList<>(); + for (FmsRecordDTO fssRecord : fsUploadRecords){ + String uniqueId = fssRecord.getUniqueId(); + String fileAccessToken = UUIDUtils.uuid(); + //String fileAccessUrlForOutter = "http://192.168.0.135:8150/fs/File/";//本地路径 + String path = fileAccessUrlForOutter + uniqueId + "?fileAccessToken=" + fileAccessToken; + // 缓存文件token(方式一) +// FsAccessTokenResult fsAccessToken = fsAccessService.cacheFileAccessToken(fileAccessToken, expiredTime, maxAccessCount, uniqueId, type); +// if (fsAccessToken == null) { +// throw new AppException(FSSCodeBean.FSSCode.FAIL.code); +// } + // 缓存文件token(方式二) + FmsAccessTokenResult fmsAccessTokenResult = new FmsAccessTokenResult(expiredTime, maxAccessCount, uniqueId, type); + if (expiredTime > 0){ + redisTemplate.opsForValue().set(fileAccessToken , JSON.toJSONString(fmsAccessTokenResult) , expiredTime , TimeUnit.MINUTES); + }else { + FmsAccessTokenDTO fssFmsAccessTokenDto = new FmsAccessTokenDTO(); + fssFmsAccessTokenDto.setFileAccessToken(fileAccessToken); + fssFmsAccessTokenDto.setType(type); + fssFmsAccessTokenDto.setUniqueId(uniqueId); + fssAccessTokens.add(fssFmsAccessTokenDto); + } + FmsFileRecordPathResult fmsFileRecordPathResult = new FmsFileRecordPathResult(); + fmsFileRecordPathResult.setPath(path); + fmsFileRecordPathResult.setUniqueId(uniqueId); + recordPathResults.add(fmsFileRecordPathResult); + } + if (!CollectionUtils.isEmpty(fssAccessTokens)){ + fmsAccessTokenMapper.multipartInsert(fssAccessTokens); + } + return CommonResult.success(recordPathResults); + } + + +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsRecordServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsRecordServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a2d8f7c30978adb1375c332637ef3504a03c4920 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsRecordServiceImpl.java @@ -0,0 +1,37 @@ +package com.itools.core.service.impl; + +import com.itools.core.dto.fileRecord.FmsRecordDTO; +import com.itools.core.mapper.FmsFileRecordMapper; +import com.itools.core.result.FmsDetailRecordResult; +import com.itools.core.service.FmsRecordService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Service +public class FmsRecordServiceImpl implements FmsRecordService { + + @Autowired + private FmsFileRecordMapper fmsFileRecordMapper; + + @Override + public FmsDetailRecordResult queryFssRecordByUniqueId(String uniqueId) { + FmsRecordDTO queryParams = new FmsRecordDTO(); + queryParams.setUniqueId(uniqueId); + List list = fmsFileRecordMapper.selectbySelective(queryParams); + if (list.size() > 0) { + FmsRecordDTO record = list.get(0); + FmsDetailRecordResult result = new FmsDetailRecordResult(); + BeanUtils.copyProperties(record , result); + return result; + } + return null; + } + +} diff --git a/itools-fms/itools-fms-server/src/main/resources/application-dev.yml b/itools-fms/itools-fms-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..6dffb7544dddd52217bc3d141fa8263b816f0e46 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/application-dev.yml @@ -0,0 +1,90 @@ +#组件中的全局异常处理的code +system: + errorCode: FMSCODE00 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple + +#fms文件系统配置项 +spring: + servlet: + multipart: + # 最大支持文件大小 + max-file-size: 20MB + # 最大支持请求大小 + max-request-size: 200MB + # 上传文件的临时目录 +# location: /home/hash/AppSvr01/installedApps/fsnsh_backend-master/temp + + +fileStoreRootPath: /data/fmsFile #文件存储路径,此路径应该是多个FSS服务的共享磁盘 +fileAccessUrlForOutter: http://127.0.0.1:8002/File/ +fileUploadLimitSize: 10 #批量上传限制的个数 +fileMaxRequestSize: 204800 #最大的请求数,单位KB,200MB +fileMaxFileSize: 20480 #单个文件最大大小,单位KB,20MB + +fms: + #系统文件管理策略【nio原生的文件系统,miniio文件系统,fastdfs文件系统】 + strategy: nio + #文件的token的过期时间单位分钟 + token-expiration-time: 5 + #文件配置 + file: + store-root-path: /data/fmsFile #文件存储路径,此路径应该是多个fms服务的共享磁盘 + access-url-for-outter: http://127.0.0.1:8002/File/ #文件访问 + upload-limit-size: 10 #批量上传限制的个数 + max-request-size: 204800 #最大的请求数,单位KB,200MB + max-file-size: 20480 #单个文件最大大小,单位KB,20MB + #minio文件配置 + minio: + endpoint: 172.28.43.29 + port: 9000 + accessKey: minio + secretKey: miniopassword + secure: false + bucketName: "hope-bucket" + configDir: "/home/data/" + + +#脚本自动初始化执行 +initDB: + #启用开关 + is-use: false + # ddl脚本,可以支持多个,用,分割,文件建议最多5个 + ddl: init/ddl.sql + # dml脚本,可以支持多个,用,分割,文件建议最多5个 + dml: init/dml.sql + # function脚本,可以支持多个,用,分割,文件建议最多5个 + function: init/function.sql + # 分支环境 + env: dev + # 版本 + version: 1.0 + # 脚本设置的分隔符 + delimiter: ; + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +#security: +# oauth2: +# client: +# client-id: product-server +# client-secret: 123456 +# user-authorization-uri: http://127.0.0.1:8888/oauth/authorize +# access-token-uri: http://127.0.0.1:8888/oauth/token +# resource: +# jwt: +# key-uri: http://127.0.0.1:8888/oauth/token_key \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/application.yml b/itools-fms/itools-fms-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..c197c554dc7269a4e972f042bdc1a96d2fc99196 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/application.yml @@ -0,0 +1,33 @@ +server: + port: 8002 +spring: + application: + name: itools-fms-server + profiles: + active: dev + redis: + host: 127.0.0.1 # id + password: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器连接端口 + port: 6379 + # 连接超时时间 单位 ms(毫秒) + timeout: 3000 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/itools?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + diff --git a/itools-fms/itools-fms-server/src/main/resources/bootstrap.yml b/itools-fms/itools-fms-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..94abc03c5d8f9970c038099a293134e50b57e0a8 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/bootstrap.yml @@ -0,0 +1,17 @@ +# Nacos Server 的地址 +spring: +# profiles: +# active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-fms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..766b770495e7cdebb05f4cac0b79f7fc77ebc840 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + file_access_token, unique_id, type + + + + + + + insert into itools_fms_access_token (file_access_token, unique_id, type) + values (#{item.fileAccessToken,jdbcType=VARCHAR}, #{item.uniqueId,jdbcType=VARCHAR}, #{item.type,jdbcType=VARCHAR}) + + + + \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..66f2f052f4d2657945587965bf0832f123490773 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + ID, BUSINESS_TYPE, REMARK, FS_SIZE, FS_TYPE, CREATE_DATE, UPDATE_DATE + + + + + delete from itools_fms_business_type + where ID = #{id,jdbcType=BIGINT} + + + + insert into itools_fms_business_type + + + ID, + + + BUSINESS_TYPE, + + + REMARK, + + + FS_SIZE, + + + FS_TYPE, + + + CREATE_DATE, + + + UPDATE_DATE, + + + + + #{id,jdbcType=BIGINT}, + + + #{businessType,jdbcType=VARCHAR}, + + + #{remark,jdbcType=VARCHAR}, + + + #{fsSize,jdbcType=VARCHAR}, + + + #{fsType,jdbcType=VARCHAR}, + + + #{createDate,jdbcType=TIMESTAMP}, + + + #{updateDate,jdbcType=TIMESTAMP}, + + + + + update itools_fms_business_type + + + BUSINESS_TYPE = #{businessType,jdbcType=VARCHAR}, + + + REMARK = #{remark,jdbcType=VARCHAR}, + + + FS_SIZE = #{fsSize,jdbcType=VARCHAR}, + + + FS_TYPE = #{fsType,jdbcType=VARCHAR}, + + + CREATE_DATE = #{createDate,jdbcType=TIMESTAMP}, + + + UPDATE_DATE = #{updateDate,jdbcType=TIMESTAMP}, + + + where ID = #{id,jdbcType=BIGINT} + + + update itools_fms_business_type + set BUSINESS_TYPE = #{businessType,jdbcType=VARCHAR}, + REMARK = #{remark,jdbcType=VARCHAR}, + FS_SIZE = #{fsSize,jdbcType=VARCHAR}, + FS_TYPE = #{fsType,jdbcType=VARCHAR}, + CREATE_DATE = #{createDate,jdbcType=TIMESTAMP}, + UPDATE_DATE = #{updateDate,jdbcType=TIMESTAMP} + where ID = #{id,jdbcType=BIGINT} + + \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..33c2199480354421958816e3b782759c25544cd2 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + ID, CONTENT_TYPE, FROM_SYSTEM_ID, FILE_SIZE, UNIQUE_ID, REMARK, ORIG_FILE_NAME, CREATE_TIME, + UPDATE_TIME, DELETE_TIME, DELETED + + + + + delete from itools_fms_record + where ID = #{id,jdbcType=DECIMAL} + + + + insert into itools_fms_record + + + ID, + + + CONTENT_TYPE, + + + FROM_SYSTEM_ID, + + + FILE_SIZE, + + + UNIQUE_ID, + + + REMARK, + + + ORIG_FILE_NAME, + + + CREATE_TIME, + + + UPDATE_TIME, + + + DELETE_TIME, + + + DELETED, + + + + + #{id,jdbcType=DECIMAL}, + + + #{contentType,jdbcType=VARCHAR}, + + + #{fromSystemId,jdbcType=VARCHAR}, + + + #{fileSize,jdbcType=DECIMAL}, + + + #{uniqueId,jdbcType=VARCHAR}, + + + #{remark,jdbcType=VARCHAR}, + + + #{origFileName,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + #{updateTime,jdbcType=TIMESTAMP}, + + + #{deleteTime,jdbcType=TIMESTAMP}, + + + #{deleted,jdbcType=CHAR}, + + + + + update itools_fms_record + + + CONTENT_TYPE = #{contentType,jdbcType=VARCHAR}, + + + FROM_SYSTEM_ID = #{fromSystemId,jdbcType=VARCHAR}, + + + FILE_SIZE = #{fileSize,jdbcType=DECIMAL}, + + + UNIQUE_ID = #{uniqueId,jdbcType=VARCHAR}, + + + REMARK = #{remark,jdbcType=VARCHAR}, + + + ORIG_FILE_NAME = #{origFileName,jdbcType=VARCHAR}, + + + CREATE_TIME = #{createTime,jdbcType=TIMESTAMP}, + + + UPDATE_TIME = #{updateTime,jdbcType=TIMESTAMP}, + + + DELETE_TIME = #{deleteTime,jdbcType=TIMESTAMP}, + + + DELETED = #{deleted,jdbcType=CHAR}, + + + where ID = #{id,jdbcType=DECIMAL} + + + update itools_fms_record + set CONTENT_TYPE = #{contentType,jdbcType=VARCHAR}, + FROM_SYSTEM_ID = #{fromSystemId,jdbcType=VARCHAR}, + FILE_SIZE = #{fileSize,jdbcType=DECIMAL}, + UNIQUE_ID = #{uniqueId,jdbcType=VARCHAR}, + REMARK = #{remark,jdbcType=VARCHAR}, + ORIG_FILE_NAME = #{origFileName,jdbcType=VARCHAR}, + CREATE_TIME = #{createTime,jdbcType=TIMESTAMP}, + UPDATE_TIME = #{updateTime,jdbcType=TIMESTAMP}, + DELETE_TIME = #{deleteTime,jdbcType=TIMESTAMP}, + DELETED = #{deleted,jdbcType=CHAR} + where ID = #{id,jdbcType=DECIMAL} + + \ No newline at end of file diff --git a/itools-fms/pom.xml b/itools-fms/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..71ee5d66e5f7cd433e559f2fa8d2bfc8e0b60446 --- /dev/null +++ b/itools-fms/pom.xml @@ -0,0 +1,107 @@ + + + + itools-backend + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + pom + itools-fms + + + itools-fms-model + itools-fms-core + itools-fms-server + + + 3.3.2 + 1.1.21 + 2.1.7.RELEASE + 1.2.30 + 1.27.0.0 + + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + Greenwich.SR3 + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2.1.1.RELEASE + pom + import + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + net.oschina.zcx7878 + fastdfs-client-java + ${fastdfs-client-java.version} + + + + + + com.itools.core + itools-core + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + + + + + mysql + mysql-connector-java + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + \ No newline at end of file diff --git a/itools-fms/script/itools-fms b/itools-fms/script/itools-fms new file mode 100644 index 0000000000000000000000000000000000000000..1d276a9b46a4b0826b6947922425cd52c3d8683d --- /dev/null +++ b/itools-fms/script/itools-fms @@ -0,0 +1,40 @@ +CREATE TABLE `itools_fms_record` ( + `id` bigint(32) NOT NULL COMMENT '主键', + `content_type` varchar(100) DEFAULT NULL COMMENT 'HTTP协议定义的文件类型,文件上传从文件流中获取文件存取时使用该字段填入response的content-type字段', + `from_system_id` varchar(50) DEFAULT NULL COMMENT '件所属系统,文件上传时指定', + `file_size` bigint(20) DEFAULT NULL COMMENT '文件大小,单位为字节', + `unique_id` varchar(50) DEFAULT NULL COMMENT '文件的全局唯一ID,后续访问均使用该ID', + `remark` varchar(200) DEFAULT NULL COMMENT '文件描述信息', + `orig_file_name` varchar(200) DEFAULT NULL COMMENT '原文件名,来自入参', + `create_time` timestamp(6) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` timestamp(6) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` timestamp(6) NULL DEFAULT NULL COMMENT '删除时间', + `deleted` char(1) DEFAULT NULL COMMENT '是否已删除文件本身,也即是说,删除文件时数据库记录仅仅将该字段置位1,并不实际删除数据库记录0:没有删除1:已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UK_FS_ID` (`unique_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; + + +CREATE TABLE `itools_fms_access_token` ( + `file_access_token` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '文件访问token', + `unique_id` char(32) COLLATE utf8mb4_bin NOT NULL COMMENT '文件的唯一id', + `type` varchar(10) COLLATE utf8mb4_bin NOT NULL COMMENT 'show或者download', + PRIMARY KEY (`file_access_token`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='该表存储的是永久的accessToken,用于支持文件的永久访问或者下载'; + +CREATE TABLE `itools_fms_business_type` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `business_type` varchar(20) DEFAULT NULL COMMENT '01-营业执照 02-身份证正面 03-身份证背面 04-授权文件 05-政策图片 06-金融机构图片 07-金融产品图片', + `remark` varchar(100) DEFAULT NULL COMMENT '说明', + `fs_size` varchar(20) DEFAULT NULL COMMENT '文件大小, 单位kb', + `fs_type` varchar(100) DEFAULT NULL COMMENT '文件类型,逗号分隔符,例如: jpg,png', + `create_date` timestamp(6) NULL DEFAULT NULL COMMENT '创建时间', + `update_date` timestamp(6) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UK_FS_BT` (`business_type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; + +INSERT INTO `itools_fms_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (1, '01', '文件上传', '102400', 'multipart/form-data,application/octet-stream,text/plain', '2019-08-20 18:48:57.000000', '2019-08-20 18:48:59.000000'); +INSERT INTO `itools_fms_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (2, '02', 'jar包上传', '102400', 'application/octet-stream,text/plain,multipart/form-data', '2019-08-28 15:24:34.000000', '2019-08-28 15:24:37.000000'); +INSERT INTO `itools_fms_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (3, 'currency', '通用类型', '102400', 'currency', '2019-10-14 20:21:37.000000', '2019-10-14 20:21:40.000000'); +INSERT INTO `itools_fms_business_type`(`id`, `business_type`, `remark`, `fs_size`, `fs_type`, `create_date`, `update_date`) VALUES (4, '03', 'doc', '102400', 'multipart/form-data; boundary=----WebKitFormBoundaryV9UszO8Om8qv8nNM', '2020-07-30 16:05:31.000000', '2020-07-30 16:05:35.000000'); \ No newline at end of file diff --git a/itools-gms/pom.xml b/itools-gms/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..6907651882e6a31056943d6184ad788bca0471cb --- /dev/null +++ b/itools-gms/pom.xml @@ -0,0 +1,15 @@ + + + + itools-backend + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + pom + itools-gms + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-common/pom.xml b/itools-oms/itools-oms-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..838d3bbe4e9a6e75d6db14df2f41d70dc4c01aef --- /dev/null +++ b/itools-oms/itools-oms-common/pom.xml @@ -0,0 +1,15 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-common + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-core/pom.xml b/itools-oms/itools-oms-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..81f99d92f61ef9eb16b05c878fd02b6839bae08c --- /dev/null +++ b/itools-oms/itools-oms-core/pom.xml @@ -0,0 +1,27 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-core + + + + com.itools.core + itools-oms-model + 1.0-SNAPSHOT + + + com.itools.core + itools-oms-common + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/pom.xml b/itools-oms/itools-oms-jwt-sample-example/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..548c710bd7fedd3bc74134c427ccc587eb874f72 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/pom.xml @@ -0,0 +1,44 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-jwt-sample-example + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/OmsSampleApplication.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/OmsSampleApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..f50ed748e30a6167435d9f3898476522d6e46ca0 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/OmsSampleApplication.java @@ -0,0 +1,17 @@ +package com.itools.core; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-30 16:29 + */ +@SpringBootApplication +public class OmsSampleApplication { + public static void main(String[] args) { + SpringApplication.run(OmsSampleApplication.class); + } +} diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..8d12cec6c3af651d5b10c6df2a70609b6262b6cc --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java @@ -0,0 +1,31 @@ +package com.itools.core.config; + +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity(debug = true) +@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true) +@EnableOAuth2Sso +public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http + .antMatcher("/**") + .authorizeRequests() + .antMatchers("/oauth/**","/login/**","/logout/**").permitAll() + .anyRequest().authenticated(); + } +} diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java new file mode 100644 index 0000000000000000000000000000000000000000..8472d8a308e171dbf3a05aecc93c39486e972d7f --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java @@ -0,0 +1,23 @@ +package com.itools.core.config; + +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; + +import java.util.HashMap; +import java.util.Map; + +public class JwtTokenEnhancer implements TokenEnhancer { + + @Override + public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, + OAuth2Authentication authentication) { + Map info = new HashMap<>(); + info.put("enhance", "enhance info"); + info.put("appId", "appId"); + ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); + return accessToken; + } + +} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..90075763f41bc7423c3a90ba6a87f8c02a576541 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java @@ -0,0 +1,29 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; + +@Configuration +public class JwtTokenStoreConfig { + + @Bean + public TokenStore jwtTokenStore(){ + return new JwtTokenStore(jwtAccessTokenConverter()); + } + + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter(){ + JwtAccessTokenConverter accessTokenConverter = new + JwtAccessTokenConverter(); + //配置JWT使用的秘钥 + accessTokenConverter.setSigningKey("123123"); + return accessTokenConverter; + } + @Bean + public JwtTokenEnhancer jwtTokenEnhancer() { + return new JwtTokenEnhancer(); + } +} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java new file mode 100644 index 0000000000000000000000000000000000000000..d29fdbed5d2a564d8be57d2bd34567278c741c27 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java @@ -0,0 +1,54 @@ +package com.itools.core.config; + +import java.util.ArrayList; +import java.util.List; + +class Solution { + public static int clumsy(int N) { + List sit = new ArrayList<>(); + sit.add("*"); + sit.add("/"); + sit.add("+"); + sit.add("-"); + int sum = N; + if(N<=0){ + return sum; + } + int index = 0; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(N); + while((N-1) >0 ){ + if(index >= sit.size()){ + index = 0; + } + + stringBuilder.append(sit.get(index)); + stringBuilder.append(N-1); + sum = run(sit.get(index),sum,N-1); + index++; + N--; + } + System.out.println(stringBuilder.toString()); + return sum; + + } + private static int run(String sit,int left,int right){ + int res = 0; + System.out.println(left+sit+right); + if(sit.equals("*")){ + res = left * right; + }else if(sit.equals("/")){ + res = left / right; + }else if(sit.equals("+")){ + res = left + right; + }else{ + res = left - right; + } + + return res; + } + + public static void main(String[] args) { + System.out.println(clumsy(10)); + } +} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java new file mode 100644 index 0000000000000000000000000000000000000000..6884fb0dbb3d3c0e393c229be68fbd1e3876c745 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java @@ -0,0 +1,15 @@ +package com.itools.core.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/product") +public class ProductController { + + @RequestMapping("/selectProductInfoById") + public String selectProductInfoById(long id) { + + return "获取产品信息成功"+id; + } +} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml b/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..7bad5bedb4de920b6e98a850d4487fde72e784ea --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml @@ -0,0 +1,17 @@ +server: + port: 8085 +spring: + application: + name: example-server + main: + allow-bean-definition-overriding: true +security: + oauth2: + client: + client-id: example-server + client-secret: 123456 + user-authorization-uri: http://127.0.0.1:8888/oauth/authorize + access-token-uri: http://127.0.0.1:8888/oauth/token + resource: + jwt: + key-uri: http://127.0.0.1:8888/oauth/token_key diff --git a/itools-oms/itools-oms-model/pom.xml b/itools-oms/itools-oms-model/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f02df7953bf194ea131bca92a30067f3f6c8d9f1 --- /dev/null +++ b/itools-oms/itools-oms-model/pom.xml @@ -0,0 +1,15 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-model + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysPermission.java b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..e63ffb0fef93677a5ec138dff83e383cd6056768 --- /dev/null +++ b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysPermission.java @@ -0,0 +1,34 @@ +package com.itools.core.param; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysPermission implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String url; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysRole.java b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysRole.java new file mode 100644 index 0000000000000000000000000000000000000000..1e1d2d462d65bee4a03e6c79d8a62e37e8156ef6 --- /dev/null +++ b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysRole.java @@ -0,0 +1,32 @@ +package com.itools.core.param; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysRole implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysUser.java b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysUser.java new file mode 100644 index 0000000000000000000000000000000000000000..8d3f6985a6f4664ce00cca9f1ea714a60ffe8474 --- /dev/null +++ b/itools-oms/itools-oms-model/src/main/java/com/itools/core/param/SysUser.java @@ -0,0 +1,34 @@ +package com.itools.core.param; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysUser implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String password; + + private String phone; + + private String email; + + private Date created; + + private Date updated; + + +} diff --git a/itools-oms/itools-oms-server/pom.xml b/itools-oms/itools-oms-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0cf87fc7475865b7b40044686901c1fee6eff74 --- /dev/null +++ b/itools-oms/itools-oms-server/pom.xml @@ -0,0 +1,97 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-server + + + + + com.itools.core + itools-oms-common + 1.0-SNAPSHOT + + + com.itools.core + itools-oms-core + 1.0-SNAPSHOT + + + com.itools.core + itools-oms-model + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.security + spring-security-jwt + 1.0.9.RELEASE + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java new file mode 100644 index 0000000000000000000000000000000000000000..44f9dc002601253b029f4d30ac3bba2790370d40 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java @@ -0,0 +1,56 @@ +package com.itools.core; + +import com.itools.core.snowflake.config.EnableSequenceService; +import com.itools.core.validate.EnableValidator; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@EnableSequenceService +@EnableValidator +@EnableAuthorizationServer +@MapperScan("com.itools.core.mapper") +public class Oauth2Application { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Oauth2Application.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + // 允许cookies跨域 + config.setAllowCredentials(true); + // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedOrigin("*"); + // 允许访问的头信息,*表示全部 + config.addAllowedHeader("*"); + // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.setMaxAge(18000L); + // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + config.addAllowedMethod("*"); + config.addAllowedMethod("HEAD"); + // 允许Get的请求方法 + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5004e9b94225a02665c86f2dfb7e18821238feb7 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java @@ -0,0 +1,148 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; +import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: 继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。 + * ClientDetailsServiceConfigurer :用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。 + * AuthorizationServerEndpointsConfigurer :用来配置令牌(token)的访问端点和令牌服务(token services)。 + * AuthorizationServerSecurityConfigurer :用来配置令牌端点的安全约束. + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Configuration +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + @Autowired + private AuthenticationManager authenticationManager; + + @Qualifier("dataSource") + @Autowired + private DataSource dataSource; + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private RedisConnectionFactory redisConnectionFactory; +// @Autowired +// @Qualifier("jwtTokenStore") +// private TokenStore tokenStore; + @Autowired + private JwtAccessTokenConverter jwtAccessTokenConverter; + @Autowired + private JwtTokenEnhancer jwtTokenEnhancer; + @Bean + public TokenStore tokenStore(){ + // access_token + return new RedisTokenStore(redisConnectionFactory); + } + @Bean + public JwtTokenEnhancer jwtTokenEnhancer() { + return new JwtTokenEnhancer(); + } + + /** + * 配置client_details + * @param clients + * @throws Exception + */ + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + clients.withClientDetails(clientDetails()); + } + + /** + * 读取oauth_client_details表 + * @return + */ + @Bean + public ClientDetailsService clientDetails() { + return new JdbcClientDetailsService(dataSource); + } + +// @Override +// public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { +// //使用密码模式需要配置 +// endpoints.authenticationManager(authenticationManager) +// //指定token存储到redis +//// .tokenStore(tokenStore()) +// .tokenStore(tokenStore) +// .accessTokenConverter(jwtAccessTokenConverter) +// //refresh_token是否重复使用 +// .reuseRefreshTokens(false) +// //刷新令牌授权包含对用户信息的检查 +// .userDetailsService(userDetailsService) +// //支持GET,POST请求 +// .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); +// } + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + //配置JWT的内容增强器 + TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); + List delegates = new ArrayList<>(); + delegates.add(jwtTokenEnhancer); + delegates.add(jwtAccessTokenConverter); + enhancerChain.setTokenEnhancers(delegates); + //使用密码模式需要配置 + endpoints.authenticationManager(authenticationManager) + //配置存储令牌策略 +// .tokenStore(tokenStore) + .tokenStore(tokenStore()) + .accessTokenConverter(jwtAccessTokenConverter) + //配置tokenEnhancer + .tokenEnhancer(enhancerChain) + //refresh_token是否重复使用 + .reuseRefreshTokens(false) + //刷新令牌授权包含对用户信息的检查 + .userDetailsService(userDetailsService); + //支持GET,POST请求 PUT DELETE + endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE); + } +// @Override +// public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { +// //允许表单认证 +// security.allowFormAuthenticationForClients() +// // 配置校验token需要带入clientId 和clientSeret配置 +// .checkTokenAccess("isAuthenticated()"); +// } + @Override + public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { + oauthServer + .tokenKeyAccess("permitAll()") + .checkTokenAccess("permitAll()") + .allowFormAuthenticationForClients(); + } +// @Bean +// public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter) { +// FilterRegistrationBean registration = new FilterRegistrationBean(); +// registration.setFilter(oAuth2ClientContextFilter); +// registration.setOrder(-100); +// return registration; +// } +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java new file mode 100644 index 0000000000000000000000000000000000000000..fd85b844914a3c86e648fee2f180bf72057dc762 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java @@ -0,0 +1,27 @@ +package com.itools.core.config; + +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; + +import java.util.HashMap; +import java.util.Map; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class JwtTokenEnhancer implements TokenEnhancer { + + @Override + public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, + OAuth2Authentication authentication) { + Map info = new HashMap<>(); + info.put("enhance", "enhance info"); + info.put("appId", "appId"); + ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); + return accessToken; + } +} \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ef1614e5b58268917d45eca0f7aff915309fe1a --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java @@ -0,0 +1,29 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Configuration +public class JwtTokenStoreConfig { + + @Bean + public TokenStore jwtTokenStore(){ + return new JwtTokenStore(jwtAccessTokenConverter()); + } + + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter(){ + JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); + //配置JWT使用的秘钥 + accessTokenConverter.setSigningKey("123123"); + return accessTokenConverter; + } +} \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb8fb0a00c9f5287c158c565daf4d50a37f534c --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,36 @@ +//package com.itools.core.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.core.annotation.Order; +//import org.springframework.security.config.annotation.web.builders.HttpSecurity; +//import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +//import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +// +///** +// * @project: itools-backend +// * @description: 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 +// * @author: XUCHANG +// * @create: 2021-03-28 15:51 +// */ +//@Order(6) +//@Configuration +//@EnableResourceServer +//public class ResourceServerConfig extends ResourceServerConfigurerAdapter { +// +// @Override +// public void configure(HttpSecurity http) throws Exception { +// +// +// http.csrf().disable()//禁用了 csrf 功能 +// .authorizeRequests()//限定签名成功的请求 +//// .antMatchers("/decision/**","/govern/**").hasAnyRole("USER","ADMIN") +//// .antMatchers("/admin/**").hasRole("ADMIN") +// .antMatchers("/**").authenticated()//签名成功后可访问,不受role限制 +//// .antMatchers("/admin/login","/oauth/**").permitAll() +// .anyRequest().permitAll()//其他没有限定的请求,允许访问 +// .and().anonymous()//对于没有配置权限的其他请求允许匿名访问 +// .and().formLogin()//使用 spring security 默认登录页面 +// .and().httpBasic();//启用http 基础验证 +// } +// +//} \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..99d049083688212e0fd5dca30407359e813276b5 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java @@ -0,0 +1,88 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Order(2) +@Configuration +@EnableWebSecurity(debug = true) +@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + + /** + * 重写PasswordEncoder 接口中的方法,实例化加密策略 + * 这里采用bcrypt的加密策略,采用这种策略的好处是Bcrypt是单向Hash加密算法,类似Pbkdf2算法 不可反向破解生成明文,而且还兼容之前采用别的加密算法 + */ + @Bean + public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.formLogin().permitAll() + .and().authorizeRequests() + .antMatchers("/user/getCurrentUser","/login","/oauth/**","/oauth/authorize","/login/**","/logout/**","/error").permitAll() + .anyRequest().authenticated() + .and().httpBasic() + .and().csrf().disable(); +// http +// .antMatcher("/**") +// .authorizeRequests() +// .antMatchers( +// HttpMethod.GET, +// "/*.html", +// "/**/*.html", +// "/**/*.css", +// "/**/*.js", +// "/**/*.jpg", +// "/**/*.png", +// "/error", +// "/favicon.ico" +// ).permitAll() +// .antMatchers("/swagger-ui.html").anonymous() +// .antMatchers("/swagger-resources/**").anonymous() +// .antMatchers("/user/getCurrentUser","/oauth/**","/oauth2/**","/login/**","/logout/**").permitAll() +// .anyRequest().authenticated(); +// http +// .antMatcher("/**") +// .authorizeRequests() +// .antMatchers("/oauth/**","/login/**","/logout/**").permitAll() +// .anyRequest().authenticated() +// .and() +// .formLogin().permitAll(); + } + +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..7ab985c3f2cc03fc46b73361c760642a960b07c4 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java @@ -0,0 +1,51 @@ +package com.itools.core.controller; + +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 14:29 + */ +@RestController +@RequestMapping("/user") +public class UserController { + + @Qualifier("jwtTokenStore") + @Autowired + private TokenStore tokenService; + +// @RequestMapping("/getCurrentUser") +// public Object getCurrentUser(@RequestParam("access_token") String token) { +// OAuth2Authentication oAuth2Authentication = tokenService.readAuthentication(token); +// +// return oAuth2Authentication.getUserAuthentication()/*.getPrincipal()*/; +// } + @RequestMapping("/getCurrentUser") + public Object getCurrentUser(Authentication authentication, + HttpServletRequest request) { + String header = request.getHeader("Authorization"); + String token = null; + if(header!=null){ + token = header.substring(header.indexOf("bearer") + 7); + }else { + token = request.getParameter("access_token"); + } + return Jwts.parser() + .setSigningKey("123123".getBytes(StandardCharsets.UTF_8)) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/PermissionMapper.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/PermissionMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..23b9ec2bf43b2e58a9118c470a8068e54a8c128b --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/PermissionMapper.java @@ -0,0 +1,33 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.param.SysPermission; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface PermissionMapper extends BaseMapper { + + @Select("SELECT\n" + + " p.*\n" + + "FROM\n" + + " tb_user AS u\n" + + " LEFT JOIN tb_user_role AS ur\n" + + " ON u.id = ur.user_id\n" + + " LEFT JOIN tb_role AS r\n" + + " ON r.id = ur.role_id\n" + + " LEFT JOIN tb_role_permission AS rp\n" + + " ON r.id = rp.role_id\n" + + " LEFT JOIN tb_permission AS p\n" + + " ON p.id = rp.permission_id\n" + + "WHERE u.id = #{userId}") + List selectByUserId(Long userId); +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/UserMapper.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..3fdb1d655417a58d940d14d4ad49e4d938e45754 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/mapper/UserMapper.java @@ -0,0 +1,19 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.param.SysUser; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface UserMapper extends BaseMapper { + + @Select("select * from tb_user where username=#{username}") + SysUser getByUsername(String username); +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/package-info.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a0dfc6590c9787b7a0393b8bfb926313477b0fc3 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/package-info.java @@ -0,0 +1,35 @@ +/** + * @project: itools-backend + * @description: + * 1、WebAsyncManagerIntegrationFilter 将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成。 + * 2、SecurityContextPersistenceFilter 在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除 例如在Session中维护一个用户的安全信息就是这个过滤器处理的。 + * 3、HeaderWriterFilter 用于将头信息加入响应中 + * 4、CsrfFilter 用于处理跨站请求伪造 + * 5、LogoutFilter 用于处理退出登录 + * 6、UsernamePasswordAuthenticationFilter 用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。 从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。 + * 7、DefaultLoginPageGeneratingFilter 如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。 + * 8、BasicAuthenticationFilter 处理请求头信息,DigestAuthenticationFilter + * 9、RequestCacheAwareFilter 用来处理请求的缓存 + * 10、SecurityContextHolderAwareRequestFilter + * 11、AnonymousAuthenticationFilter + * 12、SessionManagementFilter + * 13、ExceptionTranslationFilter 处理 AccessDeniedException 和 AuthenticationException 异常 + * 14、FilterSecurityInterceptor AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor + * + * 接口 + * /oauth/authorize:授权端点 + * /oauth/token:令牌端点 + * /oauth/confirm_access:用户确认授权提交端点 + * /oauth/error:授权服务错误信息端点 + * /oauth/check_token:用于资源服务访问的令牌解析端点 + * /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话 + * + * 授权模式 + * 授权码模式(authorization code) + * 简化模式(implicit) + * 密码模式(resource owner password credentials) + * 客户端模式(client credentials) + * @author: XUCHANG + * @create: 2021-03-29 17:30 + */ +package com.itools.core; \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/PermissionService.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..b39fba3c962ebcab3466be04b580d74f11255dfa --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/PermissionService.java @@ -0,0 +1,16 @@ +package com.itools.core.service; + +import com.itools.core.param.SysPermission; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +public interface PermissionService { + + List selectByUserId(Long userId); +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/UserService.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..9b4cdb2b9b8e598d85db88d474cf8bdc5d5934f8 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/UserService.java @@ -0,0 +1,15 @@ +package com.itools.core.service; + +import com.itools.core.param.SysUser; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +public interface UserService extends UserDetailsService { + + SysUser getByUsername(String username); +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/PermissionServiceImpl.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/PermissionServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..faa340b7b0df9b57c39ff30f842118b267fdad1a --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/PermissionServiceImpl.java @@ -0,0 +1,28 @@ +package com.itools.core.service.impl; + +import com.itools.core.mapper.PermissionMapper; +import com.itools.core.param.SysPermission; +import com.itools.core.service.PermissionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:58 + */ +@Service +public class PermissionServiceImpl implements PermissionService { + + @Autowired + private PermissionMapper permissionMapper; + + @Override + public List selectByUserId(Long userId) { + + return permissionMapper.selectByUserId(userId); + } +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8943eb2557e4202f1e76eadcc7bb2939e3deae93 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java @@ -0,0 +1,56 @@ +package com.itools.core.service.impl; + +import com.itools.core.mapper.PermissionMapper; +import com.itools.core.mapper.UserMapper; +import com.itools.core.param.SysPermission; +import com.itools.core.param.SysUser; +import com.itools.core.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 14:00 + */ +@Service("userDetailsService") +public class UserServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + @Autowired + private PermissionMapper permissionMapper; + + @Override + public SysUser getByUsername(String username) { + return userMapper.getByUsername(username); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = getByUsername(username); + List authorities = new ArrayList<>(); + if (user==null){ + throw new UsernameNotFoundException("用户名不存在"); + } + List permissions = permissionMapper.selectByUserId(user.getId()); + + permissions.forEach(permission -> { + if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){ + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getUrl()); + authorities.add(grantedAuthority); + } + }); + return new User(user.getUsername(),user.getPassword(),authorities); + } +} diff --git a/itools-oms/itools-oms-server/src/main/resources/application-dev.yml b/itools-oms/itools-oms-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..f7ecb63eff2fbe49a81923d1b7adfe52c0f4a395 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/resources/application-dev.yml @@ -0,0 +1,48 @@ +#组件中的全局异常处理的code +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple +#脚本自动初始化执行 +initDB: + #启用开关 + is-use: false + # ddl脚本,可以支持多个,用,分割,文件建议最多5个 + ddl: init/ddl.sql + # dml脚本,可以支持多个,用,分割,文件建议最多5个 + dml: init/dml.sql + # function脚本,可以支持多个,用,分割,文件建议最多5个 + function: init/function.sql + # 分支环境 + env: dev + # 版本 + version: 1.0 + # 脚本设置的分隔符 + delimiter: ; + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +#security: +# oauth2: +# client: +# client-id: product-server +# client-secret: 123456 +# user-authorization-uri: http://127.0.0.1:8888/oauth/authorize +# access-token-uri: http://127.0.0.1:8888/oauth/token +# resource: +# jwt: +# key-uri: http://127.0.0.1:8888/oauth/token_key \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/resources/application.yml b/itools-oms/itools-oms-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..54620a6b6eec4245be937d2c43687c02b89844cf --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 8888 +spring: + application: + name: itools-oms-server +# profiles: +# active: dev + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/oauth2-sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + diff --git a/itools-oms/itools-oms-server/src/main/resources/bootstrap.yml b/itools-oms/itools-oms-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc10673b57d21e4c29c5bea9cbc45b323f6375ce --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/resources/bootstrap.yml @@ -0,0 +1,17 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev diff --git a/itools-oms/pom.xml b/itools-oms/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..020d67b8f3a526416a5ba02b9e1572104cf22648 --- /dev/null +++ b/itools-oms/pom.xml @@ -0,0 +1,99 @@ + + + + + itools-backend + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + pom + itools-oms + + + itools-oms-common + itools-oms-model + itools-oms-core + itools-oms-server + itools-oms-jwt-sample-example + + + + 3.3.2 + 1.1.21 + 2.1.7.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + Greenwich.SR3 + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2.1.1.RELEASE + pom + import + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + + + + com.itools.core + itools-core + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + + + + + mysql + mysql-connector-java + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + \ No newline at end of file diff --git "a/itools-sso/doc/XXL-SSO\345\256\230\346\226\271\346\226\207\346\241\243.md" "b/itools-sso/doc/XXL-SSO\345\256\230\346\226\271\346\226\207\346\241\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..506c9da10af2e9d42ee38e3dbb78bf95fd6704f6 --- /dev/null +++ "b/itools-sso/doc/XXL-SSO\345\256\230\346\226\271\346\226\207\346\241\243.md" @@ -0,0 +1,400 @@ +## 《分布式单点登录框架XXL-SSO》 + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-sso/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-sso/) +[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-sso.svg)](https://github.com/xuxueli/xxl-sso/releases) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) +[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat-square)](http://www.xuxueli.com/page/donate.html) + +## 一、简介 + +### 1.1 概述 +XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 +拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性;。现已开放源代码,开箱即用。 + +### 1.2 特性 +- 1、简洁:API直观简洁,可快速上手; +- 2、轻量级:环境依赖小,部署与接入成本较低; +- 3、单点登录:只需要登录一次就可以访问所有相互信任的应用系统。 +- 4、分布式:接入SSO认证中心的应用,支持分布式部署; +- 5、HA:Server端与Client端,均支持集群部署,提高系统可用性; +- 6、跨域:支持跨域应用接入SSO认证中心; +- 7、Cookie+Token均支持:支持基于Cookie和基于Token两种接入方式,并均提供Sample项目; +- 8、Web+APP均支持:支持Web和APP接入; +- 9、实时性:系统登陆、注销状态,全部Server与Client端实时共享; +- 10、CS结构:基于CS结构,包括Server"认证中心"与Client"受保护应用"; +- 11、记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期; +- 12、路径排除:支持自定义多个排除路径,支持Ant表达式。用于排除SSO客户端不需要过滤的路径; + +### 1.3 下载 + +#### 文档地址 + +- [中文文档](http://www.xuxueli.com/xxl-sso/) + +#### 源码仓库地址 + +源码仓库地址 | Release Download +--- | --- +[https://github.com/xuxueli/xxl-sso](https://github.com/xuxueli/xxl-sso) | [Download](https://github.com/xuxueli/xxl-sso/releases) +[https://gitee.com/xuxueli0323/xxl-sso](https://gitee.com/xuxueli0323/xxl-sso) | [Download](https://gitee.com/xuxueli0323/xxl-sso/releases) + + +#### 技术交流 +- [社区交流](http://www.xuxueli.com/page/community.html) + +### 1.4 环境 +- JDK:1.7+ +- Redis:4.0+ +- Mysql:5.6+ + + +## 二、快速入门(基于Cookie) + +> 基于Cookie,相关概念可参考 "章节 4.6"; + + +### 2.1:系统数据库初始化 + + +### 2.2:源码编译 + +``` +- xxl-sso-server:中央认证服务,支持集群; +- xxl-sso-core:Client端依赖; +- xxl-sso-samples:单点登陆Client端接入示例项目; + - xxl-sso-web-sample-springboot:基于Cookie接入方式,供用户浏览器访问,springboot版本 + - xxl-sso-token-sample-springboot:基于Token接入方式,常用于无法使用Cookie的场景使用,如APP、Cookie被禁用等,springboot版本 +``` + +### 2.3 部署 "认证中心(SSO Server)" + +``` +项目名:xxl-sso-server +``` + +#### 配置说明 + +配置文件位置:application.properties +``` +…… + +// redis 地址: 如 "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";多地址逗号分隔 +xxl.sso.redis.address=redis://127.0.0.1:6379 + +// 登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期; +xxl.sso.redis.expire.minite=1440 + +``` + +### 2.4 部署 "单点登陆Client端接入示例项目" + +``` +项目名:xxl-sso-web-sample-springboot +``` + +#### maven依赖 + +``` + + com.xuxueli + xxl-sso-core + ${最新稳定版} + +``` + +#### 配置 XxlSsoFilter + +参考代码:XxlSsoConfig +``` +@Bean +public FilterRegistrationBean xxlSsoFilterRegistration() { + + // xxl-sso, redis init + JedisUtil.init(xxlSsoRedisAddress); + + // xxl-sso, filter init + FilterRegistrationBean registration = new FilterRegistrationBean(); + + registration.setName("XxlSsoWebFilter"); + registration.setOrder(1); + registration.addUrlPatterns("/*"); + registration.setFilter(new XxlSsoWebFilter()); + registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer); + registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath); + + return registration; +} +``` + + +#### 配置说明 + +配置文件位置:application.properties +``` +…… + +### xxl-sso (CLient端SSO配置) + +##### SSO Server认证中心地址(推荐以域名方式配置认证中心,本机可参考章节"2.5"修改host文件配置域名指向) +xxl.sso.server=http://xxlssoserver.com:8080/xxl-sso-server + +##### 注销登陆path,值为Client端应用的相对路径 +xxl.sso.logout.path=/logout + +##### 路径排除Path,允许设置多个,且支持Ant表达式。用于排除SSO客户端不需要过滤的路径 +xxl-sso.excluded.paths= + +### redis // redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated +xxl.sso.redis.address=redis://xxl-sso:password@127.0.0.1:6379/0 +``` + +### 2.5 验证 + +- 环境准备:启动Redis、初始化Mysql表数据; + +- 修改Host文件:域名方式访问认证中心,模拟跨域与线上真实环境 +``` +### 在host文件中添加以下内容0 +127.0.0.1 xxlssoserver.com +127.0.0.1 xxlssoclient1.com +127.0.0.1 xxlssoclient2.com +``` + +- 分别运行 "xxl-sso-server" 与 "xxl-sso-web-sample-springboot" + + + 1、SSO认证中心地址: + http://xxlssoserver.com:8080/xxl-sso-server + + 2、Client01应用地址: + http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/ + + 3、Client02应用地址: + http://xxlssoclient2.com:8081/xxl-sso-web-sample-springboot/ + + +- SSO登录/注销流程验证 + + + 正常情况下,登录流程如下: + 1、访问 "Client01应用地址" ,将会自动 redirect 到 "SSO认证中心地址" 登录界面; + 2、成功登录后,将会自动 redirect 返回到 "Client01应用地址",并切换为已登录状态; + 3、此时,访问 "Client02应用地址",不需登陆将会自动切换为已登录状态; + + 正常情况下,注销流程如下: + 1、访问 "Client01应用地址" 配置的 "注销登陆path",将会自动 redirect 到 "SSO认证中心地址" 并自动注销登陆状态; + 2、此时,访问 "Client02应用地址",也将会自动注销登陆状态; + + + +## 三、快速入门(基于Token) + +> 基于Token,相关概念可参考 "章节 4.7";(在一些无法使用Cookie的场景下,可使用该方式,否则可以忽略本章节) + +### 3.1 "认证中心(SSO Server)" 搭建 +> 可参考 "章节二" 搭建; + +"认证中心" 搭建成功后,默认为Token方式登陆提供API接口如下: + +- 1、登陆接口:/app/login + - 参数:POST参数 + - username:账号 + - password:账号 + - 响应:JSON格式 + - code:200 表示成功、其他失败; + - msg:错误提示 + - data: 登陆用户的 sso sessionid + +- 2、注销接口:/app/logout + - 参数:POST参数 + - sessionId:登陆用户的 sso sessionid + - 响应:JSON格式 + - code:200 表示成功、其他失败; + - msg:错误提示 + +- 3、登陆状态校验接口:/app/logincheck + - 参数:POST参数 + - sessionId:登陆用户的 sso sessionid + - 响应:JSON格式 + - code:200 表示成功、其他失败; + - msg:错误提示 + - data:登陆用户信息 + - userid:用户ID + - username:用户名 + +### 2.2 部署 "单点登陆Client端接入示例项目" (Token方式) + +``` +项目名:xxl-sso-token-sample-springboot +``` + +> 可参考 "章节 2.4" 部署 "单点登陆Client端接入示例项目",唯一不同点是:将web应用的 "XxlSsoFilter" 更换为app应用 "XxlSsoTokenFilter"; + +### 2.3 验证 (模拟请求 Token 方式接入SSO的接口) + +- 环境准备:启动Redis、初始化Mysql表数据; + +- 修改Host文件:域名方式访问认证中心,模拟跨域与线上真实环境 +``` +### 在host文件中添加以下内容0 +127.0.0.1 xxlssoserver.com +127.0.0.1 xxlssoclient1.com +127.0.0.1 xxlssoclient2.com +``` + +- 分别运行 "xxl-sso-server" 与 "xxl-sso-token-sample-springboot" + + + 1、SSO认证中心地址: + http://xxlssoserver.com:8080/xxl-sso-server + + 2、Client01应用地址: + http://xxlssoclient1.com:8082/xxl-sso-token-sample-springboot/ + + 3、Client02应用地址: + http://xxlssoclient2.com:8082/xxl-sso-token-sample-springboot/ + + +- SSO登录/注销流程验证 +> 可参考测试用例 :com.xxl.app.sample.test.TokenClientTest + + + 正常情况下,登录流程如下: + 1、获取用户输入的账号密码后,请求SSO Server的登录接口,获取用户 sso sessionid ;(参考代码:TokenClientTest.loginTest) + 2、登陆成功后,获取到 sso sessionid ,需要主动存储,后续请求时需要设置在 Header参数 中; + 3、此时,使用 sso sessionid 访问受保护的 "Client01应用" 和 "Client02应用" 提供的接口,接口均正常返回;(参考代码:TokenClientTest.clientApiRequestTest) + + 正常情况下,注销流程如下: + 1、请求SSO Server的注销接口,注销登陆凭证 sso sessionid ;(参考代码:TokenClientTest.logoutTest) + 2、注销成功后,sso sessionid 将会全局失效; + 3、此时,使用 sso sessionid 访问受保护的 "Client01应用" 和 "Client02应用" 提供的接口,接口请求将会被拦截,提示未登录并返回状态码 501 ;(参考代码:TokenClientTest.clientApiRequestTest) + + +## 四、总体设计 + +### 4.1 架构图 + +![输入图片说明](https://raw.githubusercontent.com/xuxueli/xxl-sso/master/doc/images/img_01.png "在这里输入图片标题") + +### 4.2 功能定位 + +XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 + +借助 XXL-SSO,可以快速实现分布式系统单点登录。 + +### 4.3 核心概念 + +概念 | 说明 +--- | --- +SSO Server | 中央认证服务,支持集群; +SSO Client | 接入SSO认证中心的Client应用; +SSO SessionId | 登录用户会话ID,SSO 登录成功为用户自动分配; +SSO User | 登录用户信息,与 SSO SessionId 相对应; + +### 4.4 登录流程剖析 + +- 用户于Client端应用访问受限资源时,将会自动 redirect 到 SSO Server 进入统一登录界面。 +- 用户登录成功之后将会为用户分配 SSO SessionId 并 redirect 返回来源Client端应用,同时附带分配的 SSO SessionId。 +- 在Client端的SSO Filter里验证 SSO SessionId 无误,将 SSO SessionId 写入到用户浏览器Client端域名下 cookie 中。 +- SSO Filter验证 SSO SessionId 通过,受限资源请求放行; + +### 4.5 注销流程剖析 + +- 用户与Client端应用请求注销Path时,将会 redirect 到 SSO Server 自动销毁全局 SSO SessionId,实现全局销毁; +- 然后,访问接入SSO保护的任意Client端应用时,SSO Filter 均会拦截请求并 redirect 到 SSO Server 的统一登录界面。 + +### 4.6 基于Cookie,相关感念 + +- 登陆凭证存储:登陆成功后,用户登陆凭证被自动存储在浏览器Cookie中; +- Client端校验登陆状态:通过校验请求Cookie中的是否包含用户登录凭证判断; +- 系统角色模型: + - SSO Server:认证中心,提供用户登陆、注销以及登陆状态校验等功能。 + - Client应用:受SSO保护的Client端Web应用,为用户浏览器访问提供服务; + - 用户:发起请求的用户,使用浏览器访问。 + + +### 4.7 基于Token,相关感念 + +- 登陆凭证存储:登陆成功后,获取到登录凭证(xxl_sso_sessionid=xxx),需要主动存储,如存储在 localStorage、Sqlite 中; +- Client端校验登陆状态:通过校验请求 Header参数 中的是否包含用户登录凭证(xxl_sso_sessionid=xxx)判断;因此,发送请求时需要在 Header参数 中设置登陆凭证; +- 系统角色模型: + - SSO Server:认证中心,提供用户登陆、注销以及登陆状态校验等功能。 + - Client应用:受SSO保护的Client端Web应用,为用户请求提供接口服务; + - 用户:发起请求的用户,如使用Android、IOS、桌面客户端等请求访问。 + +### 4.8 未登录状态请求处理 + +基于Cookie,未登录状态请求: +- 页面请求:redirect 到SSO Server登录界面; +- JSON请求:返回未登录的JSON格式响应数据 + - 数据格式: + - code:501 错误码 + - msg:sso not login. + +基于Token,未登录状态请求: +- 返回未登录的JSON格式响应数据 + - 数据格式: + - code:501 错误码 + - msg:sso not login. + + +### 4.9 登录态自动延期 +支持自定义登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期; + +### 4.10 记住密码 +未记住密码时,关闭浏览器则登录态失效;记住密码时,登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期; + +### 4.11 路径排除 +自定义路径排除Path,允许设置多个,且支持Ant表达式。用于排除SSO客户端不需要过滤的路径 + + +## 五、版本更新日志 + +### 5.1 版本 v0.1.0,新特性[2018-04-04] +- 1、简洁:API直观简洁,可快速上手; +- 2、轻量级:环境依赖小,部署与接入成本较低; +- 3、单点登录:只需要登录一次就可以访问所有相互信任的应用系统。 +- 4、分布式:接入SSO认证中心的应用,支持分布式部署; +- 5、HA:Server端与Client端,均支持集群部署,提高系统可用性; +- 6、实时性:系统登陆、注销状态,全部Server与Client端实时共享; +- 7、CS结构:基于CS结构,包括Server"认证中心"与Client"受保护应用"; +- 8、跨域:支持跨域应用接入SSO认证中心; + +### 5.2 版本 v1.1.0 Release Notes [2018-11-06] +- 1、Redis配置方式增强,支持自定义DB、密码、IP、PORT等等;; +- 2、Token接入方式;除了常规Cookie方式外,新增Token接入方式,并提供Sample项目; +- 3、登录态自动延期:支持自定义登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期; +- 4、"记住密码" 功能优化:未记住密码时,关闭浏览器则登录态失效;记住密码时,登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期; +- 5、sessionId数据结构优化,进一步提升暴露破解难度; +- 6、认证数据存储结构调整,避免登陆信息存储冗余; +- 7、认证中心用户登录校验改为Mock数据方式,取消对DB强依赖,降低部署难度; +- 8、Client端依赖Core包,slf4j依赖优化,移除log4j强依赖; +- 9、Ajax请求未登录处理逻辑优化,返回JSON格式提示数据; +- 10、项目结构梳理,清理冗余依赖,升级多项依赖版本至较近版本; +- 11、路径排除:新增自定义属性 "excludedPaths",允许设置多个,且支持Ant表达式。用于排除SSO客户端不需要过滤的路径 + +### TODO LIST +- 1、认证中心与接入端交互数据加密,增强安全性;redirect_url必须和临时AccessToken配合才会生效,AccessToken有效期60s; +- 2、SSO SessionId 与IP绑定,增强用户增强安全性; +- 3、支持认证分组,分组内共享登陆状态,分组之间登录态隔离;待考虑; +- 4、客户端新增属性 "xxl.sso.server",用于构建跳转连接,防止跳转第三方导致登陆漏洞; + + + +## 六、其他 + +### 6.1 项目贡献 +欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-sso/issues/) 讨论新特性或者变更。 + +### 6.2 用户接入登记 +更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-sso/issues/1 ) 登记,登记仅仅为了产品推广。 + +### 6.3 开源协议和版权 +产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。 + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +--- +### 捐赠 +无论金额多少都足够表达您这份心意,非常感谢 :) [前往捐赠](http://www.xuxueli.com/page/donate.html ) diff --git "a/itools-sso/doc/XXL-SSO\346\236\266\346\236\204\345\233\276.pptx" "b/itools-sso/doc/XXL-SSO\346\236\266\346\236\204\345\233\276.pptx" new file mode 100644 index 0000000000000000000000000000000000000000..3331f736e931ae3ebec63ba03ebd61deba2548bc Binary files /dev/null and "b/itools-sso/doc/XXL-SSO\346\236\266\346\236\204\345\233\276.pptx" differ diff --git a/itools-sso/doc/images/img_01.png b/itools-sso/doc/images/img_01.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a37085d83e83ed3d87f0167cff77c996056f61 Binary files /dev/null and b/itools-sso/doc/images/img_01.png differ diff --git a/itools-sso/itools-sso-core/pom.xml b/itools-sso/itools-sso-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f62be5179eaf4b5984f6766b7b4a1b9bb0992383 --- /dev/null +++ b/itools-sso/itools-sso-core/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + itools-sso + com.itools.core + 1.0-SNAPSHOT + + itools-sso-core + jar + + + + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + + redis.clients + jedis + ${jedis.version} + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/conf/Conf.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/conf/Conf.java new file mode 100644 index 0000000000000000000000000000000000000000..45889a1eaa9a88615ce343c5a1a9d76aa4d2faa1 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/conf/Conf.java @@ -0,0 +1,61 @@ +package com.itools.sso.core.conf; + +import com.itools.sso.core.entity.ReturnT; + +/** + * conf + * + * @author xuxueli 2018-04-02 19:18:04 + */ +public class Conf { + + /** + * sso sessionid, between browser and sso-server (web + token client) + */ + public static final String SSO_SESSIONID = "xxl_sso_sessionid"; + + + /** + * redirect url (web client) + */ + public static final String REDIRECT_URL = "redirect_url"; + + /** + * sso user, request attribute (web client) + */ + public static final String SSO_USER = "xxl_sso_user"; + + + /** + * sso server address (web + token client) + */ + public static final String SSO_SERVER = "sso_server"; + + /** + * login url, server relative path (web client) + */ + public static final String SSO_LOGIN = "/login"; + /** + * logout url, server relative path (web client) + */ + public static final String SSO_LOGOUT = "/logout"; + + + /** + * logout path, client relatice path + */ + public static final String SSO_LOGOUT_PATH = "SSO_LOGOUT_PATH"; + + /** + * excluded paths, client relatice path, include path can be set by "filter-mapping" + */ + public static final String SSO_EXCLUDED_PATHS = "SSO_EXCLUDED_PATHS"; + + + /** + * login fail result + */ + public static final ReturnT SSO_LOGIN_FAIL_RESULT = new ReturnT(501, "sso not login."); + + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/entity/ReturnT.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/entity/ReturnT.java new file mode 100644 index 0000000000000000000000000000000000000000..1c8031e5bd3b444aa0d6ab3d1953c37750fa5496 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/entity/ReturnT.java @@ -0,0 +1,51 @@ +package com.itools.sso.core.entity; + +import java.io.Serializable; + +/** + * common return + * + * @author xuxueli 2015-12-4 16:32:31 + * @param + */ +public class ReturnT implements Serializable { + public static final long serialVersionUID = 42L; + + public static final int SUCCESS_CODE = 200; + public static final int FAIL_CODE = 500; + public static final ReturnT SUCCESS = new ReturnT(null); + public static final ReturnT FAIL = new ReturnT(FAIL_CODE, null); + + private int code; + private String msg; + private T data; + + public ReturnT(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ReturnT(T data) { + this.code = SUCCESS_CODE; + this.data = data; + } + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public String getMsg() { + return msg; + } + public void setMsg(String msg) { + this.msg = msg; + } + public T getData() { + return data; + } + public void setData(T data) { + this.data = data; + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/exception/XxlSsoException.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/exception/XxlSsoException.java new file mode 100644 index 0000000000000000000000000000000000000000..fe201a01269aac079dfceec8d0be13ae531e2912 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/exception/XxlSsoException.java @@ -0,0 +1,22 @@ +package com.itools.sso.core.exception; + +/** + * @author xuxueli 2018-04-02 21:01:41 + */ +public class XxlSsoException extends RuntimeException { + + private static final long serialVersionUID = 42L; + + public XxlSsoException(String msg) { + super(msg); + } + + public XxlSsoException(String msg, Throwable cause) { + super(msg, cause); + } + + public XxlSsoException(Throwable cause) { + super(cause); + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoTokenFilter.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..7bedb8cac254678e4a78df7f5e6403775b9cf9e0 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoTokenFilter.java @@ -0,0 +1,101 @@ +package com.itools.sso.core.filter; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.entity.ReturnT; +import com.itools.sso.core.login.SsoTokenLoginHelper; +import com.itools.sso.core.path.impl.AntPathMatcher; +import com.itools.sso.core.user.XxlSsoUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * app sso filter + * + * @author xuxueli 2018-04-08 21:30:54 + */ +public class SsoTokenFilter extends HttpServlet implements Filter { + private static Logger logger = LoggerFactory.getLogger(SsoTokenFilter.class); + + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + private String ssoServer; + private String logoutPath; + private String excludedPaths; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + ssoServer = filterConfig.getInitParameter(Conf.SSO_SERVER); + logoutPath = filterConfig.getInitParameter(Conf.SSO_LOGOUT_PATH); + excludedPaths = filterConfig.getInitParameter(Conf.SSO_EXCLUDED_PATHS); + + logger.info("XxlSsoTokenFilter init."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + // make url + String servletPath = req.getServletPath(); + + // excluded path check + if (excludedPaths!=null && excludedPaths.trim().length()>0) { + for (String excludedPath:excludedPaths.split(",")) { + String uriPattern = excludedPath.trim(); + + // 支持ANT表达式 + if (antPathMatcher.match(uriPattern, servletPath)) { + // excluded path, allow + chain.doFilter(request, response); + return; + } + + } + } + + // logout filter + if (logoutPath!=null + && logoutPath.trim().length()>0 + && logoutPath.equals(servletPath)) { + + // logout + SsoTokenLoginHelper.logout(req); + + // response + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().println("{\"code\":"+ ReturnT.SUCCESS_CODE+", \"msg\":\"\"}"); + + return; + } + + // login filter + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(req); + if (xxlUser == null) { + + // response + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}"); + return; + } + + // ser sso user + request.setAttribute(Conf.SSO_USER, xxlUser); + + + // already login, allow + chain.doFilter(request, response); + return; + } + + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoWebFilter.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoWebFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..a0e13b0c299d1352eb6f4915b4eb98aaa819139b --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/filter/SsoWebFilter.java @@ -0,0 +1,116 @@ +package com.itools.sso.core.filter; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.login.SsoWebLoginHelper; +import com.itools.sso.core.path.impl.AntPathMatcher; +import com.itools.sso.core.user.XxlSsoUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * web sso filter + * + * @author xuxueli 2018-04-03 + */ +public class SsoWebFilter extends HttpServlet implements Filter { + private static Logger logger = LoggerFactory.getLogger(SsoWebFilter.class); + + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + private String ssoServer; + private String logoutPath; + private String excludedPaths; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + ssoServer = filterConfig.getInitParameter(Conf.SSO_SERVER); + logoutPath = filterConfig.getInitParameter(Conf.SSO_LOGOUT_PATH); + excludedPaths = filterConfig.getInitParameter(Conf.SSO_EXCLUDED_PATHS); + + logger.info("XxlSsoWebFilter init."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + // make url + String servletPath = req.getServletPath(); + + // excluded path check + if (excludedPaths!=null && excludedPaths.trim().length()>0) { + for (String excludedPath:excludedPaths.split(",")) { + String uriPattern = excludedPath.trim(); + + // 支持ANT表达式 + if (antPathMatcher.match(uriPattern, servletPath)) { + // excluded path, allow + chain.doFilter(request, response); + return; + } + + } + } + + // logout path check + if (logoutPath!=null + && logoutPath.trim().length()>0 + && logoutPath.equals(servletPath)) { + + // remove cookie + SsoWebLoginHelper.removeSessionIdByCookie(req, res); + + // redirect logout + String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT); + res.sendRedirect(logoutPageUrl); + + return; + } + + // valid login user, cookie + redirect + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res); + + // valid login fail + if (xxlUser == null) { + + String header = req.getHeader("content-type"); + boolean isJson= header!=null && header.contains("json"); + if (isJson) { + + // json msg + res.setContentType("application/json;charset=utf-8"); + res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}"); + return; + } else { + + // total link + String link = req.getRequestURL().toString(); + + // redirect logout + String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN) + + "?" + Conf.REDIRECT_URL + "=" + link; + + res.sendRedirect(loginPageUrl); + return; + } + + } + + // ser sso user + request.setAttribute(Conf.SSO_USER, xxlUser); + + + // already login, allow + chain.doFilter(request, response); + return; + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoTokenLoginHelper.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoTokenLoginHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..e320072d9162f6fe2defc46090eba806756d70b8 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoTokenLoginHelper.java @@ -0,0 +1,99 @@ +package com.itools.sso.core.login; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.store.SsoLoginStore; +import com.itools.sso.core.user.XxlSsoUser; +import com.itools.sso.core.store.SsoSessionIdHelper; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author xuxueli 2018-11-15 15:54:40 + */ +public class SsoTokenLoginHelper { + + /** + * client login + * + * @param sessionId + * @param xxlUser + */ + public static void login(String sessionId, XxlSsoUser xxlUser) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId); + } + + SsoLoginStore.put(storeKey, xxlUser); + } + + /** + * client logout + * + * @param sessionId + */ + public static void logout(String sessionId) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + return; + } + + SsoLoginStore.remove(storeKey); + } + /** + * client logout + * + * @param request + */ + public static void logout(HttpServletRequest request) { + String headerSessionId = request.getHeader(Conf.SSO_SESSIONID); + logout(headerSessionId); + } + + + /** + * login check + * + * @param sessionId + * @return + */ + public static XxlSsoUser loginCheck(String sessionId){ + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + return null; + } + + XxlSsoUser xxlUser = SsoLoginStore.get(storeKey); + if (xxlUser != null) { + String version = SsoSessionIdHelper.parseVersion(sessionId); + if (xxlUser.getVersion().equals(version)) { + + // After the expiration time has passed half, Auto refresh + if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinite()/2) { + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + SsoLoginStore.put(storeKey, xxlUser); + } + + return xxlUser; + } + } + return null; + } + + + /** + * login check + * + * @param request + * @return + */ + public static XxlSsoUser loginCheck(HttpServletRequest request){ + String headerSessionId = request.getHeader(Conf.SSO_SESSIONID); + return loginCheck(headerSessionId); + } + + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoWebLoginHelper.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoWebLoginHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..8021215a14f6ef51117303cc7f9176c496ebd756 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/login/SsoWebLoginHelper.java @@ -0,0 +1,119 @@ +package com.itools.sso.core.login; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.store.SsoLoginStore; +import com.itools.sso.core.user.XxlSsoUser; +import com.itools.sso.core.util.CookieUtil; +import com.itools.sso.core.store.SsoSessionIdHelper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author xuxueli 2018-04-03 + */ +public class SsoWebLoginHelper { + + /** + * client login + * + * @param response + * @param sessionId + * @param ifRemember true: cookie not expire, false: expire when browser close (server cookie) + * @param xxlUser + */ + public static void login(HttpServletResponse response, + String sessionId, + XxlSsoUser xxlUser, + boolean ifRemember) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId); + } + + SsoLoginStore.put(storeKey, xxlUser); + CookieUtil.set(response, Conf.SSO_SESSIONID, sessionId, ifRemember); + } + + /** + * client logout + * + * @param request + * @param response + */ + public static void logout(HttpServletRequest request, + HttpServletResponse response) { + + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + if (cookieSessionId==null) { + return; + } + + String storeKey = SsoSessionIdHelper.parseStoreKey(cookieSessionId); + if (storeKey != null) { + SsoLoginStore.remove(storeKey); + } + + CookieUtil.remove(request, response, Conf.SSO_SESSIONID); + } + + + + /** + * login check + * + * @param request + * @param response + * @return + */ + public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){ + + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + + // cookie user + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId); + if (xxlUser != null) { + return xxlUser; + } + + // redirect user + + // remove old cookie + SsoWebLoginHelper.removeSessionIdByCookie(request, response); + + // set new cookie + String paramSessionId = request.getParameter(Conf.SSO_SESSIONID); + xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId); + if (xxlUser != null) { + CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false); // expire when browser close (client cookie) + return xxlUser; + } + + return null; + } + + + /** + * client logout, cookie only + * + * @param request + * @param response + */ + public static void removeSessionIdByCookie(HttpServletRequest request, HttpServletResponse response) { + CookieUtil.remove(request, response, Conf.SSO_SESSIONID); + } + + /** + * get sessionid by cookie + * + * @param request + * @return + */ + public static String getSessionIdByCookie(HttpServletRequest request) { + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + return cookieSessionId; + } + + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/PathMatcher.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/PathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..76ceadde218b9a8cbd8c615db392d8d96991895e --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/PathMatcher.java @@ -0,0 +1,106 @@ +package com.itools.sso.core.path; + +import com.itools.sso.core.path.impl.AntPathMatcher; + +import java.util.Comparator; +import java.util.Map; + +/** + * Strategy interface for {@code String}-based path matching. + * + *

The default implementation is {@link AntPathMatcher}, supporting the Ant-style pattern syntax. (borrowed from spring) + */ +public interface PathMatcher { + + /** + * Does the given {@code path} represent a pattern that can be matched by an implementation of this interface? + * + *

If the return value is {@code false}, then the {@link #match} + * method does not have to be used because direct equality comparisons + * on the static path Strings will lead to the same result. + * + * @param path the path String to check + * @return {@code true} if the given {@code path} represents a pattern + */ + boolean isPattern(String path); + + /** + * Match the given {@code path} against the given {@code pattern}, according to this PathMatcher's matching strategy. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean match(String pattern, String path); + + /** + * Match the given {@code path} against the corresponding part of the given {@code pattern}, according to this PathMatcher's matching strategy. + * + *

Determines whether the pattern at least matches as far as the given base path goes, assuming that a full path may then match as well. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean matchStart(String pattern, String path); + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + * + *

This method is supposed to find out which part of the path is matched dynamically through an actual pattern, + * that is, it strips off a statically defined leading path from the given full path, + * returning only the actually pattern-matched part of the path. + * + *

For example: For "myroot/*.html" as pattern and "myroot/myfile.html" as full path, this method should return "myfile.html". + * The detailed determination rules are specified to this PathMatcher's matching strategy. + * + *

A simple implementation may return the given full path as-is in case of an actual pattern, + * and the empty String in case of the pattern not containing any dynamic parts (i.e. the {@code pattern} parameter being + * a static path that wouldn't qualify as an actual {@link #isPattern pattern}). + * A sophisticated implementation will differentiate between the static parts and the dynamic parts of the given path pattern. + * + * @param pattern the path pattern + * @param path the full path to introspect + * @return the pattern-mapped part of the given {@code path} + * (never {@code null}) + */ + String extractPathWithinPattern(String pattern, String path); + + /** + * Given a pattern and a full path, extract the URI template variables. URI template variables are expressed through curly brackets ('{' and '}'). + * + *

For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will return a map containing "hotel"->"1". + * + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as values + */ + Map extractUriTemplateVariables(String pattern, String path); + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of explicitness for that path. + * + *

The full algorithm used depends on the underlying implementation, but generally, + * the returned {@code Comparator} will {@linkplain java.util.Collections#sort(java.util.List, java.util.Comparator) sort} + * a list so that more specific patterns come before generic patterns. + * + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + Comparator getPatternComparator(String path); + + /** + * Combines two patterns into a new pattern that is returned. + * + *

The full algorithm used for combining the two pattern depends on the underlying implementation. + * + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + String combine(String pattern1, String pattern2); + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/impl/AntPathMatcher.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/impl/AntPathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..88c3b45d94dd5c64ee11b4eaaace5d2d077862b0 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/path/impl/AntPathMatcher.java @@ -0,0 +1,881 @@ +package com.itools.sso.core.path.impl; + +import com.itools.sso.core.path.PathMatcher; +import com.itools.sso.core.util.StringUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@link PathMatcher} implementation for Ant-style path patterns. + * + *

Part of this mapping code has been kindly borrowed from Apache Ant. (borrowed from spring) + * + *

The mapping matches URLs using the following rules:
+ * + *

    + *
  • {@code ?} matches one character
  • + *
  • {@code *} matches zero or more characters
  • + *
  • {@code **} matches zero or more directories in a path
  • + *
  • {@code {spring:[a-z]+}} matches the regexp {@code [a-z]+} as a path variable named "spring"
  • + *
+ * + *

Examples

+ *
    + *
  • {@code com/t?st.jsp} — matches {@code com/test.jsp} but also {@code com/tast.jsp} or {@code com/txst.jsp}
  • + *
  • {@code com/*.jsp} — matches all {@code .jsp} files in the {@code com} directory
  • + *
  • com/**/test.jsp — matches all {@code test.jsp} files underneath the {@code com} path
  • + *
  • org/springframework/**/*.jsp — matches all {@code .jsp} files underneath the {@code org/springframework} path
  • + *
  • org/**/servlet/bla.jsp — matches {@code org/springframework/servlet/bla.jsp} but also + * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}
  • + *
  • {@code com/{filename:\\w+}.jsp} will match {@code com/test.jsp} and assign the value {@code test} to the {@code filename} variable
  • + *
+ * + *

Note: a pattern and a path must both be absolute or must both be relative in order for the two to match. + * Therefore it is recommended that users of this implementation to sanitize patterns + * in order to prefix them with "/" as it makes sense in the context in which they're used. + */ +public class AntPathMatcher implements PathMatcher { + + /** Default path separator: "/" */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); + + private static final char[] WILDCARD_CHARS = { '*', '?', '{' }; + + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = false; + + private volatile Boolean cachePatterns; + + private final Map tokenizedPatternCache = new ConcurrentHashMap(256); + + final Map stringMatcherCache = new ConcurrentHashMap(256); + + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + */ + public AntPathMatcher() { + this.pathSeparator = DEFAULT_PATH_SEPARATOR; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * @param pathSeparator the path separator to use, must not be {@code null}. + * @since 4.1 + */ + public AntPathMatcher(String pathSeparator) { + if (pathSeparator == null) { + throw new IllegalArgumentException("'pathSeparator' is required"); + } + + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + + /** + * Set the path separator to use for pattern parsing. + *

Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + /** + * Specify whether to perform pattern matching in a case-sensitive fashion. + *

Default is {@code true}. Switch this to {@code false} for case-insensitive matching. + * @since 4.2 + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify whether to trim tokenized paths and patterns. + *

Default is {@code false}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * @since 4.0.1 + * @see #getStringMatcher(String) + */ + public void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + + @Override + public boolean isPattern(String path) { + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + + @Override + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + @Override + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * @param pattern 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 + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + + 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], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + 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], uriTemplateVariables)) { + 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, uriTemplateVariables)) { + 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 boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + 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 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 int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + /** + * Tokenize the given path String into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

This method may be overridden to implement a custom cache strategy. + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • + *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • + *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • + *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + */ + @Override + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + StringBuilder builder = new StringBuilder(); + boolean pathStarted = false; + + for (int segment = 0; segment < patternParts.length; segment++) { + String patternPart = patternParts[segment]; + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + for (; segment < pathParts.length; segment++) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[segment]); + pathStarted = true; + } + } + } + + return builder.toString(); + } + + @Override + public Map extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Combine two patterns into a new pattern. + *

This implementation simply concatenates the two patterns, unless + * the first pattern contains a file extension match (e.g., {@code *.html}). + * In that case, the second pattern will be merged into the first. Otherwise, + * an {@code IllegalArgumentException} will be thrown. + *

Examples

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Pattern 1Pattern 2Result
{@code null}{@code null} 
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txt{@code IllegalArgumentException}
+ * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException if the two patterns cannot be combined + */ + @Override + public String combine(String pattern1, String pattern2) { + if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) { + return ""; + } + if (!StringUtils.hasText(pattern1)) { + return pattern2; + } + if (!StringUtils.hasText(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + + // /hotels/* + /booking -> /hotels/booking + // /hotels/* + booking -> /hotels/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + + // /hotels/** + /booking -> /hotels/**/booking + // /hotels/** + booking -> /hotels/**/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + // simply concatenate the two patterns + return concat(pattern1, pattern2); + } + + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.equals("")); + boolean ext2All = (ext2.equals(".*") || ext2.equals("")); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = (ext1All ? ext2 : ext1); + return file2 + ext; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + this.pathSeparator + path2; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

This{@code Comparator} will {@linkplain java.util.Collections#sort(List, Comparator) sort} + * a list so that more specific patterns (without uri templates or wild cards) come before + * generic patterns. So given a list with the following patterns: + *

    + *
  1. {@code /hotels/new}
  2. + *
  3. {@code /hotels/{hotel}}
  4. {@code /hotels/*}
  5. + *
+ * the returned comparator will sort this list so that the order will be as indicated. + *

The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + @Override + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final Pattern pattern; + + private final List variableNames = new LinkedList(); + + public AntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, 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); + this.variableNames.add(matcher.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + else { + return false; + } + } + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

In order, the most "generic" pattern is determined by the following: + *

    + *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • + *
  • if the other pattern is an actual match
  • + *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • + *
  • if it's got more "*" than the other pattern
  • + *
  • if it's got more "{foo}" than the other pattern
  • + *
  • if it's shorter than the other pattern
  • + *
+ */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + @Override + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } + else if (info1.isLeastSpecific()) { + return 1; + } + else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(path); + boolean pattern2EqualsPath = pattern2.equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } + else if (pattern1EqualsPath) { + return -1; + } + else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } + else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } + else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } + else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + public PatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } + else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } + else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } + else { + pos++; + } + } + else { + pos++; + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); + } + return this.length; + } + } + } + + + /** + * A simple cache for patterns that depend on the configured path separator. + */ + private static class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + public PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + public String getEndsOnWildCard() { + return this.endsOnWildCard; + } + + public String getEndsOnDoubleWildCard() { + return this.endsOnDoubleWildCard; + } + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoLoginStore.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoLoginStore.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2a606a0439310f0b6f5fde754dd4f7b2db151 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoLoginStore.java @@ -0,0 +1,70 @@ +package com.itools.sso.core.store; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.user.XxlSsoUser; +import com.itools.sso.core.util.JedisUtil; + +/** + * local login store + * + * @author xuxueli 2018-04-02 20:03:11 + */ +public class SsoLoginStore { + /** + * 1440 minite, 24 hour + */ + private static int redisExpireMinite = 1440; + public static void setRedisExpireMinite(int redisExpireMinite) { + if (redisExpireMinite < 30) { + redisExpireMinite = 30; + } + SsoLoginStore.redisExpireMinite = redisExpireMinite; + } + public static int getRedisExpireMinite() { + return redisExpireMinite; + } + + /** + * get + * + * @param storeKey + * @return + */ + public static XxlSsoUser get(String storeKey) { + + String redisKey = redisKey(storeKey); + Object objectValue = JedisUtil.getObjectValue(redisKey); + if (objectValue != null) { + XxlSsoUser xxlUser = (XxlSsoUser) objectValue; + return xxlUser; + } + return null; + } + + /** + * remove + * + * @param storeKey + */ + public static void remove(String storeKey) { + String redisKey = redisKey(storeKey); + JedisUtil.del(redisKey); + } + + /** + * put + * + * @param storeKey + * @param xxlUser + */ + public static void put(String storeKey, XxlSsoUser xxlUser) { + String redisKey = redisKey(storeKey); + // minite to second + JedisUtil.setObjectValue(redisKey, xxlUser, redisExpireMinite * 60); + } + + private static String redisKey(String sessionId){ + return Conf.SSO_SESSIONID.concat("#").concat(sessionId); + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoSessionIdHelper.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoSessionIdHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..69383b1c3f6a3ba7e669a007fe496b5718a2ae09 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/store/SsoSessionIdHelper.java @@ -0,0 +1,70 @@ +package com.itools.sso.core.store; + +import com.itools.sso.core.user.XxlSsoUser; + +/** + * make client sessionId + * + * client: cookie = [userid#version] + * server: redis + * key = [userid] + * value = user (user.version, valid this) + * + * // group The same group shares the login status, Different groups will not interact + * + * @author xuxueli 2018-11-15 15:45:08 + */ + +public class SsoSessionIdHelper { + + + /** + * make client sessionId + * + * @param xxlSsoUser + * @return + */ + public static String makeSessionId(XxlSsoUser xxlSsoUser){ + String sessionId = xxlSsoUser.getUserid().concat("_").concat(xxlSsoUser.getVersion()); + return sessionId; + } + + /** + * parse storeKey from sessionId + * + * @param sessionId + * @return + */ + public static String parseStoreKey(String sessionId) { + if (sessionId!=null && sessionId.indexOf("_")>-1) { + String[] sessionIdArr = sessionId.split("_"); + if (sessionIdArr.length==2 + && sessionIdArr[0]!=null + && sessionIdArr[0].trim().length()>0) { + String userId = sessionIdArr[0].trim(); + return userId; + } + } + return null; + } + + /** + * parse version from sessionId + * + * @param sessionId + * @return + */ + public static String parseVersion(String sessionId) { + if (sessionId!=null && sessionId.indexOf("_")>-1) { + String[] sessionIdArr = sessionId.split("_"); + if (sessionIdArr.length==2 + && sessionIdArr[1]!=null + && sessionIdArr[1].trim().length()>0) { + String version = sessionIdArr[1].trim(); + return version; + } + } + return null; + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/user/XxlSsoUser.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/user/XxlSsoUser.java new file mode 100644 index 0000000000000000000000000000000000000000..808a3f597f73f8fd1b3b983818c8e17bee9a8bc6 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/user/XxlSsoUser.java @@ -0,0 +1,73 @@ +package com.itools.sso.core.user; + +import java.io.Serializable; +import java.util.Map; + +/** + * xxl sso user + * + * @author xuxueli 2018-04-02 19:59:49 + */ +public class XxlSsoUser implements Serializable { + private static final long serialVersionUID = 42L; + + // field + private String userid; + private String username; + private Map plugininfo; + + private String version; + private int expireMinite; + private long expireFreshTime; + + + // set get + public String getUserid() { + return userid; + } + + public void setUserid(String userid) { + this.userid = userid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Map getPlugininfo() { + return plugininfo; + } + + public void setPlugininfo(Map plugininfo) { + this.plugininfo = plugininfo; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public int getExpireMinite() { + return expireMinite; + } + + public void setExpireMinite(int expireMinite) { + this.expireMinite = expireMinite; + } + + public long getExpireFreshTime() { + return expireFreshTime; + } + + public void setExpireFreshTime(long expireFreshTime) { + this.expireFreshTime = expireFreshTime; + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/CookieUtil.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/CookieUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..88a650d805f217552343dae0f98e473776893963 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/CookieUtil.java @@ -0,0 +1,98 @@ +package com.itools.sso.core.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie.Util + * + * @author xuxueli 2015-12-12 18:01:06 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/JedisUtil.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/JedisUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1a1cf1f6e1e8db8f8f76e8877ad96d8e3c9331a9 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/JedisUtil.java @@ -0,0 +1,391 @@ +package com.itools.sso.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.ShardedJedis; +import redis.clients.jedis.ShardedJedisPool; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + + +/** + * Redis client base on jedis + * + * @author xuxueli 2015-7-10 18:34:07 + */ +public class JedisUtil { + private static Logger logger = LoggerFactory.getLogger(JedisUtil.class); + + /** + * redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated + */ + private static String address; + + public static void init(String address) { + JedisUtil.address = address; + + getInstance(); + } + + // ------------------------ ShardedJedisPool ------------------------ + /** + * 方式01: Redis单节点 + Jedis单例 : Redis单节点压力过重, Jedis单例存在并发瓶颈 》》不可用于线上 + * new Jedis("127.0.0.1", 6379).get("cache_key"); + * 方式02: Redis单节点 + JedisPool单节点连接池 》》 Redis单节点压力过重,负载和容灾比较差 + * new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 10000).getResource().get("cache_key"); + * 方式03: Redis分片(通过client端集群,一致性哈希方式实现) + Jedis多节点连接池 》》Redis集群,负载和容灾较好, ShardedJedisPool一致性哈希分片,读写均匀,动态扩充 + * new ShardedJedisPool(new JedisPoolConfig(), new LinkedList()); + * 方式03: Redis集群; + * new JedisCluster(jedisClusterNodes); // TODO + */ + + private static ShardedJedisPool shardedJedisPool; + private static ReentrantLock INSTANCE_INIT_LOCL = new ReentrantLock(false); + + /** + * 获取ShardedJedis实例 + * + * @return + */ + private static ShardedJedis getInstance() { + if (shardedJedisPool == null) { + try { + if (INSTANCE_INIT_LOCL.tryLock(2, TimeUnit.SECONDS)) { + + try { + + if (shardedJedisPool == null) { + + // JedisPoolConfig + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(200); + config.setMaxIdle(50); + config.setMinIdle(8); + config.setMaxWaitMillis(10000); // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 + config.setTestOnBorrow(true); // 在获取连接的时候检查有效性, 默认false + config.setTestOnReturn(false); // 调用returnObject方法时,是否进行有效检查 + config.setTestWhileIdle(true); // Idle时进行连接扫描 + config.setTimeBetweenEvictionRunsMillis(30000); // 表示idle object evitor两次扫描之间要sleep的毫秒数 + config.setNumTestsPerEvictionRun(10); // 表示idle object evitor每次扫描的最多的对象数 + config.setMinEvictableIdleTimeMillis(60000); // 表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义 + + + // JedisShardInfo List + List jedisShardInfos = new LinkedList(); + + String[] addressArr = address.split(","); + for (int i = 0; i < addressArr.length; i++) { + JedisShardInfo jedisShardInfo = new JedisShardInfo(addressArr[i]); + jedisShardInfos.add(jedisShardInfo); + } + shardedJedisPool = new ShardedJedisPool(config, jedisShardInfos); + logger.info(">>>>>>>>>>> xxl-sso, JedisUtil.ShardedJedisPool init success."); + } + + } finally { + INSTANCE_INIT_LOCL.unlock(); + } + } + + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + if (shardedJedisPool == null) { + throw new NullPointerException(">>>>>>>>>>> xxl-sso, JedisUtil.ShardedJedisPool is null."); + } + + ShardedJedis shardedJedis = shardedJedisPool.getResource(); + return shardedJedis; + } + + public static void close() throws IOException { + if(shardedJedisPool != null) { + shardedJedisPool.close(); + } + } + + + // ------------------------ serialize and unserialize ------------------------ + + /** + * 将对象-->byte[] (由于jedis中不支持直接存储object所以转换成byte[]存入) + * + * @param object + * @return + */ + private static byte[] serialize(Object object) { + ObjectOutputStream oos = null; + ByteArrayOutputStream baos = null; + try { + // 序列化 + baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(object); + byte[] bytes = baos.toByteArray(); + return bytes; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + oos.close(); + baos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + /** + * 将byte[] -->Object + * + * @param bytes + * @return + */ + private static Object unserialize(byte[] bytes) { + ByteArrayInputStream bais = null; + try { + // 反序列化 + bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + return ois.readObject(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + bais.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + // ------------------------ jedis util ------------------------ + /** + * 存储简单的字符串或者是Object 因为jedis没有分装直接存储Object的方法,所以在存储对象需斟酌下 + * 存储对象的字段是不是非常多而且是不是每个字段都用到,如果是的话那建议直接存储对象, + * 否则建议用集合的方式存储,因为redis可以针对集合进行日常的操作很方便而且还可以节省空间 + */ + + /** + * Set String + * + * @param key + * @param value + * @param seconds 存活时间,单位/秒 + * @return + */ + public static String setStringValue(String key, String value, int seconds) { + String result = null; + ShardedJedis client = getInstance(); + try { + result = client.setex(key, seconds, value); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * Set Object + * + * @param key + * @param obj + * @param seconds 存活时间,单位/秒 + */ + public static String setObjectValue(String key, Object obj, int seconds) { + String result = null; + ShardedJedis client = getInstance(); + try { + result = client.setex(key.getBytes(), seconds, serialize(obj)); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * Get String + * + * @param key + * @return + */ + public static String getStringValue(String key) { + String value = null; + ShardedJedis client = getInstance(); + try { + value = client.get(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return value; + } + + /** + * Get Object + * + * @param key + * @return + */ + public static Object getObjectValue(String key) { + Object obj = null; + ShardedJedis client = getInstance(); + try { + byte[] bytes = client.get(key.getBytes()); + if (bytes != null && bytes.length > 0) { + obj = unserialize(bytes); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return obj; + } + + /** + * Delete key + * + * @param key + * @return Integer reply, specifically: + * an integer greater than 0 if one or more keys were removed + * 0 if none of the specified key existed + */ + public static Long del(String key) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.del(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * incrBy i(+i) + * + * @param key + * @param i + * @return new value after incr + */ + public static Long incrBy(String key, int i) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.incrBy(key, i); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * exists valid + * + * @param key + * @return Boolean reply, true if the key exists, otherwise false + */ + public static boolean exists(String key) { + Boolean result = null; + ShardedJedis client = getInstance(); + try { + result = client.exists(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * expire reset + * + * @param key + * @param seconds 存活时间,单位/秒 + * @return Integer reply, specifically: + * 1: the timeout was set. + * 0: the timeout was not set since the key already has an associated timeout (versions lt 2.1.3), or the key does not exist. + */ + public static long expire(String key, int seconds) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.expire(key, seconds); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * expire at unixTime + * + * @param key + * @param unixTime + * @return + */ + public static long expireAt(String key, long unixTime) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.expireAt(key, unixTime); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + public static void main(String[] args) { + String xxlSsoRedisAddress = "redis://xxl-sso:password@127.0.0.1:6379/0"; + xxlSsoRedisAddress = "redis://127.0.0.1:6379/0"; + init(xxlSsoRedisAddress); + + setObjectValue("key", "666", 2*60*60); + System.out.println(getObjectValue("key")); + + } + +} diff --git a/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/StringUtils.java b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/StringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..ca732766dbe696ac0a5026b8dcfc539756fd9060 --- /dev/null +++ b/itools-sso/itools-sso-core/src/main/java/com/itools/sso/core/util/StringUtils.java @@ -0,0 +1,79 @@ +package com.itools.sso.core.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; + +public abstract class StringUtils { + + /** + * + * @param str + * @return + */ + public static boolean hasLength(String str) { + return (str != null && !str.isEmpty()); + } + + /** + * + * @param str + * @return + */ + public static boolean hasText(String str) { + return (hasLength(str) && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * + * @param str + * @param delimiters + * @param trimTokens + * @param ignoreEmptyTokens + * @return + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * + * @param collection + * @return + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + +} diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/pom.xml b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..05db2564e6ff31f6fbdebac381818a0ec8f8e7df --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.itools.core + itools-sso-samples + 1.0-SNAPSHOT + + itools-sso-token-sample-springboot + jar + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + com.itools.core + itools-sso-core + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-devtools + provided + true + + + + + org.apache.httpcomponents + httpclient + 4.5.6 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + true + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..51fa05ab8ab0f8946915cbdd7b59d0e22f56b5e2 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java @@ -0,0 +1,16 @@ +package com.itools.sso.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-04-08 21:49:41 + */ +@SpringBootApplication +public class SsoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SsoClientApplication.class, args); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6f549b08915bd156588a12621ae6173549f9c9a2 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java @@ -0,0 +1,59 @@ +package com.itools.sso.sample.config; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.filter.SsoTokenFilter; +import com.itools.sso.core.util.JedisUtil; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author xuxueli 2018-11-15 + */ +@Configuration +public class XxlSsoConfig implements DisposableBean { + + + @Value("${xxl.sso.server}") + private String xxlSsoServer; + + @Value("${xxl.sso.logout.path}") + private String xxlSsoLogoutPath; + + @Value("${xxl.sso.redis.address}") + private String xxlSsoRedisAddress; + + @Value("${xxl-sso.excluded.paths}") + private String xxlSsoExcludedPaths; + + + @Bean + public FilterRegistrationBean xxlSsoFilterRegistration() { + + // xxl-sso, redis init + JedisUtil.init(xxlSsoRedisAddress); + + // xxl-sso, filter init + FilterRegistrationBean registration = new FilterRegistrationBean(); + + registration.setName("XxlSsoWebFilter"); + registration.setOrder(1); + registration.addUrlPatterns("/*"); + registration.setFilter(new SsoTokenFilter()); + registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer); + registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath); + registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths); + + return registration; + } + + @Override + public void destroy() throws Exception { + + // xxl-sso, redis close + JedisUtil.close(); + } + +} diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java new file mode 100644 index 0000000000000000000000000000000000000000..577eb8b35c29f23779563c870c4d5f917ca6b71d --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java @@ -0,0 +1,25 @@ +package com.itools.sso.sample.controller; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.entity.ReturnT; +import com.itools.sso.core.user.XxlSsoUser; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author xuxueli 2017-08-01 21:39:47 + */ +@Controller +public class IndexController { + + @RequestMapping("/") + @ResponseBody + public ReturnT index(HttpServletRequest request) { + XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER); + return new ReturnT(xxlUser); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/application.properties b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..95a16b36591427b1f3042d718767ec52d1c9b61a --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/application.properties @@ -0,0 +1,9 @@ +### web +server.port=8092 +server.context-path=/xxl-sso-token-sample-springboot + +### xxl-sso +xxl.sso.server=http://xxlssoserver.com:8090/xxl-sso-server +xxl.sso.logout.path=/logout +xxl-sso.excluded.paths= +xxl.sso.redis.address=redis://127.0.0.1:6379 diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/logback.xml b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..cb7f94eac2f799541d4ab7af3e50455db98ca32b --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + logback + + + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + ${log.path} + + ${log.path}.%d{yyyy-MM-dd}.zip + + + %date %level [%thread] %logger{36} [%file : %line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/TokenClientTest.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/TokenClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a7d17ec0a22bbbb079c521d270866333a38cb240 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/TokenClientTest.java @@ -0,0 +1,182 @@ +package com.xxl.app.sample.test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xxl.app.sample.test.util.HttpClientUtil; +import com.itools.sso.core.conf.Conf; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author xuxueli 2018-04-09 11:38:15 + */ +public class TokenClientTest { + private static Logger logger = LoggerFactory.getLogger(TokenClientTest.class); + + public static String ssoServer = "http://xxlssoserver.com:8080/xxl-sso-server"; + + public static String client01 = "http://xxlssoclient1.com:8082/xxl-sso-token-sample-springboot/"; + public static String client02 = "http://xxlssoclient2.com:8082/xxl-sso-token-sample-springboot/"; + + @Test + public void test() throws Exception { + + // 登录:获取 sso sessionId + String sessionId = loginTest(); + Assert.assertNotNull(sessionId); + + // 登陆状态校验 + String username = logincheckTest(sessionId); + Assert.assertNotNull(username); + + clientApiRequestTest(client01, sessionId); + clientApiRequestTest(client02, sessionId); + + // 注销:销毁 sso sessionId + boolean loginoutResult = logoutTest(sessionId); + Assert.assertTrue(loginoutResult); + + // 登陆状态校验 + username = logincheckTest(sessionId); + Assert.assertNull(username); + + clientApiRequestTest(client01, sessionId); + clientApiRequestTest(client02, sessionId); + } + + /** + * Client API Request, SSO APP Filter + * + * @param clientApiUrl + * @return + */ + private void clientApiRequestTest(String clientApiUrl, String sessionId) throws IOException { + + Map headerParam = new HashMap<>(); + headerParam.put(Conf.SSO_SESSIONID, sessionId); + + + String resultJson = HttpClientUtil.post(clientApiUrl, null, headerParam); + Map loginResult = new ObjectMapper().readValue(resultJson, Map.class); + + int code = (int) loginResult.get("code"); + if (code == 200) { + + Map user = (Map) loginResult.get("data"); + String username = (String) user.get("username"); + + logger.info("模拟请求APP应用接口,请求成功,登陆用户 = " + username); + } else { + + String failMsg = (String) loginResult.get("msg"); + + logger.info("模拟请求APP应用接口,请求失败:" + failMsg); + } + } + + /** + * SSO Login + * + * @return + */ + private String loginTest() throws IOException { + // login url + String loginUrl = ssoServer + "/app/login"; + + // login param + Map loginParam = new HashMap<>(); + loginParam.put("username", "user"); + loginParam.put("password", "123456"); + + String loginResultJson = HttpClientUtil.post(loginUrl, loginParam, null); + Map loginResult = new ObjectMapper().readValue(loginResultJson, Map.class); + + int code = (int) loginResult.get("code"); + if (code == 200) { + + String sessionId = (String) loginResult.get("data"); + logger.info("登录成功,sessionid = " + sessionId); + + return sessionId; + } else { + + String failMsg = (String) loginResult.get("msg"); + logger.info("登录失败:" + failMsg); + + return null; + } + + } + + /** + * SSO Logout + * + * @param sessionId + * @return + */ + private boolean logoutTest(String sessionId) throws IOException { + // logout url + String logoutUrl = ssoServer + "/app/logout"; + + // logout param + Map logoutParam = new HashMap<>(); + logoutParam.put("sessionId", sessionId); + + String logoutResultJson = HttpClientUtil.post(logoutUrl, logoutParam, null); + Map logoutResult = new ObjectMapper().readValue(logoutResultJson, Map.class); + + int code = (int) logoutResult.get("code"); + if (code == 200) { + + logger.info("注销成功"); + return true; + } else { + + String failMsg = (String) logoutResult.get("msg"); + logger.info("注销失败:" + failMsg); + + return false; + } + + } + + /** + * SSO Login Check + * + * @param sessionId + * @return + */ + private String logincheckTest(String sessionId) throws IOException { + // logout url + String logincheckUrl = ssoServer + "/app/logincheck"; + + // logout param + Map logincheckParam = new HashMap<>(); + logincheckParam.put("sessionId", sessionId); + + String logincheckResultJson = HttpClientUtil.post(logincheckUrl, logincheckParam, null); + Map logincheckResult = new ObjectMapper().readValue(logincheckResultJson, Map.class); + + int code = (int) logincheckResult.get("code"); + if (code == 200) { + + Map user = (Map) logincheckResult.get("data"); + String username = (String) user.get("username"); + + logger.info("当前为登录状态,登陆用户 = " + username); + + return username; + } else { + + logger.info("当前为注销状态"); + return null; + } + + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e1cc92612a7448c980d2433fb3fd94ab520fae0c --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java @@ -0,0 +1,90 @@ +package com.xxl.app.sample.test.util; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * http util to send data + * + * @author xuxueli + * @version 2015-11-28 15:30:59 + */ +public class HttpClientUtil { + private static Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); + + public static String post(String url, Map params, Map headers){ + HttpPost httpPost = null; + CloseableHttpClient httpClient = null; + try{ + // httpPost config + httpPost = new HttpPost(url); + if (params != null && !params.isEmpty()) { + List formParams = new ArrayList(); + for(Map.Entry entry : params.entrySet()){ + formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + httpPost.setEntity(new UrlEncodedFormEntity(formParams, "UTF-8")); + } + RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(5000).setConnectTimeout(5000).build(); + httpPost.setConfig(requestConfig); + + // headers + if (headers!=null && headers.size()>0) { + for (Map.Entry headerItem: headers.entrySet()) { + httpPost.setHeader(headerItem.getKey(), headerItem.getValue()); + } + } + + // httpClient = HttpClients.createDefault(); // default retry 3 times + // httpClient = HttpClients.custom().setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)).build(); + httpClient = HttpClients.custom().disableAutomaticRetries().build(); + + // parse response + HttpResponse response = httpClient.execute(httpPost); + HttpEntity entity = response.getEntity(); + if (null != entity) { + if (response.getStatusLine().getStatusCode() == 200) { + String responseMsg = EntityUtils.toString(entity, "UTF-8"); + EntityUtils.consume(entity); + return responseMsg; + } + EntityUtils.consume(entity); + } + logger.info("http statusCode error, statusCode:" + response.getStatusLine().getStatusCode()); + return null; + } catch (Exception e) { + e.printStackTrace(); + /*StringWriter out = new StringWriter(); + e.printStackTrace(new PrintWriter(out)); + callback.setMsg(out.toString());*/ + return e.getMessage(); + } finally{ + if (httpPost!=null) { + httpPost.releaseConnection(); + } + if (httpClient!=null) { + try { + httpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/resources/logback.xml b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..1780ec4434b2113c6eef9d6c232e2f04bd379703 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + logback + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/pom.xml b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce8775f42dca689c499af255e46fd072dbe7d283 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + itools-sso-samples + com.itools.core + 1.0-SNAPSHOT + + itools-sso-web-sample-springboot + jar + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + com.itools.core + itools-sso-core + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-devtools + provided + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + true + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..d253107509f261036c9175388a67f29efc2287e6 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/SsoClientApplication.java @@ -0,0 +1,16 @@ +package com.itools.sso.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-03-22 23:41:47 + */ +@SpringBootApplication +public class SsoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SsoClientApplication.class, args); + } + +} diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..c28af4f64febdb8359b92599f108d1b9cf3910d8 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/config/XxlSsoConfig.java @@ -0,0 +1,59 @@ +package com.itools.sso.sample.config; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.filter.SsoWebFilter; +import com.itools.sso.core.util.JedisUtil; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author xuxueli 2018-11-15 + */ +@Configuration +public class XxlSsoConfig implements DisposableBean { + + + @Value("${xxl.sso.server}") + private String xxlSsoServer; + + @Value("${xxl.sso.logout.path}") + private String xxlSsoLogoutPath; + + @Value("${xxl-sso.excluded.paths}") + private String xxlSsoExcludedPaths; + + @Value("${xxl.sso.redis.address}") + private String xxlSsoRedisAddress; + + + @Bean + public FilterRegistrationBean xxlSsoFilterRegistration() { + + // xxl-sso, redis init + JedisUtil.init(xxlSsoRedisAddress); + + // xxl-sso, filter init + FilterRegistrationBean registration = new FilterRegistrationBean(); + + registration.setName("XxlSsoWebFilter"); + registration.setOrder(1); + registration.addUrlPatterns("/*"); + registration.setFilter(new SsoWebFilter()); + registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer); + registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath); + registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths); + + return registration; + } + + @Override + public void destroy() throws Exception { + + // xxl-sso, redis close + JedisUtil.close(); + } + +} diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java new file mode 100644 index 0000000000000000000000000000000000000000..7a9cb77f55ee28b4f6a1013d4a6cc830ec81525f --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/java/com/itools/sso/sample/controller/IndexController.java @@ -0,0 +1,34 @@ +package com.itools.sso.sample.controller; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.entity.ReturnT; +import com.itools.sso.core.user.XxlSsoUser; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author xuxueli 2017-08-01 21:39:47 + */ +@Controller +public class IndexController { + + @RequestMapping("/") + public String index(Model model, HttpServletRequest request) { + + XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER); + model.addAttribute("xxlUser", xxlUser); + return "index"; + } + + @RequestMapping("/json") + @ResponseBody + public ReturnT json(Model model, HttpServletRequest request) { + XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER); + return new ReturnT(xxlUser); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/application.properties b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..eff4911a016ec74f1491ff7085a01cc8c6efa964 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/application.properties @@ -0,0 +1,18 @@ +### web +server.port=8091 +server.context-path=/xxl-sso-web-sample-springboot + +### freemarker +spring.freemarker.request-context-attribute=request +spring.freemarker.cache=false + +### resource (default: /** + classpath:/static/ ) +spring.mvc.static-path-pattern=/static/** +spring.resources.static-locations=classpath:/static/ + +### xxl-sso +xxl.sso.server=http://xxlssoserver.com:8090/xxl-sso-server +xxl.sso.logout.path=/logout +xxl-sso.excluded.paths= +xxl.sso.redis.address=redis://127.0.0.1:6379 + diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/logback.xml b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..03a50742eca70a447033511d01f653fedf3941e8 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + logback + + + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + ${log.path} + + ${log.path}.%d{yyyy-MM-dd}.zip + + + %date %level [%thread] %logger{36} [%file : %line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/templates/index.ftl b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/templates/index.ftl new file mode 100644 index 0000000000000000000000000000000000000000..9f74c4d2f72d8203e19a65a13d6ea39f29157e59 --- /dev/null +++ b/itools-sso/itools-sso-samples/itools-sso-web-sample-springboot/src/main/resources/templates/index.ftl @@ -0,0 +1,17 @@ + + + + + SSO Client + + + +
+

【${xxlUser.username}】login success.

+ + Logout + +
+ + + \ No newline at end of file diff --git a/itools-sso/itools-sso-samples/pom.xml b/itools-sso/itools-sso-samples/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0a97c5e77aaaa5040bbc9329040ba805d5a0a8c4 --- /dev/null +++ b/itools-sso/itools-sso-samples/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + itools-sso + com.itools.core + 1.0-SNAPSHOT + + itools-sso-samples + pom + + + itools-sso-web-sample-springboot + itools-sso-token-sample-springboot + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-server/pom.xml b/itools-sso/itools-sso-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e03ac65f1783705655e3e2eadcbe337b7a8cacf --- /dev/null +++ b/itools-sso/itools-sso-server/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + itools-sso + com.itools.core + 1.0-SNAPSHOT + + itools-sso-server + jar + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + com.itools.core + itools-sso-core + ${parent.version} + + + + + org.springframework.boot + spring-boot-devtools + provided + true + + + org.springframework.boot + spring-boot-starter-log4j2 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + true + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/SsoServerApplication.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/SsoServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..e538030ea7161057e64713c797ce636590f6e6cb --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/SsoServerApplication.java @@ -0,0 +1,16 @@ +package com.itools.sso.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-03-22 23:41:47 + */ +@SpringBootApplication +public class SsoServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SsoServerApplication.class, args); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/config/XxlSsoConfig.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/config/XxlSsoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..fe91d5e2eda403e9f315880893dcff5b896cc110 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/config/XxlSsoConfig.java @@ -0,0 +1,33 @@ +package com.itools.sso.server.config; + +import com.itools.sso.core.store.SsoLoginStore; +import com.itools.sso.core.util.JedisUtil; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * @author xuxueli 2018-04-03 20:41:07 + */ +@Configuration +public class XxlSsoConfig implements InitializingBean, DisposableBean { + + @Value("${xxl.sso.redis.address}") + private String redisAddress; + + @Value("${xxl.sso.redis.expire.minite}") + private int redisExpireMinite; + + @Override + public void afterPropertiesSet() throws Exception { + SsoLoginStore.setRedisExpireMinite(redisExpireMinite); + JedisUtil.init(redisAddress); + } + + @Override + public void destroy() throws Exception { + JedisUtil.close(); + } + +} diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/AppController.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/AppController.java new file mode 100644 index 0000000000000000000000000000000000000000..dc7abc33246a4e7cf1741d8567426039da679e3f --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/AppController.java @@ -0,0 +1,99 @@ +package com.itools.sso.server.controller; + +import com.itools.sso.core.login.SsoTokenLoginHelper; +import com.itools.sso.core.store.SsoLoginStore; +import com.itools.sso.core.user.XxlSsoUser; +import com.itools.sso.core.store.SsoSessionIdHelper; +import com.itools.sso.server.core.model.UserInfo; +import com.itools.sso.server.core.result.ReturnT; +import com.itools.sso.server.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.UUID; + +/** + * sso server (for app) + * + * @author xuxueli 2018-04-08 21:02:54 + */ +@Controller +@RequestMapping("/app") +public class AppController { + + @Autowired + private UserService userService; + + + /** + * Login + * + * @param username + * @param password + * @return + */ + @RequestMapping("/login") + @ResponseBody + public ReturnT login(String username, String password) { + + // valid login + ReturnT result = userService.findUser(username, password); + if (result.getCode() != ReturnT.SUCCESS_CODE) { + return new ReturnT(result.getCode(), result.getMsg()); + } + + // 1、make xxl-sso user + XxlSsoUser xxlUser = new XxlSsoUser(); + xxlUser.setUserid(String.valueOf(result.getData().getUserid())); + xxlUser.setUsername(result.getData().getUsername()); + xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", "")); + xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite()); + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + + + // 2、generate sessionId + storeKey + String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser); + + // 3、login, store storeKey + SsoTokenLoginHelper.login(sessionId, xxlUser); + + // 4、return sessionId + return new ReturnT(sessionId); + } + + + /** + * Logout + * + * @param sessionId + * @return + */ + @RequestMapping("/logout") + @ResponseBody + public ReturnT logout(String sessionId) { + // logout, remove storeKey + SsoTokenLoginHelper.logout(sessionId); + return ReturnT.SUCCESS; + } + + /** + * logincheck + * + * @param sessionId + * @return + */ + @RequestMapping("/logincheck") + @ResponseBody + public ReturnT logincheck(String sessionId) { + + // logout + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(sessionId); + if (xxlUser == null) { + return new ReturnT(ReturnT.FAIL_CODE, "sso not login."); + } + return new ReturnT(xxlUser); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/WebController.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/WebController.java new file mode 100644 index 0000000000000000000000000000000000000000..79cfacd821445dcd374d1a79dbbb9dbe8ce12194 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/WebController.java @@ -0,0 +1,151 @@ +package com.itools.sso.server.controller; + +import com.itools.sso.core.conf.Conf; +import com.itools.sso.core.login.SsoWebLoginHelper; +import com.itools.sso.core.store.SsoLoginStore; +import com.itools.sso.core.user.XxlSsoUser; +import com.itools.sso.core.store.SsoSessionIdHelper; +import com.itools.sso.server.core.model.UserInfo; +import com.itools.sso.server.core.result.ReturnT; +import com.itools.sso.server.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.UUID; + +/** + * sso server (for web) + * + * @author xuxueli 2017-08-01 21:39:47 + */ +@Controller +public class WebController { + + @Autowired + private UserService userService; + + @RequestMapping("/") + public String index(Model model, HttpServletRequest request, HttpServletResponse response) { + + // login check + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response); + + if (xxlUser == null) { + return "redirect:/login"; + } else { + model.addAttribute("xxlUser", xxlUser); + return "index"; + } + } + + /** + * Login page + * + * @param model + * @param request + * @return + */ + @RequestMapping(Conf.SSO_LOGIN) + public String login(Model model, HttpServletRequest request, HttpServletResponse response) { + + // login check + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response); + + if (xxlUser != null) { + + // success redirect + String redirectUrl = request.getParameter(Conf.REDIRECT_URL); + if (redirectUrl!=null && redirectUrl.trim().length()>0) { + + String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request); + String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;; + + return "redirect:" + redirectUrlFinal; + } else { + return "redirect:/"; + } + } + + model.addAttribute("errorMsg", request.getParameter("errorMsg")); + model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "login"; + } + + /** + * Login + * + * @param request + * @param redirectAttributes + * @param username + * @param password + * @return + */ + @RequestMapping("/doLogin") + public String doLogin(HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes, + String username, + String password, + String ifRemember) { + + boolean ifRem = (ifRemember!=null&&"on".equals(ifRemember))?true:false; + + // valid login + ReturnT result = userService.findUser(username, password); + if (result.getCode() != ReturnT.SUCCESS_CODE) { + redirectAttributes.addAttribute("errorMsg", result.getMsg()); + + redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "redirect:/login"; + } + + // 1、make xxl-sso user + XxlSsoUser xxlUser = new XxlSsoUser(); + xxlUser.setUserid(String.valueOf(result.getData().getUserid())); + xxlUser.setUsername(result.getData().getUsername()); + xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", "")); + xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite()); + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + + + // 2、make session id + String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser); + + // 3、login, store storeKey + cookie sessionId + SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem); + + // 4、return, redirect sessionId + String redirectUrl = request.getParameter(Conf.REDIRECT_URL); + if (redirectUrl!=null && redirectUrl.trim().length()>0) { + String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId; + return "redirect:" + redirectUrlFinal; + } else { + return "redirect:/"; + } + + } + + /** + * Logout + * + * @param request + * @param redirectAttributes + * @return + */ + @RequestMapping(Conf.SSO_LOGOUT) + public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) { + + // logout + SsoWebLoginHelper.logout(request, response); + + redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "redirect:/login"; + } + + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..6ef1a9af3fb30ce517ee082769803dad1668a6f9 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java @@ -0,0 +1,21 @@ +package com.itools.sso.server.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * web mvc config + * + * @author xuxueli 2018-04-02 20:48:20 + */ +@Configuration +public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new PermissionInterceptor()).addPathPatterns("/**"); + super.addInterceptors(registry); + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/PermissionInterceptor.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..c44ffc5feb64f4e5c8f11f0d5092d8d8a2fd584b --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,38 @@ +package com.itools.sso.server.controller.interceptor; + +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author xuxueli 2015-12-12 18:09:04 + */ +public class PermissionInterceptor implements HandlerInterceptor { + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; + } + + // TODO + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + + } + +} diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/resolver/WebExceptionResolver.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/resolver/WebExceptionResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..b195429725534a082ad3a902ef7799f3d27c1690 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,72 @@ +package com.itools.sso.server.controller.resolver; + +import com.itools.sso.core.exception.XxlSsoException; +import com.itools.sso.server.core.result.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 统一异常处理(Controller切面方式实现) + * + * 1、@ControllerAdvice:扫描所有Controller; + * 2、@ControllerAdvice(annotations=RestController.class):扫描指定注解类型的Controller; + * 3、@ControllerAdvice(basePackages={"com.aaa","com.bbb"}):扫描指定package下的Controller + * + * @author xuxueli 2017-08-01 21:51:21 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + + logger.error("WebExceptionResolver:{}", ex); + + // if json + boolean isJson = false; + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + + // error result + ReturnT errorResult = null; + if (ex instanceof XxlSsoException) { + errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.getMessage()); + } else { + errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "
")); + } + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print("{\"code\":"+errorResult.getCode()+", \"msg\":\""+ errorResult.getMsg() +"\"}"); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/model/UserInfo.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/model/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..1181bba55ab358c10429b5e3ba10657669693beb --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/model/UserInfo.java @@ -0,0 +1,36 @@ +package com.itools.sso.server.core.model; + +/** + * @author xuxueli 2018-03-22 23:51:51 + */ +public class UserInfo { + + private int userid; + private String username; + private String password; + + public int getUserid() { + return userid; + } + + public void setUserid(int userid) { + this.userid = userid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/result/ReturnT.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/result/ReturnT.java new file mode 100644 index 0000000000000000000000000000000000000000..8e16b4ff641feba1d883305e873435b5fdb21cc3 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/core/result/ReturnT.java @@ -0,0 +1,51 @@ +package com.itools.sso.server.core.result; + +import java.io.Serializable; + +/** + * common return + * + * @author xuxueli 2015-12-4 16:32:31 + * @param + */ +public class ReturnT implements Serializable { + public static final long serialVersionUID = 42L; + + public static final int SUCCESS_CODE = 200; + public static final int FAIL_CODE = 500; + public static final ReturnT SUCCESS = new ReturnT(null); + public static final ReturnT FAIL = new ReturnT(FAIL_CODE, null); + + private int code; + private String msg; + private T data; + + public ReturnT(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ReturnT(T data) { + this.code = SUCCESS_CODE; + this.data = data; + } + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public String getMsg() { + return msg; + } + public void setMsg(String msg) { + this.msg = msg; + } + public T getData() { + return data; + } + public void setData(T data) { + this.data = data; + } + +} diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/UserService.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..95fa71164e3929e048ea138d722a4b2895aef2f2 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/UserService.java @@ -0,0 +1,10 @@ +package com.itools.sso.server.service; + +import com.itools.sso.server.core.model.UserInfo; +import com.itools.sso.server.core.result.ReturnT; + +public interface UserService { + + public ReturnT findUser(String username, String password); + +} diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/impl/UserServiceImpl.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8dbce48b37dfb32e44b8992fe38572a3f0801291 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/service/impl/UserServiceImpl.java @@ -0,0 +1,46 @@ +package com.itools.sso.server.service.impl; + +import com.itools.sso.server.core.model.UserInfo; +import com.itools.sso.server.core.result.ReturnT; +import com.itools.sso.server.service.UserService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class UserServiceImpl implements UserService { + + private static List mockUserList = new ArrayList<>(); + static { + for (int i = 0; i <5; i++) { + UserInfo userInfo = new UserInfo(); + userInfo.setUserid(1000+i); + userInfo.setUsername("user" + (i>0?String.valueOf(i):"")); + userInfo.setPassword("123456"); + mockUserList.add(userInfo); + } + } + + @Override + public ReturnT findUser(String username, String password) { + + if (username==null || username.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "Please input username."); + } + if (password==null || password.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "Please input password."); + } + + // mock user + for (UserInfo mockUser: mockUserList) { + if (mockUser.getUsername().equals(username) && mockUser.getPassword().equals(password)) { + return new ReturnT(mockUser); + } + } + + return new ReturnT(ReturnT.FAIL_CODE, "username or password is invalid."); + } + + +} diff --git a/itools-sso/itools-sso-server/src/main/resources/application.properties b/itools-sso/itools-sso-server/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..d349c4d7f69a0c0debab361ae0247587c0e8b3ae --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/resources/application.properties @@ -0,0 +1,18 @@ +### web +server.port=8090 +server.context-path=/xxl-sso-server + +### resources +spring.mvc.static-path-pattern=/static/** +spring.resources.static-locations=classpath:/static/ + +### freemarker +spring.freemarker.templateLoaderPath=classpath:/templates/ +spring.freemarker.suffix=.ftl +spring.freemarker.charset=UTF-8 +spring.freemarker.request-context-attribute=request +spring.freemarker.settings.number_format=0.########## + +### xxl-sso +xxl.sso.redis.address=redis://127.0.0.1:6379 +xxl.sso.redis.expire.minite=1440 diff --git a/itools-sso/itools-sso-server/src/main/resources/logback.xml b/itools-sso/itools-sso-server/src/main/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..9b5ae60891299ba7bf1bc66ce5334d2e14189d75 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + logback + + + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + ${log.path} + + ${log.path}.%d{yyyy-MM-dd}.zip + + + %date %level [%thread] %logger{36} [%file : %line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/itools-sso/itools-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map b/itools-sso/itools-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map new file mode 100644 index 0000000000000000000000000000000000000000..f010c82d113c25336f5ae7df32ce5de0caaddbd2 --- /dev/null +++ b/itools-sso/itools-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"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AW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;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ukDH;AexkDC;EACE,oBAAA;Cf0kDH;Ae3kDC;EACE,WAAA;Cf6kDH;Ae9kDC;EACE,oBAAA;CfglDH;AejlDC;EACE,mBAAA;CfmlDH;Ae/kDC;EACE,YAAA;CfilDH;AejmDC;EACE,WAAA;CfmmDH;AepmDC;EACE,mBAAA;CfsmDH;AevmDC;EACE,mBAAA;CfymDH;Ae1mDC;EACE,UAAA;Cf4mDH;Ae7mDC;EACE,mBAAA;Cf+mDH;AehnDC;EACE,mBAAA;CfknDH;AennDC;EACE,UAAA;CfqnDH;AetnDC;EACE,mBAAA;CfwnDH;AeznDC;EACE,mBAAA;Cf2nDH;Ae5nDC;EACE,UAAA;Cf8nDH;Ae/nDC;EACE,mBAAA;CfioDH;AeloDC;EACE,kBAAA;CfooDH;AehoDC;EACE,WAAA;CfkoDH;AepnDC;EACE,kBAAA;CfsnDH;AevnDC;EACE,0BAAA;CfynDH;Ae1nDC;EACE,0BAAA;Cf4nDH;Ae7nDC;EACE,iBAAA;Cf+nDH;AehoDC;EACE,0BAAA;CfkoDH;AenoDC;EACE,0BAAA;CfqoDH;AetoDC;EACE,iBAAA;CfwoDH;AezoDC;EACE,0BAAA;Cf2oDH;Ae5oDC;EACE,0BAAA;Cf8oDH;Ae/oDC;EACE,iBAAA;CfipDH;AelpDC;EACE,0BAAA;CfopDH;AerpDC;EACE,yBAAA;CfupDH;AexpDC;EACE,gBAAA;Cf0pDH;Aa1pDD;EElCI;IACE,YAAA;Gf+rDH;EexrDD;IACE,YAAA;Gf0rDD;Ee3rDD;IACE,oBAAA;Gf6rDD;Ee9rDD;IACE,oBAAA;GfgsDD;EejsDD;IACE,WAAA;GfmsDD;EepsDD;IACE,oBAAA;GfssDD;EevsDD;IACE,oBAAA;GfysDD;Ee1sDD;IACE,WAAA;Gf4sDD;Ee7sDD;IACE,oBAAA;Gf+sDD;EehtDD;IACE,oBAAA;GfktDD;EentDD;IACE,WAAA;GfqtDD;EettDD;IACE,oBAAA;GfwtDD;EeztDD;IACE,mBAAA;Gf2tDD;Ee7sDD;IACE,YAAA;Gf+sDD;EehtDD;IACE,oBAAA;GfktDD;EentDD;IACE,oBAAA;GfqtDD;EettDD;IACE,WAAA;GfwtDD;EeztDD;IACE,oBAAA;Gf2tDD;Ee5tDD;IACE,oBAAA;Gf8tDD;Ee/tDD;IACE,WAAA;GfiuDD;EeluDD;IACE,oBAAA;GfouDD;EeruDD;IACE,oBAAA;GfuuDD;EexuDD;IACE,WAAA;Gf0uDD;Ee3uDD;IACE,oBAAA;Gf6uDD;Ee9uDD;IACE,mBAAA;GfgvDD;Ee5uDD;IACE,YAAA;Gf8uDD;Ee9vDD;IACE,WAAA;GfgwDD;EejwDD;IACE,mBAAA;GfmwDD;EepwDD;IACE,mBAAA;GfswDD;EevwDD;IACE,UAAA;GfywDD;Ee1wDD;IACE,mBAAA;Gf4wDD;Ee7wDD;IACE,mBAAA;Gf+wDD;EehxDD;IACE,UAAA;GfkxDD;EenxDD;IACE,mBAAA;GfqxDD;EetxDD;IACE,mBAAA;GfwxDD;EezxDD;IACE,UAAA;Gf2xDD;Ee5xDD;IACE,mBAAA;Gf8xDD;Ee/xDD;IACE,kBAAA;GfiyDD;Ee7xDD;IACE,WAAA;Gf+xDD;EejxDD;IACE,kBAAA;GfmxDD;EepxDD;IACE,0BAAA;GfsxDD;EevxDD;IACE,0BAAA;GfyxDD;Ee1xDD;IACE,iBAAA;Gf4xDD;Ee7xDD;IACE,0BAAA;Gf+xDD;EehyDD;IACE,0BAAA;GfkyDD;EenyDD;IACE,iBAAA;GfqyDD;EetyDD;IACE,0BAAA;GfwyDD;EezyDD;IACE,0BAAA;Gf2yDD;Ee5yDD;IACE,iBAAA;Gf8yDD;Ee/yDD;IACE,0BAAA;GfizDD;EelzDD;IACE,yBAAA;GfozDD;EerzDD;IACE,gBAAA;GfuzDD;CACF;Aa/yDD;EE3CI;IACE,YAAA;Gf61DH;Eet1DD;IACE,YAAA;Gfw1DD;Eez1DD;IACE,oBAAA;Gf21DD;Ee51DD;IACE,oBAAA;Gf81DD;Ee/1DD;IACE,WAAA;Gfi2DD;Eel2DD;IACE,oBAAA;Gfo2DD;Eer2DD;IACE,oBAAA;Gfu2DD;Eex2DD;IACE,WAAA;Gf02DD;Ee32DD;IACE,oBAAA;Gf62DD;Ee92DD;IACE,oBAAA;Gfg3DD;Eej3DD;IACE,WAAA;Gfm3DD;Eep3DD;IACE,oBAAA;Gfs3DD;Eev3DD;IACE,mBAAA;Gfy3DD;Ee32DD;IACE,YAAA;Gf62DD;Ee92DD;IACE,oBAAA;Gfg3DD;Eej3DD;IACE,oBAAA;Gfm3DD;Eep3DD;IACE,WAAA;Gfs3DD;Eev3DD;IACE,oBAAA;Gfy3DD;Ee13DD;IACE,oBAAA;Gf43DD;Ee73DD;IACE,WAAA;Gf+3DD;Eeh4DD;IACE,oBAAA;Gfk4DD;Een4DD;IACE,oBAAA;Gfq4DD;Eet4DD;IACE,WAAA;Gfw4DD;Eez4DD;IACE,oBAAA;Gf24DD;Ee54DD;IACE,mBAAA;Gf84DD;Ee14DD;IACE,YAAA;Gf44DD;Ee55DD;IACE,WAAA;Gf85DD;Ee/5DD;IACE,mBAAA;Gfi6DD;Eel6DD;IACE,mBAAA;Gfo6DD;Eer6DD;IACE,UAAA;Gfu6DD;Eex6DD;IACE,mBAAA;Gf06DD;Ee36DD;IACE,mBAAA;Gf66DD;Ee96DD;IACE,UAAA;Gfg7DD;Eej7DD;IACE,mBAAA;Gfm7DD;Eep7DD;IACE,mBAAA;Gfs7DD;Eev7DD;IACE,UAAA;Gfy7DD;Ee17DD;IACE,mBAAA;Gf47DD;Ee77DD;IACE,kBAAA;Gf+7DD;Ee37DD;IACE,WAAA;Gf67DD;Ee/6DD;IACE,kBAAA;Gfi7DD;Eel7DD;IACE,0BAAA;Gfo7DD;Eer7DD;IACE,0BAAA;Gfu7DD;Eex7DD;IACE,iBAAA;Gf07DD;Ee37DD;IACE,0BAAA;Gf67DD;Ee97DD;IACE,0BAAA;Gfg8DD;Eej8DD;IACE,iBAAA;Gfm8DD;Eep8DD;IACE,0BAAA;Gfs8DD;Eev8DD;IACE,0BAAA;Gfy8DD;Ee18DD;IACE,iBAAA;Gf48DD;Ee78DD;IACE,0BAAA;Gf+8DD;Eeh9DD;IACE,yBAAA;Gfk9DD;Een9DD;IACE,gBAAA;Gfq9DD;CACF;Aa18DD;EE9CI;IACE,YAAA;Gf2/DH;Eep/DD;IACE,YAAA;Gfs/DD;Eev/DD;IACE,oBAAA;Gfy/DD;Ee1/DD;IACE,oBAAA;Gf4/DD;Ee7/DD;IACE,WAAA;Gf+/DD;EehgED;IACE,oBAAA;GfkgED;EengED;IACE,oBAAA;GfqgED;EetgED;IACE,WAAA;GfwgED;EezgED;IACE,oBAAA;Gf2gED;Ee5gED;IACE,oBAAA;Gf8gED;Ee/gED;IACE,WAAA;GfihED;EelhED;IACE,oBAAA;GfohED;EerhED;IACE,mBAAA;GfuhED;EezgED;IACE,YAAA;Gf2gED;Ee5gED;IACE,oBAAA;Gf8gED;Ee/gED;IACE,oBAAA;GfihED;EelhED;IACE,WAAA;GfohED;EerhED;IACE,oBAAA;GfuhED;EexhED;IACE,oBAAA;Gf0hED;Ee3hED;IACE,WAAA;Gf6hED;Ee9hED;IACE,oBAAA;GfgiED;EejiED;IACE,oBAAA;GfmiED;EepiED;IACE,WAAA;GfsiED;EeviED;IACE,oBAAA;GfyiED;Ee1iED;IACE,mBAAA;Gf4iED;EexiED;IACE,YAAA;Gf0iED;Ee1jED;IACE,WAAA;Gf4jED;Ee7jED;IACE,mBAAA;Gf+jED;EehkED;IACE,mBAAA;GfkkED;EenkED;IACE,UAAA;GfqkED;EetkED;IACE,mBAAA;GfwkED;EezkED;IACE,mBAAA;Gf2kED;Ee5kED;IACE,UAAA;Gf8kED;Ee/kED;IACE,mBAAA;GfilED;EellED;IACE,mBAAA;GfolED;EerlED;IACE,UAAA;GfulED;EexlED;IACE,mBAAA;Gf0lED;Ee3lED;IACE,kBAAA;Gf6lED;EezlED;IACE,WAAA;Gf2lED;Ee7kED;IACE,kBAAA;Gf+kED;EehlED;IACE,0BAAA;GfklED;EenlED;IACE,0BAAA;GfqlED;EetlED;IACE,iBAAA;GfwlED;EezlED;IACE,0BAAA;Gf2lED;Ee5lED;IACE,0BAAA;Gf8lED;Ee/lED;IACE,iBAAA;GfimED;EelmED;IACE,0BAAA;GfomED;EermED;IACE,0BAAA;GfumED;EexmED;IACE,iBAAA;Gf0mED;Ee3mED;IACE,0BAAA;Gf6mED;Ee9mED;IACE,yBAAA;GfgnED;EejnED;IACE,gBAAA;GfmnED;CACF;AgBvrED;EACE,8BAAA;ChByrED;AgBvrED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChByrED;AgBvrED;EACE,iBAAA;ChByrED;AgBnrED;EACE,YAAA;EACA,gBA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","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n border: 0;\n background-color: transparent;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n min-height: 34px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #fff;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #fff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #ccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #fff;\n border: 1px solid #ddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #fff;\n border-color: #ddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #fff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #fff;\n line-height: 1;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n text-decoration: none;\n color: #555;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 12px;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #fff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #fff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #fff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #fff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n margin-top: -10px;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n line-height: 1;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #fff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: floor((@gutter / 2));\n padding-right: ceil((@gutter / 2));\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: ceil((@gutter / -2));\n margin-right: floor((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: ceil((@grid-gutter-width / 2));\n padding-right: floor((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius; // Note: This has no effect on s in CSS.\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Unstyle the caret on ``\n// element gets special love because it's special, and that's a fact!\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n height: @input-height;\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n\n select& {\n height: @input-height;\n line-height: @input-height;\n }\n\n textarea&,\n select[multiple]& {\n height: auto;\n }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n display: inline-block;\n margin-bottom: 0; // For input.btn\n font-weight: @btn-font-weight;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n white-space: nowrap;\n .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base);\n .user-select(none);\n\n &,\n &:active,\n &.active {\n &:focus,\n &.focus {\n .tab-focus();\n }\n }\n\n &:hover,\n &:focus,\n &.focus {\n color: @btn-default-color;\n text-decoration: none;\n }\n\n &:active,\n &.active {\n outline: 0;\n background-image: none;\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n .opacity(.65);\n .box-shadow(none);\n }\n\n a& {\n &.disabled,\n fieldset[disabled] & {\n pointer-events: none; // Future-proof disabling of clicks on `` elements\n }\n }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n color: @link-color;\n font-weight: normal;\n border-radius: 0;\n\n &,\n &:active,\n &.active,\n &[disabled],\n fieldset[disabled] & {\n background-color: transparent;\n .box-shadow(none);\n }\n &,\n &:hover,\n &:focus,\n &:active {\n border-color: transparent;\n }\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n background-color: transparent;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @btn-link-disabled-color;\n text-decoration: none;\n }\n }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n // line-height: ensure even-numbered height of button next to large input\n .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large);\n}\n.btn-sm {\n // line-height: ensure proper height of button next to small input\n .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n.btn-xs {\n .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n &.btn-block {\n width: 100%;\n }\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 25%);\n }\n &:hover {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n\n &:hover,\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 17%);\n border-color: darken(@border, 25%);\n }\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus,\n &.focus {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n","// Opacity\n\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.\n\n.fade {\n opacity: 0;\n .transition(opacity .15s linear);\n &.in {\n opacity: 1;\n }\n}\n\n.collapse {\n display: none;\n\n &.in { display: block; }\n tr&.in { display: table-row; }\n tbody&.in { display: table-row-group; }\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n .transition-property(~\"height, visibility\");\n .transition-duration(.35s);\n .transition-timing-function(ease);\n}\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: @caret-width-base dashed;\n border-top: @caret-width-base solid ~\"\\9\"; // IE8\n border-right: @caret-width-base solid transparent;\n border-left: @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropup,\n.dropdown {\n position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: @zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0; // override default ul\n list-style: none;\n font-size: @font-size-base;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n background-color: @dropdown-bg;\n border: 1px solid @dropdown-fallback-border; // IE8 fallback\n border: 1px solid @dropdown-border;\n border-radius: @border-radius-base;\n .box-shadow(0 6px 12px rgba(0,0,0,.175));\n background-clip: padding-box;\n\n // Aligns the dropdown menu to right\n //\n // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n &.pull-right {\n right: 0;\n left: auto;\n }\n\n // Dividers (basically an hr) within the dropdown\n .divider {\n .nav-divider(@dropdown-divider-bg);\n }\n\n // Links within the dropdown menu\n > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: @line-height-base;\n color: @dropdown-link-color;\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n &:hover,\n &:focus {\n text-decoration: none;\n color: @dropdown-link-hover-color;\n background-color: @dropdown-link-hover-bg;\n }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-active-color;\n text-decoration: none;\n outline: 0;\n background-color: @dropdown-link-active-bg;\n }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-disabled-color;\n }\n\n // Nuke hover/focus effects\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none; // Remove CSS gradient\n .reset-filter();\n cursor: @cursor-disabled;\n }\n}\n\n// Open state for the dropdown\n.open {\n // Show the menu\n > .dropdown-menu {\n display: block;\n }\n\n // Remove the outline when :focus is triggered\n > a {\n outline: 0;\n }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n left: auto; // Reset the default from `.dropdown-menu`\n right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: @font-size-small;\n line-height: @line-height-base;\n color: @dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n // Reverse the caret\n .caret {\n border-top: 0;\n border-bottom: @caret-width-base dashed;\n border-bottom: @caret-width-base solid ~\"\\9\"; // IE8\n content: \"\";\n }\n // Different positioning for bottom up menu\n .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-right {\n .dropdown-menu {\n .dropdown-menu-right();\n }\n // Necessary for overrides of the default right aligned menu.\n // Will remove come v4 in all likelihood.\n .dropdown-menu-left {\n .dropdown-menu-left();\n }\n }\n}\n","// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle; // match .btn alignment given font-size hack above\n > .btn {\n position: relative;\n float: left;\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active,\n &.active {\n z-index: 2;\n }\n }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n .btn + .btn,\n .btn + .btn-group,\n .btn-group + .btn,\n .btn-group + .btn-group {\n margin-left: -1px;\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n margin-left: -5px; // Offset the first child's margin\n &:extend(.clearfix all);\n\n .btn,\n .btn-group,\n .input-group {\n float: left;\n }\n > .btn,\n > .btn-group,\n > .input-group {\n margin-left: 5px;\n }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n margin-left: 0;\n &:not(:last-child):not(.dropdown-toggle) {\n .border-right-radius(0);\n }\n}\n// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-right-radius(0);\n }\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n .box-shadow(none);\n }\n}\n\n\n// Reposition the caret\n.btn .caret {\n margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n border-width: @caret-width-large @caret-width-large 0;\n border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n > .btn,\n > .btn-group,\n > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n }\n\n // Clear floats so dropdown menus can be properly placed\n > .btn-group {\n &:extend(.clearfix all);\n > .btn {\n float: none;\n }\n }\n\n > .btn + .btn,\n > .btn + .btn-group,\n > .btn-group + .btn,\n > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n }\n}\n\n.btn-group-vertical > .btn {\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n &:first-child:not(:last-child) {\n .border-top-radius(@btn-border-radius-base);\n .border-bottom-radius(0);\n }\n &:last-child:not(:first-child) {\n .border-top-radius(0);\n .border-bottom-radius(@btn-border-radius-base);\n }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-bottom-radius(0);\n }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-top-radius(0);\n}\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n > .btn,\n > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n }\n > .btn-group .btn {\n width: 100%;\n }\n\n > .btn-group .dropdown-menu {\n left: auto;\n }\n}\n\n\n// Checkbox and radio options\n//\n// In order to support the browser's form validation feedback, powered by the\n// `required` attribute, we have to \"hide\" the inputs via `clip`. We cannot use\n// `display: none;` or `visibility: hidden;` as that also hides the popover.\n// Simply visually hiding the inputs via `opacity` would leave them clickable in\n// certain cases which is prevented by using `clip` and `pointer-events`.\n// This way, we ensure a DOM element is visible to position the popover from.\n//\n// See https://github.com/twbs/bootstrap/pull/12794 and\n// https://github.com/twbs/bootstrap/pull/14559 for more information.\n\n[data-toggle=\"buttons\"] {\n > .btn,\n > .btn-group > .btn {\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0,0,0,0);\n pointer-events: none;\n }\n }\n}\n","// Single side border-radius\n\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n position: relative; // For dropdowns\n display: table;\n border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n // Undo padding and float of grid classes\n &[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n }\n\n .form-control {\n // Ensure that the input is always above the *appended* addon button for\n // proper border colors.\n position: relative;\n z-index: 2;\n\n // IE9 fubars the placeholder attribute in text inputs and the arrows on\n // select elements in input groups. To fix it, we float the input. Details:\n // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n float: left;\n\n width: 100%;\n margin-bottom: 0;\n\n &:focus {\n z-index: 3;\n }\n }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n .input-lg();\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n .input-sm();\n}\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n font-weight: normal;\n line-height: 1;\n color: @input-color;\n text-align: center;\n background-color: @input-group-addon-bg;\n border: 1px solid @input-group-addon-border-color;\n border-radius: @input-border-radius;\n\n // Sizing\n &.input-sm {\n padding: @padding-small-vertical @padding-small-horizontal;\n font-size: @font-size-small;\n border-radius: @input-border-radius-small;\n }\n &.input-lg {\n padding: @padding-large-vertical @padding-large-horizontal;\n font-size: @font-size-large;\n border-radius: @input-border-radius-large;\n }\n\n // Nuke default margins from checkboxes and radios to vertically center within.\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n margin-top: 0;\n }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n .border-right-radius(0);\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n .border-left-radius(0);\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n position: relative;\n // Jankily prevent input button groups from wrapping with `white-space` and\n // `font-size` in combination with `inline-block` on buttons.\n font-size: 0;\n white-space: nowrap;\n\n // Negative margin for spacing, position for bringing hovered/focused/actived\n // element above the siblings.\n > .btn {\n position: relative;\n + .btn {\n margin-left: -1px;\n }\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active {\n z-index: 2;\n }\n }\n\n // Negative margin to only have a 1px border between the two\n &:first-child {\n > .btn,\n > .btn-group {\n margin-right: -1px;\n }\n }\n &:last-child {\n > .btn,\n > .btn-group {\n z-index: 2;\n margin-left: -1px;\n }\n }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n margin-bottom: 0;\n padding-left: 0; // Override default ul/ol\n list-style: none;\n &:extend(.clearfix all);\n\n > li {\n position: relative;\n display: block;\n\n > a {\n position: relative;\n display: block;\n padding: @nav-link-padding;\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: @nav-link-hover-bg;\n }\n }\n\n // Disabled state sets text to gray and nukes hover/tab effects\n &.disabled > a {\n color: @nav-disabled-link-color;\n\n &:hover,\n &:focus {\n color: @nav-disabled-link-hover-color;\n text-decoration: none;\n background-color: transparent;\n cursor: @cursor-disabled;\n }\n }\n }\n\n // Open dropdowns\n .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @nav-link-hover-bg;\n border-color: @link-color;\n }\n }\n\n // Nav dividers (deprecated with v3.0.1)\n //\n // This should have been removed in v3 with the dropping of `.nav-list`, but\n // we missed it. We don't currently support this anywhere, but in the interest\n // of maintaining backward compatibility in case you use it, it's deprecated.\n .nav-divider {\n .nav-divider();\n }\n\n // Prevent IE8 from misplacing imgs\n //\n // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n > li > a > img {\n max-width: none;\n }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n border-bottom: 1px solid @nav-tabs-border-color;\n > li {\n float: left;\n // Make the list-items overlay the bottom border\n margin-bottom: -1px;\n\n // Actual tabs (as links)\n > a {\n margin-right: 2px;\n line-height: @line-height-base;\n border: 1px solid transparent;\n border-radius: @border-radius-base @border-radius-base 0 0;\n &:hover {\n border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n }\n }\n\n // Active state, and its :hover to override normal :hover\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-tabs-active-link-hover-color;\n background-color: @nav-tabs-active-link-hover-bg;\n border: 1px solid @nav-tabs-active-link-hover-border-color;\n border-bottom-color: transparent;\n cursor: default;\n }\n }\n }\n // pulling this in mainly for less shorthand\n &.nav-justified {\n .nav-justified();\n .nav-tabs-justified();\n }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n > li {\n float: left;\n\n // Links rendered as pills\n > a {\n border-radius: @nav-pills-border-radius;\n }\n + li {\n margin-left: 2px;\n }\n\n // Active state\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-pills-active-link-hover-color;\n background-color: @nav-pills-active-link-hover-bg;\n }\n }\n }\n}\n\n\n// Stacked pills\n.nav-stacked {\n > li {\n float: none;\n + li {\n margin-top: 2px;\n margin-left: 0; // no need for this gap between nav items\n }\n }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n width: 100%;\n\n > li {\n float: none;\n > a {\n text-align: center;\n margin-bottom: 5px;\n }\n }\n\n > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n }\n\n @media (min-width: @screen-sm-min) {\n > li {\n display: table-cell;\n width: 1%;\n > a {\n margin-bottom: 0;\n }\n }\n }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n border-bottom: 0;\n\n > li > a {\n // Override margin from .nav-tabs\n margin-right: 0;\n border-radius: @border-radius-base;\n }\n\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border: 1px solid @nav-tabs-justified-link-border-color;\n }\n\n @media (min-width: @screen-sm-min) {\n > li > a {\n border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n border-radius: @border-radius-base @border-radius-base 0 0;\n }\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border-bottom-color: @nav-tabs-justified-active-link-border-color;\n }\n }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n // make dropdown border overlap tab border\n margin-top: -1px;\n // Remove the top rounded corners here since there is a hard edge above the menu\n .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n margin-bottom: @navbar-margin-bottom;\n border: 1px solid transparent;\n\n // Prevent floats from breaking the navbar\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: @navbar-border-radius;\n }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n overflow-x: visible;\n padding-right: @navbar-padding-horizontal;\n padding-left: @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n &:extend(.clearfix all);\n -webkit-overflow-scrolling: touch;\n\n &.in {\n overflow-y: auto;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border-top: 0;\n box-shadow: none;\n\n &.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0; // Override default setting\n overflow: visible !important;\n }\n\n &.in {\n overflow-y: visible;\n }\n\n // Undo the collapse side padding for navbars with containers to ensure\n // alignment of right-aligned contents.\n .navbar-fixed-top &,\n .navbar-static-top &,\n .navbar-fixed-bottom & {\n padding-left: 0;\n padding-right: 0;\n }\n }\n}\n\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n .navbar-collapse {\n max-height: @navbar-collapse-max-height;\n\n @media (max-device-width: @screen-xs-min) and (orientation: landscape) {\n max-height: 200px;\n }\n }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n > .navbar-header,\n > .navbar-collapse {\n margin-right: -@navbar-padding-horizontal;\n margin-left: -@navbar-padding-horizontal;\n\n @media (min-width: @grid-float-breakpoint) {\n margin-right: 0;\n margin-left: 0;\n }\n }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n z-index: @zindex-navbar;\n border-width: 0 0 1px;\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: @zindex-navbar-fixed;\n\n // Undo the rounded corners\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0; // override .navbar defaults\n border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n float: left;\n padding: @navbar-padding-vertical @navbar-padding-horizontal;\n font-size: @font-size-large;\n line-height: @line-height-computed;\n height: @navbar-height;\n\n &:hover,\n &:focus {\n text-decoration: none;\n }\n\n > img {\n display: block;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n .navbar > .container &,\n .navbar > .container-fluid & {\n margin-left: -@navbar-padding-horizontal;\n }\n }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: @navbar-padding-horizontal;\n padding: 9px 10px;\n .navbar-vertical-align(34px);\n background-color: transparent;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n border-radius: @border-radius-base;\n\n // We remove the `outline` here, but later compensate by attaching `:hover`\n // styles to `:focus`.\n &:focus {\n outline: 0;\n }\n\n // Bars\n .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n }\n .icon-bar + .icon-bar {\n margin-top: 4px;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n display: none;\n }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: @line-height-computed;\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n > li > a,\n .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n > li > a {\n line-height: @line-height-computed;\n &:hover,\n &:focus {\n background-image: none;\n }\n }\n }\n }\n\n // Uncollapse the nav\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin: 0;\n\n > li {\n float: left;\n > a {\n padding-top: @navbar-padding-vertical;\n padding-bottom: @navbar-padding-vertical;\n }\n }\n }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n margin-left: -@navbar-padding-horizontal;\n margin-right: -@navbar-padding-horizontal;\n padding: 10px @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n\n // Mixin behavior for optimum display\n .form-inline();\n\n .form-group {\n @media (max-width: @grid-float-breakpoint-max) {\n margin-bottom: 5px;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n }\n\n // Vertically center in expanded, horizontal navbar\n .navbar-vertical-align(@input-height-base);\n\n // Undo 100% width for pull classes\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n .box-shadow(none);\n }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n .border-top-radius(@navbar-border-radius);\n .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n .navbar-vertical-align(@input-height-base);\n\n &.btn-sm {\n .navbar-vertical-align(@input-height-small);\n }\n &.btn-xs {\n .navbar-vertical-align(22);\n }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n .navbar-vertical-align(@line-height-computed);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin-left: @navbar-padding-horizontal;\n margin-right: @navbar-padding-horizontal;\n }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n//\n// Declared after the navbar components to ensure more specificity on the margins.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-left { .pull-left(); }\n .navbar-right {\n .pull-right();\n margin-right: -@navbar-padding-horizontal;\n\n ~ .navbar-right {\n margin-right: 0;\n }\n }\n}\n\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n background-color: @navbar-default-bg;\n border-color: @navbar-default-border;\n\n .navbar-brand {\n color: @navbar-default-brand-color;\n &:hover,\n &:focus {\n color: @navbar-default-brand-hover-color;\n background-color: @navbar-default-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-default-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-default-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n\n .navbar-toggle {\n border-color: @navbar-default-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-default-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-default-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: @navbar-default-border;\n }\n\n // Dropdown menu items\n .navbar-nav {\n // Remove background color from open dropdown\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-default-link-active-bg;\n color: @navbar-default-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n > li > a {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n }\n }\n\n\n // Links in navbars\n //\n // Add a class to ensure links outside the navbar nav are colored correctly.\n\n .navbar-link {\n color: @navbar-default-link-color;\n &:hover {\n color: @navbar-default-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n }\n }\n }\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n background-color: @navbar-inverse-bg;\n border-color: @navbar-inverse-border;\n\n .navbar-brand {\n color: @navbar-inverse-brand-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-brand-hover-color;\n background-color: @navbar-inverse-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-inverse-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-inverse-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n\n // Darken the responsive nav toggle\n .navbar-toggle {\n border-color: @navbar-inverse-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-inverse-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-inverse-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: darken(@navbar-inverse-bg, 7%);\n }\n\n // Dropdowns\n .navbar-nav {\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-inverse-link-active-bg;\n color: @navbar-inverse-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display\n .open .dropdown-menu {\n > .dropdown-header {\n border-color: @navbar-inverse-border;\n }\n .divider {\n background-color: @navbar-inverse-border;\n }\n > li > a {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n }\n }\n\n .navbar-link {\n color: @navbar-inverse-link-color;\n &:hover {\n color: @navbar-inverse-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n }\n }\n }\n}\n","// Navbar vertical align\n//\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n .clearfix();\n}\n.center-block {\n .center-block();\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n display: none !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n margin-bottom: @line-height-computed;\n list-style: none;\n background-color: @breadcrumb-bg;\n border-radius: @border-radius-base;\n\n > li {\n display: inline-block;\n\n + li:before {\n content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n padding: 0 5px;\n color: @breadcrumb-color;\n }\n }\n\n > .active {\n color: @breadcrumb-active-color;\n }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: @line-height-computed 0;\n border-radius: @border-radius-base;\n\n > li {\n display: inline; // Remove list-style and block-level defaults\n > a,\n > span {\n position: relative;\n float: left; // Collapse white-space\n padding: @padding-base-vertical @padding-base-horizontal;\n line-height: @line-height-base;\n text-decoration: none;\n color: @pagination-color;\n background-color: @pagination-bg;\n border: 1px solid @pagination-border;\n margin-left: -1px;\n }\n &:first-child {\n > a,\n > span {\n margin-left: 0;\n .border-left-radius(@border-radius-base);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius-base);\n }\n }\n }\n\n > li > a,\n > li > span {\n &:hover,\n &:focus {\n z-index: 2;\n color: @pagination-hover-color;\n background-color: @pagination-hover-bg;\n border-color: @pagination-hover-border;\n }\n }\n\n > .active > a,\n > .active > span {\n &,\n &:hover,\n &:focus {\n z-index: 3;\n color: @pagination-active-color;\n background-color: @pagination-active-bg;\n border-color: @pagination-active-border;\n cursor: default;\n }\n }\n\n > .disabled {\n > span,\n > span:hover,\n > span:focus,\n > a,\n > a:hover,\n > a:focus {\n color: @pagination-disabled-color;\n background-color: @pagination-disabled-bg;\n border-color: @pagination-disabled-border;\n cursor: @cursor-disabled;\n }\n }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n","// Pagination\n\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n padding-left: 0;\n margin: @line-height-computed 0;\n list-style: none;\n text-align: center;\n &:extend(.clearfix all);\n li {\n display: inline;\n > a,\n > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: @pager-bg;\n border: 1px solid @pager-border;\n border-radius: @pager-border-radius;\n }\n\n > a:hover,\n > a:focus {\n text-decoration: none;\n background-color: @pager-hover-bg;\n }\n }\n\n .next {\n > a,\n > span {\n float: right;\n }\n }\n\n .previous {\n > a,\n > span {\n float: left;\n }\n }\n\n .disabled {\n > a,\n > a:hover,\n > a:focus,\n > span {\n color: @pager-disabled-color;\n background-color: @pager-bg;\n cursor: @cursor-disabled;\n }\n }\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: @label-color;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n\n // Add hover effects, but only for links\n a& {\n &:hover,\n &:focus {\n color: @label-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Empty labels collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for labels in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n .label-variant(@label-default-bg);\n}\n\n.label-primary {\n .label-variant(@label-primary-bg);\n}\n\n.label-success {\n .label-variant(@label-success-bg);\n}\n\n.label-info {\n .label-variant(@label-info-bg);\n}\n\n.label-warning {\n .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n .label-variant(@label-danger-bg);\n}\n","// Labels\n\n.label-variant(@color) {\n background-color: @color;\n\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base class\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: @font-size-small;\n font-weight: @badge-font-weight;\n color: @badge-color;\n line-height: @badge-line-height;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: @badge-bg;\n border-radius: @badge-border-radius;\n\n // Empty badges collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for badges in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n\n .btn-xs &,\n .btn-group-xs > .btn & {\n top: 0;\n padding: 1px 5px;\n }\n\n // Hover state, but only for links\n a& {\n &:hover,\n &:focus {\n color: @badge-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Account for badges in navs\n .list-group-item.active > &,\n .nav-pills > .active > a > & {\n color: @badge-active-color;\n background-color: @badge-active-bg;\n }\n\n .list-group-item > & {\n float: right;\n }\n\n .list-group-item > & + & {\n margin-right: 5px;\n }\n\n .nav-pills > li > a > & {\n margin-left: 3px;\n }\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n padding-top: @jumbotron-padding;\n padding-bottom: @jumbotron-padding;\n margin-bottom: @jumbotron-padding;\n color: @jumbotron-color;\n background-color: @jumbotron-bg;\n\n h1,\n .h1 {\n color: @jumbotron-heading-color;\n }\n\n p {\n margin-bottom: (@jumbotron-padding / 2);\n font-size: @jumbotron-font-size;\n font-weight: 200;\n }\n\n > hr {\n border-top-color: darken(@jumbotron-bg, 10%);\n }\n\n .container &,\n .container-fluid & {\n border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n\n .container {\n max-width: 100%;\n }\n\n @media screen and (min-width: @screen-sm-min) {\n padding-top: (@jumbotron-padding * 1.6);\n padding-bottom: (@jumbotron-padding * 1.6);\n\n .container &,\n .container-fluid & {\n padding-left: (@jumbotron-padding * 2);\n padding-right: (@jumbotron-padding * 2);\n }\n\n h1,\n .h1 {\n font-size: @jumbotron-heading-font-size;\n }\n }\n}\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n display: block;\n padding: @thumbnail-padding;\n margin-bottom: @line-height-computed;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(border .2s ease-in-out);\n\n > img,\n a > img {\n &:extend(.img-responsive);\n margin-left: auto;\n margin-right: auto;\n }\n\n // Add a hover state for linked versions only\n a&:hover,\n a&:focus,\n a&.active {\n border-color: @link-color;\n }\n\n // Image captions\n .caption {\n padding: @thumbnail-caption-padding;\n color: @thumbnail-caption-color;\n }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n padding: @alert-padding;\n margin-bottom: @line-height-computed;\n border: 1px solid transparent;\n border-radius: @alert-border-radius;\n\n // Headings for larger alerts\n h4 {\n margin-top: 0;\n // Specified for the h4 to prevent conflicts of changing @headings-color\n color: inherit;\n }\n\n // Provide class for links that match alerts\n .alert-link {\n font-weight: @alert-link-font-weight;\n }\n\n // Improve alignment and spacing of inner content\n > p,\n > ul {\n margin-bottom: 0;\n }\n\n > p + p {\n margin-top: 5px;\n }\n}\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.\n.alert-dismissible {\n padding-right: (@alert-padding + 20);\n\n // Adjust close link position\n .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n\n.alert-info {\n .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n\n.alert-warning {\n .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n\n.alert-danger {\n .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","// Alerts\n\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n overflow: hidden;\n height: @line-height-computed;\n margin-bottom: @line-height-computed;\n background-color: @progress-bg;\n border-radius: @progress-border-radius;\n .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: @font-size-small;\n line-height: @line-height-computed;\n color: @progress-bar-color;\n text-align: center;\n background-color: @progress-bar-bg;\n .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n .transition(width .6s ease);\n}\n\n// Striped bars\n//\n// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar-striped` class, which you just add to an existing\n// `.progress-bar`.\n.progress-striped .progress-bar,\n.progress-bar-striped {\n #gradient > .striped();\n background-size: 40px 40px;\n}\n\n// Call animation for the active one\n//\n// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar.active` approach.\n.progress.active .progress-bar,\n.progress-bar.active {\n .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Progress bars\n\n.progress-bar-variant(@color) {\n background-color: @color;\n\n // Deprecated parent class requirement as of v3.2.0\n .progress-striped & {\n #gradient > .striped();\n }\n}\n",".media {\n // Proper spacing between instances of .media\n margin-top: 15px;\n\n &:first-child {\n margin-top: 0;\n }\n}\n\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n\n.media-body {\n width: 10000px;\n}\n\n.media-object {\n display: block;\n\n // Fix collapse in webkit from max-width: 100% and display: table-cell.\n &.img-thumbnail {\n max-width: none;\n }\n}\n\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n\n.media-middle {\n vertical-align: middle;\n}\n\n.media-bottom {\n vertical-align: bottom;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n\n// Media list variation\n//\n// Undo default ul/ol styles\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on