88import java .nio .file .Files ;
99import java .nio .file .Path ;
1010import java .nio .file .Paths ;
11+ import java .time .Clock ;
12+ import java .time .Duration ;
1113import java .time .Instant ;
1214import java .util .Arrays ;
13- import java .util .HashMap ;
1415import java .util .Map ;
1516import java .util .Objects ;
1617import java .util .Optional ;
18+ import java .util .concurrent .ConcurrentHashMap ;
1719import java .util .concurrent .atomic .AtomicInteger ;
1820import lombok .RequiredArgsConstructor ;
1921import lombok .extern .slf4j .Slf4j ;
3234public final class HostsFileParser {
3335 private final int maxFullCacheFileSizeBytes =
3436 Integer .parseInt (System .getProperty ("dnsjava.hostsfile.max_size_bytes" , "16384" ));
37+ private final Duration fileChangeCheckInterval =
38+ Duration .ofMillis (
39+ Integer .parseInt (
40+ System .getProperty ("dnsjava.hostsfile.change_check_interval_ms" , "300000" )));
3541
36- private final Map <String , InetAddress > hostsCache = new HashMap <>();
3742 private final Path path ;
3843 private final boolean clearCacheOnChange ;
44+ private Clock clock = Clock .systemUTC ();
45+
46+ @ SuppressWarnings ("java:S3077" )
47+ private volatile Map <String , InetAddress > hostsCache ;
48+
49+ private Instant lastFileModificationCheckTime = Instant .MIN ;
3950 private Instant lastFileReadTime = Instant .MIN ;
4051 private boolean isEntireFileParsed ;
4152 private boolean hostsFileWarningLogged = false ;
53+ private long hostsFileSizeBytes ;
4254
4355 /**
4456 * Creates a new instance based on the current OS's default. Unix and alike (or rather everything
@@ -88,8 +100,7 @@ public HostsFileParser(Path path, boolean clearCacheOnChange) {
88100 * @throws IllegalArgumentException when {@code type} is not {@link org.xbill.DNS.Type#A} or{@link
89101 * org.xbill.DNS.Type#AAAA}.
90102 */
91- public synchronized Optional <InetAddress > getAddressForHost (Name name , int type )
92- throws IOException {
103+ public Optional <InetAddress > getAddressForHost (Name name , int type ) throws IOException {
93104 Objects .requireNonNull (name , "name is required" );
94105 if (type != Type .A && type != Type .AAAA ) {
95106 throw new IllegalArgumentException ("type can only be A or AAAA" );
@@ -102,13 +113,11 @@ public synchronized Optional<InetAddress> getAddressForHost(Name name, int type)
102113 return Optional .of (cachedAddress );
103114 }
104115
105- if (isEntireFileParsed || ! Files . exists ( path ) ) {
116+ if (isEntireFileParsed ) {
106117 return Optional .empty ();
107118 }
108119
109- if (Files .size (path ) <= maxFullCacheFileSizeBytes ) {
110- parseEntireHostsFile ();
111- } else {
120+ if (hostsFileSizeBytes > maxFullCacheFileSizeBytes ) {
112121 searchHostsFileForEntry (name , type );
113122 }
114123
@@ -141,8 +150,6 @@ private void parseEntireHostsFile() throws IOException {
141150 nameFailures );
142151 hostsFileWarningLogged = true ;
143152 }
144-
145- isEntireFileParsed = true ;
146153 }
147154
148155 private void searchHostsFileForEntry (Name name , int type ) throws IOException {
@@ -235,21 +242,61 @@ private String[] getLineTokens(String line) {
235242 }
236243
237244 private void validateCache () throws IOException {
238- if (clearCacheOnChange ) {
245+ if (!clearCacheOnChange ) {
246+ if (hostsCache == null ) {
247+ synchronized (this ) {
248+ if (hostsCache == null ) {
249+ readHostsFile ();
250+ }
251+ }
252+ }
253+
254+ return ;
255+ }
256+
257+ if (lastFileModificationCheckTime .plus (fileChangeCheckInterval ).isBefore (clock .instant ())) {
258+ log .debug ("Checked for changes more than 5minutes ago, checking" );
239259 // A filewatcher / inotify etc. would be nicer, but doesn't work. c.f. the write up at
240260 // https://blog.arkey.fr/2019/09/13/watchservice-and-bind-mount/
241- Instant fileTime =
242- Files .exists (path ) ? Files .getLastModifiedTime (path ).toInstant () : Instant .MAX ;
243- if (fileTime .isAfter (lastFileReadTime )) {
244- // skip logging noise when the cache is empty anyway
245- if (!hostsCache .isEmpty ()) {
246- log .info ("Local hosts database has changed at {}, clearing cache" , fileTime );
247- hostsCache .clear ();
261+
262+ synchronized (this ) {
263+ if (!lastFileModificationCheckTime
264+ .plus (fileChangeCheckInterval )
265+ .isBefore (clock .instant ())) {
266+ log .debug ("Never mind, check fulfilled in another thread" );
267+ return ;
268+ }
269+
270+ lastFileModificationCheckTime = clock .instant ();
271+ readHostsFile ();
272+ }
273+ }
274+ }
275+
276+ private void readHostsFile () throws IOException {
277+ if (Files .exists (path )) {
278+ Instant fileTime = Files .getLastModifiedTime (path ).toInstant ();
279+ if (!lastFileReadTime .equals (fileTime )) {
280+ createOrClearCache ();
281+
282+ hostsFileSizeBytes = Files .size (path );
283+ if (hostsFileSizeBytes <= maxFullCacheFileSizeBytes ) {
284+ parseEntireHostsFile ();
285+ isEntireFileParsed = true ;
248286 }
249287
250- isEntireFileParsed = false ;
251288 lastFileReadTime = fileTime ;
252289 }
290+ } else {
291+ createOrClearCache ();
292+ }
293+ }
294+
295+ private void createOrClearCache () {
296+ if (hostsCache == null ) {
297+ hostsCache = new ConcurrentHashMap <>();
298+ } else {
299+ hostsCache .clear ();
253300 }
254301 }
255302
@@ -259,6 +306,10 @@ private String key(Name name, int type) {
259306
260307 // for unit testing only
261308 int cacheSize () {
262- return hostsCache .size ();
309+ return hostsCache == null ? 0 : hostsCache .size ();
310+ }
311+
312+ void setClock (Clock clock ) {
313+ this .clock = clock ;
263314 }
264315}
0 commit comments