• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

future-architect / uroborosql / #773

13 Sep 2024 06:12PM UTC coverage: 90.173% (-0.2%) from 90.35%
#773

push

web-flow
add ReplCommandLogEventSubscriber and remove Repl Command output logic from SqlAgentImpl.java (#333)

29 of 29 new or added lines in 1 file covered. (100.0%)

18 existing lines in 2 files now uncovered.

8873 of 9840 relevant lines covered (90.17%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.71
/src/main/java/jp/co/future/uroborosql/client/SqlParamUtils.java
1
/**
2
 * Copyright (c) 2017-present, Future Corporation
3
 *
4
 * This source code is licensed under the MIT license found in the
5
 * LICENSE file in the root directory of this source tree.
6
 */
7
package jp.co.future.uroborosql.client;
8

9
import java.math.BigDecimal;
10
import java.time.LocalDate;
11
import java.time.LocalDateTime;
12
import java.time.LocalTime;
13
import java.time.OffsetDateTime;
14
import java.time.OffsetTime;
15
import java.time.ZoneId;
16
import java.time.ZonedDateTime;
17
import java.time.format.DateTimeFormatter;
18
import java.time.format.DateTimeParseException;
19
import java.util.ArrayList;
20
import java.util.Arrays;
21
import java.util.Date;
22
import java.util.LinkedHashSet;
23
import java.util.List;
24
import java.util.Objects;
25
import java.util.Optional;
26
import java.util.Set;
27
import java.util.regex.Pattern;
28
import java.util.stream.Collectors;
29

30
import jp.co.future.uroborosql.config.SqlConfig;
31
import jp.co.future.uroborosql.context.ExecutionContext;
32
import jp.co.future.uroborosql.expr.ExpressionParser;
33
import jp.co.future.uroborosql.node.BindVariableNode;
34
import jp.co.future.uroborosql.node.EmbeddedValueNode;
35
import jp.co.future.uroborosql.node.IfNode;
36
import jp.co.future.uroborosql.node.Node;
37
import jp.co.future.uroborosql.node.ParenBindVariableNode;
38
import jp.co.future.uroborosql.parameter.Parameter;
39
import jp.co.future.uroborosql.parser.SqlParser;
40
import jp.co.future.uroborosql.parser.SqlParserImpl;
41
import jp.co.future.uroborosql.utils.ObjectUtils;
42

43
/**
44
 * Sqlのバインドパラメータを操作するユーティリティ
45
 *
46
 * @author H.Sugimoto
47
 *
48
 */
49
public final class SqlParamUtils {
50
        /** 数字かどうかを判定するための正規表現 */
51
        private static final Pattern NUMBER_PAT = Pattern.compile("^[\\-\\+]?[1-9][0-9]*([Ll]|\\.\\d+[FfDd])?$");
1✔
52

53
        /** 入力内容の中で[]で囲まれた値を置換するための正規表現 */
54
        private static final Pattern PARAM_PAT = Pattern.compile("\\[(.+?)\\]");
1✔
55

56
        /**
57
         * コンストラクタ
58
         */
59
        private SqlParamUtils() {
60
                // do nothing
61
        }
62

63
        /**
64
         * 入力内容を解析し、パラメータの配列に変換する.
65
         *
66
         * @param line 入力内容
67
         * @return 入力内容をパラメータに分割した配列
68
         */
69
        public static String[] parseLine(final String line) {
70
                var sb = new StringBuilder();
1✔
71
                var matcher = PARAM_PAT.matcher(line);
1✔
72
                while (matcher.find()) {
1✔
73
                        var arrayPart = matcher.group();
1✔
74
                        matcher.appendReplacement(sb, arrayPart.replaceAll("\\s*,\\s*", ","));
1✔
75
                }
1✔
76
                matcher.appendTail(sb);
1✔
77

78
                var idx = 0;
1✔
79
                var parts = new ArrayList<String>();
1✔
80
                var bracketFlag = false;
1✔
81
                var singleQuoteFlag = false;
1✔
82
                var part = new StringBuilder();
1✔
83
                while (sb.length() > idx) {
1✔
84
                        var c = sb.charAt(idx++);
1✔
85
                        if (Character.isWhitespace(c)) {
1✔
86
                                if (bracketFlag || singleQuoteFlag) {
1✔
87
                                        // 囲み文字の中なのでそのまま追加する
88
                                        part.append(c);
1✔
89
                                } else {
90
                                        parts.add(part.toString());
1✔
91
                                        part = new StringBuilder();
1✔
92
                                }
93
                        } else {
94
                                if (c == '[') {
1✔
95
                                        bracketFlag = true;
1✔
96
                                } else if (c == ']') {
1✔
97
                                        bracketFlag = false;
1✔
98
                                } else if (c == '\'') {
1✔
99
                                        singleQuoteFlag = !singleQuoteFlag;
1✔
100
                                }
101
                                part.append(c);
1✔
102
                        }
103
                }
1✔
104
                if (part.length() > 0) {
1✔
105
                        parts.add(part.toString());
1✔
106
                }
107
                return parts.toArray(new String[parts.size()]);
1✔
108
        }
109

110
        /**
111
         * SQLバインドパラメータを設定する
112
         * @param sqlConfig SqlConfig
113
         * @param ctx ExecutionContext
114
         * @param paramsArray パラメータ配列
115
         */
116
        public static void setSqlParams(final SqlConfig sqlConfig, final ExecutionContext ctx,
117
                        final String... paramsArray) {
118
                var bindParams = getSqlParams(ctx.getSql(), sqlConfig);
1✔
119

120
                for (var element : paramsArray) {
1✔
121
                        var param = element.split("=");
1✔
122
                        var key = param[0];
1✔
123
                        if (bindParams.remove(key)) {
1✔
124
                                // キーがバインドパラメータに存在するときは値を設定する
125
                                if (param.length == 1) {
1✔
126
                                        // キーだけの指定は値をnullと扱う
127
                                        ctx.param(key, null);
1✔
128
                                } else {
129
                                        var val = param[1];
1✔
130
                                        setParam(ctx, key, val);
1✔
131
                                }
132
                        }
133
                }
134

135
                // 指定がなかったキーについてはnullを設定する
136
                bindParams.forEach(s -> ctx.param(s, null));
1✔
137
        }
1✔
138

139
        /**
140
         * パラメータリストをREPLコマンド用の引数文字列に変換する.
141
         *
142
         * @param params 変換するパラメータのリスト
143
         * @return REPLコマンド用の引数文字列
144
         */
145
        public static String formatPrams(final List<Parameter> params) {
146
                return params.stream()
1✔
147
                                .map(SqlParamUtils::formatParam)
1✔
148
                                .collect(Collectors.joining(" "));
1✔
149
        }
150

151
        /**
152
         * パラメータをREPLコマンド用引数文字列に変換する.
153
         * @param param 変換対象パラメータ
154
         * @return REPLコマンド引数文字列
155
         */
156
        public static String formatParam(final Parameter param) {
157
                return param.getParameterName() + "=" + formatParamValue(param.getValue());
1✔
158
        }
159

160
        /**
161
         * パラメータの値をREPLコマンド用の値文字列に変換する.
162
         * @param val 変換対象の値
163
         * @return REPLコマンド用の値文字列
164
         */
165
        private static String formatParamValue(final Object val) {
166
                if (Objects.isNull(val)) {
1✔
UNCOV
167
                        return "[NULL]";
×
168
                } else if (val instanceof Integer) {
1✔
UNCOV
169
                        return val.toString();
×
170
                } else if (val instanceof Long) {
1✔
171
                        return val.toString() + "L";
×
172
                } else if (val instanceof Float) {
1✔
173
                        return val.toString() + "F";
×
174
                } else if (val instanceof Double) {
1✔
175
                        return val.toString() + "D";
×
176
                } else if (val instanceof BigDecimal) {
1✔
177
                        return ((BigDecimal) val).toPlainString();
1✔
178
                } else if (val instanceof Date) {
1✔
UNCOV
179
                        var date = ((Date) val).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
×
UNCOV
180
                        return "'" + DateTimeFormatter.ISO_DATE.format(date) + "'";
×
181
                } else if (val instanceof LocalDate) {
1✔
UNCOV
182
                        return "'" + DateTimeFormatter.ISO_DATE.format((LocalDate) val) + "'";
×
183
                } else if (val instanceof LocalDateTime) {
1✔
UNCOV
184
                        return "'" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format((LocalDateTime) val) + "'";
×
185
                } else if (val instanceof OffsetDateTime) {
1✔
186
                        return "'" + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((OffsetDateTime) val) + "'";
×
187
                } else if (val instanceof ZonedDateTime) {
1✔
188
                        return "'" + DateTimeFormatter.ISO_ZONED_DATE_TIME.format((ZonedDateTime) val) + "'";
×
189
                } else if (val instanceof LocalTime) {
1✔
UNCOV
190
                        return "'" + DateTimeFormatter.ISO_LOCAL_TIME.format((LocalTime) val) + "'";
×
191
                } else if (val instanceof OffsetTime) {
1✔
192
                        return "'" + DateTimeFormatter.ISO_OFFSET_TIME.format((OffsetTime) val) + "'";
×
193
                } else if (val instanceof Optional) {
1✔
UNCOV
194
                        return formatParamValue(((Optional<?>) val).orElse(null));
×
195
                } else if (val instanceof List) {
1✔
UNCOV
196
                        var list = (List<?>) val;
×
UNCOV
197
                        return list.stream()
×
UNCOV
198
                                        .map(SqlParamUtils::formatParamValue)
×
UNCOV
199
                                        .collect(Collectors.joining(",", "[", "]"));
×
200
                } else if (val.getClass().isArray()) {
1✔
UNCOV
201
                        var arr = (Object[]) val;
×
UNCOV
202
                        return Arrays.stream(arr)
×
UNCOV
203
                                        .map(SqlParamUtils::formatParamValue)
×
UNCOV
204
                                        .collect(Collectors.joining(",", "[", "]"));
×
205
                } else if ("".equals(val)) {
1✔
UNCOV
206
                        return "[EMPTY]";
×
207
                } else {
208
                        var str = val.toString();
1✔
209
                        if (str.contains(" ")) {
1✔
210
                                return "'" + str + "'";
1✔
211
                        } else {
212
                                return str;
1✔
213
                        }
214
                }
215

216
        }
217

218
        /**
219
         * 1つのパラメータの設定
220
         *
221
         * パラメータ値は以下の表記が可能
222
         * <dl>
223
         *         <dh>[NULL]</dh>
224
         *  <dd><code>null</code>を設定する</dd>
225
         *         <dh>[EMPTY]</dh>
226
         *  <dd>""(空文字)を設定する</dd>
227
         *  <dh>'値'</dh>
228
         *  <dd>文字列として設定する. 空白を含めることもできる</dd>
229
         *         <dh>[値1,値2,...]</dh>
230
         *  <dd>配列として設定する</dd>
231
         *         <dh>その他</dh>
232
         *  <dd>文字列として設定する</dd>
233
         * </dl>
234
         *
235
         * @param ctx ExecutionContext
236
         * @param key パラメータキー
237
         * @param val パラメータ値
238
         */
239
        private static void setParam(final ExecutionContext ctx, final String key, final String val) {
240
                if (val.startsWith("[") && val.endsWith("]") && !"[NULL]".equals(val) && !"[EMPTY]".equals(val)) {
1✔
241
                        // [] で囲まれた値は配列に変換する。ex) [1, 2] => {"1", "2"}
242
                        var parts = val.substring(1, val.length() - 1).split("\\s*,\\s*");
1✔
243
                        var vals = new Object[parts.length];
1✔
244
                        for (var i = 0; i < parts.length; i++) {
1✔
245
                                vals[i] = convertSingleValue(parts[i]);
1✔
246
                        }
247
                        ctx.param(key, List.of(vals));
1✔
248
                } else {
1✔
249
                        ctx.param(key, convertSingleValue(val));
1✔
250
                }
251
        }
1✔
252

253
        /**
254
         * パラメータで渡された単独の値を型変換する
255
         *
256
         * @param val 値の文字列
257
         * @return 変換後オブジェクト
258
         */
259
        private static Object convertSingleValue(final String val) {
260
                var value = val == null ? null : val.trim();
1✔
261
                if (ObjectUtils.isEmpty(value) || "[NULL]".equalsIgnoreCase(value)) {
1✔
262
                        return null;
1✔
263
                } else if ("[EMPTY]".equalsIgnoreCase(value)) {
1✔
264
                        return "";
1✔
265
                } else if (value.startsWith("'") && value.endsWith("'")) {
1✔
266
                        // ''で囲まれた値は文字列として扱う。空白を含むこともできる。 ex) 'This is a pen'
267
                        return value.substring(1, value.length() - 1);
1✔
268
                } else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
1✔
269
                        return Boolean.TRUE;
1✔
270
                } else if (Boolean.FALSE.toString().equalsIgnoreCase(value)) {
1✔
271
                        return Boolean.FALSE;
1✔
272
                } else if (isNumber(value)) {
1✔
273
                        return createNumber(value);
1✔
274
                } else {
275
                        try {
276
                                // 日時に変換できるか
277
                                return LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
1✔
278
                        } catch (DateTimeParseException ex) {
1✔
279
                                // do nothing
280
                        }
281

282
                        try {
283
                                // 日付に変換できるか?
284
                                return LocalDate.parse(value, DateTimeFormatter.ISO_DATE);
1✔
285
                        } catch (DateTimeParseException ex) {
1✔
286
                                // do nothing
287
                        }
288

289
                        try {
290
                                // 時刻に変換できるか?
291
                                return LocalTime.parse(value, DateTimeFormatter.ISO_TIME);
1✔
292
                        } catch (DateTimeParseException ex) {
1✔
293
                                // do nothing
294
                        }
295
                        return value;
1✔
296
                }
297
        }
298

299
        /**
300
         * 判定対象文字列が数字かどうかを判定する.
301
         *
302
         * @param val 判定対象文字列
303
         * @return 数字の場合は<code>true</code>
304
         */
305
        private static boolean isNumber(final String val) {
306
                if (ObjectUtils.isEmpty(val)) {
1✔
307
                        return false;
×
308
                } else {
309
                        return NUMBER_PAT.matcher(val).matches();
1✔
310
                }
311
        }
312

313
        /**
314
         * 指定された文字列から適切なNumber型のオブジェクトを生成する.<br>
315
         * <ul>
316
         * <li>1000 -> (Integer)1000</li>
317
         * <li>+1000 -> (Integer)1000</li>
318
         * <li>-1000 -> (Integer)-1000</li>
319
         * <li>1000L -> (Long)1000L</li>
320
         * <li>+1000L -> (Long)1000L</li>
321
         * <li>-1000L -> (Long)-1000L</li>
322
         * <li>1000.01F -> (Float)1000.01F</li>
323
         * <li>+1000.01F -> (Float)1000.01F</li>
324
         * <li>-1000.01F -> (Float)-1000.01F</li>
325
         * <li>1000.01D -> (Double)1000.01D</li>
326
         * <li>+1000.01D -> (Double)1000.01D</li>
327
         * <li>-1000.01D -> (Double)-1000.01D</li>
328
         * </ul>
329
         * (※)各Number型で桁あふれした場合はBigDecimal型が返却される
330
         *
331
         * @param val 変換対象文字列
332
         * @return Number型のオブジェクト
333
         */
334
        private static Number createNumber(final String val) {
335
                // suffixがある場合はsuffixと数値部分を分離する
336
                var suffix = val.substring(val.length() - 1);
1✔
337
                var num = val;
1✔
338
                if ("0".compareTo(suffix) <= 0 && "9".compareTo(suffix) >= 0) {
1✔
339
                        suffix = "";
1✔
340
                } else {
341
                        num = val.substring(0, val.length() - 1);
1✔
342
                }
343

344
                var decimal = new BigDecimal(num);
1✔
345
                try {
346
                        if ("L".equalsIgnoreCase(suffix)) {
1✔
347
                                return decimal.longValueExact();
1✔
348
                        } else if ("F".equalsIgnoreCase(suffix)) {
1✔
349
                                return decimal.floatValue();
1✔
350
                        } else if ("D".equalsIgnoreCase(suffix)) {
1✔
351
                                return decimal.doubleValue();
1✔
352
                        } else {
353
                                return decimal.intValueExact();
1✔
354
                        }
355
                } catch (ArithmeticException ex) {
1✔
356
                        // BigDecimalから指定の型に変換できない場合はBigDecimalを返す
357
                        return decimal;
1✔
358
                }
359
        }
360

361
        /**
362
         * SQLパラメータの解析
363
         *
364
         * @param sql 解析対象SQL
365
         * @param sqlConfig SqlConfig
366
         * @return SQLを解析して取得したパラメータキーのセット
367
         */
368
        public static Set<String> getSqlParams(final String sql, final SqlConfig sqlConfig) {
369
                SqlParser parser = new SqlParserImpl(sql, sqlConfig.getExpressionParser(),
1✔
370
                                sqlConfig.getDialect().isRemoveTerminator(), true);
1✔
371
                var transformer = parser.parse();
1✔
372
                var rootNode = transformer.getRoot();
1✔
373

374
                Set<String> params = new LinkedHashSet<>();
1✔
375
                traverseNode(sqlConfig.getExpressionParser(), rootNode, params);
1✔
376
                var constPattern = Pattern
1✔
377
                                .compile("^" + sqlConfig.getExecutionContextProvider().getConstParamPrefix() + "[A-Z][A-Z0-9_-]*$");
1✔
378
                params.removeIf(s -> constPattern.matcher(s).matches());
1✔
379
                return params;
1✔
380
        }
381

382
        /**
383
         * SQLの探索
384
         *
385
         * @param parser ExpressionParser
386
         * @param node SQLノード
387
         * @param params パラメータが見つかった場合に格納するSetオブジェクト
388
         */
389
        private static void traverseNode(final ExpressionParser parser, final Node node, final Set<String> params) {
390
                if (node instanceof BindVariableNode) {
1✔
391
                        params.add(((BindVariableNode) node).getExpression());
1✔
392
                } else if (node instanceof ParenBindVariableNode) {
1✔
393
                        params.add(((ParenBindVariableNode) node).getExpression());
1✔
394
                } else if (node instanceof EmbeddedValueNode) {
1✔
395
                        params.add(((EmbeddedValueNode) node).getExpression());
1✔
396
                } else if (node instanceof IfNode) {
1✔
397
                        traverseIfNode(parser, (IfNode) node, params);
1✔
398
                } else {
399
                        for (var i = 0; i < node.getChildSize(); i++) {
1✔
400
                                traverseNode(parser, node.getChild(i), params);
1✔
401
                        }
402
                }
403
        }
1✔
404

405
        /**
406
         * SQLの探索(IF分岐)
407
         *
408
         * @param parser ExpressionParser
409
         * @param ifNode SQL IFノード
410
         * @param params パラメータが見つかった場合に格納するSetオブジェクト
411
         */
412
        private static void traverseIfNode(final ExpressionParser parser, final IfNode ifNode, final Set<String> params) {
413
                parser.parse(ifNode.getExpression()).collectParams(params);
1✔
414

415
                for (var i = 0; i < ifNode.getChildSize(); i++) {
1✔
416
                        traverseNode(parser, ifNode.getChild(i), params);
1✔
417
                }
418
                if (ifNode.getElseIfNode() != null) {
1✔
419
                        traverseIfNode(parser, ifNode.getElseIfNode(), params);
1✔
420
                }
421
                if (ifNode.getElseNode() != null) {
1✔
422
                        traverseNode(parser, ifNode.getElseNode(), params);
1✔
423
                }
424
        }
1✔
425
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc