001/*
002 *  Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.util;
017
018import com.mybatisflex.core.audit.AuditMessage;
019import com.mybatisflex.core.mybatis.TypeHandlerObject;
020
021import java.lang.reflect.Array;
022import java.lang.reflect.Proxy;
023import java.sql.PreparedStatement;
024import java.sql.SQLException;
025import java.time.LocalDateTime;
026import java.time.format.DateTimeFormatter;
027import java.util.Date;
028import java.util.StringJoiner;
029import java.util.regex.Matcher;
030
031import static com.mybatisflex.core.constant.SqlConsts.*;
032
033public class SqlUtil {
034
035    private SqlUtil() {
036    }
037
038    public static void keepColumnSafely(String column) {
039        if (StringUtil.noText(column)) {
040            throw new IllegalArgumentException("Column must not be empty");
041        } else {
042            column = column.trim();
043        }
044
045        int strLen = column.length();
046        for (int i = 0; i < strLen; ++i) {
047            char ch = column.charAt(i);
048            if (Character.isWhitespace(ch)) {
049                throw new IllegalArgumentException("Column must not has space char.");
050            }
051            if (isUnSafeChar(ch)) {
052                throw new IllegalArgumentException("Column has unsafe char: [" + ch + "].");
053            }
054        }
055    }
056
057
058    /**
059     * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
060     */
061    private static final String SQL_ORDER_BY_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
062
063    public static void keepOrderBySqlSafely(String value) {
064        if (!value.matches(SQL_ORDER_BY_PATTERN)) {
065            throw new IllegalArgumentException("Order By sql not safe, order by string: " + value);
066        }
067    }
068
069
070    private static final char[] UN_SAFE_CHARS = "'`\"<>&+=#-;".toCharArray();
071
072    private static boolean isUnSafeChar(char ch) {
073        for (char c : UN_SAFE_CHARS) {
074            if (c == ch) {
075                return true;
076            }
077        }
078        return false;
079    }
080
081
082    /**
083     * 根据数据库响应结果判断数据库操作是否成功。
084     *
085     * @param result 数据库操作返回影响条数
086     * @return {@code true} 操作成功,{@code false} 操作失败。
087     */
088    public static boolean toBool(int result) {
089        return result > 0 || result == -2;
090    }
091
092    public static boolean toBool(long result) {
093        return result > 0;
094    }
095
096
097    /**
098     * 根据数据库响应结果判断数据库操作是否成功。
099     * 有 1 条数据成功便算成功
100     *
101     * @param results 操作数据的响应成功条数
102     * @return {@code true} 操作成功,{@code false} 操作失败。
103     */
104    public static boolean toBool(int[] results) {
105        for (int result : results) {
106            if (toBool(result)) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113
114    /**
115     * 替换 sql 中的问号 ?
116     *
117     * @param sql    sql 内容
118     * @param params 参数
119     * @return 完整的 sql
120     */
121    public static String replaceSqlParams(String sql, Object[] params) {
122        if (params == null || params.length == 0) {
123            return sql;
124        }
125
126        StringBuilder sqlBuilder = new StringBuilder();
127        char quote = 0;
128        int index = 0;
129        for (int i = 0; i < sql.length(); ++i) {
130            char ch = sql.charAt(i);
131            if (ch == '\'') {
132                if (quote == 0) {
133                    quote = ch;
134                } else if (quote == '\'') {
135                    quote = 0;
136                }
137            } else if (ch == '"') {
138                if (quote == 0) {
139                    quote = ch;
140                } else if (quote == '"') {
141                    quote = 0;
142                }
143            }
144            if (quote == 0 && ch == '?' && index < params.length) {
145                sqlBuilder.append(getParamString(params, index++));
146            } else {
147                sqlBuilder.append(ch);
148            }
149        }
150
151        return sqlBuilder.toString();
152    }
153
154
155    private static String getParamString(Object[] params, int index) {
156        Object value = params[index];
157        if (value == null) {
158            return "null";
159        }
160        // number or bool
161        else if (value instanceof Number || value instanceof Boolean) {
162            return value.toString();
163        }
164        // array
165        else if (ClassUtil.isArray(value.getClass())) {
166            StringJoiner joiner = new StringJoiner(",", "[", "]");
167            for (int i = 0; i < Array.getLength(value); i++) {
168                joiner.add(String.valueOf(Array.get(value, i)));
169            }
170            return joiner.toString();
171        }
172        // TypeHandlerObject
173        else if (value instanceof TypeHandlerObject) {
174            TypeHandlerObject handlerObject = (TypeHandlerObject) value;
175            String[] paramArray = new String[1];
176            PreparedStatement preparedStatement = createPreparedStatement(paramArray);
177            try {
178                handlerObject.setParameter(preparedStatement, 0);
179            } catch (SQLException e) {
180                throw new RuntimeException(e);
181            }
182            return paramArray[0];
183        }
184
185        // enum
186        else if (value.getClass().isEnum()) {
187            EnumWrapper<?> ew = EnumWrapper.of(value.getClass());
188            Object enumValue = ew.getEnumValue(value);
189            return enumValue.toString();
190        }
191
192        // other
193        else {
194            StringBuilder sb = new StringBuilder();
195            sb.append("'");
196            if (value instanceof Date) {
197                sb.append(DateUtil.toDateTimeString((Date) value));
198            } else if (value instanceof LocalDateTime) {
199                sb.append(((LocalDateTime) value).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
200            } else {
201                sb.append(value);
202            }
203            sb.append("'");
204            return Matcher.quoteReplacement(sb.toString());
205        }
206    }
207
208
209    public static String buildSqlParamPlaceholder(int count) {
210        StringBuilder sb = new StringBuilder(BRACKET_LEFT);
211        for (int i = 0; i < count; i++) {
212            sb.append(PLACEHOLDER);
213            if (i != count - 1) {
214                sb.append(DELIMITER);
215            }
216        }
217        sb.append(BRACKET_RIGHT);
218        return sb.toString();
219    }
220
221
222    private static PreparedStatement createPreparedStatement(String[] params) {
223        return (PreparedStatement) Proxy.newProxyInstance(
224            AuditMessage.class.getClassLoader(),
225            new Class[]{PreparedStatement.class}, (proxy, method, args) -> {
226                if (args != null && (args.length == 2 || args.length == 3)) {
227                    params[0] = args[1].toString();
228                }
229                return null;
230            });
231    }
232}