diff --git a/celements-messages/component/pom.xml b/celements-messages/component/pom.xml
new file mode 100644
index 00000000..64fbdd6f
--- /dev/null
+++ b/celements-messages/component/pom.xml
@@ -0,0 +1,74 @@
+
+
+
+ com.celements
+ celements-parent
+ 7.1-SNAPSHOT
+
+ 4.0.0
+ celements-messages
+ 7.2-SNAPSHOT
+ Celements Messages
+
+
+ com.celements
+ celements-core
+ provided
+
+
+ com.celements
+ celements-model
+ provided
+
+
+ com.celements
+ celements-spring-security
+ provided
+
+
+ com.celements
+ celements-xwiki-velocity
+ provided
+
+
+ com.celements
+ celements-xwiki-core
+ provided
+
+
+ org.springframework
+ spring-web
+ provided
+
+
+ org.xwiki.platform
+ xwiki-core-script
+ provided
+
+
+ javax.inject
+ javax.inject
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+ com.celements
+ celements-shared-tests
+ test
+
+
+ com.celements
+ celements-subsystem-migration-manager
+ test
+
+
+
+ scm:git:git@github.com:celements/celements-features.git
+ scm:git:git@github.com:celements/celements-features.git
+ https://github.com/celements/celements-features
+ HEAD
+
+
diff --git a/celements-messages/component/src/main/java/com/celements/messages/api/MessagesController.java b/celements-messages/component/src/main/java/com/celements/messages/api/MessagesController.java
new file mode 100644
index 00000000..3e1ea041
--- /dev/null
+++ b/celements-messages/component/src/main/java/com/celements/messages/api/MessagesController.java
@@ -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;
+ }
+}
diff --git a/celements-messages/component/src/main/java/com/celements/messages/script/MessagesScriptService.java b/celements-messages/component/src/main/java/com/celements/messages/script/MessagesScriptService.java
new file mode 100644
index 00000000..6fcdeacd
--- /dev/null
+++ b/celements-messages/component/src/main/java/com/celements/messages/script/MessagesScriptService.java
@@ -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());
+ }
+}
diff --git a/celements-messages/component/src/main/java/com/celements/messages/service/MessageService.java b/celements-messages/component/src/main/java/com/celements/messages/service/MessageService.java
new file mode 100644
index 00000000..721b891d
--- /dev/null
+++ b/celements-messages/component/src/main/java/com/celements/messages/service/MessageService.java
@@ -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 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 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 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);
+ }
+ }
+}
diff --git a/celements-messages/component/src/test/java/com/celements/messages/api/MessagesControllerTest.java b/celements-messages/component/src/test/java/com/celements/messages/api/MessagesControllerTest.java
new file mode 100644
index 00000000..103ea62a
--- /dev/null
+++ b/celements-messages/component/src/test/java/com/celements/messages/api/MessagesControllerTest.java
@@ -0,0 +1,109 @@
+package com.celements.messages.api;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.velocity.VelocityContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.aop.framework.Advised;
+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 com.celements.auth.user.UserService;
+import com.celements.common.test.AbstractComponentTest;
+import com.celements.messages.service.MessageService;
+import com.celements.rights.access.IRightsAccessFacadeRole;
+import com.celements.web.service.IPrepareVelocityContext;
+import com.xpn.xwiki.XWiki;
+
+public class MessagesControllerTest extends AbstractComponentTest {
+
+ private VelocityContext velocityContext;
+ private MessagesController controller;
+
+ @Before
+ public void prepareTest() throws Exception {
+ registerComponentMocks(VelocityManager.class, IPrepareVelocityContext.class,
+ MessageService.class, UserService.class, IRightsAccessFacadeRole.class);
+ velocityContext = new VelocityContext();
+ controller = getBeanTarget(MessagesController.class);
+ }
+
+ @Test
+ public void testEndpointMappings() throws Exception {
+ assertNotNull(MessagesController.class.getAnnotation(RestController.class));
+ assertArrayEquals(new String[] { "/v1/messages" },
+ MessagesController.class.getAnnotation(RequestMapping.class).value());
+ assertEquals("permitAll()", MessagesController.class.getAnnotation(PreAuthorize.class).value());
+ GetMapping general = MessagesController.class.getMethod("getMessages")
+ .getAnnotation(GetMapping.class);
+ assertArrayEquals(new String[0], general.value());
+ assertArrayEquals(new String[] { "application/json;charset=UTF-8" }, general.produces());
+ GetMapping validation = MessagesController.class.getMethod("getValidationMessages")
+ .getAnnotation(GetMapping.class);
+ assertArrayEquals(new String[] { "/validation" }, validation.value());
+ assertArrayEquals(new String[] { "application/json;charset=UTF-8" }, validation.produces());
+ }
+
+ @Test
+ public void testGetMessagesUsesPreparedExistingContext() throws Exception {
+ expectAnonymousAuth();
+ expectPreparedContext();
+ expect(getMock(MessageService.class).getMessages(same(velocityContext)))
+ .andReturn("{\"message\":\"value\"}");
+ replayDefault();
+
+ assertEquals("{\"message\":\"value\"}", controller.getMessages());
+
+ verifyDefault();
+ }
+
+ @Test
+ public void testGetValidationMessagesUsesPreparedExistingContext() throws Exception {
+ expectAnonymousAuth();
+ expectPreparedContext();
+ expect(getMock(MessageService.class).getValidationMessages(same(velocityContext)))
+ .andReturn("{\"required\":\"Required\"}");
+ replayDefault();
+
+ assertEquals("{\"required\":\"Required\"}", controller.getValidationMessages());
+
+ verifyDefault();
+ }
+
+ @Test
+ public void testServiceFailurePropagates() throws Exception {
+ expectAnonymousAuth();
+ expectPreparedContext();
+ expect(getMock(MessageService.class).getMessages(same(velocityContext)))
+ .andThrow(new IOException("failed"));
+ replayDefault();
+
+ assertThrows(IOException.class, controller::getMessages);
+
+ verifyDefault();
+ }
+
+ private void expectPreparedContext() {
+ expect(getMock(VelocityManager.class).getVelocityContext()).andReturn(velocityContext);
+ getMock(IPrepareVelocityContext.class).prepareVelocityContext(same(velocityContext));
+ }
+
+ private void expectAnonymousAuth() throws Exception {
+ expect(getMock(XWiki.class).checkAuth(getXContext())).andReturn(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private T getBeanTarget(Class beanClass) throws Exception {
+ T bean = getBeanFactory().getBean(beanClass);
+ return bean instanceof Advised advised
+ ? (T) advised.getTargetSource().getTarget()
+ : bean;
+ }
+}
diff --git a/celements-messages/component/src/test/java/com/celements/messages/script/MessagesScriptServiceTest.java b/celements-messages/component/src/test/java/com/celements/messages/script/MessagesScriptServiceTest.java
new file mode 100644
index 00000000..a42abae5
--- /dev/null
+++ b/celements-messages/component/src/test/java/com/celements/messages/script/MessagesScriptServiceTest.java
@@ -0,0 +1,72 @@
+package com.celements.messages.script;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.velocity.VelocityContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.stereotype.Component;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.velocity.VelocityManager;
+
+import com.celements.common.test.AbstractComponentTest;
+import com.celements.messages.service.MessageService;
+
+public class MessagesScriptServiceTest extends AbstractComponentTest {
+
+ private VelocityContext velocityContext;
+ private MessagesScriptService scriptService;
+
+ @Before
+ public void prepareTest() throws Exception {
+ registerComponentMocks(VelocityManager.class, MessageService.class);
+ velocityContext = new VelocityContext();
+ scriptService = getBeanFactory().getBean(MessagesScriptService.class);
+ }
+
+ @Test
+ public void testComponentRegistration() {
+ assertTrue(scriptService instanceof ScriptService);
+ assertEquals(MessagesScriptService.NAME,
+ MessagesScriptService.class.getAnnotation(Component.class).value());
+ }
+
+ @Test
+ public void testGetMessagesUsesActiveExistingContext() throws Exception {
+ expect(getMock(VelocityManager.class).getVelocityContext()).andReturn(velocityContext);
+ expect(getMock(MessageService.class).getMessages(same(velocityContext)))
+ .andReturn("{\"message\":\"value\"}");
+ replayDefault();
+
+ assertEquals("{\"message\":\"value\"}", scriptService.getMessages());
+
+ verifyDefault();
+ }
+
+ @Test
+ public void testGetValidationMessagesUsesActiveExistingContext() throws Exception {
+ expect(getMock(VelocityManager.class).getVelocityContext()).andReturn(velocityContext);
+ expect(getMock(MessageService.class).getValidationMessages(same(velocityContext)))
+ .andReturn("{\"required\":\"Required\"}");
+ replayDefault();
+
+ assertEquals("{\"required\":\"Required\"}", scriptService.getValidationMessages());
+
+ verifyDefault();
+ }
+
+ @Test
+ public void testServiceFailurePropagates() throws Exception {
+ expect(getMock(VelocityManager.class).getVelocityContext()).andReturn(velocityContext);
+ expect(getMock(MessageService.class).getMessages(same(velocityContext)))
+ .andThrow(new IOException("failed"));
+ replayDefault();
+
+ assertThrows(IOException.class, scriptService::getMessages);
+
+ verifyDefault();
+ }
+}
diff --git a/celements-messages/component/src/test/java/com/celements/messages/service/MessageServiceTest.java b/celements-messages/component/src/test/java/com/celements/messages/service/MessageServiceTest.java
new file mode 100644
index 00000000..1813eaf2
--- /dev/null
+++ b/celements-messages/component/src/test/java/com/celements/messages/service/MessageServiceTest.java
@@ -0,0 +1,198 @@
+package com.celements.messages.service;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.velocity.VelocityContext;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.util.FileSystemUtils;
+import org.springframework.web.context.ConfigurableWebApplicationContext;
+import org.xwiki.velocity.VelocityEngine;
+import org.xwiki.velocity.VelocityManager;
+import org.xwiki.velocity.XWikiVelocityException;
+
+import com.celements.common.test.AbstractComponentTest;
+import com.celements.sajson.JsonBuilder;
+
+public class MessageServiceTest extends AbstractComponentTest {
+
+ @ClassRule
+ public static final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private VelocityEngine velocityEngine;
+ private VelocityContext velocityContext;
+ private MessageService messageService;
+ private List evaluatedFragments;
+
+ @Override
+ protected void beforeSpringContextRefresh(ConfigurableApplicationContext context) {
+ super.beforeSpringContextRefresh(context);
+ ((ConfigurableWebApplicationContext) context).setServletContext(new MockServletContext(
+ "file:" + temporaryFolder.getRoot().getAbsolutePath()));
+ }
+
+ @Before
+ public void prepareTest() throws Exception {
+ FileSystemUtils.deleteRecursively(temporaryFolder.getRoot().toPath().resolve("templates"));
+ registerComponentMocks(VelocityManager.class);
+ velocityEngine = createDefaultMock(VelocityEngine.class);
+ velocityContext = new VelocityContext();
+ messageService = getBeanFactory().getBean(MessageService.class);
+ evaluatedFragments = new ArrayList<>();
+ }
+
+ @Test
+ public void testGetMessagesUsesLexicalDirectFragments() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ String zeta = directory + "zeta.vm";
+ createFragment(celements);
+ createFragment(zeta);
+ createFragment(directory + "nested/ignored.vm");
+ expectVelocityEngine();
+ expectFragment(celements, " \n", true, builder -> {
+ builder.addProperty("string", "value");
+ builder.addProperty("boolean", true);
+ builder.addProperty("number", 7);
+ });
+ expectFragment(zeta, "\n", true, builder -> builder.addProperty("zeta", "last"));
+ replayDefault();
+
+ String json = messageService.getMessages(velocityContext);
+
+ assertEquals(
+ "{\"string\" : \"value\", \"boolean\" : true, \"number\" : 7, "
+ + "\"zeta\" : \"last\"}",
+ json);
+ assertEquals(List.of(celements, zeta), evaluatedFragments);
+ assertFalse(velocityContext.containsKey("jsonBuilder"));
+ verifyDefault();
+ }
+
+ @Test
+ public void testGetValidationMessagesUsesOnlyValidationDirectory() throws Exception {
+ String directory = MessageService.VALIDATION_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ createFragment(celements);
+ expectVelocityEngine();
+ expectFragment(celements, "", true, builder -> builder.addProperty("required", "Required"));
+ replayDefault();
+
+ assertEquals("{\"required\" : \"Required\"}",
+ messageService.getValidationMessages(velocityContext));
+
+ assertEquals(List.of(celements), evaluatedFragments);
+ verifyDefault();
+ }
+
+ @Test
+ public void testMissingCelementsFragmentFailsCompleteRequest() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ createFragment(directory + "product.vm");
+ replayDefault();
+
+ assertThrows(IOException.class, () -> messageService.getMessages(velocityContext));
+
+ assertFalse(velocityContext.containsKey("jsonBuilder"));
+ verifyDefault();
+ }
+
+ @Test
+ public void testNonWhitespaceFragmentOutputFailsCompleteRequest() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ createFragment(celements);
+ expectVelocityEngine();
+ expectFragment(celements, "unexpected", true,
+ builder -> builder.addProperty("message", "value"));
+ replayDefault();
+
+ assertThrows(IOException.class, () -> messageService.getMessages(velocityContext));
+
+ assertFalse(velocityContext.containsKey("jsonBuilder"));
+ verifyDefault();
+ }
+
+ @Test
+ public void testFailedFragmentEvaluationFailsCompleteRequest() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ createFragment(celements);
+ expectVelocityEngine();
+ expectFragment(celements, "", false, builder -> assertNotNull(builder));
+ replayDefault();
+
+ assertThrows(XWikiVelocityException.class, () -> messageService.getMessages(velocityContext));
+
+ assertFalse(velocityContext.containsKey("jsonBuilder"));
+ verifyDefault();
+ }
+
+ @Test
+ public void testUnbalancedBuilderDepthFailsCompleteRequest() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ createFragment(celements);
+ expectVelocityEngine();
+ expectFragment(celements, "", true, builder -> builder.openDictionary("nested"));
+ replayDefault();
+
+ assertThrows(IllegalStateException.class,
+ () -> messageService.getMessages(velocityContext));
+
+ assertFalse(velocityContext.containsKey("jsonBuilder"));
+ verifyDefault();
+ }
+
+ @Test
+ public void testExistingJsonBuilderContextValueIsRestored() throws Exception {
+ String directory = MessageService.GENERAL_FRAGMENT_DIRECTORY;
+ String celements = directory + "celements.vm";
+ var previousBuilder = new JsonBuilder();
+ velocityContext.put("jsonBuilder", previousBuilder);
+ createFragment(celements);
+ expectVelocityEngine();
+ expectFragment(celements, "", true, builder -> builder.addProperty("message", "value"));
+ replayDefault();
+
+ messageService.getMessages(velocityContext);
+
+ assertSame(previousBuilder, velocityContext.get("jsonBuilder"));
+ verifyDefault();
+ }
+
+ private void createFragment(String path) throws IOException {
+ var fragment = temporaryFolder.getRoot().toPath().resolve(path.substring(1));
+ Files.createDirectories(fragment.getParent());
+ Files.writeString(fragment, "fragment:" + path, StandardCharsets.UTF_8);
+ }
+
+ private void expectVelocityEngine() throws XWikiVelocityException {
+ expect(getMock(VelocityManager.class).getVelocityEngine()).andReturn(velocityEngine);
+ }
+
+ private void expectFragment(String path, String output, boolean result,
+ Consumer builderAction) throws Exception {
+ String source = "fragment:" + path;
+ expect(velocityEngine.evaluate(same(velocityContext), isA(StringWriter.class), eq(path),
+ eq(source))).andAnswer(() -> {
+ evaluatedFragments.add(path);
+ builderAction.accept((JsonBuilder) velocityContext.get("jsonBuilder"));
+ ((StringWriter) getCurrentArguments()[1]).write(output);
+ return result;
+ });
+ }
+}
diff --git a/celements-messages/web-module/pom.xml b/celements-messages/web-module/pom.xml
new file mode 100644
index 00000000..76acdbdc
--- /dev/null
+++ b/celements-messages/web-module/pom.xml
@@ -0,0 +1,28 @@
+
+
+
+ com.celements
+ celements-parent
+ 7.1-SNAPSHOT
+
+ 4.0.0
+ celements-messages-web
+ 7.2-SNAPSHOT
+ war
+ Celements Messages Web
+
+
+ com.celements
+ celements-messages
+
+
+
+ celements-messages-web
+
+
+ scm:git:git@github.com:celements/celements-features.git
+ scm:git:git@github.com:celements/celements-features.git
+ https://github.com/celements/celements-features
+ HEAD
+
+
diff --git a/celements-messages/web-module/src/main/webapp/WEB-INF/web.xml b/celements-messages/web-module/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000..efb8ef04
--- /dev/null
+++ b/celements-messages/web-module/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/celements-messages/web-module/src/main/webapp/templates/celAjax/Messages.vm b/celements-messages/web-module/src/main/webapp/templates/celAjax/Messages.vm
new file mode 100644
index 00000000..e6a863b2
--- /dev/null
+++ b/celements-messages/web-module/src/main/webapp/templates/celAjax/Messages.vm
@@ -0,0 +1 @@
+$services.celementsMessages.getMessages()
diff --git a/celements-messages/web-module/src/main/webapp/templates/celAjax/ValidationMessages.vm b/celements-messages/web-module/src/main/webapp/templates/celAjax/ValidationMessages.vm
new file mode 100644
index 00000000..256eaa1e
--- /dev/null
+++ b/celements-messages/web-module/src/main/webapp/templates/celAjax/ValidationMessages.vm
@@ -0,0 +1 @@
+$services.celementsMessages.getValidationMessages()
diff --git a/celements-messages/web-module/src/main/webapp/templates/celMessages/general/celements.vm b/celements-messages/web-module/src/main/webapp/templates/celMessages/general/celements.vm
new file mode 100644
index 00000000..63dedd8f
--- /dev/null
+++ b/celements-messages/web-module/src/main/webapp/templates/celMessages/general/celements.vm
@@ -0,0 +1,215 @@
+$jsonBuilder.openProperty('celmeta')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('language', "$language")
+$jsonBuilder.addProperty('admin-language', "$admin_language")
+$jsonBuilder.addProperty('contextPath', "${request.contextPath}")
+$jsonBuilder.addProperty('database', "${context.database}")
+$jsonBuilder.openProperty('isAdmin')
+$jsonBuilder.addValue($isAdmin)
+$jsonBuilder.openProperty('isSuperAdmin')
+$jsonBuilder.addValue($isSuperAdmin)
+$jsonBuilder.addProperty('startupTimeStamp', "${services.celementsweb.getLastStartupTimeStamp()}")
+$jsonBuilder.closeDictionary()
+## Reconnector
+$jsonBuilder.openProperty('Reconnector')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('retryNotice', "$!adminMsg.get('cel_reconnector_retryNotice')")
+$jsonBuilder.addProperty('retryDelayNotice', "$!adminMsg.get('cel_reconnector_retryDelayNotice')")
+$jsonBuilder.closeDictionary()
+## jquery.formater
+$jsonBuilder.openProperty('jqueryFormater')
+$jsonBuilder.openDictionary()
+$jsonBuilder.openProperty('monthsFull')
+$jsonBuilder.openArray()
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_january')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_february')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_march')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_april')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_may')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_june')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_july')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_august')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_september')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_october')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_november')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsFull_december')")
+$jsonBuilder.closeArray()
+$jsonBuilder.openProperty('monthsShort')
+$jsonBuilder.openArray()
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_january')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_february')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_march')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_april')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_may')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_june')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_july')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_august')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_september')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_october')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_november')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_monthsShort_december')")
+$jsonBuilder.closeArray()
+$jsonBuilder.openProperty('daysFull')
+$jsonBuilder.openArray()
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_sunday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_monday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_tuesday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_wednesday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_thursday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_friday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysFull_saturday')")
+$jsonBuilder.closeArray()
+$jsonBuilder.openProperty('daysShort')
+$jsonBuilder.openArray()
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_sunday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_monday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_tuesday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_wednesday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_thursday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_friday')")
+$jsonBuilder.addValue("$!adminMsg.get('cel_date_formater_daysShort_saturday')")
+$jsonBuilder.closeArray()
+$jsonBuilder.closeDictionary()
+## celements photo slideShow
+$jsonBuilder.openProperty('celslideshow')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('cmPause', "$!adminMsg.get('cel_slideshow_cm_pause')")
+$jsonBuilder.addProperty('cmContinue', "$!adminMsg.get('cel_slideshow_cm_continue')")
+$jsonBuilder.addProperty('cmOpenLink', "$!adminMsg.get('cel_slideshow_cm_openlink')")
+$jsonBuilder.closeDictionary()
+## celements admin importer (import.js)
+$jsonBuilder.openProperty('celAdminImport')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('availableDocuments', "$!adminMsg.get('core.importer.availableDocuments')")
+$jsonBuilder.addProperty('importHistoryLabel', "$!adminMsg.get('core.importer.importHistory')")
+$jsonBuilder.addProperty('selectionEmpty', "$!adminMsg.get('core.importer.selectionEmptyWarning')")
+$jsonBuilder.addProperty('import', "$!adminMsg.get('core.importer.import')")
+$jsonBuilder.addProperty('package', "$!adminMsg.get('core.importer.package')")
+$jsonBuilder.addProperty('description', "$!adminMsg.get('core.importer.package.description')")
+$jsonBuilder.addProperty('version', "$!adminMsg.get('core.importer.package.version')")
+$jsonBuilder.addProperty('licence', "$!adminMsg.get('core.importer.package.licence')")
+$jsonBuilder.addProperty('author', "$!adminMsg.get('core.importer.package.author')")
+$jsonBuilder.addProperty('documentSelected', "$!adminMsg.get('core.importer.documentSelected')")
+$jsonBuilder.addProperty('whenDocumentAlreadyExists', "$!adminMsg.get('core.importer.whenDocumentAlreadyExists')")
+$jsonBuilder.addProperty('addNewVersion', "$!adminMsg.get('core.importer.addNewVersion')")
+$jsonBuilder.addProperty('replaceDocumentHistory', "$!adminMsg.get('core.importer.replaceDocumentHistory')")
+$jsonBuilder.addProperty('resetHistory', "$!adminMsg.get('core.importer.resetHistory')")
+$jsonBuilder.addProperty('importAsBackup', "$!adminMsg.get('core.importer.importAsBackup')")
+$jsonBuilder.addProperty('select', "$!adminMsg.get('core.importer.select')")
+$jsonBuilder.addProperty('all', "$!adminMsg.get('core.importer.selectAll')")
+$jsonBuilder.addProperty('none', "$!adminMsg.get('core.importer.selectNone')")
+$jsonBuilder.closeDictionary()
+## celements menu
+$jsonBuilder.openProperty('celmenu')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('supportLinkURL', "$!services.celementsweb.getSupportLinkURL()")
+$jsonBuilder.closeDictionary()
+## xwiki/actionButtons.js
+$jsonBuilder.openProperty('actionButtons')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('cancel', "$!adminMsg.get('core.shortcuts.edit.cancel')")
+$jsonBuilder.addProperty('preview', "$!adminMsg.get('core.shortcuts.edit.preview')")
+$jsonBuilder.addProperty('backtoedit', "$!adminMsg.get('core.shortcuts.edit.backtoedit')")
+$jsonBuilder.addProperty('saveandview', "$!adminMsg.get('core.shortcuts.edit.saveandview')")
+$jsonBuilder.addProperty('saveandcontinue', "$!adminMsg.get('core.shortcuts.edit.saveandcontinue')")
+$jsonBuilder.addProperty('mandatoryField', "$!adminMsg.get('core.editors.validation.mandatoryField')")
+$jsonBuilder.addProperty('commentPrompt', "$!adminMsg.get('core.comment.prompt')")
+$jsonBuilder.closeDictionary()
+## xwiki/attachments.js
+$jsonBuilder.openProperty('attachments')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('deleteConfirm', "$!adminMsg.get('core.viewers.attachments.delete.confirm')")
+$jsonBuilder.addProperty('deleteInProgress', "$!adminMsg.get('core.viewers.attachments.delete.inProgress')")
+$jsonBuilder.addProperty('deleteDone', "$!adminMsg.get('core.viewers.attachments.delete.done')")
+$jsonBuilder.addProperty('deleteFailed', "$!adminMsg.get('core.viewers.attachments.delete.failed')")
+$jsonBuilder.addProperty('addFileInput', "$!adminMsg.get('core.viewers.attachments.upload.addFileInput')")
+$jsonBuilder.addProperty('removeFileInput', "$!adminMsg.get('core.viewers.attachments.upload.removeFileInput')")
+$jsonBuilder.addProperty('removeFileInputTitle', "$!adminMsg.get('core.viewers.attachments.upload.removeFileInput.title')")
+$jsonBuilder.addProperty('extranb', "$!adminMsg.get('docextra.extranb', ['__number__'])")
+$jsonBuilder.closeDictionary()
+## xwiki/code.js
+$jsonBuilder.openProperty('code')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('showLineNumbers', "$!adminMsg.get('core.viewers.code.showLineNumbers')")
+$jsonBuilder.addProperty('hideLineNumbers', "$!adminMsg.get('core.viewers.code.hideLineNumbers')")
+$jsonBuilder.closeDictionary()
+## xwiki/comments.js
+$jsonBuilder.openProperty('comments')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('commentDeleted', "$!adminMsg.get('core.viewers.comments.commentDeleted')")
+$jsonBuilder.addProperty('deleteConfirm', "$!adminMsg.get('core.viewers.comments.delete.confirm')")
+$jsonBuilder.addProperty('deleteInProgress', "$!adminMsg.get('core.viewers.comments.delete.inProgress')")
+$jsonBuilder.addProperty('deleteDone', "$!adminMsg.get('core.viewers.comments.delete.done')")
+$jsonBuilder.addProperty('deleteFailed', "$!adminMsg.get('core.viewers.comments.delete.failed')")
+$jsonBuilder.addProperty('editFormFetchInProgress', "$!adminMsg.get('core.viewers.comments.editForm.fetch.inProgress')")
+$jsonBuilder.addProperty('editFormFetchFailed', "$!adminMsg.get('core.viewers.comments.editForm.fetch.failed')")
+$jsonBuilder.addProperty('addInProgress', "$!adminMsg.get('core.viewers.comments.add.inProgress')")
+$jsonBuilder.addProperty('addDone', "$!adminMsg.get('core.viewers.comments.add.done')")
+$jsonBuilder.addProperty('addFailed', "$!adminMsg.get('core.viewers.comments.add.failed')")
+$jsonBuilder.addProperty('previewButtonPreview', "$!adminMsg.get('core.viewers.comments.preview.button.preview')")
+$jsonBuilder.addProperty('previewInProgress', "$!adminMsg.get('core.viewers.comments.preview.inProgress')")
+$jsonBuilder.addProperty('previewFailed', "$!adminMsg.get('core.viewers.comments.preview.failed')")
+$jsonBuilder.addProperty('previewButtonBack', "$!adminMsg.get('core.viewers.comments.preview.button.back')")
+$jsonBuilder.addProperty('extranb', "$!adminMsg.get('docextra.extranb', ['__number__'])")
+$jsonBuilder.closeDictionary()
+## xwiki/confirmationBox.js
+$jsonBuilder.openProperty('confirmationBox')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('defaultQuestion', "$!adminMsg.get('core.widgets.confirmationBox.defaultQuestion')")
+$jsonBuilder.addProperty('yes', "$!adminMsg.get('core.widgets.confirmationBox.button.yes')")
+$jsonBuilder.addProperty('no', "$!adminMsg.get('core.widgets.confirmationBox.button.no')")
+$jsonBuilder.closeDictionary()
+## xwiki/confirmedAjaxRequest.js
+$jsonBuilder.openProperty('confirmedAjaxRequest')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('notificationInProgress', "$!adminMsg.get('core.widgets.confirmationBox.notification.inProgress')")
+$jsonBuilder.addProperty('notificationDone', "$!adminMsg.get('core.widgets.confirmationBox.notification.done')")
+$jsonBuilder.addProperty('notificationFailed', "$!adminMsg.get('core.widgets.confirmationBox.notification.failed')")
+$jsonBuilder.closeDictionary()
+## xwiki/dataeditors.js
+$jsonBuilder.openProperty('dataeditors')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('confirmationText', "$!adminMsg.get('core.editors.object.delete.confirm')")
+$jsonBuilder.addProperty('progressMessageText', "$!adminMsg.get('core.editors.object.delete.inProgress')")
+$jsonBuilder.addProperty('successMessageText', "$!adminMsg.get('core.editors.object.delete.done')")
+$jsonBuilder.addProperty('failureMessageText', "$!adminMsg.get('core.editors.object.delete.failed')")
+$jsonBuilder.closeDictionary()
+## xwiki/livetable.js
+$jsonBuilder.openProperty('livetable')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('resultsOf', "$!adminMsg.get('xe.pagination.results.of')")
+$jsonBuilder.addProperty('results', "$!adminMsg.get('xe.pagination.results')")
+$jsonBuilder.closeDictionary()
+## xwiki/usersandgroups.js
+$jsonBuilder.openProperty('usersandgroups')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('denyrightforcurrentuser', "$!adminMsg.get('rightsmanager.denyrightforcurrentuser')")
+$jsonBuilder.addProperty('clearrightforcurrentuserinstead', "$!adminMsg.get('rightsmanager.clearrightforcurrentuserinstead')")
+$jsonBuilder.addProperty('clearrightforcurrentuser', "$!adminMsg.get('rightsmanager.clearrightforcurrentuser')")
+$jsonBuilder.addProperty('denyrightforgroup', "$!adminMsg.get('rightsmanager.denyrightforgroup')")
+$jsonBuilder.addProperty('clearrightforgroupinstead', "$!adminMsg.get('rightsmanager.clearrightforgroupinstead')")
+$jsonBuilder.addProperty('clearrightforgroup', "$!adminMsg.get('rightsmanager.clearrightforgroup')")
+$jsonBuilder.addProperty('denyrightforuorg', "$!adminMsg.get('rightsmanager.denyrightforuorg')")
+$jsonBuilder.addProperty('clearrightforuorg', "$!adminMsg.get('rightsmanager.clearrightforuorg')")
+$jsonBuilder.addProperty('saveFailure', "$!adminMsg.get('platform.core.rightsManagement.saveFailure')")
+$jsonBuilder.addProperty('ajaxFailure', "$!adminMsg.get('platform.core.rightsManagement.ajaxFailure')")
+$jsonBuilder.addProperty('edit', "$!adminMsg.get('edit')")
+$jsonBuilder.addProperty('delete', "$!adminMsg.get('delete')")
+$jsonBuilder.addProperty('confirmdeleteuser', "$!adminMsg.get('rightsmanager.confirmdeleteuser')")
+$jsonBuilder.addProperty('confirmdeletegroup', "$!adminMsg.get('rightsmanager.confirmdeletegroup')")
+$jsonBuilder.addProperty('confirmdeletemember', "$!adminMsg.get('rightsmanager.confirmdeletemember')")
+$jsonBuilder.closeDictionary()
+## xwiki.js
+$jsonBuilder.openProperty('xwiki')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('readTechnicalInformation', "$!adminMsg.get('platform.core.rendering.error.readTechnicalInformation')")
+$jsonBuilder.addProperty('edit', "$!adminMsg.get('edit')")
+$jsonBuilder.addProperty('createAjaxError', "$!adminMsg.get('core.create.ajax.error')")
+$jsonBuilder.closeDictionary()
+## xwikiexplorer.js
+$jsonBuilder.openProperty('xwikiexplorer')
+$jsonBuilder.openDictionary()
+$jsonBuilder.addProperty('attachmentsTitle', "$!adminMsg.get('xwikiexplorer.attachments.title')")
+$jsonBuilder.addProperty('addPageTitle', "$!adminMsg.get('xwikiexplorer.addpage.title')")
+$jsonBuilder.addProperty('addAttachmentTitle', "$!adminMsg.get('xwikiexplorer.addattachment.title')")
+$jsonBuilder.closeDictionary()
diff --git a/celements-messages/web-module/src/main/webapp/templates/celMessages/validation/celements.vm b/celements-messages/web-module/src/main/webapp/templates/celMessages/validation/celements.vm
new file mode 100644
index 00000000..36d571ea
--- /dev/null
+++ b/celements-messages/web-module/src/main/webapp/templates/celMessages/validation/celements.vm
@@ -0,0 +1,19 @@
+$jsonBuilder.addProperty('language', "$language")
+$jsonBuilder.addProperty('admin-language', "$admin_language")
+$jsonBuilder.addProperty('required', "$adminMsg.get('cel_form_valid_required')")
+$jsonBuilder.addProperty('validate-dateRange', "$!adminMsg.get('cel_valid_dateRange', ['', ''])")
+$jsonBuilder.addProperty('validate-warning-header', "$!adminMsg.get('cel_tm_saving_dialog_warning_header')")
+$jsonBuilder.addProperty('validate-blog-dateRange', "$!adminMsg.get('cel_valid_blog_dateRange', ['', ''])")
+$jsonBuilder.addProperty('validate-number', "Please enter a valid number in this field.")
+$jsonBuilder.addProperty('validate-digits', "Please use numbers only in this field. please avoid spaces or other characters such as dots or commas.")
+$jsonBuilder.addProperty('validate-alpha', "Please use letters only (a-z) in this field.")
+$jsonBuilder.addProperty('validate-alphanum', "Please use only letters (a-z) or numbers (0-9) only in this field. No spaces or other characters are allowed.")
+$jsonBuilder.addProperty('validate-date', "Please enter a valid date.")
+$jsonBuilder.addProperty('validate-email', "Please enter a valid email address. For example fred@domain.com .")
+$jsonBuilder.addProperty('validate-url', "Please enter a valid URL.")
+$jsonBuilder.addProperty('validate-date-au', "Please use this date format: dd/mm/yyyy. For example 17/03/2006 for the 17th of March, 2006.")
+$jsonBuilder.addProperty('validate-currency-dollar', 'Please enter a valid $ amount. For example $100.00 .')
+$jsonBuilder.addProperty('validate-selection', 'Please make a selection')
+$jsonBuilder.addProperty('validate-one-required', 'Please select one of the above options.')
+$jsonBuilder.addProperty('validate-email-equal', "$!adminMsg.get('cel_form_valid_email-equal')")
+$jsonBuilder.addProperty('validate-docname', "$adminMsg.get('cel_tm_validate_warning')")