Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Project exclude paths
/target/
/target/
.idea/**
out/
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>net.aboodyy</groupId>
<artifactId>localtime-expansion</artifactId>
<version>1.2</version>
<version>1.3</version>

<repositories>
<repository>
Expand Down
150 changes: 114 additions & 36 deletions src/main/java/net/aboodyy/localtime/DateManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,37 @@

package net.aboodyy.localtime;

import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.*;

public class DateManager implements Listener {

private final Map<UUID, String> timezones;

DateManager() {
timezones = new HashMap<>();
private final Map<String, String> cache;
Comment thread
Opalinium marked this conversation as resolved.
Outdated
private final ScheduledExecutorService executorService;
private int retryDelay;
private final int cacheExpirationMinutes = 1440; // Cache entries expire after 1440 minutes (1 Day)

public DateManager() {
this.timezones = new ConcurrentHashMap<>();
this.cache = new ConcurrentHashMap<>();
this.retryDelay = 5; // default to 5 seconds
this.executorService = Executors.newSingleThreadScheduledExecutor();

executorService.scheduleAtFixedRate(this::removeExpiredEntries, cacheExpirationMinutes, cacheExpirationMinutes, TimeUnit.MINUTES);
}

public String getDate(String format, String timezone) {
Expand All @@ -53,55 +62,124 @@ public String getDate(String format, String timezone) {
return dateFormat.format(date);
}

public String getTimeZone(Player player) {
private boolean isCacheExpired(UUID uuid) {
String timestampStr = cache.get(uuid.toString() + "_timestamp");
if (timestampStr == null) {
return true;
}

long timestamp = Long.parseLong(timestampStr);
long currentTime = System.currentTimeMillis();
long expirationTime = cacheExpirationMinutes * 60 * 1000;

if ((currentTime - timestamp) >= expirationTime) {
timezones.remove(uuid); // Remove the player from the timezones map when cache expires
return true;
} else {
return false;
}
}

private void removeExpiredEntries() {
for (Map.Entry<String, String> entry : cache.entrySet()) {
if (entry.getKey().endsWith("_timestamp")) {
continue; // Skip keys with the "_timestamp" suffix
}

UUID uuid;
try {
uuid = UUID.fromString(entry.getKey());
} catch (IllegalArgumentException e) {
continue; // Skip non-UUID keys
}

if (isCacheExpired(uuid)) {
cache.remove(uuid.toString());
timezones.remove(uuid);
}
}
}

public CompletableFuture<String> getTimeZone(Player player) {
final String FAILED = "[LocalTime] Couldn't get " + player.getName() + "'s timezone. Will use default timezone.";
String timezone = TimeZone.getDefault().getID();

if (timezones.containsKey(player.getUniqueId()))
return timezones.get(player.getUniqueId());
String cachedTimezone = cache.get(player.getUniqueId().toString());
if (cachedTimezone != null) {
return CompletableFuture.completedFuture(cachedTimezone);
}

String timezone = timezones.get(player.getUniqueId());
if (timezone != null) {
return CompletableFuture.completedFuture(timezone);
}

InetSocketAddress address = player.getAddress();
timezones.put(player.getUniqueId(), timezone);
timezone = TimeZone.getDefault().getID();

if (address == null) {
Bukkit.getLogger().info(FAILED);
return timezone;
cache.put(player.getUniqueId().toString(), timezone);
return CompletableFuture.completedFuture(timezone);
}

new BukkitRunnable() {
@Override
public void run() {
String timezone;
final String timezoneFinal = timezone;
CompletableFuture<String> futureTimezone = CompletableFuture.supplyAsync(() -> {
String result = "undefined";
int retries = 3;

while (retries-- > 0) {
try {
URL api = new URL("https://ipapi.co/" + address.getAddress().getHostAddress() + "/timezone/");
URLConnection connection = api.openConnection();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
timezone = bufferedReader.readLine();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", "Mozilla/5.0");

// Use try-with-resources to automatically close the BufferedReader
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
result = bufferedReader.readLine();

if (result == null) {
result = "undefined";
} else {
cache.put(player.getUniqueId().toString(), result);
cache.put(player.getUniqueId().toString() + "_timestamp", String.valueOf(System.currentTimeMillis()));
}
break; // exit loop if successful
}
} catch (Exception e) {
timezone = "undefined";
}

if (timezone.equalsIgnoreCase("undefined")) {
Bukkit.getLogger().info(FAILED);
timezone = TimeZone.getDefault().getID();
result = "undefined";
Bukkit.getLogger().warning("[LocalTime] Exception while getting timezone for player " + player.getName() + ": " + e.getMessage());
try {
Thread.sleep(retryDelay * 1000);
} catch (InterruptedException ignored) {}
}
}

timezones.put(player.getUniqueId(), timezone);
if (result.equalsIgnoreCase("undefined")) {
Bukkit.getLogger().info(FAILED);
result = timezoneFinal;
}
}.runTaskAsynchronously(PlaceholderAPIPlugin.getInstance());

return timezones.get(player.getUniqueId());
timezones.put(player.getUniqueId(), result);
return result;
}, executorService);


futureTimezone.exceptionally(ex -> {
Bukkit.getLogger().warning("[LocalTime] Exception while getting timezone for player " + player.getName() + ": " + ex.getMessage());
Comment thread
Opalinium marked this conversation as resolved.
Outdated
cache.put(player.getUniqueId().toString(), timezoneFinal);
timezones.put(player.getUniqueId(), timezoneFinal);
return timezoneFinal;
});

return CompletableFuture.completedFuture(timezoneFinal);
}

public void clear() {
timezones.clear();
}

@SuppressWarnings("unused")
@EventHandler
public void onLeave(PlayerQuitEvent e) {
timezones.remove(e.getPlayer().getUniqueId());
cache.clear();
}
}


18 changes: 13 additions & 5 deletions src/main/java/net/aboodyy/localtime/LocalTimeExpansion.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@SuppressWarnings("unused")
public class LocalTimeExpansion extends PlaceholderExpansion implements Cacheable, Configurable {
Expand All @@ -43,12 +44,12 @@ public String getIdentifier() {

@Override
public String getAuthor() {
return "aBooDyy";
return "aBooDyy, Opal";
}

@Override
public String getVersion() {
return "1.2";
return "1.3";
}

@Override
Expand Down Expand Up @@ -82,7 +83,10 @@ public String onPlaceholderRequest(Player p, String identifier) {
args = identifier.split("time_");
if (args.length < 2) return null;

return dateManager.getDate(args[1], dateManager.getTimeZone(p));
CompletableFuture<String> timezoneFuture = dateManager.getTimeZone(p);
String timezone = timezoneFuture.join();

return dateManager.getDate(args[1], timezone);
}

if (identifier.startsWith("timezone_")) {
Expand All @@ -99,8 +103,12 @@ public String onPlaceholderRequest(Player p, String identifier) {
return dateManager.getDate(format, args[1]);
}

if (identifier.equalsIgnoreCase("time"))
return dateManager.getDate(format, dateManager.getTimeZone(p));
if (identifier.equalsIgnoreCase("time")) {
CompletableFuture<String> timezoneFuture = dateManager.getTimeZone(p);
String timezone = timezoneFuture.join();

return dateManager.getDate(format, timezone);
}

return null;
}
Expand Down