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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions celements-messages/component/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.celements</groupId>
<artifactId>celements-parent</artifactId>
<version>7.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>celements-messages</artifactId>
<version>7.2-SNAPSHOT</version>
<description>Celements Messages</description>
<dependencies>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-model</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-spring-security</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-xwiki-velocity</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-xwiki-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-core-script</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-shared-tests</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.celements</groupId>
<artifactId>celements-subsystem-migration-manager</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<scm>
<connection>scm:git:git@github.com:celements/celements-features.git</connection>
<developerConnection>scm:git:git@github.com:celements/celements-features.git</developerConnection>
<url>https://github.com/celements/celements-features</url>
<tag>HEAD</tag>
</scm>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.celements.messages.api;

import java.io.IOException;

import javax.inject.Inject;

import org.apache.velocity.VelocityContext;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xwiki.velocity.VelocityManager;
import org.xwiki.velocity.XWikiVelocityException;

import com.celements.messages.service.MessageService;
import com.celements.spring.security.AuthenticatedBaseController;
import com.celements.web.service.IPrepareVelocityContext;

@RestController
@RequestMapping("/v1/messages")
@PreAuthorize("permitAll()")
public class MessagesController extends AuthenticatedBaseController {

private static final String JSON_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8";

private final VelocityManager velocityManager;
private final IPrepareVelocityContext prepareVelocityContext;
private final MessageService messageService;

@Inject
public MessagesController(VelocityManager velocityManager,
IPrepareVelocityContext prepareVelocityContext, MessageService messageService) {
this.velocityManager = velocityManager;
this.prepareVelocityContext = prepareVelocityContext;
this.messageService = messageService;
}

@GetMapping(produces = JSON_MEDIA_TYPE)
public String getMessages() throws IOException, XWikiVelocityException {
return messageService.getMessages(getPreparedVelocityContext());
}

@GetMapping(value = "/validation", produces = JSON_MEDIA_TYPE)
public String getValidationMessages() throws IOException, XWikiVelocityException {
return messageService.getValidationMessages(getPreparedVelocityContext());
}

private VelocityContext getPreparedVelocityContext() {
checkAuth();
VelocityContext velocityContext = velocityManager.getVelocityContext();
prepareVelocityContext.prepareVelocityContext(velocityContext);
return velocityContext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.celements.messages.script;

import java.io.IOException;

import javax.inject.Inject;

import org.springframework.stereotype.Component;
import org.xwiki.script.service.ScriptService;
import org.xwiki.velocity.VelocityManager;
import org.xwiki.velocity.XWikiVelocityException;

import com.celements.messages.service.MessageService;

@Component(MessagesScriptService.NAME)
public class MessagesScriptService implements ScriptService {

public static final String NAME = "celementsMessages";

private final VelocityManager velocityManager;
private final MessageService messageService;

@Inject
public MessagesScriptService(VelocityManager velocityManager, MessageService messageService) {
this.velocityManager = velocityManager;
this.messageService = messageService;
}

public String getMessages() throws IOException, XWikiVelocityException {
return messageService.getMessages(velocityManager.getVelocityContext());
}

public String getValidationMessages() throws IOException, XWikiVelocityException {
return messageService.getValidationMessages(velocityManager.getVelocityContext());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.celements.messages.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import javax.inject.Inject;
import javax.servlet.ServletContext;

import org.apache.velocity.VelocityContext;
import org.springframework.stereotype.Component;
import org.xwiki.velocity.VelocityEngine;
import org.xwiki.velocity.VelocityManager;
import org.xwiki.velocity.XWikiVelocityException;

import com.celements.sajson.JsonBuilder;

@Component
public class MessageService {

static final String GENERAL_FRAGMENT_DIRECTORY = "/templates/celMessages/general/";
static final String VALIDATION_FRAGMENT_DIRECTORY = "/templates/celMessages/validation/";
private static final String REQUIRED_FRAGMENT_NAME = "celements.vm";
private static final String JSON_BUILDER_CONTEXT_KEY = "jsonBuilder";

private final ServletContext servletContext;
private final VelocityManager velocityManager;

@Inject
public MessageService(ServletContext servletContext, VelocityManager velocityManager) {
this.servletContext = servletContext;
this.velocityManager = velocityManager;
}

public String getMessages(VelocityContext velocityContext)
throws IOException, XWikiVelocityException {
return renderFragments(GENERAL_FRAGMENT_DIRECTORY, velocityContext);
}

public String getValidationMessages(VelocityContext velocityContext)
throws IOException, XWikiVelocityException {
return renderFragments(VALIDATION_FRAGMENT_DIRECTORY, velocityContext);
}

private String renderFragments(String directory, VelocityContext velocityContext)
throws IOException, XWikiVelocityException {
var jsonBuilder = new JsonBuilder();
jsonBuilder.openDictionary();
List<String> fragments = discoverFragments(directory);
requireBaseFragment(directory, fragments);
boolean hadJsonBuilder = velocityContext.containsKey(JSON_BUILDER_CONTEXT_KEY);
Object previousJsonBuilder = velocityContext.get(JSON_BUILDER_CONTEXT_KEY);
velocityContext.put(JSON_BUILDER_CONTEXT_KEY, jsonBuilder);
try {
VelocityEngine velocityEngine = velocityManager.getVelocityEngine();
for (String fragment : fragments) {
evaluateFragment(velocityEngine, velocityContext, jsonBuilder, fragment);
}
jsonBuilder.closeDictionary();
if (!jsonBuilder.isComplete()) {
throw new IllegalStateException("Messages JSON builder is incomplete");
}
return jsonBuilder.getJSON();
} finally {
if (hadJsonBuilder) {
velocityContext.put(JSON_BUILDER_CONTEXT_KEY, previousJsonBuilder);
} else {
velocityContext.remove(JSON_BUILDER_CONTEXT_KEY);
}
}
}

private List<String> discoverFragments(String directory) {
return Optional.ofNullable(servletContext.getResourcePaths(directory))
.orElseGet(Collections::emptySet)
.stream()
.filter(path -> isDirectFragment(directory, path))
.sorted()
.toList();
}

private boolean isDirectFragment(String directory, String path) {
return path.startsWith(directory) && path.endsWith(".vm")
&& !path.substring(directory.length()).contains("/");
}

private void requireBaseFragment(String directory, List<String> fragments) throws IOException {
String requiredFragment = directory + REQUIRED_FRAGMENT_NAME;
if (!fragments.contains(requiredFragment)) {
throw new IOException("Required message fragment not found: " + requiredFragment);
}
}

private void evaluateFragment(VelocityEngine velocityEngine, VelocityContext velocityContext,
JsonBuilder jsonBuilder, String fragment) throws IOException, XWikiVelocityException {
int initialDepth = jsonBuilder.getOpenCommandCount();
var writer = new StringWriter();
boolean evaluated = velocityEngine.evaluate(velocityContext, writer, fragment,
getFragmentContent(fragment));
if (!evaluated) {
throw new XWikiVelocityException("Failed to evaluate message fragment: " + fragment);
}
if (!writer.toString().trim().isEmpty()) {
throw new IOException("Message fragment emitted response content: " + fragment);
}
if (jsonBuilder.getOpenCommandCount() != initialDepth) {
throw new IllegalStateException(
"Message fragment left an unbalanced JSON builder: " + fragment);
}
}

private String getFragmentContent(String fragment) throws IOException {
try (InputStream stream = servletContext.getResourceAsStream(fragment)) {
if (stream == null) {
throw new IOException("Message fragment not found: " + fragment);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
Loading