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

future-architect / uroborosql / #769

12 Sep 2024 02:26PM UTC coverage: 90.35% (-0.9%) from 91.21%
#769

Pull #332

HidekiSugimoto189
Refactoring log-related class and method names.
Pull Request #332: Enable per-SQL-ID log suppression (#322)

384 of 587 new or added lines in 32 files covered. (65.42%)

9 existing lines in 7 files now uncovered.

8885 of 9834 relevant lines covered (90.35%)

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.log.support.ServiceLoggingSupport;
48
import jp.co.future.uroborosql.utils.ObjectUtils;
49

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

240
                generateSqlInfos();
1✔
241

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

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

268
        /**
269
         * Pathの監視
270
         */
271
        private void watchPath() {
272
                for (;;) {
273
                        //監視キーの送信を待機
274
                        WatchKey key;
275
                        try {
276
                                key = watcher.take();
1✔
277
                        } catch (InterruptedException ex) {
1✔
278
                                debugWith(LOG)
1✔
279
                                                .log("WatchService caught InterruptedException.");
1✔
280
                                break;
1✔
281
                        } catch (Throwable ex) {
×
NEW
282
                                errorWith(LOG)
×
NEW
283
                                                .setMessage("Unexpected exception occurred.")
×
NEW
284
                                                .setCause(ex)
×
NEW
285
                                                .log();
×
UNCOV
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
                                debugWith(LOG)
1✔
303
                                                .setMessage("file changed.({}). path={}")
1✔
304
                                                .addArgument(kind.name())
1✔
305
                                                .addArgument(path)
1✔
306
                                                .log();
1✔
307
                                var isSqlFile = path.toString().endsWith(fileExtension);
1✔
308
                                if (Files.isDirectory(path) || !isSqlFile) {
1✔
309
                                        // ENTRY_DELETEの時はFiles.isDirectory()がfalseになるので拡張子での判定も行う
310
                                        if (kind == ENTRY_CREATE) {
1✔
311
                                                traverseFile(path, true, false);
1✔
312
                                        } else if (kind == ENTRY_DELETE) {
1✔
313
                                                key.cancel();
1✔
314
                                                watchDirs.remove(key);
1✔
315
                                                continue;
1✔
316
                                        }
317
                                } else if (isSqlFile) {
1✔
318
                                        if (kind == ENTRY_CREATE) {
1✔
319
                                                traverseFile(path, true, false);
1✔
320
                                        } else if (kind == ENTRY_MODIFY || kind == ENTRY_DELETE) {
1✔
321
                                                var sqlName = getSqlName(path);
1✔
322
                                                sqlInfos.computeIfPresent(sqlName, (k, v) -> v.computePath(path, kind == ENTRY_DELETE));
1✔
323
                                        }
324
                                }
325
                        }
1✔
326
                        key.reset();
1✔
327
                }
1✔
328
                try {
329
                        watcher.close();
1✔
330
                } catch (IOException ex) {
×
331
                        // do nothing
332
                }
1✔
333
        }
1✔
334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

531
        }
532

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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