From 4211d02855affeefcb3d7ce2f650efccf278a0ab Mon Sep 17 00:00:00 2001 From: yulichang <570810310@qq.com> Date: Thu, 16 Mar 2023 19:18:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yulichang/toolkit/MPJStringUtils.java | 608 ++++++++++++++++++ .../yulichang/wrapper/MPJAbstractWrapper.java | 3 +- 2 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 mybatis-plus-join-core/src/main/java/com/github/yulichang/toolkit/MPJStringUtils.java diff --git a/mybatis-plus-join-core/src/main/java/com/github/yulichang/toolkit/MPJStringUtils.java b/mybatis-plus-join-core/src/main/java/com/github/yulichang/toolkit/MPJStringUtils.java new file mode 100644 index 0000000..18a597a --- /dev/null +++ b/mybatis-plus-join-core/src/main/java/com/github/yulichang/toolkit/MPJStringUtils.java @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2011-2022, baomidou (jobob@qq.com). + * + * 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.github.yulichang.toolkit; + +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils; +import com.baomidou.mybatisplus.core.toolkit.sql.StringEscape; +import com.baomidou.mybatisplus.core.toolkit.support.BiIntFunction; + +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.stream.Collectors.joining; + +/** + * String 工具类 + * + * @author D.Yang, hcl + * @author hcl + * @since 2016-08-18 + */ +@SuppressWarnings("ALL") +public final class MPJStringUtils { + + /** + * 字符串 is + */ + public static final String IS = "is"; + /** + * 下划线字符 + */ + public static final char UNDERLINE = '_'; + /** + * MP 内定义的 SQL 占位符表达式,匹配诸如 {0},{1},{2} ... 的形式 + */ + public final static Pattern MP_SQL_PLACE_HOLDER = Pattern.compile("[{](?\\d+)}"); + /** + * 验证字符串是否是数据库字段 + */ + private static final Pattern P_IS_COLUMN = Pattern.compile("^\\w\\S*[\\w\\d]*$"); + + /** + * 是否为大写命名 + */ + private static final Pattern CAPITAL_MODE = Pattern.compile("^[0-9A-Z/_]+$"); + + /** + * 字符串去除空白内容 + * + * + */ + private static final Pattern REPLACE_BLANK = Pattern.compile("'|\"|\\<|\\>|&|\\*|\\+|=|#|-|;|\\s*|\t|\r|\n"); + + /** + * 判断字符串中是否全是空白字符 + * + * @param cs 需要判断的字符串 + * @return 如果字符串序列是 null 或者全是空白,返回 true + */ + public static boolean isBlank(CharSequence cs) { + if (cs != null) { + int length = cs.length(); + for (int i = 0; i < length; i++) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + } + return true; + } + + /** + * 对象转为字符串去除左右空格 + * + * @param o 带转换对象 + * @return + */ + public static String toStringTrim(Object o) { + return String.valueOf(o).trim(); + } + + /** + * @see #isBlank(CharSequence) + */ + public static boolean isNotBlank(CharSequence cs) { + return !isBlank(cs); + } + + public static boolean isEmpty(CharSequence cs) { + return cs == null || cs.length() == 0; + } + + public static boolean isNotEmpty(CharSequence cs) { + return !isEmpty(cs); + } + + /** + * 判断字符串是不是驼峰命名 + * + *
  • 包含 '_' 不算
  • + *
  • 首字母大写的不算
  • + * + * @param str 字符串 + * @return 结果 + */ + public static boolean isCamel(String str) { + return Character.isLowerCase(str.charAt(0)) && !str.contains(StringPool.UNDERSCORE); + } + + /** + * 判断字符串是否符合数据库字段的命名 + * + * @param str 字符串 + * @return 判断结果 + */ + public static boolean isNotColumnName(String str) { + return !P_IS_COLUMN.matcher(str).matches(); + } + + /** + * 获取真正的字段名 + * + * @param column 字段名 + * @return 字段名 + */ + public static String getTargetColumn(String column) { + if (isNotColumnName(column)) { + return column.substring(1, column.length() - 1); + } + return column; + } + + /** + * 字符串驼峰转下划线格式 + * + * @param param 需要转换的字符串 + * @return 转换好的字符串 + */ + public static String camelToUnderline(String param) { + if (isBlank(param)) { + return StringPool.EMPTY; + } + int len = param.length(); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = param.charAt(i); + if (Character.isUpperCase(c) && i > 0) { + sb.append(UNDERLINE); + } + sb.append(Character.toLowerCase(c)); + } + return sb.toString(); + } + + /** + * 字符串下划线转驼峰格式 + * + * @param param 需要转换的字符串 + * @return 转换好的字符串 + */ + public static String underlineToCamel(String param) { + if (isBlank(param)) { + return StringPool.EMPTY; + } + String temp = param.toLowerCase(); + int len = temp.length(); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = temp.charAt(i); + if (c == UNDERLINE) { + if (++i < len) { + sb.append(Character.toUpperCase(temp.charAt(i))); + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 首字母转换小写 + * + * @param param 需要转换的字符串 + * @return 转换好的字符串 + */ + public static String firstToLowerCase(String param) { + if (isBlank(param)) { + return StringPool.EMPTY; + } + return param.substring(0, 1).toLowerCase() + param.substring(1); + } + + /** + * 正则表达式匹配 + * + * @param regex 正则表达式字符串 + * @param input 要匹配的字符串 + * @return 如果 input 符合 regex 正则表达式格式, 返回true, 否则返回 false; + */ + public static boolean matches(String regex, String input) { + if (null == regex || null == input) { + return false; + } + return Pattern.matches(regex, input); + } + + /** + * 替换 SQL 语句中的占位符,例如输入 SELECT * FROM test WHERE id = {0} AND name = {1} 会被替换为 + * SELECT * FROM test WHERE id = 1 AND name = 'MP' + *

    + * 当数组中参数不足时,该方法会抛出错误:数组下标越界{@link ArrayIndexOutOfBoundsException} + *

    + * + * @param content 填充内容 + * @param args 填充参数 + */ + public static String sqlArgsFill(String content, Object... args) { + if (MPJStringUtils.isNotBlank(content) && ArrayUtils.isNotEmpty(args)) { + // 索引不能使用,因为 SQL 中的占位符数字与索引不相同 + BiIntFunction handler = (m, i) -> sqlParam(args[Integer.parseInt(m.group("idx"))]); + return replace(content, MP_SQL_PLACE_HOLDER, handler).toString(); + } + return content; + } + + /** + * 根据指定的表达式替换字符串中指定格式的部分 + *

    + * BiIntFunction 中的 第二个 参数将传递 参数在字符串中的索引 + *

    + * + * @param src 源字符串 + * @param ptn 需要替换部分的正则表达式 + * @param replacer 替换处理器 + * @return 返回字符串构建起 + */ + public static StringBuilder replace(CharSequence src, Pattern ptn, BiIntFunction replacer) { + int idx = 0, last = 0, len = src.length(); + Matcher m = ptn.matcher(src); + StringBuilder sb = new StringBuilder(); + + // 扫描一次字符串 + while (m.find()) { + sb.append(src, last, m.start()).append(replacer.apply(m, idx++)); + last = m.end(); + } + // 如果表达式没有匹配或者匹配未到末尾,该判断保证字符串完整性 + if (last < len) { + sb.append(src, last, len); + } + + return sb; + } + + /** + * 获取SQL PARAMS字符串 + */ + public static String sqlParam(Object obj) { + String repStr; + if (obj instanceof Collection) { + repStr = MPJStringUtils.quotaMarkList((Collection) obj); + } else { + repStr = MPJStringUtils.quotaMark(obj); + } + return repStr; + } + + /** + * 使用单引号包含字符串 + * + * @param obj 原字符串 + * @return 单引号包含的原字符串 + */ + public static String quotaMark(Object obj) { + String srcStr = String.valueOf(obj); + if (obj instanceof CharSequence) { + // fix #79 + return StringEscape.escapeString(srcStr); + } + return srcStr; + } + + /** + * 使用单引号包含字符串 + * + * @param coll 集合 + * @return 单引号包含的原字符串的集合形式 + */ + public static String quotaMarkList(Collection coll) { + return coll.stream().map(MPJStringUtils::quotaMark) + .collect(joining(StringPool.COMMA, StringPool.LEFT_BRACKET, StringPool.RIGHT_BRACKET)); + } + + /** + * 拼接字符串第二个字符串第一个字母大写 + */ + public static String concatCapitalize(String concatStr, final String str) { + if (isBlank(concatStr)) { + concatStr = StringPool.EMPTY; + } + if (str == null || str.length() == 0) { + return str; + } + + final char firstChar = str.charAt(0); + if (Character.isTitleCase(firstChar)) { + // already capitalized + return str; + } + + return concatStr + Character.toTitleCase(firstChar) + str.substring(1); + } + + /** + * 判断对象是否不为空 + * + * @param object ignore + * @return ignore + */ + public static boolean checkValNotNull(Object object) { + if (object instanceof CharSequence) { + return isNotEmpty((CharSequence) object); + } + return object != null; + } + + /** + * 判断对象是否为空 + * + * @param object ignore + * @return ignore + */ + public static boolean checkValNull(Object object) { + return !checkValNotNull(object); + } + + /** + * 包含大写字母 + * + * @param word 待判断字符串 + * @return ignore + */ + public static boolean containsUpperCase(String word) { + for (int i = 0; i < word.length(); i++) { + char c = word.charAt(i); + if (Character.isUpperCase(c)) { + return true; + } + } + return false; + } + + /** + * 是否为大写命名 + * + * @param word 待判断字符串 + * @return ignore + */ + public static boolean isCapitalMode(String word) { + return null != word && CAPITAL_MODE.matcher(word).matches(); + } + + /** + * 是否为驼峰下划线混合命名 + * + * @param word 待判断字符串 + * @return ignore + */ + public static boolean isMixedMode(String word) { + return matches(".*[A-Z]+.*", word) && matches(".*[/_]+.*", word); + } + + /** + * 判断是否以某个字符串结尾(区分大小写) + * Check if a String ends with a specified suffix. + *

    + * nulls are handled without exceptions. Two null + * references are considered to be equal. The comparison is case sensitive. + *

    + *

    + *

    +     * StringUtils.endsWith(null, null)      = true
    +     * StringUtils.endsWith(null, "abcdef")  = false
    +     * StringUtils.endsWith("def", null)     = false
    +     * StringUtils.endsWith("def", "abcdef") = true
    +     * StringUtils.endsWith("def", "ABCDEF") = false
    +     * 
    + *

    + * + * @param str the String to check, may be null + * @param suffix the suffix to find, may be null + * @return true if the String ends with the suffix, case + * sensitive, or both null + * @see String#endsWith(String) + * @since 2.4 + */ + public static boolean endsWith(String str, String suffix) { + return endsWith(str, suffix, false); + } + + /** + * Check if a String ends with a specified suffix (optionally case + * insensitive). + * + * @param str the String to check, may be null + * @param suffix the suffix to find, may be null + * @param ignoreCase inidicates whether the compare should ignore case (case + * insensitive) or not. + * @return true if the String starts with the prefix or both + * null + * @see String#endsWith(String) + */ + private static boolean endsWith(String str, String suffix, boolean ignoreCase) { + if (str == null || suffix == null) { + return (str == null && suffix == null); + } + if (suffix.length() > str.length()) { + return false; + } + int strOffset = str.length() - suffix.length(); + return str.regionMatches(ignoreCase, strOffset, suffix, 0, suffix.length()); + } + + /** + * 是否为CharSequence类型 + * + * @param clazz class + * @return true 为是 CharSequence 类型 + */ + public static boolean isCharSequence(Class clazz) { + return clazz != null && CharSequence.class.isAssignableFrom(clazz); + } + + /** + * 前n个首字母小写,之后字符大小写的不变 + * + * @param rawString 需要处理的字符串 + * @param index 多少个字符(从左至右) + * @return ignore + */ + public static String prefixToLower(String rawString, int index) { + StringBuilder field = new StringBuilder(); + field.append(rawString.substring(0, index).toLowerCase()); + field.append(rawString.substring(index)); + return field.toString(); + } + + /** + * 删除字符前缀之后,首字母小写,之后字符大小写的不变 + *

    StringUtils.removePrefixAfterPrefixToLower( "isUser", 2 ) = user

    + *

    StringUtils.removePrefixAfterPrefixToLower( "isUserInfo", 2 ) = userInfo

    + * + * @param rawString 需要处理的字符串 + * @param index 删除多少个字符(从左至右) + * @return ignore + */ + public static String removePrefixAfterPrefixToLower(String rawString, int index) { + return prefixToLower(rawString.substring(index), 1); + } + + /** + * 驼峰转连字符 + *

    StringUtils.camelToHyphen( "managerAdminUserService" ) = manager-admin-user-service

    + * + * @param input ignore + * @return 以'-'分隔 + * @see document + */ + public static String camelToHyphen(String input) { + return wordsToHyphenCase(wordsAndHyphenAndCamelToConstantCase(input)); + } + + private static String wordsAndHyphenAndCamelToConstantCase(String input) { + StringBuilder buf = new StringBuilder(); + char previousChar = ' '; + char[] chars = input.toCharArray(); + for (char c : chars) { + boolean isUpperCaseAndPreviousIsLowerCase = (Character.isLowerCase(previousChar)) && (Character.isUpperCase(c)); + + boolean previousIsWhitespace = Character.isWhitespace(previousChar); + boolean lastOneIsNotUnderscore = (buf.length() > 0) && (buf.charAt(buf.length() - 1) != '_'); + boolean isNotUnderscore = c != '_'; + if (lastOneIsNotUnderscore && (isUpperCaseAndPreviousIsLowerCase || previousIsWhitespace)) { + buf.append(StringPool.UNDERSCORE); + } else if ((Character.isDigit(previousChar) && Character.isLetter(c))) { + buf.append(UNDERLINE); + } + if ((shouldReplace(c)) && (lastOneIsNotUnderscore)) { + buf.append(UNDERLINE); + } else if (!Character.isWhitespace(c) && (isNotUnderscore || lastOneIsNotUnderscore)) { + buf.append(Character.toUpperCase(c)); + } + previousChar = c; + } + if (Character.isWhitespace(previousChar)) { + buf.append(StringPool.UNDERSCORE); + } + return buf.toString(); + } + + private static boolean shouldReplace(char c) { + return (c == '.') || (c == '_') || (c == '-'); + } + + private static String wordsToHyphenCase(String s) { + StringBuilder buf = new StringBuilder(); + char lastChar = 'a'; + for (char c : s.toCharArray()) { + if ((Character.isWhitespace(lastChar)) && (!Character.isWhitespace(c)) + && ('-' != c) && (buf.length() > 0) + && (buf.charAt(buf.length() - 1) != '-')) { + buf.append(StringPool.DASH); + } + if ('_' == c) { + buf.append(StringPool.DASH); + } else if ('.' == c) { + buf.append(StringPool.DASH); + } else if (!Character.isWhitespace(c)) { + buf.append(Character.toLowerCase(c)); + } + lastChar = c; + } + if (Character.isWhitespace(lastChar)) { + buf.append(StringPool.DASH); + } + return buf.toString(); + } + + /** + *

    比较两个字符串,相同则返回true。字符串可为null

    + * + *

    对字符串大小写敏感

    + * + *
    +     * StringUtils.equals(null, null)   = true
    +     * StringUtils.equals(null, "abc")  = false
    +     * StringUtils.equals("abc", null)  = false
    +     * StringUtils.equals("abc", "abc") = true
    +     * StringUtils.equals("abc", "ABC") = false
    +     * 
    + * + * @param cs1 第一个字符串, 可为 {@code null} + * @param cs2 第二个字符串, 可为 {@code null} + * @return {@code true} 如果两个字符串相同, 或者都为 {@code null} + * @see Object#equals(Object) + */ + public static boolean equals(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return true; + } + if (cs1 == null || cs2 == null) { + return false; + } + if (cs1.length() != cs2.length()) { + return false; + } + if (cs1 instanceof String && cs2 instanceof String) { + return cs1.equals(cs2); + } + // Step-wise comparison + final int length = cs1.length(); + for (int i = 0; i < length; i++) { + if (cs1.charAt(i) != cs2.charAt(i)) { + return false; + } + } + return true; + } + + /** + * SQL 注入字符串去除空白内容: + *
      + *
    • \n 回车
    • + *
    • \t 水平制表符
    • + *
    • \s 空格
    • + *
    • \r 换行
    • + *
    + * + * @param str 字符串 + */ + public static String sqlInjectionReplaceBlank(String str) { + if (SqlInjectionUtils.check(str)) { + /** + * 过滤sql黑名单字符,存在 SQL 注入,去除空白内容 + */ + Matcher matcher = REPLACE_BLANK.matcher(str); + str = matcher.replaceAll(""); + + } + return str; + } +} diff --git a/mybatis-plus-join-core/src/main/java/com/github/yulichang/wrapper/MPJAbstractWrapper.java b/mybatis-plus-join-core/src/main/java/com/github/yulichang/wrapper/MPJAbstractWrapper.java index dc47144..668b1de 100644 --- a/mybatis-plus-join-core/src/main/java/com/github/yulichang/wrapper/MPJAbstractWrapper.java +++ b/mybatis-plus-join-core/src/main/java/com/github/yulichang/wrapper/MPJAbstractWrapper.java @@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.toolkit.sql.SqlUtils; import com.baomidou.mybatisplus.core.toolkit.sql.StringEscape; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.github.yulichang.toolkit.LambdaUtils; +import com.github.yulichang.toolkit.MPJStringUtils; import com.github.yulichang.toolkit.TableList; import com.github.yulichang.toolkit.sql.SqlScriptUtils; import com.github.yulichang.wrapper.enums.PrefixEnum; @@ -959,7 +960,7 @@ public abstract class MPJAbstractWrapper