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

coditory / quark-i18n / #12

pending completion
#12

push

github-actions

ogesaku
Expand localized messages api

22 of 22 new or added lines in 3 files covered. (100.0%)

1596 of 2144 relevant lines covered (74.44%)

0.74 hits per line

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

42.5
/src/main/java/com/coditory/quark/i18n/loader/I18nFileSystemLoader.java
1
package com.coditory.quark.i18n.loader;
2

3
import com.coditory.quark.i18n.I18nKey;
4
import com.coditory.quark.i18n.I18nPath;
5
import com.coditory.quark.i18n.loader.FileWatcher.FileChangedEvent;
6
import com.coditory.quark.i18n.loader.I18nPathPattern.I18nPathGroups;
7
import com.coditory.quark.i18n.parser.I18nParser;
8
import org.jetbrains.annotations.NotNull;
9
import org.slf4j.Logger;
10
import org.slf4j.LoggerFactory;
11

12
import java.io.BufferedReader;
13
import java.io.InputStreamReader;
14
import java.net.MalformedURLException;
15
import java.net.URL;
16
import java.nio.file.FileSystem;
17
import java.nio.file.FileSystems;
18
import java.nio.file.Path;
19
import java.util.ArrayList;
20
import java.util.LinkedHashMap;
21
import java.util.List;
22
import java.util.Locale;
23
import java.util.Map;
24
import java.util.Set;
25

26
import static java.util.Collections.unmodifiableList;
27
import static java.util.Objects.requireNonNull;
28

29
final class I18nFileSystemLoader implements WatchableI18nLoader {
30
    static I18nFileLoaderBuilder builder() {
31
        return builder(FileSystems.getDefault());
×
32
    }
33

34
    static I18nFileLoaderBuilder builder(FileSystem fileSystem) {
35
        return new I18nFileLoaderBuilder()
1✔
36
                .scanFileSystem(fileSystem);
1✔
37
    }
38

39
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
1✔
40
    private final List<I18nLoaderChangeListener> listeners = new ArrayList<>();
1✔
41
    private final Set<I18nPathPattern> pathPatterns;
42
    private final I18nParser parser;
43
    private final Map<String, I18nParser> parsersByExtension;
44
    private final I18nPath staticPrefix;
45
    private final FileSystem fileSystem;
46
    private final Map<String, CachedResource> cachedResources = new LinkedHashMap<>();
1✔
47
    private final Map<String, I18nMessageBundle> cachedBundles = new LinkedHashMap<>();
1✔
48
    private Thread watchThread;
49

50
    I18nFileSystemLoader(
51
            Set<I18nPathPattern> pathPatterns,
52
            FileSystem fileSystem,
53
            I18nParser fileParser,
54
            Map<String, I18nParser> parsersByExtension,
55
            I18nPath staticPrefix
56
    ) {
1✔
57
        this.staticPrefix = requireNonNull(staticPrefix);
1✔
58
        this.fileSystem = requireNonNull(fileSystem);
1✔
59
        this.pathPatterns = Set.copyOf(pathPatterns);
1✔
60
        this.parsersByExtension = Map.copyOf(parsersByExtension);
1✔
61
        this.parser = fileParser;
1✔
62
    }
1✔
63

64
    @NotNull
65
    @Override
66
    public synchronized List<I18nMessageBundle> load() {
67
        List<I18nMessageBundle> result = new ArrayList<>();
1✔
68
        for (I18nPathPattern pathPattern : pathPatterns) {
1✔
69
            List<Resource> resources = scanFiles(pathPattern);
1✔
70
            for (Resource resource : resources) {
1✔
71
                I18nMessageBundle templates = load(pathPattern, resource);
1✔
72
                result.add(templates);
1✔
73
                logger.debug("Loaded message bundle: {}", resource.url());
1✔
74
            }
1✔
75
        }
1✔
76
        return unmodifiableList(result);
1✔
77
    }
78

79
    private I18nMessageBundle load(I18nPathPattern pathPattern, Resource resource) {
80
        I18nPathGroups matchedGroups = pathPattern.matchGroups(resource.name());
1✔
81
        return load(resource, matchedGroups);
1✔
82
    }
83

84
    private I18nMessageBundle load(Resource resource, I18nPathGroups matchedGroups) {
85
        Locale locale = matchedGroups.locale();
1✔
86
        I18nPath prefix = matchedGroups.path() != null
1✔
87
                ? staticPrefix.child(matchedGroups.path())
1✔
88
                : staticPrefix;
1✔
89
        Map<I18nKey, String> parsed = parseFile(locale, resource);
1✔
90
        String urlString = resource.url().toString();
1✔
91
        I18nMessageBundle result = new I18nMessageBundle(parsed, prefix);
1✔
92
        cachedBundles.put(urlString, result);
1✔
93
        cachedResources.put(urlString, new CachedResource(resource, matchedGroups));
1✔
94
        return result;
1✔
95
    }
96

97
    private List<Resource> scanFiles(I18nPathPattern pathPattern) {
98
        return ResourceScanner.scanFiles(fileSystem, pathPattern);
1✔
99
    }
100

101
    private Map<I18nKey, String> parseFile(Locale locale, Resource resource) {
102
        String extension = getExtension(resource.name());
1✔
103
        I18nParser parser = parsersByExtension.getOrDefault(extension, this.parser);
1✔
104
        if (parser == null) {
1✔
105
            throw new I18nLoadException("No file parser defined for: " + resource.name());
×
106
        }
107
        String content = readFile(resource);
1✔
108
        if (content.isBlank()) {
1✔
109
            return Map.of();
×
110
        }
111
        try {
112
            return parser.parse(content, locale);
1✔
113
        } catch (Throwable e) {
×
114
            throw new I18nLoadException("Could not parse file: " + resource.name(), e);
×
115
        }
116
    }
117

118
    private String readFile(Resource resource) {
119
        try {
120
            StringBuilder resultStringBuilder = new StringBuilder();
1✔
121
            try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.url().openStream()))) {
1✔
122
                String line;
123
                while ((line = br.readLine()) != null) {
1✔
124
                    resultStringBuilder.append(line).append("\n");
1✔
125
                }
126
            }
