diff --git a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java index 9f86ca5e4..e9181f3ed 100644 --- a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java +++ b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java @@ -113,9 +113,9 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setTemplateLoaderPaths(FILE_PROTOCOL + haloProperties.getWorkDir() + "templates/", "classpath:/templates/"); configurer.setDefaultEncoding("UTF-8"); - if (haloProperties.isProductionEnv()) { - configurer.getConfiguration().setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - } +// if (haloProperties.isProductionEnv()) { +// configurer.getConfiguration().setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); +// } return configurer; } diff --git a/src/main/java/run/halo/app/event/ApplicationEventQueuePublisher.java b/src/main/java/run/halo/app/event/ApplicationEventQueuePublisher.java new file mode 100644 index 000000000..a42c5982b --- /dev/null +++ b/src/main/java/run/halo/app/event/ApplicationEventQueuePublisher.java @@ -0,0 +1,86 @@ +package run.halo.app.event; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.EventListener; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Event queue dispatcher. + * + * @author johnniang + * @date 19-4-20 + */ +@Slf4j +@Component +public class ApplicationEventQueuePublisher { + + private final BlockingQueue events = new LinkedBlockingQueue<>(); + + private final ApplicationListenerManager listenerManager; + + private final ExecutorService executorService; + + public ApplicationEventQueuePublisher(ApplicationListenerManager listenerManager) { + this.listenerManager = listenerManager; + this.executorService = Executors.newSingleThreadExecutor(); + + executorService.execute(new EventQueueConsumer()); + } + + public void publishEvent(Object event) { + try { + events.put(event); + } catch (InterruptedException e) { + log.warn("Failed to put event to the queue", e); + // Ignore this error + } + } + + @PreDestroy + protected void destroy() { + log.info("Shutting down all event queue consumer"); + this.executorService.shutdownNow(); + } + + @SuppressWarnings("unchecked") + private class EventQueueConsumer implements Runnable { + + @Override + public void run() { + + while (!Thread.currentThread().isInterrupted()) { + try { + // Take an event + Object event = events.take(); + + // Get listeners + List listeners = listenerManager.getListeners(event); + + // Handle the event + listeners.forEach(listener -> { + if (listener instanceof ApplicationListener && event instanceof ApplicationEvent) { + ApplicationEvent applicationEvent = (ApplicationEvent) event; + // Fire event + ((ApplicationListener) listener).onApplicationEvent(applicationEvent); + } + }); + + log.info("Event queue consumer has been shut down"); + } catch (InterruptedException e) { + log.warn("Failed to take event", e); + } catch (Exception e) { + log.error("Failed to handle event", e); + } + } + } + } +} diff --git a/src/main/java/run/halo/app/event/ApplicationListenerManager.java b/src/main/java/run/halo/app/event/ApplicationListenerManager.java new file mode 100644 index 000000000..6bb63923c --- /dev/null +++ b/src/main/java/run/halo/app/event/ApplicationListenerManager.java @@ -0,0 +1,85 @@ +package run.halo.app.event; + +import cn.hutool.core.lang.Assert; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import run.halo.app.utils.ReflectionUtils; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Application listener manager. + * + * @author johnniang + * @date 19-4-21 + */ +@Slf4j +@Component +public class ApplicationListenerManager { + + /** + * Listener Map. + */ + private final Map> listenerMap = new ConcurrentHashMap<>(); + + public ApplicationListenerManager(ApplicationContext applicationContext) { + // TODO Need to refactor + // Register all listener on starting up + applicationContext.getBeansOfType(ApplicationListener.class).values().forEach(this::register); + + log.debug("Initialized event listeners"); + } + + public List getListeners(@Nullable Object event) { + if (event == null) { + return Collections.emptyList(); + } + + // Get listeners + List listeners = listenerMap.get(event.getClass().getTypeName()); + // Clone the listeners + return listeners == null ? Collections.emptyList() : new LinkedList<>(listeners); + } + + public synchronized void register(@NonNull ApplicationListener listener) { + // Get actual generic type + Type actualType = resolveActualGenericType(listener); + + if (actualType == null) { + return; + } + + // Add this listener + listenerMap.computeIfAbsent(actualType.getTypeName(), (key) -> new LinkedList<>()).add(listener); + } + + public synchronized void unRegister(@NonNull ApplicationListener listener) { + // Get actual generic type + Type actualType = resolveActualGenericType(listener); + + if (actualType == null) { + return; + } + + // Remove it from listener map + listenerMap.getOrDefault(actualType.getTypeName(), Collections.emptyList()).remove(listener); + } + + @Nullable + private Type resolveActualGenericType(@NonNull ApplicationListener listener) { + Assert.notNull(listener, "Application listener must not be null"); + + log.debug("Attempting to resolve type of Application listener: [{}]", listener); + + ParameterizedType parameterizedType = ReflectionUtils.getParameterizedType(ApplicationListener.class, ((ApplicationListener) listener).getClass()); + + return parameterizedType == null ? null : parameterizedType.getActualTypeArguments()[0]; + } +} diff --git a/src/main/java/run/halo/app/event/LogEvent.java b/src/main/java/run/halo/app/event/LogEvent.java index 72dccf3a1..7ddd63914 100644 --- a/src/main/java/run/halo/app/event/LogEvent.java +++ b/src/main/java/run/halo/app/event/LogEvent.java @@ -1,6 +1,9 @@ package run.halo.app.event; import org.springframework.context.ApplicationEvent; +import run.halo.app.model.enums.LogType; +import run.halo.app.model.params.LogParam; +import run.halo.app.utils.ValidationUtils; /** * @author johnniang @@ -8,12 +11,28 @@ import org.springframework.context.ApplicationEvent; */ public class LogEvent extends ApplicationEvent { + private final LogParam logParam; + /** * Create a new ApplicationEvent. * - * @param source the object on which the event initially occurred (never {@code null}) + * @param source the object on which the event initially occurred (never {@code null}) + * @param logParam */ - public LogEvent(Object source) { + public LogEvent(Object source, LogParam logParam) { super(source); + + // Validate the log param + ValidationUtils.validate(logParam); + + this.logParam = logParam; + } + + public LogEvent(Object source, String logKey, LogType logType, String content) { + this(source, new LogParam(logKey, logType, content)); + } + + public LogParam getLogParam() { + return logParam; } } diff --git a/src/main/java/run/halo/app/event/LogEventListener.java b/src/main/java/run/halo/app/event/LogEventListener.java new file mode 100644 index 000000000..a037b2857 --- /dev/null +++ b/src/main/java/run/halo/app/event/LogEventListener.java @@ -0,0 +1,30 @@ +package run.halo.app.event; + +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import run.halo.app.model.entity.Log; +import run.halo.app.service.LogService; + +/** + * Log event listener. + * + * @author johnniang + * @date 19-4-21 + */ +@Component +public class LogEventListener implements ApplicationListener { + + private final LogService logService; + + public LogEventListener(LogService logService) { + this.logService = logService; + } + + @Override + public void onApplicationEvent(LogEvent event) { + // Convert to log + Log logToCreate = event.getLogParam().convertTo(); + // Create log + logService.create(logToCreate); + } +} diff --git a/src/main/java/run/halo/app/model/entity/Log.java b/src/main/java/run/halo/app/model/entity/Log.java index 3530fe817..05a6973d4 100644 --- a/src/main/java/run/halo/app/model/entity/Log.java +++ b/src/main/java/run/halo/app/model/entity/Log.java @@ -1,12 +1,13 @@ package run.halo.app.model.entity; -import run.halo.app.model.enums.LogType; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; +import run.halo.app.model.enums.LogType; +import run.halo.app.utils.ServletUtils; import javax.persistence.*; @@ -57,5 +58,16 @@ public class Log extends BaseEntity { public void prePersist() { super.prePersist(); id = null; + + if (logKey == null) { + logKey = ""; + } + + // Get ip address + ipAddress = ServletUtils.getRequestIp(); + + if (ipAddress == null) { + logKey = ""; + } } } diff --git a/src/main/java/run/halo/app/model/enums/LogType.java b/src/main/java/run/halo/app/model/enums/LogType.java index 6ce5de4e7..a2bf78032 100644 --- a/src/main/java/run/halo/app/model/enums/LogType.java +++ b/src/main/java/run/halo/app/model/enums/LogType.java @@ -6,6 +6,13 @@ package run.halo.app.model.enums; * @author johnniang */ public enum LogType implements ValueEnum { + + POST_PUBLISHED(0), + POST_EDITED(1), + POST_DELETED(5), + LOGGED_IN(2), + LOGGED_OUT(3), + LOGIN_FAILED(4), ; private final Integer value; diff --git a/src/main/java/run/halo/app/model/params/LogParam.java b/src/main/java/run/halo/app/model/params/LogParam.java new file mode 100644 index 000000000..636285370 --- /dev/null +++ b/src/main/java/run/halo/app/model/params/LogParam.java @@ -0,0 +1,33 @@ +package run.halo.app.model.params; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import run.halo.app.model.dto.base.InputConverter; +import run.halo.app.model.entity.Log; +import run.halo.app.model.enums.LogType; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * @author johnniang + * @date 19-4-21 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LogParam implements InputConverter { + + @Size(max = 1023, message = "Length of log key must not be more than {max}") + private String logKey; + + @NotNull(message = "Log type must not be null") + private LogType type; + + @NotBlank(message = "Log content must not be blank") + @Size(max = 1023, message = "Log content must not be more than 1023") + private String content; +} diff --git a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java index de5d6bc66..578b3001f 100644 --- a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java @@ -13,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import run.halo.app.event.ApplicationEventQueuePublisher; +import run.halo.app.event.LogEvent; import run.halo.app.exception.AlreadyExistsException; import run.halo.app.exception.NotFoundException; import run.halo.app.model.dto.CategoryOutputDTO; @@ -20,6 +22,7 @@ import run.halo.app.model.dto.TagOutputDTO; import run.halo.app.model.dto.post.PostMinimalOutputDTO; import run.halo.app.model.dto.post.PostSimpleOutputDTO; import run.halo.app.model.entity.*; +import run.halo.app.model.enums.LogType; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.params.PostQuery; import run.halo.app.model.vo.ArchiveMonthVO; @@ -66,12 +69,15 @@ public class PostServiceImpl extends AbstractCrudService implemen private final CommentService commentService; + private final ApplicationEventQueuePublisher eventQueuePublisher; + public PostServiceImpl(PostRepository postRepository, TagService tagService, CategoryService categoryService, PostTagService postTagService, PostCategoryService postCategoryService, - CommentService commentService) { + CommentService commentService, + ApplicationEventQueuePublisher eventQueuePublisher) { super(postRepository); this.postRepository = postRepository; this.tagService = tagService; @@ -79,6 +85,7 @@ public class PostServiceImpl extends AbstractCrudService implemen this.postTagService = postTagService; this.postCategoryService = postCategoryService; this.commentService = commentService; + this.eventQueuePublisher = eventQueuePublisher; } @Override @@ -214,13 +221,36 @@ public class PostServiceImpl extends AbstractCrudService implemen return createOrUpdate(postToUpdate, tagIds, categoryIds, this::update); } + @Override + public Post create(Post post) { + Post createdPost = super.create(post); + + // Log the creation + LogEvent logEvent = new LogEvent(this, createdPost.getId().toString(), LogType.POST_PUBLISHED, createdPost.getTitle()); + eventQueuePublisher.publishEvent(logEvent); + + return createdPost; + } + + @Override + public Post update(Post post) { + Post updatedPost = super.update(post); + + // Log the creation + LogEvent logEvent = new LogEvent(this, updatedPost.getId().toString(), LogType.POST_EDITED, updatedPost.getTitle()); + eventQueuePublisher.publishEvent(logEvent); + + return updatedPost; + } + private PostDetailVO createOrUpdate(@NonNull Post post, Set tagIds, Set categoryIds, @NonNull Function postOperation) { Assert.notNull(post, "Post param must not be null"); Assert.notNull(postOperation, "Post operation must not be null"); // Check url long count; - if (post.getId() != null) { + boolean isUpdating = post.getId() != null; + if (isUpdating) { // For updating count = postRepository.countByIdNotAndUrl(post.getId(), post.getUrl()); } else { diff --git a/src/main/java/run/halo/app/utils/CommentUtil.java b/src/main/java/run/halo/app/utils/CommentUtil.java deleted file mode 100644 index 0dbe576f1..000000000 --- a/src/main/java/run/halo/app/utils/CommentUtil.java +++ /dev/null @@ -1,72 +0,0 @@ -package run.halo.app.utils; - -/** - *
- * 拼装评论
- * 
- * - * @author : RYAN0UP - * @date : 2018/7/12 - */ -public class CommentUtil { - -// /** -// * 获取组装好的评论 -// * -// * @param commentsRoot commentsRoot -// * @return List -// */ -// public static List getComments(List commentsRoot) { -// if (CollectionUtils.isEmpty(commentsRoot)) { -// return Collections.emptyList(); -// } -// -// final List commentsResult = new ArrayList<>(); -// -// for (Comment comment : commentsRoot) { -// if (comment.getCommentParent() == 0) { -// commentsResult.add(comment); -// } -// } -// -// for (Comment comment : commentsResult) { -// comment.setChildComments(getChild(comment.getCommentId(), commentsRoot)); -// } -// // 集合倒序,最新的评论在最前面 -// Collections.reverse(commentsResult); -// return commentsResult; -// } -// -// /** -// * 获取评论的子评论 -// * -// * @param id 评论编号 -// * @param commentsRoot commentsRoot -// * @return List -// */ -// private static List getChild(Long id, List commentsRoot) { -// Assert.notNull(id, "comment id must not be null"); -// -// if (CollectionUtils.isEmpty(commentsRoot)) { -// return null; -// } -// -// final List commentsChild = new ArrayList<>(); -// for (Comment comment : commentsRoot) { -// if (comment.getCommentParent() != 0) { -// if (comment.getCommentParent().equals(id)) { -// commentsChild.add(comment); -// } -// } -// } -// for (Comment comment : commentsChild) { -// if (comment.getCommentParent() != 0) { -// comment.setChildComments(getChild(comment.getCommentId(), commentsRoot)); -// } -// } -// if (commentsChild.size() == 0) { -// return null; -// } -// return commentsChild; -// } -} diff --git a/src/main/java/run/halo/app/utils/ServletUtils.java b/src/main/java/run/halo/app/utils/ServletUtils.java new file mode 100644 index 000000000..bc5c86067 --- /dev/null +++ b/src/main/java/run/halo/app/utils/ServletUtils.java @@ -0,0 +1,46 @@ +package run.halo.app.utils; + +import cn.hutool.extra.servlet.ServletUtil; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +/** + * Servlet utilities. + * + * @author johnniang + * @date 19-4-21 + */ +public class ServletUtils { + + private ServletUtils() { + } + + /** + * Gets current http servlet request. + * + * @return an optional http servlet request + */ + @NonNull + public static Optional getCurrentRequest() { + return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .filter(requestAttributes -> requestAttributes instanceof ServletRequestAttributes) + .map(requestAttributes -> ((ServletRequestAttributes) requestAttributes)) + .map(ServletRequestAttributes::getRequest); + } + + /** + * Gets request ip. + * + * @return ip address or null + */ + @Nullable + public static String getRequestIp() { + return getCurrentRequest().map(ServletUtil::getClientIP).orElse(null); + } + +} diff --git a/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java b/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java index 326f68e7f..e8047602f 100644 --- a/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java +++ b/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java @@ -99,7 +99,6 @@ public class ContentArchiveController { * * @param url post slug url. * @param cp comment page number - * @param request request * @param model model * @return template path: theme/{theme}/post.ftl */ @@ -110,14 +109,8 @@ public class ContentArchiveController { Model model) { Post post = postService.getBy(PostStatus.PUBLISHED, url); - postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> { - log.debug("Next post: [{}]", nextPost); - model.addAttribute("nextPost", nextPost); - }); - postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> { - log.debug("Pre post: [{}]", prePost); - model.addAttribute("prePost", prePost); - }); + postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> model.addAttribute("nextPost", nextPost)); + postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> model.addAttribute("prePost", prePost)); List categories = postCategoryService.listCategoryBy(post.getId()); diff --git a/src/test/java/run/halo/app/event/LogEventListenerTest.java b/src/test/java/run/halo/app/event/LogEventListenerTest.java new file mode 100644 index 000000000..c1ee9e4b7 --- /dev/null +++ b/src/test/java/run/halo/app/event/LogEventListenerTest.java @@ -0,0 +1,31 @@ +package run.halo.app.event; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.context.ApplicationListener; +import run.halo.app.utils.ReflectionUtils; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Objects; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; + +/** + * Log event listener test. + * + * @author johnniang + * @date 19-4-21 + */ +@Slf4j +public class LogEventListenerTest { + + @Test + public void getGenericTypeTest() { + ParameterizedType parameterizedType = ReflectionUtils.getParameterizedType(ApplicationListener.class, LogEventListener.class); + Type[] actualTypeArguments = Objects.requireNonNull(parameterizedType).getActualTypeArguments(); + Type actualTypeArgument = actualTypeArguments[0]; + assertThat(actualTypeArgument.getTypeName(), equalTo(LogEvent.class.getTypeName())); + } +} \ No newline at end of file