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

future-architect / uroborosql / #743

28 Jun 2024 04:17PM UTC coverage: 90.988% (+0.5%) from 90.484%
#743

push

web-flow
add paramIfNotEmpty method (#318)

* v0.x to master merge

* fix nanoseconds diff (java8 to java11)

* eclipse cleanup and  optimize import

* eclipse format

* optimize Imports

* remove unused annotation

* library version up

* migrate v0.x to master
- update java version 8 to 11
- update junit4 to junit5
- library version update

* fix test failed

* remove unused annotation

* fixed bug

* use var

* fix java8 coding style

* Refactoring TransactionContextManager

* Refactoring SqlAgent

* 途中コミット

* fix testcase

* fix review

* fix javadoc comments

* merge v0.x PR

* cleanup code

* cleanup test code

* change build status badge

* - agent.query, update, proc, batch にそれぞれSupplierを引数にとるメソッドを追加
- SqlQuery.paramとSqlEntityUpdate.setにFunctionを受け取るメソッドを追加

* testcaseの整形

* - SqlFluent と ExtractionCondition の分離
- Arrays.asList -> List.of への変更
- テストの整形

* - v0.x系の不具合対応の追いつき
- ログ出力の整理

* - SqlKindの整理(ENTITY_XXXの追加)
- REPL_LOGの追加
- Deprecatedメソッドの削除(SqlAgent)
- SqlAgent, ExecutionContextでsetterをfluent APIに変更

* DB接続URLの修正

* add and fix testcases.

* add event testcases.

* fix typo

* add paramIfNotEmpty method and StringUtils rename ObjectUtils

* fix review comments.

* remove unused import

1695 of 1958 new or added lines in 97 files covered. (86.57%)

26 existing lines in 10 files now uncovered.

8249 of 9066 relevant lines covered (90.99%)

0.91 hits per line

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

83.94
/src/main/java/jp/co/future/uroborosql/store/SqlResourceManagerImpl.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.store;
8

9
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
10
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
11
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
12
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
13

14
import java.io.BufferedReader;
15
import java.io.IOException;
16
import java.io.InputStreamReader;
17
import java.net.JarURLConnection;
18
import java.net.URISyntaxException;
19
import java.net.URL;
20
import java.nio.charset.Charset;
21
import java.nio.file.FileSystem;
22
import java.nio.file.FileSystemNotFoundException;
23
import java.nio.file.FileSystems;
24
import java.nio.file.Files;
25
import java.nio.file.Path;
26
import java.nio.file.Paths;
27
import java.nio.file.WatchEvent;
28
import java.nio.file.WatchKey;
29
import java.nio.file.WatchService;
30
import java.nio.file.attribute.FileTime;
31
import java.util.ArrayList;
32
import java.util.Arrays;
33
import java.util.Collections;
34
import java.util.List;
35
import java.util.Map;
36
import java.util.ServiceLoader;
37
import java.util.Set;
38
import java.util.concurrent.ConcurrentHashMap;
39
import java.util.concurrent.ExecutorService;
40
import java.util.concurrent.Executors;
41
import java.util.concurrent.TimeUnit;
42
import java.util.stream.Collectors;
43
import java.util.stream.StreamSupport;
44

45
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
47

48
import jp.co.future.uroborosql.dialect.Dialect;
49
import jp.co.future.uroborosql.exception.UroborosqlRuntimeException;
50
import jp.co.future.uroborosql.utils.ObjectUtils;
51

52
/**
53
 * SQLリソース管理実装クラス
54
 *
55
 * @author H.Sugimoto
56
 */
57
public class SqlResourceManagerImpl implements SqlResourceManager {
58
        /** ロガー */
59
        private static final Logger LOG = LoggerFactory.getLogger("jp.co.future.uroborosql.log");
1✔
60

61
        /** zip, jar内のファイルのscheme */
62
        private static final String SCHEME_JAR = "jar";
63
        /** ファイルシステム上のファイルのscheme */
64
        private static final String SCHEME_FILE = "file";
65

66
        /** SQLファイルロードのデフォルトルートパス */
67
        private static final String DEFAULT_LOAD_PATH = "sql";
68

69
        /** 有効なDialectのSet */
70
        private static final Set<String> dialects = StreamSupport
1✔
71
                        .stream(ServiceLoader.load(Dialect.class).spliterator(), false)
1✔
72
                        .map(Dialect::getDatabaseType)
1✔
73
                        .collect(Collectors.toSet());
1✔
74

75
        /** SQLファイルをロードするルートパスのリスト */
76
        private final List<Path> loadPaths;
77

78
        /** SQLファイルをロードするルートパスの各階層を保持する配列(ルートパス特定用) */
79
        private final List<String[]> loadPathPartsList;
80

81
        /** SQLファイル拡張子 */
82
        private final String fileExtension;
83

84
        /** SQLファイルエンコーディング */
85
        private final Charset charset;
86

87
        /** SQLファイルの変更を検知するかどうか */
88
        private final boolean detectChanges;
89

90
        /** Dialect */
91
        private Dialect dialect;
92

93
        /** SQLファイル監視サービス */
94
        private WatchService watcher;
95

96
        /** SQLファイル監視サービスの実行サービス */
97
        private ExecutorService es;
98

99
        /** sqlNameとそれに対するSqlInfoの紐付きを持つMap */
100
        private final ConcurrentHashMap<String, SqlInfo> sqlInfos = new ConcurrentHashMap<>();
1✔
101

102
        /** WatchKeyに対するディレクトリPathを取得するためのMap */
103
        private final ConcurrentHashMap<WatchKey, Path> watchDirs = new ConcurrentHashMap<>();
1✔
104

105
        /**
106
         * コンストラクタ
107
         */
108
        public SqlResourceManagerImpl() {
109
                this(DEFAULT_LOAD_PATH);
1✔
110
        }
1✔
111

112
        /**
113
         * コンストラクタ
114
         *
115
         * @param detectChanges SQLファイルの変更を検知するかどうか
116
         */
117
        public SqlResourceManagerImpl(final boolean detectChanges) {
118
                this(DEFAULT_LOAD_PATH, null, null, detectChanges);
1✔
119
        }
1✔
120

121
        /**
122
         * コンストラクタ
123
         *
124
         * @param loadPath SQLファイルをロードするルートパス
125
         */
126
        public SqlResourceManagerImpl(final String loadPath) {
127
                this(loadPath, null);
1✔
128
        }
1✔
129

130
        /**
131
         * コンストラクタ
132
         *
133
         * @param loadPath SQLファイルをロードするルートパス
134
         * @param fileExtension SQLファイル拡張子
135
         */
136
        public SqlResourceManagerImpl(final String loadPath, final String fileExtension) {
137
                this(loadPath, fileExtension, null);
1✔
138
        }
1✔
139

140
        /**
141
         * コンストラクタ
142
         *
143
         * @param loadPath SQLファイルをロードするルートパス
144
         * @param fileExtension SQLファイル拡張子
145
         * @param charset SQLファイルエンコーディング
146
         */
147
        public SqlResourceManagerImpl(final String loadPath, final String fileExtension, final Charset charset) {
148
                this(loadPath, fileExtension, charset, false);
1✔
149
        }
1✔
150

151
        /**
152
         * コンストラクタ
153
         *
154
         * @param loadPath SQLファイルをロードするルートパス
155
         * @param fileExtension SQLファイル拡張子
156
         * @param charset SQLファイルエンコーディング
157
         * @param detectChanges SQLファイルの変更を検知するかどうか
158
         */
159
        public SqlResourceManagerImpl(final String loadPath, final String fileExtension, final Charset charset,
160
                        final boolean detectChanges) {
161
                this(List.of(loadPath), fileExtension, charset, detectChanges);
1✔
162
        }
1✔
163

164
        /**
165
         * コンストラクタ
166
         *
167
         * @param loadPaths SQLファイルをロードするルートパスのリスト
168
         */
169
        public SqlResourceManagerImpl(final List<String> loadPaths) {
170
                this(loadPaths, null);
1✔
171
        }
1✔
172

173
        /**
174
         * コンストラクタ
175
         *
176
         * @param loadPaths SQLファイルをロードするルートパスのリスト
177
         * @param fileExtension SQLファイル拡張子
178
         */
179
        public SqlResourceManagerImpl(final List<String> loadPaths, final String fileExtension) {
180
                this(loadPaths, fileExtension, null);
1✔
181
        }
1✔
182

183
        /**
184
         * コンストラクタ
185
         *
186
         * @param loadPaths SQLファイルをロードするルートパスのリスト
187
         * @param fileExtension SQLファイル拡張子
188
         * @param charset SQLファイルエンコーディング
189
         */
190
        public SqlResourceManagerImpl(final List<String> loadPaths, final String fileExtension, final Charset charset) {
191
                this(loadPaths, fileExtension, charset, false);
1✔
192
        }
1✔
193

194
        /**
195
         * コンストラクタ
196
         *
197
         * @param loadPaths SQLファイルをロードするルートパスのリスト
198
         * @param fileExtension SQLファイル拡張子
199
         * @param charset SQLファイルエンコーディング
200
         * @param detectChanges SQLファイルの変更を検知するかどうか
201
         *
202
         * @throws IllegalArgumentException loadPathsに<code>null</code>が含まれる場合
203
         */
204
        public SqlResourceManagerImpl(final List<String> loadPaths, final String fileExtension, final Charset charset,
205
                        final boolean detectChanges) {
1✔
206
                this.loadPaths = new ArrayList<>();
1✔
207
                for (var loadPath : loadPaths) {
1✔
208
                        if (loadPath == null) {
1✔
209
                                throw new IllegalArgumentException("loadPath is required.");
1✔
210
                        }
211
                        this.loadPaths.add(Paths.get(loadPath));
1✔
212
                }
1✔
213
                this.loadPathPartsList = new ArrayList<>();
1✔
214
                for (var loadPath : this.loadPaths) {
1✔
215
                        var pathList = new ArrayList<String>();
1✔
216
                        for (var part : loadPath) {
1✔
217
                                pathList.add(part.toString());
1✔
218
                        }
1✔
219
                        this.loadPathPartsList.add(pathList.toArray(new String[pathList.size()]));
1✔
220
                }
1✔
221
                this.fileExtension = fileExtension != null ? fileExtension : ".sql";
1✔
222
                this.charset = charset != null ? charset : Charset.forName(Charset.defaultCharset().displayName());
1✔
223
                this.detectChanges = detectChanges;
1✔
224
        }
1✔
225

226
        /**
227
         * {@inheritDoc}
228
         *
229
         * @see jp.co.future.uroborosql.store.SqlResourceManager#initialize()
230
         */
231
        @Override
232
        public void initialize() {
233
                if (detectChanges) {
1✔
234
                        try {
235
                                watcher = FileSystems.getDefault().newWatchService();
1✔
NEW
236
                        } catch (IOException ex) {
×
NEW
237
                                LOG.error("Can't start watcher service.", ex);
×
UNCOV
238
                                return;
×
239
                        }
1✔
240
                }
241

242
                generateSqlInfos();
1✔
243

244
                if (detectChanges) {
1✔
245
                        // Path監視用のスレッド実行
246
                        es = Executors.newSingleThreadExecutor();
1✔
247
                        es.execute(this::watchPath);
1✔
248
                }
249
        }
1✔
250

251
        /**
252
         * {@inheritDoc}
253
         *
254
         * @see jp.co.future.uroborosql.store.SqlResourceManager#shutdown()
255
         */
256
        @Override
257
        public void shutdown() {
258
                if (detectChanges) {
1✔
259
                        es.shutdown();
1✔
260
                        try {
261
                                if (!es.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
1✔
262
                                        es.shutdownNow();
1✔
263
                                }
NEW
264
                        } catch (InterruptedException ex) {
×
265
                                es.shutdownNow();
×
266
                        }
1✔
267
                }
268
        }
1✔
269

270
        /**
271
         * Pathの監視
272
         */
273
        private void watchPath() {
274
                for (;;) {
275
                        //監視キーの送信を待機
276
                        WatchKey key;
277
                        try {
278
                                key = watcher.take();
1✔
279
                        } catch (InterruptedException ex) {
1✔
280
                                if (LOG.isDebugEnabled()) {
1✔
NEW
281
                                        LOG.debug("WatchService caught InterruptedException.");
×
282
                                }
283
                                break;
1✔
284
                        } catch (Throwable ex) {
×
285
                                LOG.error("Unexpected exception occurred.", ex);
×
286
                                break;
×
287
                        }
1✔
288

289
                        for (var event : key.pollEvents()) {
1✔
290
                                var kind = event.kind();
1✔
291

292
                                if (kind == OVERFLOW) {
1✔
293
                                        continue;
×
294
                                }
295

296
                                //ファイル名はイベントのコンテキストです。
297
                                @SuppressWarnings("unchecked")
298
                                var evt = (WatchEvent<Path>) event;
1✔
299
                                var dir = watchDirs.get(key);
1✔
300
                                var path = dir.resolve(evt.context());
1✔
301

302
                                if (LOG.isDebugEnabled()) {
1✔
NEW
303
                                        LOG.debug("file changed.({}). path={}", kind.name(), path);
×
304
                                }
305
                                var isSqlFile = path.toString().endsWith(fileExtension);
1✔
306
                                if (Files.isDirectory(path) || !isSqlFile) {
1✔
307
                                        // ENTRY_DELETEの時はFiles.isDirectory()がfalseになるので拡張子での判定も行う
308
                                        if (kind == ENTRY_CREATE) {
1✔
309
                                                traverseFile(path, true, false);
1✔
310
                                        } else if (kind == ENTRY_DELETE) {
1✔
311
                                                key.cancel();
1✔
312
                                                watchDirs.remove(key);
1✔
313
                                                continue;
1✔
314
                                        }
315
                                } else if (isSqlFile) {
1✔
316
                                        if (kind == ENTRY_CREATE) {
1✔
317
                                                traverseFile(path, true, false);
1✔
318
                                        } else if (kind == ENTRY_MODIFY || kind == ENTRY_DELETE) {
1✔
319
                                                var sqlName = getSqlName(path);
1✔
320
                                                sqlInfos.computeIfPresent(sqlName, (k, v) -> v.computePath(path, kind == ENTRY_DELETE));
1✔
321
                                        }
322
                                }
323
                        }
1✔
324
                        key.reset();
1✔
325
                }
1✔
326
                try {
327
                        watcher.close();
1✔
NEW
328
                } catch (IOException ex) {
×
329
                        // do nothing
330
                }
1✔
331
        }
1✔
332

333
        /**
334
         * charset を取得します。
335
         *
336
         * @return charset
337
         */
338
        public Charset getCharset() {
339
                return charset;
1✔
340
        }
341

342
        /**
343
         * {@inheritDoc}
344
         *
345
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getDialect()
346
         */
347
        @Override
348
        public Dialect getDialect() {
349
                return dialect;
1✔
350
        }
351

352
        /**
353
         * {@inheritDoc}
354
         *
355
         * @see jp.co.future.uroborosql.store.SqlResourceManager#setDialect(jp.co.future.uroborosql.dialect.Dialect)
356
         */
357
        @Override
358
        public void setDialect(final Dialect dialect) {
359
                this.dialect = dialect;
1✔
360
        }
1✔
361

362
        /**
363
         * {@inheritDoc}
364
         *
365
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getSqlPathList()
366
         */
367
        @Override
368
        public List<String> getSqlPathList() {
369
                var list = Collections.list(this.sqlInfos.keys());
1✔
370
                Collections.sort(list);
1✔
371
                return list;
1✔
372
        }
373

374
        /**
375
         * {@inheritDoc}
376
         *
377
         * @see jp.co.future.uroborosql.store.SqlResourceManager#existSql(java.lang.String)
378
         */
379
        @Override
380
        public boolean existSql(final String sqlName) {
381
                return sqlInfos.containsKey(sqlName);
1✔
382
        }
383

384
        /**
385
         * {@inheritDoc}
386
         *
387
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getSql(java.lang.String)
388
         */
389
        @Override
390
        public String getSql(final String sqlName) {
391
                if (existSql(sqlName)) {
1✔
392
                        return sqlInfos.get(sqlName).getSqlBody();
1✔
393
                } else {
394
                        throw new UroborosqlRuntimeException("sql file not found. sqlName : " + sqlName);
1✔
395
                }
396
        }
397

398
        /**
399
         * sqlNameとそれに対するSqlInfoのMapを生成する
400
         */
401
        protected void generateSqlInfos() {
402
                try {
403
                        for (var loadPath : this.loadPaths) {
1✔
404
                                var loadPathSlash = loadPath.toString().replace('\\', '/');
1✔
405
                                var root = Thread.currentThread().getContextClassLoader().getResources(loadPathSlash);
1✔
406

407
                                while (root.hasMoreElements()) {
1✔
408
                                        var url = root.nextElement();
1✔
409
                                        var scheme = url.toURI().getScheme();
1✔
410
                                        if (SCHEME_FILE.equalsIgnoreCase(scheme)) {
1✔
411
                                                traverseFile(Paths.get(url.toURI()), detectChanges && true, false);
1✔
412
                                        } else if (SCHEME_JAR.equalsIgnoreCase(scheme)) {
1✔
413
                                                traverseJar(url, loadPathSlash);
1✔
414
                                        } else {
NEW
415
                                                if (LOG.isWarnEnabled()) {
×
NEW
416
                                                        LOG.warn("Unsupported scheme. scheme : {}, url : {}", scheme, url);
×
417
                                                }
418
                                        }
419
                                }
1✔
420
                        }
1✔
NEW
421
                } catch (IOException | URISyntaxException ex) {
×
NEW
422
                        LOG.error("Can't load sql files.", ex);
×
423
                }
1✔
424
        }
1✔
425

426
        /**
427
         *
428
         * {@inheritDoc}
429
         *
430
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getSqlName(java.nio.file.Path)
431
         */
432
        @Override
433
        public String getSqlName(final Path path) {
434
                var builder = new StringBuilder();
1✔
435

436
                var dialectFlag = true;
1✔
437
                for (var part : relativePath(path)) {
1✔
438
                        var s = part.toString();
1✔
439
                        if (dialectFlag) {
1✔
440
                                // loadPathの直下がdialectと一致する場合はその下のフォルダから名前を付ける
441
                                dialectFlag = false;
1✔
442
                                if (dialects.contains(s.toLowerCase())) {
1✔
443
                                        continue;
1✔
444
                                }
445
                        }
446
                        builder.append(s).append("/");
1✔
447
                }
1✔
448

449
                return builder.substring(0, builder.length() - (fileExtension.length() + 1));
1✔
450
        }
451

452
        /**
453
         *
454
         * {@inheritDoc}
455
         *
456
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getSqlPath(java.lang.String)
457
         */
458
        @Override
459
        public Path getSqlPath(final String sqlName) {
460
                if (existSql(sqlName)) {
1✔
461
                        return sqlInfos.get(sqlName).getPath();
1✔
462
                } else {
463
                        throw new UroborosqlRuntimeException("sql file not found. sqlName : " + sqlName);
×
464
                }
465
        }
466

467
        /**
468
         * loadPathからの相対パスを取得する.
469
         * loadPathと一致する部分がなかった場合は、引数のpathの値をそのまま返却する
470
         *
471
         * @param path 相対パスを取得する元のパス
472
         * @return 相対パス
473
         */
474
        private Path relativePath(final Path path) {
475
                var pathList = new ArrayList<Path>();
1✔
476
                for (var part : path) {
1✔
477
                        pathList.add(part);
1✔
478
                }
1✔
479

480
                for (var loadPathParts : this.loadPathPartsList) {
1✔
481
                        var loadPathSize = loadPathParts.length;
1✔
482

483
                        // loadPathのフォルダの並びと一致する場所を特定し、その下を相対パスとして返却する
484
                        for (var i = 0; i < pathList.size() - loadPathSize; i++) {
1✔
485
                                var paths = pathList.subList(i, i + loadPathSize).stream()
1✔
486
                                                .map(Path::toString)
1✔
487
                                                .toArray(String[]::new);
1✔
488
                                if (Arrays.equals(loadPathParts, paths)) {
1✔
489
                                        return path.subpath(i + loadPathSize, path.getNameCount());
1✔
490
                                }
491
                        }
492
                }
1✔
493
                // loadPathと一致しなかった場合は元のpathを返却する
494
                return path;
1✔
495
        }
496

497
        /**
498
         * 引数で指定したパスがDialect指定なし、または現在のDialect指定に対応するパスかどうかを判定する
499
         * <pre>
500
         *  Dialect=postgresqlの場合
501
         *
502
         *  - example/test.sql            : true
503
         *  - postgresql/example/test.sql : true
504
         *  - oracle/example/test.sql     : false
505
         *  - example                     : true
506
         *  - postgresql                  : true
507
         *  - oracle                      : false
508
         * </pre>
509
         *
510
         *
511
         * @param path 検査対象のPath
512
         * @return 妥当なPathの場合<code>true</code>
513
         */
514
        private boolean validPath(final Path path) {
515
                var relativePath = relativePath(path);
1✔
516
                if (relativePath.equals(path)) {
1✔
517
                        return true;
1✔
518
                }
519

520
                var d = relativePath.getName(0).toString().toLowerCase();
1✔
521
                // loadPathの直下が現在のdialect以外と一致する場合は無効なパスと判定する
522
                return !dialects.contains(d) || this.dialect.getDatabaseType().equals(d);
1✔
523

524
        }
525

526
        /**
527
         * 指定されたPath配下のファイルを順次追跡し、sqlInfosに格納、または削除を行う。<br>
528
         * また、監視対象指定があり、Pathがディレクトリの場合は、監視サービスに登録する。
529
         *
530
         * @param path 追跡を行うディレクトリ、またはファイルのPath
531
         * @param watch 監視対象指定。<code>true</code>の場合監視対象
532
         * @param remove 削除指定。<code>true</code>の場合、指定のPathを除外する。<code>false</code>の場合は格納する
533
         */
534
        private void traverseFile(final Path path, final boolean watch, final boolean remove) {
535
                if (LOG.isTraceEnabled()) {
1✔
NEW
536
                        LOG.trace("traverseFile start. path : {}, watch : {}, remove : {}.", path, watch, remove);
×
537
                }
538
                if (Files.notExists(path)) {
1✔
539
                        return;
×
540
                }
541
                if (Files.isDirectory(path)) {
1✔
542
                        if (validPath(path)) {
1✔
543
                                try (var ds = Files.newDirectoryStream(path)) {
1✔
544
                                        if (watch) {
1✔
545
                                                var key = path.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
1✔
546
                                                watchDirs.put(key, path);
1✔
547
                                        }
548
                                        for (var child : ds) {
1✔
549
                                                traverseFile(child, watch, remove);
1✔
550
                                        }
1✔
551
                                } catch (IOException ex) {
×
552
                                        throw new UroborosqlRuntimeException("I/O error occurred.", ex);
×
553
                                }
1✔
554
                        }
555
                } else if (path.toString().endsWith(fileExtension)) {
1✔
556
                        var sqlName = getSqlName(path);
1✔
557
                        this.sqlInfos.compute(sqlName, (k, v) -> v == null ? new SqlInfo(sqlName, path, loadPaths, dialect, charset)
1✔
558
                                        : v.computePath(path, remove));
1✔
559
                }
560
        }
1✔
561

562
        /**
563
         * 指定されたjarのURL配下のファイルを順次追跡し、sqlInfosに格納を行う。<br>
564
         *
565
         * @param url 追跡を行うディレクトリ、またはファイルのURL
566
         * @param loadPath SQLファイルをロードするパス
567
         */
568
        @SuppressWarnings("resource")
569
        private void traverseJar(final URL url, final String loadPath) {
570
                if (LOG.isTraceEnabled()) {
1✔
NEW
571
                        LOG.trace("traverseJar start. url : {}, loadPath : {}.", url, loadPath);
×
572
                }
573
                FileSystem fs = null;
1✔
574
                try {
575
                        var uri = url.toURI();
1✔
576
                        try {
577
                                fs = FileSystems.getFileSystem(uri);
×
578
                        } catch (FileSystemNotFoundException ex) {
1✔
579
                                fs = FileSystems.newFileSystem(uri, Map.of("create", "false"));
1✔
580
                        }
×
581

582
                        var conn = (JarURLConnection) url.openConnection();
1✔
583
                        try (var jarFile = conn.getJarFile()) {
1✔
584
                                var jarEntries = jarFile.entries();
1✔
585
                                while (jarEntries.hasMoreElements()) {
1✔
586
                                        var jarEntry = jarEntries.nextElement();
1✔
587
                                        var name = jarEntry.getName();
1✔
588
                                        if (!jarEntry.isDirectory() && name.startsWith(loadPath) && name.endsWith(fileExtension)) {
1✔
589
                                                var path = fs.getPath(name);
1✔
590
                                                var sqlName = getSqlName(path);
1✔
591
                                                this.sqlInfos.compute(sqlName, (k, v) -> v == null
1✔
592
                                                                ? new SqlInfo(sqlName, path, loadPaths, dialect, charset)
1✔
593
                                                                : v.computePath(path, false));
1✔
594
                                        }
595
                                }
1✔
596
                        }
597
                } catch (IOException | URISyntaxException ex) {
×
598
                        throw new UroborosqlRuntimeException("I/O error occurred.", ex);
×
599
                } finally {
600
                        if (fs != null) {
1✔
601
                                try {
602
                                        fs.close();
1✔
603
                                } catch (IOException ex) {
×
604
                                        throw new UroborosqlRuntimeException("I/O error occurred.", ex);
×
605
                                }
1✔
606
                        }
607
                }
608
        }
1✔
609

610
        /**
611
         * SQLファイルの情報を保持するオブジェクト
612
         */
613
        public static class SqlInfo {
614
                /** キーとなるsqlName */
615
                private final String sqlName;
616
                /** 対象のDialect */
617
                private final Dialect dialect;
618
                /** Sqlファイルの文字コード */
619
                private final Charset charset;
620
                /** sqlNameに対応するPathのList. ソートされて優先度が高いものから順に並んでいる. 適用されるのは先頭のPathになる. */
621
                private final List<Path> pathList = new ArrayList<>();
1✔
622
                /** SQLファイルをロードするルートパスのリスト */
623
                private final List<Path> loadPaths;
624
                /** SQLファイルの内容. <code>null</code>の場合、getSqlBody()が呼び出された段階でロードして格納する. */
625
                private String sqlBody;
626
                /** 適用されたPathの最終更新日時。SQLファイルが更新されたかどうかの判定に利用する */
627
                private FileTime lastModified;
628

629
                /**
630
                 * コンストラクタ
631
                 * @param sqlName sqlName
632
                 * @param path path
633
                 * @param dialect dialect
634
                 * @param charset charset
635
                 */
636
                SqlInfo(final String sqlName,
637
                                final Path path,
638
                                final List<Path> loadPaths,
639
                                final Dialect dialect,
640
                                final Charset charset) {
1✔
641
                        if (LOG.isTraceEnabled()) {
1✔
NEW
642
                                LOG.trace("SqlInfo - sqlName : {}, path : {}, dialect : {}, charset : {}.",
×
643
                                                sqlName, path, dialect, charset);
644
                        }
645
                        this.sqlName = sqlName;
1✔
646
                        this.dialect = dialect;
1✔
647
                        this.charset = charset;
1✔
648
                        this.pathList.add(path);
1✔
649
                        this.loadPaths = loadPaths;
1✔
650
                        this.lastModified = getLastModifiedTime(path);
1✔
651
                        this.sqlBody = null;
1✔
652
                }
1✔
653

654
                /**
655
                 * 指定されたPathの最終更新日時を取得する
656
                 * @param path 対象のPath
657
                 * @return 最終更新日時
658
                 */
659
                private static FileTime getLastModifiedTime(final Path path) {
660
                        if (SCHEME_FILE.equalsIgnoreCase(path.toUri().getScheme())) {
1✔
661
                                try {
662
                                        return Files.getLastModifiedTime(path);
1✔
NEW
663
                                } catch (IOException ex) {
×
NEW
664
                                        if (LOG.isWarnEnabled()) {
×
NEW
665
                                                LOG.warn("Can't get lastModifiedTime. path:{}", path, ex);
×
666
                                        }
667
                                }
668
                        }
669
                        return FileTime.fromMillis(0L);
1✔
670
                }
671

672
                /**
673
                 * 指定されたPathがDialect指定のPathかどうかを判定する
674
                 * @param path 判定対象Path
675
                 * @return Dialect指定Pathの場合<code>true</code>
676
                 */
677
                private boolean hasDialect(final Path path) {
678
                        for (var p : path) {
1✔
679
                                if (this.dialect.getDatabaseType().equals(p.toString())) {
1✔
680
                                        return true;
1✔
681
                                }
682
                        }
1✔
683
                        return false;
1✔
684
                }
685

686
                /**
687
                 * sqlName を取得します。
688
                 *
689
                 * @return sqlName
690
                 */
691
                public String getSqlName() {
692
                        return sqlName;
×
693
                }
694

695
                /**
696
                 * 現在有効な path を取得します。
697
                 *
698
                 * @return path 現在有効なPath
699
                 */
700
                public Path getPath() {
701
                        return pathList.get(0);
1✔
702
                }
703

704
                /**
705
                 * sql文字列を取得します。
706
                 * sqlBodyが<code>null</code>の場合、現在有効なPathからファイルの内容をロードし、sqlBodyに格納したうえ返却します。
707
                 *
708
                 * @return sqlBody sql文字列
709
                 */
710
                private String getSqlBody() {
711
                        if (sqlBody == null) {
1✔
712
                                var path = getPath();
1✔
713
                                var scheme = path.toUri().getScheme();
1✔
714

715
                                if (SCHEME_FILE.equalsIgnoreCase(scheme)) {
1✔
716
                                        // ファイルパスの場合
717
                                        if (Files.notExists(path)) {
1✔
718
                                                throw new UroborosqlRuntimeException("SQL template could not found.["
×
719
                                                                + path.toAbsolutePath().toString() + "]");
×
720
                                        }
721
                                        synchronized (sqlName) {
1✔
722
                                                try {
723
                                                        var body = new String(Files.readAllBytes(path), charset);
1✔
724
                                                        sqlBody = formatSqlBody(body);
1✔
725
                                                        if (LOG.isDebugEnabled()) {
1✔
NEW
726
                                                                LOG.debug("Loaded SQL template.[{}]", path);
×
727
                                                        }
NEW
728
                                                } catch (IOException ex) {
×
729
                                                        throw new UroborosqlRuntimeException("Failed to load SQL template["
×
NEW
730
                                                                        + path.toAbsolutePath().toString() + "].", ex);
×
731
                                                }
1✔
732
                                        }
1✔
733
                                } else {
734
                                        // jarパスの場合
735
                                        var url = Thread.currentThread().getContextClassLoader().getResource(getResourcePath(path));
1✔
736
                                        if (url == null) {
1✔
737
                                                throw new UroborosqlRuntimeException("SQL template could not found.["
×
738
                                                                + path.toAbsolutePath().toString() + "]");
×
739
                                        }
740
                                        synchronized (sqlName) {
1✔
741
                                                try {
742
                                                        var conn = url.openConnection();
1✔
743
                                                        try (var reader = new BufferedReader(
1✔
744
                                                                        new InputStreamReader(conn.getInputStream(), charset))) {
1✔
745
                                                                var body = reader.lines()
1✔
746
                                                                                .collect(Collectors.joining(System.lineSeparator()));
1✔
747
                                                                sqlBody = formatSqlBody(body);
1✔
748
                                                                if (LOG.isDebugEnabled()) {
1✔
NEW
749
                                                                        LOG.debug("Loaded SQL template.[{}]", path);
×
750
                                                                }
751
                                                        }
NEW
752
                                                } catch (IOException ex) {
×
753
                                                        throw new UroborosqlRuntimeException("Failed to load SQL template["
×
NEW
754
                                                                        + path.toAbsolutePath().toString() + "].", ex);
×
755
                                                }
1✔
756
                                        }
1✔
757
                                }
758
                        }
759
                        return sqlBody;
1✔
760
                }
761

762
                /**
763
                 * SQL文の不要な文字削除と末尾の改行文字付与を行う.
764
                 *
765
                 * @param sqlBody 元となるSQL文
766
                 * @return 整形後のSQL文
767
                 */
768
                protected String formatSqlBody(final String sqlBody) {
769
                        var newBody = sqlBody.trim();
1✔
770
                        if (newBody.endsWith("/") && !newBody.endsWith("*/")) {
1✔
NEW
771
                                newBody = ObjectUtils.removeEnd(newBody, "/");
×
772
                        } else {
773
                                newBody = newBody + System.lineSeparator();
1✔
774
                        }
775
                        return newBody;
1✔
776
                }
777

778
                /**
779
                 * 同じSqlNameになるPathの優先度判定を行い、優先度が高いPathが指定された場合保持しているPathの置き換えを行う
780
                 *
781
                 * @param newPath 判定用Path
782
                 * @param remove 指定した判定用Pathを削除する場合に<code>true</code>を指定
783
                 *
784
                 * @return 判定後のSqlInfo
785
                 */
786
                private SqlInfo computePath(final Path newPath, final boolean remove) {
787
                        synchronized (sqlName) {
1✔
788
                                // 変更前の有効Pathを保持しておく
789
                                var oldPath = getPath();
1✔
790

791
                                // 引数で渡された判定用PathをpathListへ追加、またはpathListから削除する
792
                                if (!pathList.contains(newPath)) {
1✔
793
                                        if (!remove) {
1✔
794
                                                pathList.add(newPath);
1✔
795
                                        }
796
                                } else {
797
                                        if (remove) {
1✔
798
                                                pathList.remove(newPath);
1✔
799
                                                if (pathList.isEmpty()) {
1✔
800
                                                        // pathListが空になった場合はこのSqlInfoをsqlInfosから除外するためにnullを返す
801
                                                        return null;
1✔
802
                                                }
803
                                        }
804
                                }
805

806
                                if (pathList.size() > 1) {
1✔
807
                                        // 優先度が高いPathが先頭に来るようにソートを行う
808
                                        // 1. Dialect付Pathを優先
809
                                        // 2. file を jar よりも優先
810
                                        // 3. Path同士のcompare
811
                                        pathList.sort((p1, p2) -> {
1✔
812
                                                if (p1 == null && p2 == null) {
1✔
813
                                                        return 0;
×
814
                                                } else if (p1 != null && p2 == null) {
1✔
815
                                                        return -1;
×
816
                                                } else if (p1 == null && p2 != null) {
1✔
817
                                                        return 1;
×
818
                                                }
819

820
                                                // DialectPathの比較
821
                                                var p1HasDialect = hasDialect(p1);
1✔
822
                                                var p2HasDialect = hasDialect(p2);
1✔
823

824
                                                if (p1HasDialect && !p2HasDialect) {
1✔
825
                                                        return -1;
1✔
826
                                                } else if (!p1HasDialect && p2HasDialect) {
1✔
827
                                                        return 1;
1✔
828
                                                }
829

830
                                                // schemeの比較
831
                                                var p1Scheme = p1.toUri().getScheme();
1✔
832
                                                var p2Scheme = p2.toUri().getScheme();
1✔
833

834
                                                if (!p1Scheme.equals(p2Scheme)) {
1✔
835
                                                        if (SCHEME_FILE.equals(p1Scheme)) {
1✔
836
                                                                return -1;
×
837
                                                        } else {
838
                                                                return 1;
1✔
839
                                                        }
840
                                                }
841

842
                                                // LoadPathsの並び順にソート
843
                                                var p1Pos = 0;
1✔
844
                                                var p2Pos = 0;
1✔
845
                                                for (var pos = 0; pos < this.loadPaths.size(); pos++) {
1✔
846
                                                        var loadPath = this.loadPaths.get(pos);
1✔
847
                                                        var loadPathSize = loadPath.getNameCount();
1✔
848

849
                                                        for (var i = 0; i < p1.getNameCount() - loadPathSize; i++) {
1✔
850
                                                                var p1SubPath = p1.subpath(i, i + loadPathSize);
1✔
851
                                                                if (p1SubPath.equals(loadPath)) {
1✔
852
                                                                        p1Pos = pos + 1;
1✔
853
                                                                        break;
1✔
854
                                                                }
855
                                                        }
856
                                                        for (var i = 0; i < p2.getNameCount() - loadPathSize; i++) {
1✔
857
                                                                var p2SubPath = p2.subpath(i, i + loadPathSize);
1✔
858
                                                                if (p2SubPath.equals(loadPath)) {
1✔
859
                                                                        p2Pos = pos + 1;
1✔
860
                                                                        break;
1✔
861
                                                                }
862
                                                        }
863
                                                        if (p1Pos > 0 && p2Pos > 0) {
1✔
864
                                                                break;
1✔
865
                                                        }
866
                                                }
867
                                                if (p1Pos != p2Pos) {
1✔
868
                                                        return p1Pos - p2Pos;
1✔
869
                                                }
870

871
                                                return p1.compareTo(p2);
1✔
872
                                        });
873
                                }
874

875
                                var replaceFlag = false;
1✔
876
                                // ソートによる再計算後の有効Pathを取得する
877
                                var currentPath = getPath();
1✔
878
                                var currentTimeStamp = getLastModifiedTime(currentPath);
1✔
879
                                if (!oldPath.equals(currentPath)) {
1✔
880
                                        replaceFlag = true;
1✔
881
                                        if (LOG.isDebugEnabled()) {
1✔
NEW
882
                                                LOG.debug("sql file switched. sqlName={}, oldPath={}, newPath={}, lastModified={}", sqlName,
×
NEW
883
                                                                oldPath, currentPath, currentTimeStamp.toString());
×
884
                                        }
885
                                } else {
886
                                        if (!this.lastModified.equals(currentTimeStamp)) {
1✔
887
                                                replaceFlag = true;
×
NEW
888
                                                if (LOG.isDebugEnabled()) {
×
NEW
889
                                                        LOG.debug("sql file changed. sqlName={}, path={}, lastModified={}", sqlName, currentPath,
×
NEW
890
                                                                        currentTimeStamp.toString());
×
891
                                                }
892
                                        }
893
                                }
894

895
                                if (replaceFlag) {
1✔
896
                                        this.lastModified = currentTimeStamp;
1✔
897
                                        this.sqlBody = null;
1✔
898
                                }
899
                                return this;
1✔
900
                        }
901
                }
902

903
                /**
904
                 * クラスローダーから取得する際のリソースパス(loadPathで始まるパス)を取得する.<br>
905
                 * loadPathと一致する部分がなかった場合は、引数のpathの値をそのまま返却する
906
                 *
907
                 * @param path 計算対象のパス
908
                 * @return クラスローダーから取得する際のリソースパス
909
                 */
910
                private String getResourcePath(final Path path) {
911
                        var pathSize = path.getNameCount();
1✔
912

913
                        for (var loadPath : this.loadPaths) {
1✔
914
                                var loadPathSize = loadPath.getNameCount();
1✔
915

916
                                for (var i = 0; i < pathSize - loadPathSize; i++) {
1✔
917
                                        var subPath = path.subpath(i, i + loadPathSize);
1✔
918
                                        if (loadPath.equals(subPath)) {
1✔
919
                                                return path.subpath(i, pathSize).toString();
×
920
                                        }
921
                                }
922
                        }
1✔
923
                        return path.toString(); // loadPathと一致しなかった場合はパスをそのまま返却する。
1✔
924
                }
925
        }
926
}
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