127
            return resultStringBuilder.toString();
1✔
128
        } catch (Throwable e) {
×
129
            throw new I18nLoadException("Could not read classpath resource: " + resource.name(), e);
×
130
        }
131
    }
132

133
    private String getExtension(String resourceName) {
134
        int idx = resourceName.lastIndexOf('.');
1✔
135
        return idx == 0 || idx == resourceName.length() - 1
1✔
136
                ? null
×
137
                : resourceName.substring(idx + 1);
1✔
138
    }
139

140
    @Override
141
    public synchronized void startWatching() {
142
        if (watchThread != null) {
×
143
            throw new I18nLoadException("Loader is already watching for changes");
×
144
        }
145
        if (cachedBundles.isEmpty()) {
×
146
            load();
×
147
        }
148
        watchThread = FileWatcher.builder()
×
149
                .addListener(this::onFileChange)
×
150
                .fileSystem(fileSystem)
×
151
                .addPathPatterns(pathPatterns)
×
152
                .startWatchingThread();
×
153
    }
×
154

155
    @Override
156
    public synchronized void stopWatching() {
157
        if (watchThread == null) {
×
158
            return;
×
159
        }
160
        watchThread.interrupt();
×
161
        try {
162
            watchThread.join();
×
163
        } catch (InterruptedException e) {
×
164
            throw new I18nLoadException("Interrupted join with watching thread", e);
×
165
        }
×
166
    }
×
167

168
    @Override
169
    public synchronized void addChangeListener(I18nLoaderChangeListener listener) {
170
        listeners.add(listener);
×
171
    }
×
172

173
    private synchronized void onFileChange(FileChangedEvent event) {
174
        Path path = event.path();
×
175
        URL url = pathToUrl(path);
×
176
        String urlString = url.toString();
×
177
        Resource resource = new Resource(path.toString(), url);
×
178
        switch (event.changeType()) {
×
179
            case DELETE -> {
180
                I18nMessageBundle prev = cachedBundles.remove(urlString);
×
181
                if (prev != null) {
×
182
                    logger.info("Removed messages from file: {}", relativize(path));
×
183
                }
184
            }
×
185
            case MODIFY -> {
186
                I18nMessageBundle prev = cachedBundles.remove(urlString);
×
187
                loadToCache(resource);
×
188
                if (prev != null) {
×
189
                    logger.info("Reloaded messages from file: {}", relativize(path));
×
190
                }
191
            }
×
192
            case CREATE -> {
193
                loadToCache(resource);
×
194
                logger.info("Loaded messages from file: {}", relativize(path));
×
195
            }
196
        }
197
        List<I18nMessageBundle> bundles = cachedBundles.values()
×
198
                .stream()
×
199
                .toList();
×
200
        for (I18nLoaderChangeListener listener : listeners) {
×
201
            listener.onChange(bundles);
×
202
        }
×
203
    }
×
204

205
    private void loadToCache(Resource resource) {
206
        String urlString = resource.url().toString();
×
207
        if (cachedResources.containsKey(urlString)) {
×
208
            CachedResource cachedResource = cachedResources.get(urlString);
×
209
            load(resource, cachedResource.matchedGroups());
×
210
            return;
×
211
        }
212
        pathPatterns.stream()
×
213
                .filter(path -> path.matches(resource.name()))
×
214
                .findFirst()
×
215
                .map(pattern -> new CachedResource(resource, pattern.matchGroups(resource.name())))
×
216
                .ifPresent(cachedResource -> load(resource, cachedResource.matchedGroups()));
×
217
    }
×
218

219
    private URL pathToUrl(Path path) {
220
        try {
221
            return path.toUri().toURL();
×
222
        } catch (MalformedURLException e) {
×
223
            throw new I18nLoadException("Could not convert path to URL. Path: " + path, e);
×
224
        }
225
    }
226

227
    private Path relativize(Path path) {
228
        Path base = fileSystem.getPath("").toAbsolutePath();
×
229
        return path.startsWith(base)
×
230
                ? base.relativize(path)
×
231
                : path;
×
232
    }
233

234
    private record CachedResource(Resource resource, I18nPathGroups matchedGroups) {
1✔
235
    }
236
}
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