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

future-architect / uroborosql / #767

26 Aug 2024 05:08PM UTC coverage: 90.274% (-0.9%) from 91.21%
#767

push

HidekiSugimoto189
各Logger用のインタフェースを作成し必要なクラスがimplementsするように修正。そのうえでSLF4J 2.xのAPIを利用するように変更(性能改善)

360 of 568 new or added lines in 32 files covered. (63.38%)

9 existing lines in 7 files now uncovered.

8864 of 9819 relevant lines covered (90.27%)

0.9 hits per line

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

83.29
/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 jp.co.future.uroborosql.dialect.Dialect;
46
import jp.co.future.uroborosql.exception.UroborosqlRuntimeException;
47
import jp.co.future.uroborosql.utils.ObjectUtils;
48

49
/**
50
 * SQLリソース管理実装クラス
51
 *
52
 * @author H.Sugimoto
53
 */
54
public class SqlResourceManagerImpl implements SqlResourceManager {
55
        /** zip, jar内のファイルのscheme */
56
        private static final String SCHEME_JAR = "jar";
57
        /** ファイルシステム上のファイルのscheme */
58
        private static final String SCHEME_FILE = "file";
59

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

63
        /** 有効なDialectのSet */
64
        private static final Set<String> dialects = StreamSupport
1✔
65
                        .stream(ServiceLoader.load(Dialect.class).spliterator(), false)
1✔
66
                        .map(Dialect::getDatabaseType)
1✔
67
                        .collect(Collectors.toSet());
1✔
68

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

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

75
        /** SQLファイル拡張子 */
76
        private final String fileExtension;
77

78
        /** SQLファイルエンコーディング */
79
        private final Charset charset;
80

81
        /** SQLファイルの変更を検知するかどうか */
82
        private final boolean detectChanges;
83

84
        /** Dialect */
85
        private Dialect dialect;
86

87
        /** SQLファイル監視サービス */
88
        private WatchService watcher;
89

90
        /** SQLファイル監視サービスの実行サービス */
91
        private ExecutorService es;
92

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

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

99
        /**
100
         * コンストラクタ
101
         */
102
        public SqlResourceManagerImpl() {
103
                this(DEFAULT_LOAD_PATH);
1✔
104
        }
1✔
105

106
        /**
107
         * コンストラクタ
108
         *
109
         * @param detectChanges SQLファイルの変更を検知するかどうか
110
         */
111
        public SqlResourceManagerImpl(final boolean detectChanges) {
112
                this(DEFAULT_LOAD_PATH, null, null, detectChanges);
1✔
113
        }
1✔
114

115
        /**
116
         * コンストラクタ
117
         *
118
         * @param loadPath SQLファイルをロードするルートパス
119
         */
120
        public SqlResourceManagerImpl(final String loadPath) {
121
                this(loadPath, null);
1✔
122
        }
1✔
123

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

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

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

158
        /**
159
         * コンストラクタ
160
         *
161
         * @param loadPaths SQLファイルをロードするルートパスのリスト
162
         */
163
        public SqlResourceManagerImpl(final List<String> loadPaths) {
164
                this(loadPaths, null);
1✔
165
        }
1✔
166

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

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

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

220
        /**
221
         * {@inheritDoc}
222
         *
223
         * @see jp.co.future.uroborosql.store.SqlResourceManager#initialize()
224
         */
225
        @Override
226
        public void initialize() {
227
                if (detectChanges) {
1✔
228
                        try {
229
                                watcher = FileSystems.getDefault().newWatchService();
1✔
230
                        } catch (IOException ex) {
×
NEW
231
                                LOG.atError()
×
NEW
232
                                                .setMessage("Can't start watcher service.")
×
NEW
233
                                                .setCause(ex)
×
NEW
234
                                                .log();
×
UNCOV
235
                                return;
×
236
                        }
1✔
237
                }
238

239
                generateSqlInfos();
1✔
240

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

432
        /**
433
         *
434
         * {@inheritDoc}
435
         *
436
         * @see jp.co.future.uroborosql.store.SqlResourceManager#getSqlName(java.nio.file.Path)
437
         */
438
        @Override
439
        public String getSqlName(final Path path) {
440
                var builder = new StringBuilder();
1✔
441

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

455
                return builder.substring(0, builder.length() - (fileExtension.length() + 1));
1✔
456
        }
457

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

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

486
                for (var loadPathParts : this.loadPathPartsList) {
1✔
487
                        var loadPathSize = loadPathParts.length;
1✔
488

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

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

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

530
        }
531

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

571
        /**
572
         * 指定されたjarのURL配下のファイルを順次追跡し、sqlInfosに格納を行う。<br>
573
         *
574
         * @param url 追跡を行うディレクトリ、またはファイルのURL
575
         * @param loadPath SQLファイルをロードするパス
576
         */
577
        @SuppressWarnings("resource")
578
        private void traverseJar(final URL url, final String loadPath) {
579
                LOG.atTrace()
1✔
580
                                .setMessage("traverseJar start. url : {}, loadPath : {}.")
1✔
581
                                .addArgument(url)
1✔
582
                                .addArgument(loadPath)
1✔
583
                                .log();
1✔
584
                FileSystem fs = null;
1✔
585
                try {
586
                        var uri = url.toURI();
1✔
587
                        try {
588
                                fs = FileSystems.getFileSystem(uri);
×
589
                        } catch (FileSystemNotFoundException ex) {
1✔
590
                                fs = FileSystems.newFileSystem(uri, Map.of("create", "false"));
1✔
591
                        }
×
592

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

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

640
                /**
641
                 * コンストラクタ
642
                 * @param sqlName sqlName
643
                 * @param path path
644
                 * @param dialect dialect
645
                 * @param charset charset
646
                 */
647
                SqlInfo(final String sqlName,
648
                                final Path path,
649
                                final List<Path> loadPaths,
650
                                final Dialect dialect,
651
                                final Charset charset) {
1✔
652
                        LOG.atTrace()
1✔
653
                                        .setMessage("SqlInfo - sqlName : {}, path : {}, dialect : {}, charset : {}.")
1✔
654
                                        .addArgument(sqlName)
1✔
655
                                        .addArgument(path)
1✔
656
                                        .addArgument(dialect)
1✔
657
                                        .addArgument(charset)
1✔
658
                                        .log();
1✔
659
                        this.sqlName = sqlName;
1✔
660
                        this.dialect = dialect;
1✔
661
                        this.charset = charset;
1✔
662
                        this.pathList.add(path);
1✔
663
                        this.loadPaths = loadPaths;
1✔
664
                        this.lastModified = getLastModifiedTime(path);
1✔
665
                        this.sqlBody = null;
1✔
666
                }
1✔
667

668
                /**
669
                 * 指定されたPathの最終更新日時を取得する
670
                 * @param path 対象のPath
671
                 * @return 最終更新日時
672
                 */
673
                private static FileTime getLastModifiedTime(final Path path) {
674
                        if (SCHEME_FILE.equalsIgnoreCase(path.toUri().getScheme())) {
1✔
675
                                try {
676
                                        return Files.getLastModifiedTime(path);
1✔
677
                                } catch (IOException ex) {
×
NEW
678
                                        LOG.atWarn()
×
NEW
679
                                                        .setMessage("Can't get lastModifiedTime. path:{}")
×
NEW
680
                                                        .addArgument(path)
×
NEW
681
                                                        .setCause(ex)
×
NEW
682
                                                        .log();
×
683
                                }
684
                        }
685
                        return FileTime.fromMillis(0L);
1✔
686
                }
687

688
                /**
689
                 * 指定されたPathがDialect指定のPathかどうかを判定する
690
                 * @param path 判定対象Path
691
                 * @return Dialect指定Pathの場合<code>true</code>
692
                 */
693
                private boolean hasDialect(final Path path) {
694
                        for (var p : path) {
1✔
695
                                if (this.dialect.getDatabaseType().equals(p.toString())) {
1✔
696
                                        return true;
1✔
697
                                }
698
                        }
1✔
699
                        return false;
1✔
700
                }
701

702
                /**
703
                 * sqlName を取得します。
704
                 *
705
                 * @return sqlName
706
                 */
707
                public String getSqlName() {
708
                        return sqlName;
×
709
                }
710

711
                /**
712
                 * 現在有効な path を取得します。
713
                 *
714
                 * @return path 現在有効なPath
715
                 */
716
                public Path getPath() {
717
                        return pathList.get(0);
1✔
718
                }
719

720
                /**
721
                 * sql文字列を取得します。
722
                 * sqlBodyが<code>null</code>の場合、現在有効なPathからファイルの内容をロードし、sqlBodyに格納したうえ返却します。
723
                 *
724
                 * @return sqlBody sql文字列
725
                 */
726
                private String getSqlBody() {
727
                        if (sqlBody == null) {
1✔
728
                                var path = getPath();
1✔
729
                                var scheme = path.toUri().getScheme();
1✔
730

731
                                if (SCHEME_FILE.equalsIgnoreCase(scheme)) {
1✔
732
                                        // ファイルパスの場合
733
                                        if (Files.notExists(path)) {
1✔
734
                                                throw new UroborosqlRuntimeException("SQL template could not found.["
×
735
                                                                + path.toAbsolutePath().toString() + "]");
×
736
                                        }
737
                                        synchronized (sqlName) {
1✔
738
                                                try {
739
                                                        var body = new String(Files.readAllBytes(path), charset);
1✔
740
                                                        sqlBody = formatSqlBody(body);
1✔
741
                                                        LOG.atDebug()
1✔
742
                                                                        .setMessage("Loaded SQL template.[{}]")
1✔
743
                                                                        .addArgument(path)
1✔
744
                                                                        .log();
1✔
745
                                                } catch (IOException ex) {
×
746
                                                        throw new UroborosqlRuntimeException("Failed to load SQL template["
×
747
                                                                        + path.toAbsolutePath().toString() + "].", ex);
×
748
                                                }
1✔
749
                                        }
1✔
750
                                } else {
751
                                        // jarパスの場合
752
                                        var url = Thread.currentThread().getContextClassLoader().getResource(getResourcePath(path));
1✔
753
                                        if (url == null) {
1✔
754
                                                throw new UroborosqlRuntimeException("SQL template could not found.["
×
755
                                                                + path.toAbsolutePath().toString() + "]");
×
756
                                        }
757
                                        synchronized (sqlName) {
1✔
758
                                                try {
759
                                                        var conn = url.openConnection();
1✔
760
                                                        try (var reader = new BufferedReader(
1✔
761
                                                                        new InputStreamReader(conn.getInputStream(), charset))) {
1✔
762
                                                                var body = reader.lines()
1✔
763
                                                                                .collect(Collectors.joining(System.lineSeparator()));
1✔
764
                                                                sqlBody = formatSqlBody(body);
1✔
765
                                                                LOG.atDebug()
1✔
766
                                                                                .setMessage("Loaded SQL template.[{}]")
1✔
767
                                                                                .addArgument(path)
1✔
768
                                                                                .log();
1✔
769
                                                        }
770
                                                } catch (IOException ex) {
×
771
                                                        throw new UroborosqlRuntimeException("Failed to load SQL template["
×
772
                                                                        + path.toAbsolutePath().toString() + "].", ex);
×
773
                                                }
1✔
774
                                        }
1✔
775
                                }
776
                        }
777
                        return sqlBody;
1✔
778
                }
779

780
                /**
781
                 * SQL文の不要な文字削除と末尾の改行文字付与を行う.
782
                 *
783
                 * @param sqlBody 元となるSQL文
784
                 * @return 整形後のSQL文
785
                 */
786
                protected String formatSqlBody(final String sqlBody) {
787
                        var newBody = sqlBody.trim();
1✔
788
                        if (newBody.endsWith("/") && !newBody.endsWith("*/")) {
1✔
789
                                newBody = ObjectUtils.removeEnd(newBody, "/");
×
790
                        } else {
791
                                newBody = newBody + System.lineSeparator();
1✔
792
                        }
793
                        return newBody;
1✔
794
                }
795

796
                /**
797
                 * 同じSqlNameになるPathの優先度判定を行い、優先度が高いPathが指定された場合保持しているPathの置き換えを行う
798
                 *
799
                 * @param newPath 判定用Path
800
                 * @param remove 指定した判定用Pathを削除する場合に<code>true</code>を指定
801
                 *
802
                 * @return 判定後のSqlInfo
803
                 */
804
                private SqlInfo computePath(final Path newPath, final boolean remove) {
805
                        synchronized (sqlName) {
1✔
806
                                // 変更前の有効Pathを保持しておく
807
                                var oldPath = getPath();
1✔
808

809
                                // 引数で渡された判定用PathをpathListへ追加、またはpathListから削除する
810
                                if (!pathList.contains(newPath)) {
1✔
811
                                        if (!remove) {
1✔
812
                                                pathList.add(newPath);
1✔
813
                                        }
814
                                } else {
815
                                        if (remove) {
1✔
816
                                                pathList.remove(newPath);
1✔
817
                                                if (pathList.isEmpty()) {
1✔
818
                                                        // pathListが空になった場合はこのSqlInfoをsqlInfosから除外するためにnullを返す
819
                                                        return null;
1✔
820
                                                }
821
                                        }
822
                                }
823

824
                                if (pathList.size() > 1) {
1✔
825
                                        // 優先度が高いPathが先頭に来るようにソートを行う
826
                                        // 1. Dialect付Pathを優先
827
                                        // 2. file を jar よりも優先
828
                                        // 3. Path同士のcompare
829
                                        pathList.sort((p1, p2) -> {
1✔
830
                                                if (p1 == null && p2 == null) {
1✔
831
                                                        return 0;
×
832
                                                } else if (p1 != null && p2 == null) {
1✔
833
                                                        return -1;
×
834
                                                } else if (p1 == null && p2 != null) {
1✔
835
                                                        return 1;
×
836
                                                }
837

838
                                                // DialectPathの比較
839
                                                var p1HasDialect = hasDialect(p1);
1✔
840
                                                var p2HasDialect = hasDialect(p2);
1✔
841

842
                                                if (p1HasDialect && !p2HasDialect) {
1✔
843
                                                        return -1;
1✔
844
                                                } else if (!p1HasDialect && p2HasDialect) {
1✔
845
                                                        return 1;
1✔
846
                                                }
847

848
                                                // schemeの比較
849
                                                var p1Scheme = p1.toUri().getScheme();
1✔
850
                                                var p2Scheme = p2.toUri().getScheme();
1✔
851

852
                                                if (!p1Scheme.equals(p2Scheme)) {
1✔
853
                                                        if (SCHEME_FILE.equals(p1Scheme)) {
1✔
854
                                                                return -1;
×
855
                                                        } else {
856
                                                                return 1;
1✔
857
                                                        }
858
                                                }
859

860
                                                // LoadPathsの並び順にソート
861
                                                var p1Pos = 0;
1✔
862
                                                var p2Pos = 0;
1✔
863
                                                for (var pos = 0; pos < this.loadPaths.size(); pos++) {
1✔
864
                                                        var loadPath = this.loadPaths.get(pos);
1✔
865
                                                        var loadPathSize = loadPath.getNameCount();
1✔
866

867
                                                        for (var i = 0; i < p1.getNameCount() - loadPathSize; i++) {
1✔
868
                                                                var p1SubPath = p1.subpath(i, i + loadPathSize);
1✔
869
                                                                if (p1SubPath.equals(loadPath)) {
1✔
870
                                                                        p1Pos = pos + 1;
1✔
871
                                                                        break;
1✔
872
                                                                }
873
                                                        }
874
                                                        for (var i = 0; i < p2.getNameCount() - loadPathSize; i++) {
1✔
875
                                                                var p2SubPath = p2.subpath(i, i + loadPathSize);
1✔
876
                                                                if (p2SubPath.equals(loadPath)) {
1✔
877
                                                                        p2Pos = pos + 1;
1✔
878
                                                                        break;
1✔
879
                                                                }
880
                                                        }
881
                                                        if (p1Pos > 0 && p2Pos > 0) {
1✔
882
                                                                break;
1✔
883
                                                        }
884
                                                }
885
                                                if (p1Pos != p2Pos) {
1✔
886
                                                        return p1Pos - p2Pos;
1✔
887
                                                }
888

889
                                                return p1.compareTo(p2);
1✔
890
                                        });
891
                                }
892

893
                                var replaceFlag = false;
1✔
894
                                // ソートによる再計算後の有効Pathを取得する
895
                                var currentPath = getPath();
1✔
896
                                var currentTimeStamp = getLastModifiedTime(currentPath);
1✔
897
                                if (!oldPath.equals(currentPath)) {
1✔
898
                                        replaceFlag = true;
1✔
899
                                        LOG.atDebug()
1✔
900
                                                        .setMessage("sql file switched. sqlName={}, oldPath={}, newPath={}, lastModified={}")
1✔
901
                                                        .addArgument(sqlName)
1✔
902
                                                        .addArgument(oldPath)
1✔
903
                                                        .addArgument(currentPath)
1✔
904
                                                        .addArgument(currentTimeStamp)
1✔
905
                                                        .log();
1✔
906
                                } else {
907
                                        if (!this.lastModified.equals(currentTimeStamp)) {
1✔
908
                                                replaceFlag = true;
×
NEW
909
                                                LOG.atDebug()
×
NEW
910
                                                                .setMessage("sql file changed. sqlName={}, path={}, lastModified={}")
×
NEW
911
                                                                .addArgument(sqlName)
×
NEW
912
                                                                .addArgument(currentPath)
×
NEW
913
                                                                .addArgument(currentTimeStamp)
×
NEW
914
                                                                .log();
×
915
                                        }
916
                                }
917

918
                                if (replaceFlag) {
1✔
919
                                        this.lastModified = currentTimeStamp;
1✔
920
                                        this.sqlBody = null;
1✔
921
                                }
922
                                return this;
1✔
923
                        }
924
                }
925

926
                /**
927
                 * クラスローダーから取得する際のリソースパス(loadPathで始まるパス)を取得する.<br>
928
                 * loadPathと一致する部分がなかった場合は、引数のpathの値をそのまま返却する
929
                 *
930
                 * @param path 計算対象のパス
931
                 * @return クラスローダーから取得する際のリソースパス
932
                 */
933
                private String getResourcePath(final Path path) {
934
                        var pathSize = path.getNameCount();
1✔
935

936
                        for (var loadPath : this.loadPaths) {
1✔
937
                                var loadPathSize = loadPath.getNameCount();
1✔
938

939
                                for (var i = 0; i < pathSize - loadPathSize; i++) {
1✔
940
                                        var subPath = path.subpath(i, i + loadPathSize);
1✔
941
                                        if (loadPath.equals(subPath)) {
1✔
942
                                                return path.subpath(i, pathSize).toString();
×
943
                                        }
944
                                }
945
                        }
1✔
946
                        return path.toString(); // loadPathと一致しなかった場合はパスをそのまま返却する。
1✔
947
                }
948
        }
949
}
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