Profile logic for UI
This commit is contained in:
@@ -10,6 +10,7 @@ import org.to.telegramfinalproject.Models.SearchRequestModel;
|
||||
import org.to.telegramfinalproject.Models.SearchResultModel;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -26,6 +27,8 @@ public class ActionHandler {
|
||||
private final Scanner scanner;
|
||||
public static volatile boolean forceExitChat = false;
|
||||
public static ActionHandler instance;
|
||||
private final DataOutputStream outBin; // NEW
|
||||
|
||||
|
||||
//use for UI
|
||||
private volatile String lastStatus = "error"; // success | error
|
||||
@@ -57,14 +60,23 @@ public class ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
|
||||
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
|
||||
this.out = out;
|
||||
this.in = in;
|
||||
this.outBin = outBin; // NEW
|
||||
this.scanner = scanner;
|
||||
ActionHandler.instance = this;
|
||||
|
||||
}
|
||||
|
||||
// public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
|
||||
// this.out = out;
|
||||
// this.in = in;
|
||||
// this.scanner = scanner;
|
||||
// ActionHandler.instance = this;
|
||||
//
|
||||
// }
|
||||
|
||||
public void login(String username , String password){
|
||||
JSONObject request = new JSONObject();
|
||||
request.put("action", "login");
|
||||
|
||||
@@ -39,16 +39,31 @@ public class IncomingMessageListener implements Runnable {
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
|
||||
if (TelegramClient.mediaBusy.get()) {
|
||||
try { Thread.sleep(15); } catch (InterruptedException ignored) {}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
JSONObject response = new JSONObject(line);
|
||||
System.out.println("📥 Received raw line: " + line);
|
||||
|
||||
//if it has reqID answer
|
||||
String mid = response.optString("message_id", "");
|
||||
if (!mid.isEmpty()) {
|
||||
BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid);
|
||||
if (q != null) {
|
||||
q.put(response);
|
||||
continue; // این پیام مصرف شد
|
||||
}
|
||||
}
|
||||
|
||||
if (response.has("request_id")) {
|
||||
String requestId = response.getString("request_id");
|
||||
System.out.println("📬 Response with request_id: " + requestId);
|
||||
System.out.println("📬 Full response: " + response.toString(2));
|
||||
|
||||
|
||||
BlockingQueue<JSONObject> queue = TelegramClient.pendingResponses.get(requestId);
|
||||
if (queue != null) {
|
||||
queue.put(response);
|
||||
|
||||
@@ -128,10 +128,7 @@ package org.to.telegramfinalproject.Client;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
@@ -158,6 +155,14 @@ public class TelegramClient {
|
||||
public static final BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
|
||||
public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
|
||||
public static UUID loggedInUserId = null;
|
||||
private DataInputStream inBin; // NEW
|
||||
private DataOutputStream outBin; // NEW
|
||||
private static SocketMediaDownloader downloader; // NEW
|
||||
public static final java.util.concurrent.atomic.AtomicBoolean mediaBusy = new java.util.concurrent.atomic.AtomicBoolean(false); // NEW
|
||||
|
||||
public static SocketMediaDownloader getDownloader() { return downloader; }
|
||||
public DataInputStream getInBin() { return inBin; }
|
||||
public DataOutputStream getOutBin() { return outBin; }
|
||||
|
||||
private volatile boolean listenerStarted = false;
|
||||
|
||||
@@ -219,12 +224,26 @@ public class TelegramClient {
|
||||
socket = new Socket(SERVER_HOST, SERVER_PORT);
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
out = new PrintWriter(socket.getOutputStream(), true);
|
||||
|
||||
InputStream rawIn = socket.getInputStream();
|
||||
OutputStream rawOut = socket.getOutputStream();
|
||||
|
||||
in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8));
|
||||
out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
|
||||
// NEW: binary streams
|
||||
inBin = new DataInputStream(rawIn);
|
||||
outBin = new DataOutputStream(rawOut);
|
||||
|
||||
// NEW: media downloader helper
|
||||
downloader = new SocketMediaDownloader(out, inBin, outBin);
|
||||
|
||||
System.out.println("✅ Connected to Telegram Server");
|
||||
}
|
||||
|
||||
private synchronized void initHandlerIfNeeded() {
|
||||
if (handler == null) {
|
||||
handler = new ActionHandler(out, in, scanner);
|
||||
handler = new ActionHandler(out, in, outBin, scanner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package org.to.telegramfinalproject.Database;
|
||||
|
||||
import org.to.telegramfinalproject.Models.FileAttachment;
|
||||
import org.to.telegramfinalproject.Models.MediaRow;
|
||||
import org.to.telegramfinalproject.Models.Message;
|
||||
import org.to.telegramfinalproject.Utils.ChannelPermissionUtil;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MessageDatabase {
|
||||
@@ -364,6 +363,209 @@ public class MessageDatabase {
|
||||
return s.substring(0, Math.max(0, maxLen - 1)).trim() + "…";
|
||||
}
|
||||
|
||||
public static boolean saveMessageWithOptionalAttachments(
|
||||
UUID messageId, UUID senderId, UUID receiverId,
|
||||
String receiverType, String content, String messageType,
|
||||
List<FileAttachment> attachments
|
||||
) {
|
||||
Connection conn = null;
|
||||
try {
|
||||
conn = ConnectionDb.connect();
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
boolean isText = "TEXT".equalsIgnoreCase(messageType);
|
||||
boolean isImage = "IMAGE".equalsIgnoreCase(messageType);
|
||||
boolean isAudio = "AUDIO".equalsIgnoreCase(messageType);
|
||||
if (!isText && !isImage && !isAudio) {
|
||||
throw new IllegalArgumentException("messageType must be TEXT, IMAGE, or AUDIO");
|
||||
}
|
||||
|
||||
if (isText) {
|
||||
if (attachments != null && !attachments.isEmpty())
|
||||
throw new IllegalArgumentException("TEXT must not have attachments");
|
||||
if (content == null || content.isBlank())
|
||||
throw new IllegalArgumentException("TEXT must have non-empty content");
|
||||
} else {
|
||||
if (attachments == null || attachments.isEmpty())
|
||||
throw new IllegalArgumentException("Non-TEXT must have at least one attachment");
|
||||
|
||||
for (FileAttachment a : attachments) {
|
||||
if (a == null) throw new IllegalArgumentException("Attachment is null");
|
||||
String ft = a.getFileType();
|
||||
if (isImage && !"IMAGE".equalsIgnoreCase(ft))
|
||||
throw new IllegalArgumentException("All attachments must be IMAGE for messageType=IMAGE");
|
||||
if (isAudio && !"AUDIO".equalsIgnoreCase(ft))
|
||||
throw new IllegalArgumentException("All attachments must be AUDIO for messageType=AUDIO");
|
||||
}
|
||||
}
|
||||
|
||||
insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType.toUpperCase());
|
||||
if (!isText) insertAttachmentsTx(conn, messageId, attachments);
|
||||
|
||||
conn.commit();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {}
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
try { conn.setAutoCommit(true); } catch (SQLException ignored) {}
|
||||
try { conn.close(); } catch (SQLException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static boolean insertMessageTx(Connection conn, UUID messageId, UUID senderId, UUID receiverId,
|
||||
String receiverType, String content, String messageType) throws SQLException {
|
||||
String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?)";
|
||||
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setObject(1, messageId);
|
||||
ps.setObject(2, senderId);
|
||||
ps.setString(3, receiverType);
|
||||
ps.setObject(4, receiverId);
|
||||
if (content == null || content.isBlank()) ps.setNull(5, java.sql.Types.VARCHAR); else ps.setString(5, content);
|
||||
ps.setString(6, messageType);
|
||||
return ps.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List<FileAttachment> attachments) throws SQLException {
|
||||
if (attachments == null || attachments.isEmpty()) return true;
|
||||
|
||||
final String sql = """
|
||||
INSERT INTO message_attachments(
|
||||
attachment_id, message_id,
|
||||
file_url, file_type, file_name, file_size, mime_type,
|
||||
width, height, duration_seconds, thumbnail_url,
|
||||
media_key, storage_path
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
for (FileAttachment att : attachments) {
|
||||
if (att == null) throw new IllegalArgumentException("Attachment is null");
|
||||
UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID();
|
||||
UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // سادهترین حالت
|
||||
|
||||
String ft = att.getFileType();
|
||||
if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) {
|
||||
throw new IllegalArgumentException("file_type must be IMAGE or AUDIO");
|
||||
}
|
||||
if (att.getStoragePath() == null || att.getStoragePath().isBlank()) {
|
||||
throw new IllegalArgumentException("storage_path is required for socket downloads");
|
||||
}
|
||||
|
||||
int i = 1;
|
||||
ps.setObject(i++, attachmentId);
|
||||
ps.setObject(i++, messageId);
|
||||
//file url (display link)
|
||||
if (att.getFileUrl() == null || att.getFileUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR);
|
||||
else ps.setString(i++, att.getFileUrl());
|
||||
|
||||
ps.setString(i++, ft.toUpperCase());
|
||||
ps.setString(i++, att.getFileName());
|
||||
if (att.getFileSize() == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, att.getFileSize());
|
||||
if (att.getMimeType() == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, att.getMimeType());
|
||||
if (att.getWidth() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getWidth());
|
||||
if (att.getHeight() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getHeight());
|
||||
if (att.getDurationSeconds() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getDurationSeconds());
|
||||
if (att.getThumbnailUrl() == null || att.getThumbnailUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR);
|
||||
else ps.setString(i++, att.getThumbnailUrl());
|
||||
|
||||
ps.setObject(i++, mediaKey);
|
||||
ps.setString(i++, att.getStoragePath());
|
||||
|
||||
ps.addBatch();
|
||||
|
||||
att.setAttachmentId(attachmentId);
|
||||
att.setMediaKey(mediaKey);
|
||||
}
|
||||
ps.executeBatch();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
ma.attachment_id,
|
||||
ma.message_id,
|
||||
ma.media_key,
|
||||
ma.file_name,
|
||||
ma.file_size,
|
||||
ma.mime_type,
|
||||
ma.file_type,
|
||||
ma.width,
|
||||
ma.height,
|
||||
ma.duration_seconds,
|
||||
ma.thumbnail_url,
|
||||
ma.file_url,
|
||||
ma.storage_path,
|
||||
m.receiver_type,
|
||||
m.receiver_id,
|
||||
m.sender_id
|
||||
FROM message_attachments ma
|
||||
JOIN messages m ON m.message_id = ma.message_id
|
||||
WHERE ma.media_key = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
try (Connection c = ConnectionDb.connect();
|
||||
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setObject(1, mediaKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
|
||||
MediaRow a = new MediaRow();
|
||||
a.attachmentId = (UUID) rs.getObject("attachment_id");
|
||||
a.messageId = (UUID) rs.getObject("message_id");
|
||||
a.mediaKey = (UUID) rs.getObject("media_key");
|
||||
a.fileName = rs.getString("file_name");
|
||||
|
||||
long sz = rs.getLong("file_size");
|
||||
a.fileSize = rs.wasNull() ? null : sz; // MediaRow.fileSize = Long
|
||||
|
||||
a.mimeType = rs.getString("mime_type");
|
||||
a.fileType = rs.getString("file_type");
|
||||
int w = rs.getInt("width"); a.width = rs.wasNull() ? null : w;
|
||||
int h = rs.getInt("height"); a.height = rs.wasNull() ? null : h;
|
||||
int d = rs.getInt("duration_seconds"); a.durationSeconds = rs.wasNull() ? null : d;
|
||||
a.thumbnailUrl = rs.getString("thumbnail_url");
|
||||
a.fileUrl = rs.getString("file_url");
|
||||
a.storagePath = rs.getString("storage_path");
|
||||
a.receiverType = rs.getString("receiver_type");
|
||||
a.receiverId = (UUID) rs.getObject("receiver_id");
|
||||
a.senderId = (UUID) rs.getObject("sender_id");
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean canAccess(UUID requester, MediaRow mr) {
|
||||
if (requester == null || mr == null || mr.receiverType == null) return false;
|
||||
|
||||
// اختیاری: فرستنده همیشه مجاز
|
||||
if (requester.equals(mr.senderId)) return true;
|
||||
|
||||
switch (mr.receiverType.toLowerCase(Locale.ROOT)) {
|
||||
case "private":
|
||||
// receiver_id در پیامهای private = UUID چت خصوصی
|
||||
return PrivateChatDatabase.isParticipant(mr.receiverId, requester);
|
||||
|
||||
case "group":
|
||||
return GroupDatabase.isMember(mr.receiverId, requester);
|
||||
|
||||
case "channel":
|
||||
return ChannelPermissionUtil.isUserInChannel(requester, mr.receiverId);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void markMessageAsRead(UUID messageId, UUID userId) {
|
||||
|
||||
@@ -12,6 +12,9 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@@ -20,6 +23,15 @@ public class ClientHandler implements Runnable {
|
||||
private final AuthService authService = new AuthService();
|
||||
private User currentUser;
|
||||
|
||||
// ClientHandler.java
|
||||
private static void log(String msg) {
|
||||
System.out.println(java.time.LocalDateTime.now() + " [ClientHandler] " + msg);
|
||||
}
|
||||
private static void logf(String fmt, Object... args) {
|
||||
log(String.format(fmt, args));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClientHandler(Socket socket) {
|
||||
this.socket = socket;
|
||||
@@ -30,11 +42,73 @@ public class ClientHandler implements Runnable {
|
||||
UUID userId = null;
|
||||
|
||||
try (
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
|
||||
|
||||
// InputStream rawIn = socket.getInputStream();
|
||||
// OutputStream rawOut = socket.getOutputStream();
|
||||
//
|
||||
// BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8));
|
||||
// PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
|
||||
// DataInputStream dis = new DataInputStream(rawIn);
|
||||
// DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(rawOut));
|
||||
|
||||
// BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
// PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
|
||||
// BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
|
||||
// DataInputStream dis = new DataInputStream(bis); //for binary headers
|
||||
|
||||
//PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
InputStream rawIn = socket.getInputStream();
|
||||
OutputStream rawOut = socket.getOutputStream();
|
||||
|
||||
BufferedInputStream bis = new BufferedInputStream(rawIn);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(rawOut);
|
||||
|
||||
DataInputStream dis = new DataInputStream(bis);
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
PrintWriter out = new PrintWriter(new OutputStreamWriter(bos, java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
|
||||
) {
|
||||
|
||||
// DataInputStream bin = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
|
||||
|
||||
String inputLine;
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
while ((inputLine = readUtf8Line(bis)) != null) {
|
||||
|
||||
String line = inputLine.trim();
|
||||
|
||||
|
||||
if("AVATAR".equalsIgnoreCase(line)){
|
||||
|
||||
out.println(new JSONObject().put("status","ready").toString());
|
||||
out.flush();
|
||||
handleAvatarFrame(dis, out, this.currentUser.getInternal_uuid());
|
||||
continue;
|
||||
|
||||
|
||||
}
|
||||
|
||||
if ("MEDIA".equalsIgnoreCase(inputLine.trim())) {
|
||||
handleMediaFrame(dis, out);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ("MEDIA_DL".equalsIgnoreCase(line)) {
|
||||
UUID cu = (currentUser == null ? null : currentUser.getInternal_uuid());
|
||||
logf("MEDIA_DL received. currentUser.internal_uuid=%s", cu);
|
||||
|
||||
if (cu == null) {
|
||||
log("MEDIA_DL rejected: currentUser is null or no internal_uuid");
|
||||
sendDlErr(dos, "not authorized");
|
||||
continue;
|
||||
}
|
||||
handleMediaDownload(dis, dos, cu);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
JSONObject requestJson = new JSONObject(inputLine);
|
||||
String action = requestJson.getString("action");
|
||||
ResponseModel response = null;
|
||||
@@ -3145,4 +3219,540 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final int MAGIC_AVATAR = 0x41565431; // "AVT1"
|
||||
private static final long MAX_AVATAR = 3L * 1024 * 1024;
|
||||
|
||||
private void handleAvatarFrame(DataInputStream dis, PrintWriter out, UUID currentUserId) {
|
||||
try {
|
||||
int magic = dis.readInt();
|
||||
if (magic != MAGIC_AVATAR) { out.println(err("bad magic")); out.flush(); return; }
|
||||
|
||||
int headerLen = dis.readInt();
|
||||
if (headerLen <= 0 || headerLen > 64*1024) { out.println(err("bad header length")); out.flush(); return; }
|
||||
|
||||
byte[] headerBytes = dis.readNBytes(headerLen);
|
||||
if (headerBytes.length != headerLen) { out.println(err("header truncated")); out.flush(); return; }
|
||||
|
||||
JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
long contentLen = dis.readLong();
|
||||
if (contentLen <= 0 || contentLen > MAX_AVATAR) {
|
||||
skip(dis, contentLen);
|
||||
out.println(err("file too large/invalid")); out.flush(); return;
|
||||
}
|
||||
|
||||
String targetType = h.optString("target_type", "user").toLowerCase();
|
||||
UUID targetId;
|
||||
if ("user".equals(targetType)) {
|
||||
String s = h.optString("target_id", "");
|
||||
targetId = s.isEmpty() ? currentUserId : UUID.fromString(s);
|
||||
if (!targetId.equals(currentUserId)) {
|
||||
skip(dis, contentLen);
|
||||
out.println(err("forbidden")); out.flush(); return;
|
||||
}
|
||||
} else {
|
||||
String s = h.optString("target_id", "");
|
||||
if (s.isEmpty()) { skip(dis, contentLen); out.println(err("missing target_id")); out.flush(); return; }
|
||||
targetId = UUID.fromString(s);
|
||||
if (!hasManagePermission(currentUserId, targetType, targetId)) {
|
||||
skip(dis, contentLen);
|
||||
out.println(err("forbidden")); out.flush(); return;
|
||||
}
|
||||
}
|
||||
|
||||
String fileName = h.optString("file_name", "avatar.bin");
|
||||
String mimeType = h.optString("mime_type", "application/octet-stream");
|
||||
if (!isAllowedImageMime(mimeType)) { skip(dis, contentLen); out.println(err("unsupported mime")); out.flush(); return; }
|
||||
if (fileName.length() > 200) fileName = fileName.substring(0,200);
|
||||
|
||||
java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize();
|
||||
String subdir = "avatars/" + java.time.LocalDate.now();
|
||||
java.nio.file.Path dir = baseDir.resolve(subdir).normalize();
|
||||
java.nio.file.Files.createDirectories(dir);
|
||||
|
||||
String ext = guessExt_Profile(fileName, mimeType); // ⬅️ این یکی را صدا بزن
|
||||
String storedName = java.util.UUID.randomUUID() + ext;
|
||||
java.nio.file.Path targetPath = dir.resolve(storedName).normalize();
|
||||
|
||||
try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream(
|
||||
targetPath, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) {
|
||||
long remaining = contentLen;
|
||||
byte[] buf = new byte[8192];
|
||||
while (remaining > 0) {
|
||||
int toRead = (int)Math.min(buf.length, remaining);
|
||||
int n = dis.read(buf, 0, toRead);
|
||||
if (n == -1) throw new EOFException("stream ended early");
|
||||
fos.write(buf, 0, n);
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
String requestId = h.optString("message_id", null); // ⬅️ اضافه
|
||||
|
||||
long fileSize = java.nio.file.Files.size(targetPath);
|
||||
String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; // /avatars/YYYY-MM-DD/uuid.jpg
|
||||
|
||||
boolean ok = updateProfileImageUrl(targetType, targetId, fileUrl);
|
||||
|
||||
JSONObject ack = new JSONObject()
|
||||
.put("status", ok ? "success" : "error")
|
||||
.put("message_id", requestId == null ? JSONObject.NULL : requestId) // ⬅️ اضافه
|
||||
.put("target_type", targetType)
|
||||
.put("target_id", targetId.toString())
|
||||
.put("file_name", fileName)
|
||||
.put("mime_type", mimeType)
|
||||
.put("file_size", fileSize)
|
||||
.put("display_url", fileUrl);
|
||||
|
||||
out.println(ack.toString());
|
||||
out.flush();
|
||||
|
||||
if (ok) {
|
||||
// (اختیاری) Broadcast به اعضای مرتبط
|
||||
// broadcastProfileChange(targetType, targetId, fileUrl);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
out.println(err("exception"));
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static String err(String m) { return new JSONObject().put("status","error").put("message",m).toString(); }
|
||||
|
||||
private static boolean isAllowedImageMime(String mime) {
|
||||
if (mime == null) return false;
|
||||
return mime.equals("image/jpeg") || mime.equals("image/png") || mime.equals("image/webp");
|
||||
}
|
||||
|
||||
private static String guessExt_Profile(String fileName, String mime) {
|
||||
String lower = fileName.toLowerCase();
|
||||
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return ".jpg";
|
||||
if (lower.endsWith(".png")) return ".png";
|
||||
if (lower.endsWith(".webp")) return ".webp";
|
||||
return switch (mime) {
|
||||
case "image/jpeg" -> ".jpg";
|
||||
case "image/png" -> ".png";
|
||||
case "image/webp" -> ".webp";
|
||||
default -> ".bin";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
private boolean updateProfileImageUrl(String targetType, UUID targetId, String url) {
|
||||
String sql = switch (targetType) {
|
||||
case "user" -> "UPDATE users SET image_url=? WHERE internal_uuid=?";
|
||||
case "channel" -> "UPDATE channels SET image_url=? WHERE id=?";
|
||||
case "group" -> "UPDATE groups SET image_url=? WHERE id=?";
|
||||
default -> null;
|
||||
};
|
||||
if (sql == null) return false;
|
||||
try (Connection c = ConnectionDb.connect(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, url);
|
||||
ps.setObject(2, targetId);
|
||||
return ps.executeUpdate() > 0;
|
||||
} catch (SQLException e) { e.printStackTrace(); return false; }
|
||||
}
|
||||
|
||||
private void handleMediaFrame(DataInputStream dis, PrintWriter out) {
|
||||
try {
|
||||
final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1"
|
||||
int magic = dis.readInt();
|
||||
if (magic != MAGIC_EXPECTED) {
|
||||
out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
int headerLen = dis.readInt();
|
||||
if (headerLen <= 0 || headerLen > 64 * 1024) {
|
||||
out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
byte[] headerBytes = dis.readNBytes(headerLen);
|
||||
if (headerBytes.length != headerLen) {
|
||||
out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
long contentLen = dis.readLong();
|
||||
long MAX_MEDIA = 25L * 1024 * 1024;
|
||||
if (contentLen <= 0 || contentLen > MAX_MEDIA) {
|
||||
skip(dis, contentLen);
|
||||
out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
UUID messageId = UUID.fromString(h.getString("message_id"));
|
||||
UUID senderId = UUID.fromString(h.getString("sender_id"));
|
||||
String rType = h.getString("receiver_type"); // private/group/channel
|
||||
UUID receiverId = UUID.fromString(h.getString("receiver_id"));
|
||||
String messageType = h.getString("message_type").toUpperCase(); // IMAGE | AUDIO
|
||||
|
||||
if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) {
|
||||
skip(dis, contentLen);
|
||||
out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
String fileName = h.optString("file_name", "file.bin");
|
||||
String mimeType = h.optString("mime_type", "application/octet-stream");
|
||||
String text = h.optString("text", ""); // کپشن اختیاری
|
||||
|
||||
Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null;
|
||||
Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null;
|
||||
|
||||
if (fileName.length() > 200) fileName = fileName.substring(0, 200);
|
||||
|
||||
java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize();
|
||||
java.nio.file.Files.createDirectories(baseDir);
|
||||
String kind = "IMAGE".equals(messageType) ? "images" : "audios";
|
||||
String subdir = kind + "/" + java.time.LocalDate.now();
|
||||
java.nio.file.Path dir = baseDir.resolve(subdir).normalize();
|
||||
java.nio.file.Files.createDirectories(dir);
|
||||
|
||||
String ext = guessExt(fileName, mimeType);
|
||||
String storedName = java.util.UUID.randomUUID() + ext;
|
||||
java.nio.file.Path target = dir.resolve(storedName).normalize();
|
||||
|
||||
try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream(
|
||||
target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) {
|
||||
long remaining = contentLen;
|
||||
byte[] buf = new byte[8192];
|
||||
while (remaining > 0) {
|
||||
int toRead = (int) Math.min(buf.length, remaining);
|
||||
int n = dis.read(buf, 0, toRead);
|
||||
if (n == -1) throw new EOFException("stream ended early");
|
||||
fos.write(buf, 0, n);
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
|
||||
long fileSize = java.nio.file.Files.size(target);
|
||||
|
||||
String storagePath = target.toString();
|
||||
String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName;
|
||||
String mt = messageType; // "IMAGE" یا "AUDIO"
|
||||
int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0;
|
||||
int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0;
|
||||
FileAttachment att = new FileAttachment();
|
||||
att.setFileUrl(fileUrl);
|
||||
att.setFileType(messageType); // IMAGE/AUDIO
|
||||
att.setFileName(fileName);
|
||||
att.setFileSize(fileSize);
|
||||
att.setMimeType(mimeType);
|
||||
att.setWidth(safeWidth);
|
||||
att.setHeight(safeHeight);
|
||||
att.setDurationSeconds(0);
|
||||
att.setThumbnailUrl(null);
|
||||
att.setStoragePath(storagePath);
|
||||
java.util.List<FileAttachment> atts = java.util.List.of(att);
|
||||
|
||||
boolean ok = MessageDatabase.saveMessageWithOptionalAttachments(
|
||||
messageId, senderId, receiverId, rType, text, messageType, atts
|
||||
);
|
||||
|
||||
UUID mediaKey = null;
|
||||
try (PreparedStatement q = ConnectionDb.connect().prepareStatement(
|
||||
"SELECT media_key FROM message_attachments WHERE message_id = ? AND storage_path = ? LIMIT 1"
|
||||
)) {
|
||||
q.setObject(1, messageId);
|
||||
q.setString(2, storagePath);
|
||||
try (ResultSet rs = q.executeQuery()) {
|
||||
if (rs.next()) mediaKey = (UUID) rs.getObject(1);
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
sqle.printStackTrace();
|
||||
}
|
||||
|
||||
JSONObject ack = new JSONObject()
|
||||
.put("status", ok ? "success" : "error")
|
||||
.put("message_id", messageId.toString())
|
||||
.put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL)
|
||||
.put("file_name", fileName)
|
||||
.put("file_size", fileSize)
|
||||
.put("mime_type", mimeType)
|
||||
.put("display_path", fileUrl);
|
||||
|
||||
out.println(ack.toString());
|
||||
out.flush();
|
||||
|
||||
|
||||
// بعد از out.flush(); و فقط اگر ok==true
|
||||
if (ok) {
|
||||
try {
|
||||
// 1) دریافت پیام از DB تا send_at و... دقیق باشد
|
||||
Message m = MessageDatabase.findById(messageId); // اگر چنین متدی نداری، با پارامترهای همین متد بساز/پر کن
|
||||
|
||||
// 2) لیست دریافتکنندگان بر اساس نوع چت
|
||||
List<UUID> receivers = getReceiversForChat(receiverId, rType.toLowerCase());
|
||||
|
||||
|
||||
// 3) ساخت payload شامل اتچمنت (media)
|
||||
User sender = userDatabase.findByInternalUUID(senderId);
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("action", "new_message")
|
||||
.put("data", new JSONObject()
|
||||
.put("id", m.getMessage_id().toString())
|
||||
.put("chat_id", receiverId.toString())
|
||||
.put("chat_type", rType.toLowerCase())
|
||||
.put("sender_id", senderId.toString())
|
||||
.put("sender_name", sender != null ? sender.getProfile_name() : JSONObject.NULL)
|
||||
.put("message_type", messageType.toLowerCase())
|
||||
.put("text", (text == null || text.isEmpty()) ? JSONObject.NULL : text)
|
||||
.put("media", new JSONObject()
|
||||
.put("media_id", mediaKey != null ? mediaKey.toString() : JSONObject.NULL)
|
||||
.put("file_name", fileName)
|
||||
.put("mime_type", mimeType)
|
||||
.put("size_bytes", fileSize)
|
||||
.put("url", fileUrl)
|
||||
.put("thumbnail_url", JSONObject.NULL)
|
||||
.put("width", safeWidth)
|
||||
.put("height", safeHeight)
|
||||
.put("duration_ms", 0)
|
||||
)
|
||||
.put("send_at", m.getSend_at().toString())
|
||||
.put("status", "SENT")
|
||||
);
|
||||
|
||||
// 4) ارسال به همه اعضا (از جمله خودِ فرستنده اگر میخواهی UI آن هم یکپارچه آپدیت شود)
|
||||
for (UUID uid : receivers) {
|
||||
RealTimeEventDispatcher.sendToUser(uid, payload);
|
||||
}
|
||||
|
||||
// (اختیاری) رویداد آپدیت چتلیست برای sort بر اساس آخرین پیام
|
||||
RealTimeEventDispatcher.notifyChatUpdated(receiverId, rType, m);
|
||||
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
// اگر ذخیره شد ولی Broadcast شکست خورد، میتوانی Log کنی یا Retry سبک انجام دهی
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
out.println(new JSONObject().put("status","error").put("message","exception").toString());
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MAGIC_DL = 0x4D444D32; // "MDM2"
|
||||
|
||||
// private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) {
|
||||
// try {
|
||||
// int magic = inBin.readInt();
|
||||
// if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; }
|
||||
//
|
||||
// int hlen = inBin.readInt();
|
||||
// if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; }
|
||||
//
|
||||
// byte[] hb = inBin.readNBytes(hlen);
|
||||
// if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; }
|
||||
//
|
||||
// JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8));
|
||||
// if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; }
|
||||
//
|
||||
// UUID mediaKey = UUID.fromString(hdr.getString("media_key"));
|
||||
// long offset = Math.max(0L, hdr.optLong("offset", 0L));
|
||||
//
|
||||
// MediaRow mr = MessageDatabase.findMediaByKey(mediaKey);
|
||||
// if (mr == null) { sendDlErr(outBin, "not found"); return; }
|
||||
// if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; }
|
||||
//
|
||||
// java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize();
|
||||
// long size = java.nio.file.Files.size(path);
|
||||
// if (offset > size) offset = 0L;
|
||||
//
|
||||
// JSONObject ok = new JSONObject()
|
||||
// .put("status","success")
|
||||
// .put("media_key", mediaKey.toString())
|
||||
// .put("file_name", mr.fileName)
|
||||
// .put("mime_type", mr.mimeType)
|
||||
// .put("file_size", size);
|
||||
//
|
||||
// byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
//
|
||||
// outBin.writeInt(MAGIC_DL);
|
||||
// outBin.writeInt(okb.length);
|
||||
// outBin.write(okb);
|
||||
// outBin.writeLong(size - offset);
|
||||
//
|
||||
// try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) {
|
||||
// if (offset > 0) fis.skipNBytes(offset);
|
||||
// byte[] buf = new byte[8192];
|
||||
// long remain = size - offset;
|
||||
// while (remain > 0) {
|
||||
// int n = fis.read(buf, 0, (int) Math.min(buf.length, remain));
|
||||
// if (n == -1) break;
|
||||
// outBin.write(buf, 0, n);
|
||||
// remain -= n;
|
||||
// }
|
||||
// }
|
||||
// outBin.flush();
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) {
|
||||
try {
|
||||
logf("MEDIA_DL start. requester=%s", requesterId);
|
||||
|
||||
int magic = inBin.readInt();
|
||||
if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; }
|
||||
|
||||
int hlen = inBin.readInt();
|
||||
if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; }
|
||||
|
||||
byte[] hb = inBin.readNBytes(hlen);
|
||||
if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; }
|
||||
|
||||
String hdrStr = new String(hb, java.nio.charset.StandardCharsets.UTF_8);
|
||||
logf("MEDIA_DL header: %s", hdrStr);
|
||||
|
||||
JSONObject hdr = new JSONObject(hdrStr);
|
||||
if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; }
|
||||
|
||||
UUID mediaKey = UUID.fromString(hdr.getString("media_key"));
|
||||
long offset = Math.max(0L, hdr.optLong("offset", 0L));
|
||||
logf("Parsed mediaKey=%s offset=%d", mediaKey, offset);
|
||||
|
||||
MediaRow mr = MessageDatabase.findMediaByKey(mediaKey);
|
||||
if (mr == null) { sendDlErr(outBin, "not found"); return; }
|
||||
|
||||
logf("MediaRow: chatType=%s chatId=%s sender=%s receiver=%s storage=%s",
|
||||
mr.chatType, mr.chatId, mr.senderId, mr.receiverId, mr.storagePath);
|
||||
|
||||
try (java.sql.Connection c = ConnectionDb.connect();
|
||||
java.sql.PreparedStatement st = c.prepareStatement(
|
||||
"SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1")) {
|
||||
st.setObject(1, mr.chatId, java.sql.Types.OTHER);
|
||||
st.setObject(2, requesterId, java.sql.Types.OTHER);
|
||||
boolean direct;
|
||||
try (java.sql.ResultSet r = st.executeQuery()) { direct = r.next(); }
|
||||
logf("[DL] direct channel membership ch=%s user=%s => %s", mr.chatId, requesterId, direct);
|
||||
} catch (Exception e) {
|
||||
logf("[DL] direct membership check ERROR: %s", e.toString());
|
||||
}
|
||||
|
||||
boolean allowed = MessageDatabase.canAccess(requesterId, mr);
|
||||
logf("canAccess(..) -> %s", allowed);
|
||||
if (!allowed) { sendDlErr(outBin, "not authorized"); return; }
|
||||
|
||||
java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize();
|
||||
long size = java.nio.file.Files.size(path);
|
||||
if (offset > size) offset = 0L;
|
||||
|
||||
JSONObject ok = new JSONObject()
|
||||
.put("status","success")
|
||||
.put("media_key", mediaKey.toString())
|
||||
.put("file_name", mr.fileName)
|
||||
.put("mime_type", mr.mimeType)
|
||||
.put("file_size", size);
|
||||
|
||||
byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
outBin.writeInt(MAGIC_DL);
|
||||
outBin.writeInt(okb.length);
|
||||
outBin.write(okb);
|
||||
outBin.writeLong(size - offset);
|
||||
logf("Sending OK header. file=%s size=%d offset=%d", mr.fileName, size, offset);
|
||||
|
||||
try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) {
|
||||
if (offset > 0) fis.skipNBytes(offset);
|
||||
byte[] buf = new byte[8192];
|
||||
long remain = size - offset;
|
||||
while (remain > 0) {
|
||||
int n = fis.read(buf, 0, (int) Math.min(buf.length, remain));
|
||||
if (n == -1) break;
|
||||
outBin.write(buf, 0, n);
|
||||
remain -= n;
|
||||
}
|
||||
}
|
||||
outBin.flush();
|
||||
log("MEDIA_DL done.");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDlErr(DataOutputStream outBin, String msg) throws java.io.IOException {
|
||||
JSONObject j = new JSONObject().put("status","error").put("message", msg);
|
||||
byte[] b = j.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
outBin.writeInt(MAGIC_DL);
|
||||
outBin.writeInt(b.length);
|
||||
outBin.write(b);
|
||||
outBin.writeLong(0L);
|
||||
outBin.flush();
|
||||
}
|
||||
|
||||
private static void skip(DataInputStream dis, long n) throws IOException {
|
||||
if (n <= 0) return;
|
||||
byte[] buf = new byte[8192];
|
||||
long left = n;
|
||||
while (left > 0) {
|
||||
int toRead = (int) Math.min(buf.length, left);
|
||||
int r = dis.read(buf, 0, toRead);
|
||||
if (r == -1) break; // EOF
|
||||
left -= r;
|
||||
}
|
||||
}
|
||||
|
||||
private static String guessExt(String original, String mime) {
|
||||
if (original != null && original.contains(".")) {
|
||||
String ext = original.substring(original.lastIndexOf('.'));
|
||||
if (ext.length() <= 10) return ext.toLowerCase();
|
||||
}
|
||||
if (mime == null) return "";
|
||||
|
||||
String m = mime.toLowerCase();
|
||||
|
||||
if (m.equals("image/png")) return ".png";
|
||||
if (m.equals("image/jpeg") || m.equals("image/jpg")) return ".jpg";
|
||||
if (m.equals("image/gif")) return ".gif";
|
||||
if (m.equals("image/webp")) return ".webp";
|
||||
|
||||
if (m.equals("audio/mpeg") || m.equals("audio/mp3")) return ".mp3";
|
||||
if (m.equals("audio/ogg")) return ".ogg";
|
||||
if (m.equals("audio/opus")) return ".opus";
|
||||
if (m.equals("audio/wav") || m.equals("audio/x-wav")) return ".wav";
|
||||
if (m.equals("audio/m4a") || m.equals("audio/mp4")) return ".m4a";
|
||||
|
||||
// if (m.equals("video/mp4")) return ".mp4";
|
||||
// if (m.equals("video/webm")) return ".webm";
|
||||
|
||||
// fallback
|
||||
if (m.startsWith("image/")) return "";
|
||||
if (m.startsWith("audio/")) return "";
|
||||
if (m.startsWith("video/")) return "";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private static String readUtf8Line(BufferedInputStream bis) throws java.io.IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (true) {
|
||||
int b = bis.read();
|
||||
if (b == -1) {
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
if (b == '\n') {
|
||||
int len = sb.length();
|
||||
if (len > 0 && sb.charAt(len - 1) == '\r') sb.setLength(len - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
sb.append((char) b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user