Compare commits

..

No commits in common. "2207390eb235c27ca39a1e0abc9e3f2afc17701d" and "67af13530b2df57e9429ff4b49ceb03a86e24d56" have entirely different histories.

44 changed files with 143 additions and 297 deletions

4
.gitignore vendored
View File

@ -5,7 +5,3 @@ target
.classpath .classpath
.project .project
.claude .claude
.vscode
.DS_Store
*.log
plans

View File

@ -43,7 +43,7 @@
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
<transformers> <transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.kamma.fabka.app.HttpServerApplication</mainClass> <mainClass>cz.kamma.fabka.httpserver.HttpServerApplication</mainClass>
</transformer> </transformer>
</transformers> </transformers>
</configuration> </configuration>

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.app; package cz.kamma.fabka.httpserver;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.app; package cz.kamma.fabka.httpserver;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -16,41 +16,41 @@ import java.util.stream.Collectors;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import cz.kamma.fabka.auth.AuthService; import cz.kamma.fabka.httpserver.auth.AuthService;
import cz.kamma.fabka.auth.AuthenticatedUser; import cz.kamma.fabka.httpserver.auth.AuthenticatedUser;
import cz.kamma.fabka.auth.DatabaseAuthService; import cz.kamma.fabka.httpserver.auth.DatabaseAuthService;
import cz.kamma.fabka.http.ClasspathStaticFileHandler; import cz.kamma.fabka.httpserver.http.ClasspathStaticFileHandler;
import cz.kamma.fabka.http.MultipartFormData; import cz.kamma.fabka.httpserver.http.MultipartFormData;
import cz.kamma.fabka.http.RequestContext; import cz.kamma.fabka.httpserver.http.RequestContext;
import cz.kamma.fabka.http.Responses; import cz.kamma.fabka.httpserver.http.Responses;
import cz.kamma.fabka.http.Router; import cz.kamma.fabka.httpserver.http.Router;
import cz.kamma.fabka.repository.model.AttachmentData; import cz.kamma.fabka.httpserver.repository.AttachmentData;
import cz.kamma.fabka.repository.model.ChatLine; import cz.kamma.fabka.httpserver.repository.ChatLine;
import cz.kamma.fabka.repository.ChatRepository; import cz.kamma.fabka.httpserver.repository.ChatRepository;
import cz.kamma.fabka.repository.model.ChatVoteStats; import cz.kamma.fabka.httpserver.repository.ChatVoteStats;
import cz.kamma.fabka.repository.model.ForumAttachment; import cz.kamma.fabka.httpserver.repository.ForumAttachment;
import cz.kamma.fabka.repository.model.ForumDetail; import cz.kamma.fabka.httpserver.repository.ForumDetail;
import cz.kamma.fabka.repository.model.ForumDisplayView; import cz.kamma.fabka.httpserver.repository.ForumDisplayView;
import cz.kamma.fabka.repository.model.ForumMessage; import cz.kamma.fabka.httpserver.repository.ForumMessage;
import cz.kamma.fabka.repository.ForumRepository; import cz.kamma.fabka.httpserver.repository.ForumRepository;
import cz.kamma.fabka.repository.model.ForumSummary; import cz.kamma.fabka.httpserver.repository.ForumSummary;
import cz.kamma.fabka.repository.model.MemberProfile; import cz.kamma.fabka.httpserver.repository.MemberProfile;
import cz.kamma.fabka.repository.MemberRepository; import cz.kamma.fabka.httpserver.repository.MemberRepository;
import cz.kamma.fabka.repository.model.MessageRenderSettings; import cz.kamma.fabka.httpserver.repository.MessageRenderSettings;
import cz.kamma.fabka.repository.MysqlClientRepository; import cz.kamma.fabka.httpserver.repository.MysqlClientRepository;
import cz.kamma.fabka.repository.model.PrivateMessageItem; import cz.kamma.fabka.httpserver.repository.PrivateMessageItem;
import cz.kamma.fabka.repository.PrivateMessageRepository; import cz.kamma.fabka.httpserver.repository.PrivateMessageRepository;
import cz.kamma.fabka.repository.model.PrivateMessageStats; import cz.kamma.fabka.httpserver.repository.PrivateMessageStats;
import cz.kamma.fabka.repository.model.PrivateThreadRoot; import cz.kamma.fabka.httpserver.repository.PrivateThreadRoot;
import cz.kamma.fabka.repository.model.PrivateThreadSummary; import cz.kamma.fabka.httpserver.repository.PrivateThreadSummary;
import cz.kamma.fabka.repository.model.QuotedTextItem; import cz.kamma.fabka.httpserver.repository.QuotedTextItem;
import cz.kamma.fabka.repository.SettingsRepository; import cz.kamma.fabka.httpserver.repository.SettingsRepository;
import cz.kamma.fabka.repository.model.UserIcon; import cz.kamma.fabka.httpserver.repository.UserIcon;
import cz.kamma.fabka.repository.UserIconRepository; import cz.kamma.fabka.httpserver.repository.UserIconRepository;
import cz.kamma.fabka.repository.model.VoteStats; import cz.kamma.fabka.httpserver.repository.VoteStats;
import cz.kamma.fabka.session.SessionData; import cz.kamma.fabka.httpserver.session.SessionData;
import cz.kamma.fabka.session.SessionManager; import cz.kamma.fabka.httpserver.session.SessionManager;
import cz.kamma.fabka.web.Pages; import cz.kamma.fabka.httpserver.web.Pages;
public class HttpServerApplication { public class HttpServerApplication {

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.auth; package cz.kamma.fabka.httpserver.auth;
public interface AuthService { public interface AuthService {
AuthenticatedUser authenticate(String username, String password); AuthenticatedUser authenticate(String username, String password);

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.auth; package cz.kamma.fabka.httpserver.auth;
public class AuthenticatedUser { public class AuthenticatedUser {
private final long userId; private final long userId;

View File

@ -1,6 +1,6 @@
package cz.kamma.fabka.auth; package cz.kamma.fabka.httpserver.auth;
import cz.kamma.fabka.crypto.Md5; import cz.kamma.fabka.httpserver.crypto.Md5;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.auth; package cz.kamma.fabka.httpserver.auth;
import java.util.Objects; import java.util.Objects;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.crypto; package cz.kamma.fabka.httpserver.crypto;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;

View File

@ -1,9 +1,9 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import cz.kamma.fabka.session.SessionData; import cz.kamma.fabka.httpserver.session.SessionData;
import cz.kamma.fabka.session.SessionManager; import cz.kamma.fabka.httpserver.session.SessionManager;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder; import java.net.URLDecoder;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
@FunctionalInterface @FunctionalInterface
public interface RouteHandler { public interface RouteHandler {

View File

@ -1,8 +1,8 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpHandler;
import cz.kamma.fabka.session.SessionManager; import cz.kamma.fabka.httpserver.session.SessionManager;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.http; package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpHandler;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class AttachmentData { public class AttachmentData {
private final String name; private final String name;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class ChatLine { public class ChatLine {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -9,10 +9,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import cz.kamma.fabka.repository.model.ChatLine;
import cz.kamma.fabka.repository.model.ChatVoteStats;
public class ChatRepository { public class ChatRepository {
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
@ -84,26 +80,23 @@ public class ChatRepository {
if (chatId <= 0) { if (chatId <= 0) {
return new ChatVoteStats("", ""); return new ChatVoteStats("", "");
} }
long startTime = System.nanoTime();
ChatVoteStats result = null;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(CHAT_VOTES_USERS_SQL)) { PreparedStatement ps = conn.prepareStatement(CHAT_VOTES_USERS_SQL)) {
ps.setLong(1, chatId); ps.setLong(1, chatId);
ps.setLong(2, chatId); ps.setLong(2, chatId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) { if (!rs.next()) {
result = new ChatVoteStats( return new ChatVoteStats("", "");
valueOrDefault(rs.getString("thumbup"), ""),
valueOrDefault(rs.getString("thumbdown"), "")
);
} }
return new ChatVoteStats(
valueOrDefault(rs.getString("thumbup"), ""),
valueOrDefault(rs.getString("thumbdown"), "")
);
} }
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
return new ChatVoteStats("", "");
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] CHAT_VOTES_USERS_SQL: chatId=" + chatId + ", duration=" + durationMs + "ms");
return result != null ? result : new ChatVoteStats("", "");
} }
public void addChatMessage(long userId, String message) { public void addChatMessage(long userId, String message) {
@ -127,7 +120,6 @@ public class ChatRepository {
return lines; return lines;
} }
int safeLimit = limit <= 0 ? 40 : limit; int safeLimit = limit <= 0 ? 40 : limit;
long startTime = System.nanoTime();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(CHAT_LINES_SQL)) { PreparedStatement ps = conn.prepareStatement(CHAT_LINES_SQL)) {
ps.setLong(1, currentUserId); ps.setLong(1, currentUserId);
@ -151,8 +143,6 @@ public class ChatRepository {
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] CHAT_LINES_SQL: userId=" + currentUserId + ", limit=" + safeLimit + ", lines=" + lines.size() + ", duration=" + durationMs + "ms");
return lines; return lines;
} }

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class ChatVoteStats { public class ChatVoteStats {
private final String thumbUpUsers; private final String thumbUpUsers;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class ForumAttachment { public class ForumAttachment {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class ForumDetail { public class ForumDetail {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
import java.util.List; import java.util.List;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
import java.util.List; import java.util.List;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -16,31 +16,12 @@ import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import cz.kamma.fabka.repository.model.AttachmentData;
import cz.kamma.fabka.repository.model.ForumAttachment;
import cz.kamma.fabka.repository.model.ForumDetail;
import cz.kamma.fabka.repository.model.ForumMessage;
import cz.kamma.fabka.repository.model.ForumSummary;
import cz.kamma.fabka.repository.model.QuotedTextItem;
import cz.kamma.fabka.repository.model.VoteStats;
public class ForumRepository { public class ForumRepository {
private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague"); private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");
private static final String FORUM_DETAIL_SQL = private static final String FORUM_DETAIL_SQL =
"SELECT id, name, description, countdown FROM forum WHERE id=? AND active=1 LIMIT 1"; "SELECT id, name, description, countdown FROM forum WHERE id=? AND active=1 LIMIT 1";
private static final String FORUM_MESSAGES_SQL_DEBUG =
"SELECT fi.id, fi.text, fi.created, fi.quoteitem, fi.sticky, ua.id AS author_id, ua.username, ua.created AS author_created, ua.city, " +
" (SELECT COUNT(*) FROM forum_items fi2 WHERE fi2.createdby=ua.id AND fi2.deleted=0) AS author_posts, " +
" COALESCE((SELECT SUM(votevalue) FROM voting v WHERE v.forumitemid=fi.id), 0) AS vvalue, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes_users, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no_users " +
"FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby " +
"WHERE fi.forumid=? AND fi.deleted=0 ORDER BY fi.created DESC";
private static final String QUOTED_ITEM_SQL = private static final String QUOTED_ITEM_SQL =
"SELECT fi.text, ua.username FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby WHERE fi.id=? LIMIT 1"; "SELECT fi.text, ua.username FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby WHERE fi.id=? LIMIT 1";
private static final String ATTACHMENTS_SQL = private static final String ATTACHMENTS_SQL =
@ -115,7 +96,6 @@ public class ForumRepository {
public List<ForumSummary> listActiveForums() { public List<ForumSummary> listActiveForums() {
List<ForumSummary> forums = new ArrayList<>(); List<ForumSummary> forums = new ArrayList<>();
long startTime = System.nanoTime();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(FORUM_SQL); PreparedStatement ps = conn.prepareStatement(FORUM_SQL);
ResultSet rs = ps.executeQuery()) { ResultSet rs = ps.executeQuery()) {
@ -134,8 +114,6 @@ public class ForumRepository {
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] FORUM_SQL: forums=" + forums.size() + ", duration=" + durationMs + "ms");
return forums; return forums;
} }
@ -158,7 +136,6 @@ public class ForumRepository {
if (userId <= 0) { if (userId <= 0) {
return result; return result;
} }
long startTime = System.nanoTime();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(NEW_MESSAGES_COUNT_SQL)) { PreparedStatement ps = conn.prepareStatement(NEW_MESSAGES_COUNT_SQL)) {
ps.setLong(1, userId); ps.setLong(1, userId);
@ -174,8 +151,6 @@ public class ForumRepository {
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] NEW_MESSAGES_COUNT_SQL: userId=" + userId + ", forums=" + result.size() + ", duration=" + durationMs + "ms");
return result; return result;
} }
@ -183,27 +158,24 @@ public class ForumRepository {
if (forumId <= 0) { if (forumId <= 0) {
return null; return null;
} }
long startTime = System.nanoTime();
ForumDetail result = null;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(FORUM_DETAIL_SQL)) { PreparedStatement ps = conn.prepareStatement(FORUM_DETAIL_SQL)) {
ps.setLong(1, forumId); ps.setLong(1, forumId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) { if (!rs.next()) {
result = new ForumDetail( return null;
rs.getLong("id"),
rs.getString("name"),
rs.getString("description"),
rs.getString("countdown")
);
} }
return new ForumDetail(
rs.getLong("id"),
rs.getString("name"),
rs.getString("description"),
rs.getString("countdown")
);
} }
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
return null;
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] FORUM_DETAIL_SQL: forumId=" + forumId + ", duration=" + durationMs + "ms");
return result;
} }
public List<ForumMessage> listMessagesByForumId(long forumId) { public List<ForumMessage> listMessagesByForumId(long forumId) {
@ -211,103 +183,46 @@ public class ForumRepository {
if (forumId <= 0) { if (forumId <= 0) {
return messages; return messages;
} }
long startTime = System.nanoTime(); if (!loadMessages(messages, forumId, FORUM_MESSAGES_SQL)) {
if (!loadMessages(messages, forumId, FORUM_MESSAGES_SQL_DEBUG)) {
loadMessages(messages, forumId, FORUM_MESSAGES_SQL_NO_STICKY); loadMessages(messages, forumId, FORUM_MESSAGES_SQL_NO_STICKY);
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] FORUM_MESSAGES_SQL: forumId=" + forumId + ", rows=" + messages.size() + ", duration=" + durationMs + "ms");
return messages; return messages;
} }
private boolean loadMessages(List<ForumMessage> out, long forumId, String sql) { private boolean loadMessages(List<ForumMessage> out, long forumId, String sql) {
out.clear(); out.clear();
long queryStartTime = System.nanoTime();
// First, fetch all messages and collect item IDs for batch attachment loading
List<Long> itemIds = new ArrayList<>();
List<Timestamp> createdTimes = new ArrayList<>();
List<Long> authorIds = new ArrayList<>();
List<String> usernames = new ArrayList<>();
List<String> cities = new ArrayList<>();
List<String> texts = new ArrayList<>();
List<Long> authorPostCounts = new ArrayList<>();
List<Integer> vvalues = new ArrayList<>();
List<Integer> voteYes = new ArrayList<>();
List<Integer> voteNo = new ArrayList<>();
List<String> voteYesUsers = new ArrayList<>();
List<String> voteNoUsers = new ArrayList<>();
List<Long> ids = new ArrayList<>();
List<Long> quoteItemIds = new ArrayList<>();
List<Integer> stickies = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) { PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, forumId); ps.setLong(1, forumId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { while (rs.next()) {
ids.add(rs.getLong("id")); Timestamp createdTs = rs.getTimestamp("created");
itemIds.add(rs.getLong("id")); long quoteItemId = rs.getLong("quoteitem");
createdTimes.add(rs.getTimestamp("created")); QuotedTextItem quotedItem = quoteItemId > 0 ? findQuotedItem(quoteItemId) : null;
authorIds.add(rs.getLong("author_id")); out.add(new ForumMessage(
usernames.add(rs.getString("username")); rs.getLong("id"),
cities.add(rs.getString("city")); rs.getLong("author_id"),
authorPostCounts.add(rs.getLong("author_posts")); valueOrDefault(rs.getString("username"), "N/A"),
texts.add(rs.getString("text")); formatTs(rs.getTimestamp("author_created")),
vvalues.add(rs.getInt("vvalue")); valueOrDefault(rs.getString("city"), ""),
voteYes.add(rs.getInt("vote_yes")); rs.getLong("author_posts"),
voteNo.add(rs.getInt("vote_no")); createdTs == null ? 0L : createdTs.getTime(),
voteYesUsers.add(rs.getString("vote_yes_users")); rs.getInt("vvalue"),
voteNoUsers.add(rs.getString("vote_no_users")); rs.getInt("vote_yes"),
quoteItemIds.add(rs.getLong("quoteitem")); rs.getInt("vote_no"),
stickies.add(rs.getInt("sticky")); valueOrDefault(rs.getString("vote_yes_users"), ""),
valueOrDefault(rs.getString("vote_no_users"), ""),
quoteItemId,
quotedItem,
formatTs(createdTs),
valueOrDefault(rs.getString("text"), ""),
listAttachmentsByForumItemId(rs.getLong("id")),
rs.getInt("sticky") == 1
));
} }
} }
// Batch load attachments for all items
long attachmentsStartTime = System.nanoTime();
Map<Long, List<ForumAttachment>> attachmentsByItemId = new LinkedHashMap<>();
if (!itemIds.isEmpty()) {
attachmentsByItemId = loadAttachmentsBatch(itemIds);
}
long attachmentsDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - attachmentsStartTime);
System.out.println("[DEBUG] Batch attachment load: " + attachmentsDurationMs + "ms, " + itemIds.size() + " items");
// Build message objects
for (int i = 0; i < ids.size(); i++) {
long messageId = ids.get(i);
long quoteItemId = quoteItemIds.get(i);
QuotedTextItem quotedItem = quoteItemId > 0 ? findQuotedItem(quoteItemId) : null;
List<ForumAttachment> attachments = attachmentsByItemId.getOrDefault(messageId, new ArrayList<>());
out.add(new ForumMessage(
messageId,
authorIds.get(i),
valueOrDefault(usernames.get(i), "N/A"),
formatTs(createdTimes.get(i)),
valueOrDefault(cities.get(i), ""),
authorPostCounts.get(i),
createdTimes.get(i) == null ? 0L : createdTimes.get(i).getTime(),
vvalues.get(i),
voteYes.get(i),
voteNo.get(i),
valueOrDefault(voteYesUsers.get(i), ""),
valueOrDefault(voteNoUsers.get(i), ""),
quoteItemId,
quotedItem,
formatTs(createdTimes.get(i)),
valueOrDefault(texts.get(i), ""),
attachments,
stickies.get(i) == 1
));
}
long totalDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - queryStartTime);
System.out.println("[DEBUG] loadMessages completed: total=" + totalDurationMs + "ms, rows=" + out.size());
return true; return true;
} catch (Exception ex) { } catch (Exception ex) {
System.err.println("[DEBUG] loadMessages ERROR: " + ex.getMessage());
ex.printStackTrace();
return false; return false;
} }
} }
@ -451,25 +366,22 @@ public class ForumRepository {
if (messageId <= 0) { if (messageId <= 0) {
return null; return null;
} }
long startTime = System.nanoTime();
QuotedTextItem result = null;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(QUOTED_ITEM_SQL)) { PreparedStatement ps = conn.prepareStatement(QUOTED_ITEM_SQL)) {
ps.setLong(1, messageId); ps.setLong(1, messageId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) { if (!rs.next()) {
result = new QuotedTextItem( return null;
valueOrDefault(rs.getString("username"), "N/A"),
valueOrDefault(rs.getString("text"), "")
);
} }
return new QuotedTextItem(
valueOrDefault(rs.getString("username"), "N/A"),
valueOrDefault(rs.getString("text"), "")
);
} }
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
return null;
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] QUOTED_ITEM_SQL: messageId=" + messageId + ", duration=" + durationMs + "ms");
return result;
} }
public long addReply(long forumId, long userId, String message, Long quoteItem) { public long addReply(long forumId, long userId, String message, Long quoteItem) {
@ -589,7 +501,6 @@ public class ForumRepository {
if (forumItemId <= 0) { if (forumItemId <= 0) {
return out; return out;
} }
long startTime = System.nanoTime();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(ATTACHMENTS_SQL)) { PreparedStatement ps = conn.prepareStatement(ATTACHMENTS_SQL)) {
ps.setLong(1, forumItemId); ps.setLong(1, forumItemId);
@ -606,10 +517,6 @@ public class ForumRepository {
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
if (durationMs > 1) {
System.out.println("[DEBUG] ATTACHMENTS_SQL: forumItemId=" + forumItemId + ", count=" + out.size() + ", duration=" + durationMs + "ms");
}
return out; return out;
} }
@ -644,36 +551,6 @@ public class ForumRepository {
} }
} }
private Map<Long, List<ForumAttachment>> loadAttachmentsBatch(List<Long> itemIds) {
Map<Long, List<ForumAttachment>> result = new LinkedHashMap<>();
if (itemIds.isEmpty()) {
return result;
}
String sql = "SELECT forumitemsid, id, name, ispicture, width, size FROM attachments WHERE forumitemsid IN (" +
String.join(",", java.util.Collections.nCopies(itemIds.size(), "?")) + ") ORDER BY forumitemsid, id";
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < itemIds.size(); i++) {
ps.setLong(i + 1, itemIds.get(i));
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long forumItemId = rs.getLong("forumitemsid");
ForumAttachment attachment = new ForumAttachment(
rs.getLong("id"),
valueOrDefault(rs.getString("name"), "attachment"),
rs.getInt("ispicture") == 1,
rs.getInt("width")
);
result.computeIfAbsent(forumItemId, k -> new ArrayList<>()).add(attachment);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return result;
}
private static String valueOrDefault(String value, String defaultValue) { private static String valueOrDefault(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value; return value == null || value.isBlank() ? defaultValue : value;
} }

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class ForumSummary { public class ForumSummary {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class MemberProfile { public class MemberProfile {
private final long id; private final long id;

View File

@ -1,6 +1,6 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import cz.kamma.fabka.crypto.Md5; import cz.kamma.fabka.httpserver.crypto.Md5;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -10,8 +10,6 @@ import java.sql.Timestamp;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import cz.kamma.fabka.repository.model.MemberProfile;
public class MemberRepository { public class MemberRepository {
private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague"); private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class MessageRenderSettings { public class MessageRenderSettings {
private final String youtubeSnippet; private final String youtubeSnippet;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class PrivateMessageItem { public class PrivateMessageItem {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -10,12 +10,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import cz.kamma.fabka.repository.model.PrivateMessageItem;
import cz.kamma.fabka.repository.model.PrivateMessageStats;
import cz.kamma.fabka.repository.model.PrivateThreadRoot;
import cz.kamma.fabka.repository.model.PrivateThreadSummary;
public class PrivateMessageRepository { public class PrivateMessageRepository {
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
@ -57,7 +51,6 @@ public class PrivateMessageRepository {
"where (m.to_user=? or m.from_user=?) and m.deleted=0 and m.reply_to is null " + "where (m.to_user=? or m.from_user=?) and m.deleted=0 and m.reply_to is null " +
"order by " + order; "order by " + order;
long startTime = System.nanoTime();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword); try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) { PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId); ps.setLong(1, userId);
@ -79,8 +72,6 @@ public class PrivateMessageRepository {
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
System.out.println("[DEBUG] PM listThreads: userId=" + userId + ", threads=" + out.size() + ", duration=" + durationMs + "ms");
return out; return out;
} }

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class PrivateMessageStats { public class PrivateMessageStats {
private final int unread; private final int unread;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class PrivateThreadRoot { public class PrivateThreadRoot {
private final long rootId; private final long rootId;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class PrivateThreadSummary { public class PrivateThreadSummary {
private final long id; private final long id;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class QuotedTextItem { public class QuotedTextItem {
private final String author; private final String author;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -7,8 +7,6 @@ import java.sql.ResultSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import cz.kamma.fabka.repository.model.MessageRenderSettings;
public class SettingsRepository { public class SettingsRepository {
private static final String SETTING_SQL = "SELECT value FROM settings WHERE name=? LIMIT 1"; private static final String SETTING_SQL = "SELECT value FROM settings WHERE name=? LIMIT 1";
private static final String USER_SETTINGS_SQL = "SELECT name, value FROM settings WHERE userid=?"; private static final String USER_SETTINGS_SQL = "SELECT name, value FROM settings WHERE userid=?";

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class UserIcon { public class UserIcon {
private final byte[] data; private final byte[] data;

View File

@ -1,12 +1,10 @@
package cz.kamma.fabka.repository; package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import cz.kamma.fabka.repository.model.UserIcon;
public class UserIconRepository { public class UserIconRepository {
private static final String USER_ICON_SQL = "SELECT data, mimetype FROM user_icon WHERE userid=? ORDER BY id DESC LIMIT 1"; private static final String USER_ICON_SQL = "SELECT data, mimetype FROM user_icon WHERE userid=? ORDER BY id DESC LIMIT 1";
private static final String INSERT_USER_ICON_SQL = "INSERT INTO user_icon (userid, data, mimetype) VALUES (?,?,?)"; private static final String INSERT_USER_ICON_SQL = "INSERT INTO user_icon (userid, data, mimetype) VALUES (?,?,?)";

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.repository.model; package cz.kamma.fabka.httpserver.repository;
public class VoteStats { public class VoteStats {
private final int yes; private final int yes;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.session; package cz.kamma.fabka.httpserver.session;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.session; package cz.kamma.fabka.httpserver.session;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.web; package cz.kamma.fabka.httpserver.web;
import java.util.StringTokenizer; import java.util.StringTokenizer;

View File

@ -1,4 +1,4 @@
package cz.kamma.fabka.web; package cz.kamma.fabka.httpserver.web;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -8,21 +8,19 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import cz.kamma.fabka.repository.MysqlClientRepository; import cz.kamma.fabka.httpserver.repository.ForumAttachment;
import cz.kamma.fabka.repository.model.AttachmentData; import cz.kamma.fabka.httpserver.repository.ForumDetail;
import cz.kamma.fabka.repository.model.ForumAttachment; import cz.kamma.fabka.httpserver.repository.ForumDisplayView;
import cz.kamma.fabka.repository.model.ForumDetail; import cz.kamma.fabka.httpserver.repository.ForumMessage;
import cz.kamma.fabka.repository.model.ForumDisplayView; import cz.kamma.fabka.httpserver.repository.ForumSummary;
import cz.kamma.fabka.repository.model.ForumMessage; import cz.kamma.fabka.httpserver.repository.MemberProfile;
import cz.kamma.fabka.repository.model.ForumSummary; import cz.kamma.fabka.httpserver.repository.MessageRenderSettings;
import cz.kamma.fabka.repository.model.MemberProfile; import cz.kamma.fabka.httpserver.repository.MysqlClientRepository;
import cz.kamma.fabka.repository.model.MessageRenderSettings; import cz.kamma.fabka.httpserver.repository.PrivateMessageItem;
import cz.kamma.fabka.repository.model.PrivateMessageItem; import cz.kamma.fabka.httpserver.repository.PrivateMessageStats;
import cz.kamma.fabka.repository.model.PrivateMessageStats; import cz.kamma.fabka.httpserver.repository.PrivateThreadRoot;
import cz.kamma.fabka.repository.model.PrivateThreadRoot; import cz.kamma.fabka.httpserver.repository.PrivateThreadSummary;
import cz.kamma.fabka.repository.model.PrivateThreadSummary; import cz.kamma.fabka.httpserver.repository.QuotedTextItem;
import cz.kamma.fabka.repository.model.QuotedTextItem;
import cz.kamma.fabka.repository.model.UserIcon;
public final class Pages { public final class Pages {
private static final String LOGIN_TEMPLATE = readTemplate("webapp/login.html"); private static final String LOGIN_TEMPLATE = readTemplate("webapp/login.html");