diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..1cc8080 --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + com.ecs160 + annotations + 1.0-SNAPSHOT + + src/test/java + + + src/main/resources + + **/*.json + + + desktop.ini + + + + target/generated-sources/annotations + + desktop.ini + + + + + + maven-shade-plugin + 3.4.1 + + + package + + shade + + + true + + + com.ecs160.MyApp + + + + + *:* + + META-INF/MANIFEST.MF + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + maven-jar-plugin + 3.2.0 + + + + com.ecs160.MyApp + + + + + + maven-jar-plugin + 3.3.0 + + + + com.ecs160.hw1.SocialMediaAnalyzerDriver + + + + + + maven-surefire-plugin + 3.0.0 + + + + + UTF-8 + 21 + 21 + + diff --git a/newpost.java b/newpost.java new file mode 100644 index 0000000..2e34bab --- /dev/null +++ b/newpost.java @@ -0,0 +1,112 @@ +package com.ecs160; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; + +@Persistable +public class Post { + + @PersistableId + @SerializedName("cid") + private String postId; + + @PersistableField + @SerializedName("post") + private PostData post; // ✅ `record` 现在在 `post` 里面 + + @PersistableField + @SerializedName("thread") + private Thread thread; // ✅ `replies` 仍然在 `thread` 里 + + @LazyLoad + @PersistableListField(className = "Post") + @SerializedName("replyIds") + private List replyIds; + + private transient List replies; + + public String getPostId() { + return (post != null) ? post.getCid() : null; + } + + // ✅ `record` 现在在 `post` 里 + public String getPostContent() { + if (post != null && post.getRecord() != null) { + return maskUrls(post.getRecord().getText()); + } + return null; + } + + private String maskUrls(String text) { + if (text == null) return null; + return text.replaceAll("(https?://\\S+|\\S+\\.\\S+)", "[link]"); + } + + // ✅ 解析 `thread.replies` + public List getReplies() { + if (replies == null && thread != null && thread.getReplies() != null) { + replies = thread.getReplies(); + } + return replies; + } + + public void setReplies(List replies) { + if (replies != null) { + this.replies = replies; + this.replyIds = replies.stream().map(Post::getPostId).toList(); + } + } + + @Override + public String toString() { + return "Post{" + + "postId=" + getPostId() + + ", postContent='" + getPostContent() + '\'' + + ", replies=" + (replyIds != null ? replyIds : "[]") + + '}'; + } + + // ✅ `Thread` 仍然保留,因为 `replies` 在 `thread` 里 + public static class Thread { + @SerializedName("post") + private PostData post; + + @SerializedName("replies") + private List replies; // ✅ `replies` 仍然在 `thread` 里 + + public PostData getPost() { + return post; + } + + public List getReplies() { + return replies; + } + } + + public static class PostData { + @SerializedName("cid") + private String cid; + + @SerializedName("record") + private Record record; // ✅ `record` 现在在 `post` 里 + + public String getCid() { + return cid; + } + + public Record getRecord() { + return record; + } + } + + public static class Record { + @SerializedName("text") + private String text; + + public String getText() { + return text; + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8373c71..4fee3f6 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + com.ecs160.MyApp + + + + org.apache.maven.plugins maven-jar-plugin diff --git a/src/main/java/com/ecs160/JsonLoader.java b/src/main/java/com/ecs160/JsonLoader.java new file mode 100644 index 0000000..d3b8988 --- /dev/null +++ b/src/main/java/com/ecs160/JsonLoader.java @@ -0,0 +1,29 @@ +package com.ecs160; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; + +public class JsonLoader { + private static final Gson gson = new Gson(); + + public static List loadPosts(String filePath) throws IOException { + try (FileReader reader = new FileReader(filePath)) { + JsonObject jsonObject = gson.fromJson(reader, JsonObject.class); + JsonArray feedArray = jsonObject.getAsJsonArray("feed"); + Type listType = new TypeToken>() {}.getType(); + List posts = gson.fromJson(feedArray, listType); + + + /* + for (int i = 0; i < Math.min(posts.size(), 10); i++) { + System.out.println("加载 Post ID: " + posts.get(i).getPostId()); + } + */ + return posts; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/MyApp.java b/src/main/java/com/ecs160/MyApp.java index d873b0d..1fd3cf0 100644 --- a/src/main/java/com/ecs160/MyApp.java +++ b/src/main/java/com/ecs160/MyApp.java @@ -1,13 +1,101 @@ package com.ecs160; -import java.io.FileNotFoundException; -import java.lang.reflect.InvocationTargetException; -import java.net.URISyntaxException; +import com.ecs160.persistence.Session; +import java.util.Scanner; import java.util.List; public class MyApp { - public static void main(String[] args) throws FileNotFoundException, URISyntaxException, NoSuchFieldException { + public static void main(String[] args) { + + + Session.getredisSession().CleanDataBase(); + + try { + String filePath = "src/main/resources/input.json"; + List posts = JsonLoader.loadPosts(filePath); + int count = 0; + Session session = Session.getredisSession(); + for (Post post : posts) { + + session.add(post); + //TestIfGotPostsFromJson(count,post); + } + session.persistAll(); + + //PrintFist10Posts(posts); + + UserInput(); + + } catch (Exception e) { + e.printStackTrace(); + } } -} + + + public static void UserInput() { + Scanner scanner = new Scanner(System.in); + while (true) { + System.out.print("\nPlease Enter Post ID (or type 'exit' to quit): "); + String inputPostId = scanner.nextLine().trim(); + + if (inputPostId.equalsIgnoreCase("exit")) { + System.out.println("Exiting UserInput mode..."); + break; + } + + // 加载 Post + Post post = (Post) Session.getredisSession().load(Post.class, inputPostId); + + if (post != null) { + // 成功查询到 Post + System.out.println("\nLoad Post Successfully..."); + System.out.println("Post Content: " + post.getPostContent()); + System.out.println("Reply IDs: " + (post.getReplyIds() != null ? post.getReplyIds() : "[]")); + List replyTexts = post.getReplyTexts(); + if (!replyTexts.isEmpty()) { + System.out.println(" Replies:"); + for (String replyText : replyTexts) { + System.out.println(" " + replyText); + } + } else { + System.out.println(" No Replies Found."); + } + } else { + System.out.println("Post Not Found. Please try again."); + } + } + scanner.close(); + } + + public static void TestIfGotPostsFromJson(int count, Post post) { + if (count <=20) { + System.out.println("\n========== DEBUG: Loading Posts =========="+ " Number Of "+ count); + System.out.println("\uD83D\uDCCC" + post); + System.out.println("✅ Debug: if have replies In the Posts: " + post.getReplyIds()); + } + } + + public static void PrintFist10Posts(List posts) { + int J= 0; + for (int i = 0; i < Math.min(posts.size(), 2); i++) { + String postId = posts.get(i).getPostId(); + + Post loadedPost = (Post) Session.getredisSession().load(Post.class, postId); + if (loadedPost != null) { + + System.out.println("\n========== DEBUG: Loading Posts From Redis =========="); + + System.out.println("✅ Post ID: " + loadedPost.getPostId()); + System.out.println("✅ Post Content: " + loadedPost.getPostContent()); + System.out.println("✅ Reply IDs from Redis: " + + (loadedPost.getReplyIds() != null ? loadedPost.getReplyIds() : "[]")); + + + //System.out.println("\uD83D\uDCCC" + loadedPost.getReplyTexts()); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/Post.java b/src/main/java/com/ecs160/Post.java new file mode 100644 index 0000000..f06fc31 --- /dev/null +++ b/src/main/java/com/ecs160/Post.java @@ -0,0 +1,142 @@ +package com.ecs160; + +import com.ecs160.persistence.*; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Persistable +public class Post { + + @PersistableField + @SerializedName("thread") + private Post.Thread thread; + + @PersistableField + @PersistableListField(className = "Post") + private List replyIds; + + @PersistableId + private String postId; + + + public String getPostId() { + if (postId == null && thread != null && thread.post != null) { + postId = thread.post.getCid(); + } + return postId; + } + + public String getPostContent() { + if (thread != null && thread.post != null && thread.post.getRecord() != null) { + return formatURL(thread.post.getRecord().getText()) ; + } + return null; + } + + public List getReplies() { + if (thread != null && thread.getReplies() != null) { + return thread.getReplies(); + } + return List.of(); + } + + public List getReplyIds() { + if (getReplies() != null) { + this.replyIds = getReplies().stream() + .map(t -> t.getPost().getCid()) + .toList(); + } else { + this.replyIds = List.of(); + } + return replyIds; + } + + public void setReplyIds(List replyIds) { + this.replyIds = replyIds; + } + + public void setPostId(String postId) { + this.postId = postId; + } + + private String formatURL(String text) {if (text == null) return null;text = text.replaceAll("[\\n\\r]", " ");return text.replaceAll("(https?://\\S+|\\S+\\.\\S+)", "[link]");} + + public List getReplyTexts() { + if (this.getReplies() == null) { + return List.of(); + } + + return this.getReplies().stream() + .map(reply -> { + if (reply.getPost() != null && reply.getPost().getRecord() != null) { + return reply.getPost().getRecord().getText(); + } + return "(No text available)"; + }) + .toList(); + } + + + + @Override + public String toString() { + return "Post{" + + "postId=" + getPostId() + + ", postContent='" + getPostContent() + '\'' + + ", replies=" + (getReplies() != null ? + getReplies().stream().map(t -> t.getPost().getCid()).toList() : "[]") + + '}'; + } + + + public static class Thread { + @PersistableField + @SerializedName("post") + private PostData post; + + @PersistableListField(className = "Post") + @SerializedName("replies") + private List replies; + + public PostData getPost() { + return post; + } + + public List getReplies() { + return replies; + } + public void setReplies(List replies) { + this.replies = replies; + } + + } + + + public static class PostData { + @SerializedName("cid") + private String cid; + + @SerializedName("record") + private Record record; + + public String getCid() { + return cid; + } + + public Record getRecord() { + return record; + } + } + + public static class Record { + @SerializedName("text") + private String text; + + public String getText() { + return text; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/persistence/LazyLoad.java b/src/main/java/com/ecs160/persistence/LazyLoad.java new file mode 100644 index 0000000..f308751 --- /dev/null +++ b/src/main/java/com/ecs160/persistence/LazyLoad.java @@ -0,0 +1,11 @@ +package com.ecs160.persistence; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LazyLoad { +} diff --git a/src/main/java/com/ecs160/persistence/Persistable.java b/src/main/java/com/ecs160/persistence/Persistable.java index d155ac2..f3617f7 100644 --- a/src/main/java/com/ecs160/persistence/Persistable.java +++ b/src/main/java/com/ecs160/persistence/Persistable.java @@ -7,3 +7,4 @@ public @interface Persistable { } + diff --git a/src/main/java/com/ecs160/persistence/PersistableField.java b/src/main/java/com/ecs160/persistence/PersistableField.java new file mode 100644 index 0000000..6934d84 --- /dev/null +++ b/src/main/java/com/ecs160/persistence/PersistableField.java @@ -0,0 +1,12 @@ +package com.ecs160.persistence; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +// ✅ 只有被标记的字段才会被 `Session` 存入 Redis +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface PersistableField { +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/persistence/PersistableId.java b/src/main/java/com/ecs160/persistence/PersistableId.java new file mode 100644 index 0000000..48304e1 --- /dev/null +++ b/src/main/java/com/ecs160/persistence/PersistableId.java @@ -0,0 +1,12 @@ +package com.ecs160.persistence; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface PersistableId { +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/persistence/PersistableListField.java b/src/main/java/com/ecs160/persistence/PersistableListField.java new file mode 100644 index 0000000..e4e5200 --- /dev/null +++ b/src/main/java/com/ecs160/persistence/PersistableListField.java @@ -0,0 +1,13 @@ +package com.ecs160.persistence; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface PersistableListField { + String className(); +} \ No newline at end of file diff --git a/src/main/java/com/ecs160/persistence/Session.java b/src/main/java/com/ecs160/persistence/Session.java index 9a84147..1499032 100644 --- a/src/main/java/com/ecs160/persistence/Session.java +++ b/src/main/java/com/ecs160/persistence/Session.java @@ -1,41 +1,156 @@ package com.ecs160.persistence; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javassist.util.proxy.MethodHandler; -import javassist.util.proxy.ProxyFactory; +import com.ecs160.Post; +import com.google.gson.Gson; import redis.clients.jedis.Jedis; +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; - - -// Assumption - only support int/long/and string values public class Session { - - private Jedis jedisSession; + private static Session redisSession; + private final Jedis jedisSession; + private final Gson gson; + private final Map cache; private Session() { - jedisSession = new Jedis("localhost", 6379);; + this.jedisSession = new Jedis("localhost", 6379); + this.gson = new Gson(); + this.cache = new HashMap<>(); } + public static Session getredisSession() { + if (redisSession == null) { + redisSession = new Session(); + } + return redisSession; + + } public void add(Object obj) { + if (obj instanceof Post) { + ((Post) obj).getPostId(); + } + + try { + Field idField = getPersistableIdField(obj); + if (idField == null) { + return; + } + idField.setAccessible(true); + String postId = (String) idField.get(obj); + + if (postId == null || postId.isEmpty()) { + return; + } + String key = obj.getClass().getSimpleName() + ":" + postId; + cache.put(key, obj); + //System.out.println("DEBUG: Cache after adding post -> " + cache); + + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void persistAll() { + + for (Map.Entry entry : cache.entrySet()) { + String key = entry.getKey(); + Object obj = entry.getValue(); + + if (obj instanceof Post p) {p.getReplyIds();} //Nor sure if this is allowed.... + // but I really spent over 10 hours to findi hits solution + + + jedisSession.hset(key, "data", gson.toJson(obj)); + + for (Field field : obj.getClass().getDeclaredFields()) { + field.setAccessible(true); + + if (field.isAnnotationPresent(PersistableListField.class)) { + try { + //System.out.println("This is field.get(obj)" + field.get(obj)); + //System.out.println("This is replyIds " + replyIds); + //System.out.println("This is ReplyIds " + replyIds); + List replyIdsList = (List) field.get(obj); + String replyIds = String.join(",", replyIdsList); + //System.out.println("This is replyIds: " + replyIds); + jedisSession.hset(key, field.getName(), replyIds); + // System.out.println("Saving to Redis Key: " + key + ", Field: " + field.getName() + ", Value: " + replyIds); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + } + } + } - public void persistAll() { + + public Object load(Class clazz, String postId) { + if (postId == null || clazz == null) { + return null; + } + + String key = clazz.getSimpleName() + ":" + postId; + Map redisData = jedisSession.hgetAll(key); + + if (redisData.isEmpty()) { + return null; + } + + try { + Post post = gson.fromJson(redisData.get("data"), Post.class); + + // 解析 replyIds 并转换为 Post 对象 + if (redisData.containsKey("replyIds")) { + String replyIdsStr = redisData.get("replyIds"); + List replyIdsList = replyIdsStr.isEmpty() ? List.of() : Arrays.asList(replyIdsStr.split(",")); + post.setReplyIds(replyIdsList); + } + + return post; + } catch (Exception e) { + e.printStackTrace(); + return null; + } } - public Object load(Object object) { + + + private Field getPersistableIdField(Object obj) { + for (Field field : obj.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(PersistableId.class)) { + return field; + } + } return null; } + private String getObjectId(Object obj) { + if (obj == null) { + return "null"; + } + try { + Field idField = getPersistableIdField(obj); + if (idField != null) { + idField.setAccessible(true); + String id = (String) idField.get(obj); + return (id != null) ? id : "null"; + } + } catch (Exception e) { + e.printStackTrace(); + } + return "null"; + } -} + + public void CleanDataBase() { + jedisSession.flushDB(); + } +} \ No newline at end of file