diff --git a/ext/mvc-thymeleaf/pom.xml b/ext/mvc-thymeleaf/pom.xml
new file mode 100644
index 0000000000..db531b0a1b
--- /dev/null
+++ b/ext/mvc-thymeleaf/pom.xml
@@ -0,0 +1,81 @@
+
+
+
+
+ 4.0.0
+
+
+ project
+ org.glassfish.jersey.ext
+ 3.0.99-SNAPSHOT
+
+
+ jersey-mvc-thymeleaf
+ jersey-ext-mvc-thymeleaf
+
+
+ Jersey extension module providing support for Thymeleaf templates.
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+ true
+
+
+ org.glassfish.jersey.server.mvc.thymeleaf.*;version=${project.version}
+
+ true
+
+
+
+
+
+ ${project.build.directory}/legal
+
+
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${servlet5.version}
+ provided
+
+
+
+ org.glassfish.jersey.ext
+ jersey-mvc
+ ${project.version}
+
+
+
+ org.thymeleaf
+ thymeleaf
+ ${thymeleaf.version}
+
+
+
+
\ No newline at end of file
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafConfigurationFactory.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafConfigurationFactory.java
new file mode 100644
index 0000000000..aba4aac990
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafConfigurationFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
+
+import org.thymeleaf.TemplateEngine;
+
+public interface ThymeleafConfigurationFactory {
+ TemplateEngine getTemplateEngine();
+}
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafDefaultConfigurationFactory.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafDefaultConfigurationFactory.java
new file mode 100644
index 0000000000..588c47542b
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafDefaultConfigurationFactory.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
+
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.messageresolver.IMessageResolver;
+import org.thymeleaf.messageresolver.StandardMessageResolver;
+import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
+import org.thymeleaf.templateresolver.ITemplateResolver;
+
+import java.util.Map;
+
+/**
+ * Handy {@link ThymeleafConfigurationFactory} that supplies a minimally
+ * configured {@link org.thymeleaf.TemplateEngine } able to
+ * render Thymeleaf templates.
+ * The recommended method to provide custom Thymeleaf engine settings is to
+ * sub-class this class, further customize the
+ * {@link org.thymeleaf.TemplateEngine settings} as desired in that
+ * class, and then register the sub-class with the {@link ThymeleafMvcFeature}
+ * TEMPLATE_OBJECT_FACTORY property.
+ *
+ * @author Dmytro Dovnar (dimonmc@gmail.com)
+ */
+public class ThymeleafDefaultConfigurationFactory implements ThymeleafConfigurationFactory {
+ private final Configuration config;
+ private final TemplateEngine templateEngine;
+
+ public ThymeleafDefaultConfigurationFactory(Configuration config) {
+ this.config = config;
+ this.templateEngine = initTemplateEngine();
+ }
+
+ @Override
+ public TemplateEngine getTemplateEngine() {
+ return templateEngine;
+ }
+
+ private ITemplateResolver getTemplateResolver() {
+ Map properties = config.getProperties();
+ String basePath = (String) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.templateBasePath" + ThymeleafMvcFeature.SUFFIX,
+ String.class, (Map) null);
+ if (basePath == null) {
+ basePath = (String) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.templateBasePath", "", (Map) null);
+ }
+
+ if (basePath != null && !basePath.startsWith("/")) {
+ basePath = "/" + basePath;
+ }
+
+ String templateFileSuffix = (String) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.templateFileSuffix" + ThymeleafMvcFeature.SUFFIX,
+ ".html", (Map) null);
+
+ String templateFileMode = (String) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.templateMode" + ThymeleafMvcFeature.SUFFIX,
+ "HTML5", (Map) null);
+
+ Boolean cacheEnabled = (Boolean) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.caching" + ThymeleafMvcFeature.SUFFIX, Boolean.class, (Map) null);
+ if (cacheEnabled == null) {
+ cacheEnabled = (Boolean) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.caching", false, (Map) null);
+ }
+
+ Long cacheLiveMs = (Long) PropertiesHelper.getValue(properties,
+ "jersey.config.server.mvc.cacheTTLMs" + ThymeleafMvcFeature.SUFFIX, 3600000L, (Map) null);
+
+ ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
+ templateResolver.setPrefix(basePath);
+ templateResolver.setSuffix(templateFileSuffix);
+ templateResolver.setTemplateMode(templateFileMode);
+ templateResolver.setCacheTTLMs(cacheLiveMs);
+ templateResolver.setCacheable(cacheEnabled);
+ return templateResolver;
+ }
+
+ private TemplateEngine initTemplateEngine() {
+ TemplateEngine templateEngine = new TemplateEngine();
+ templateEngine.setTemplateResolver(getTemplateResolver());
+ return templateEngine;
+ }
+
+ private IMessageResolver getMessageResolver() {
+ StandardMessageResolver messageResolver = new StandardMessageResolver();
+ return messageResolver;
+ }
+}
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafMvcFeature.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafMvcFeature.java
new file mode 100644
index 0000000000..b43b9a6b48
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafMvcFeature.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
+
+import jakarta.ws.rs.ConstrainedTo;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.Feature;
+import jakarta.ws.rs.core.FeatureContext;
+import org.glassfish.jersey.server.mvc.MvcFeature;
+
+@ConstrainedTo(RuntimeType.SERVER)
+public final class ThymeleafMvcFeature implements Feature {
+ public static final String SUFFIX = ".thymeleaf";
+ public static final String TEMPLATE_BASE_PATH = MvcFeature.TEMPLATE_BASE_PATH + SUFFIX;
+ public static final String CACHE_TEMPLATES = MvcFeature.CACHE_TEMPLATES + SUFFIX;
+ public static final String TEMPLATE_OBJECT_FACTORY = MvcFeature.TEMPLATE_OBJECT_FACTORY + SUFFIX;
+ public static final String ENCODING = MvcFeature.ENCODING + SUFFIX;
+
+ public static final String TEMPLATE_FILE_SUFFIX = "jersey.config.server.mvc.templateFileSuffix" + SUFFIX;
+ public static final String TEMPLATE_MODE = "jersey.config.server.mvc.templateMode" + SUFFIX;
+ public static final String CACHE_TTLMS = "jersey.config.server.mvc.cacheTTLMs" + SUFFIX;
+
+ @Override
+ public boolean configure(FeatureContext context) {
+ final Configuration config = context.getConfiguration();
+
+ if (!config.isRegistered(ThymeleafViewProcessor.class)) {
+ context.register(ThymeleafViewProcessor.class);
+
+ // MvcFeature.
+ if (!config.isRegistered(MvcFeature.class)) {
+ context.register(MvcFeature.class);
+ }
+
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafSuppliedConfigurationFactory.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafSuppliedConfigurationFactory.java
new file mode 100644
index 0000000000..4f21ce1cb6
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafSuppliedConfigurationFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
+
+import org.thymeleaf.TemplateEngine;
+
+/**
+ * {@link ThymeleafConfigurationFactory} that supplies an unchanged
+ * {@link ThymeleafConfigurationFactory Configuration} as passed-in to
+ * the constructor.
+ *
+ * Used to support backwards-compatibility in {@link ThymeleafViewProcessor}
+ * to wrap directly-configured {@link org.thymeleaf.TemplateEngine}
+ * objects instead of the recommended {@link ThymeleafDefaultConfigurationFactory}
+ * or a sub-class thereof.
+ *
+ * @author Dmytro Dovnar (dimonmc@gmail.com)
+ */
+public class ThymeleafSuppliedConfigurationFactory implements ThymeleafConfigurationFactory {
+ private final ThymeleafConfigurationFactory configurationFactory;
+
+ public ThymeleafSuppliedConfigurationFactory(ThymeleafConfigurationFactory configurationFactory) {
+ this.configurationFactory = configurationFactory;
+ }
+
+ @Override
+ public TemplateEngine getTemplateEngine() {
+ return configurationFactory.getTemplateEngine();
+ }
+
+}
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafViewProcessor.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafViewProcessor.java
new file mode 100644
index 0000000000..fc8139c65b
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/ThymeleafViewProcessor.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
+
+import jakarta.inject.Inject;
+import jakarta.servlet.ServletContext;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import org.glassfish.jersey.internal.inject.InjectionManager;
+import org.glassfish.jersey.internal.util.collection.Values;
+import org.glassfish.jersey.server.mvc.Viewable;
+import org.glassfish.jersey.server.mvc.spi.AbstractTemplateProcessor;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * {@link org.glassfish.jersey.server.mvc.spi.TemplateProcessor Template processor} providing support for Thymeleaf templates.
+ *
+ * @author Dmytro Dovnar (dimonmc@gmail.com)
+ */
+public final class ThymeleafViewProcessor extends AbstractTemplateProcessor {
+ private final ThymeleafConfigurationFactory factory;
+
+ /**
+ * Create an instance of this processor with injected {@link jakarta.ws.rs.core.Configuration config}.
+ *
+ * @param config config to configure this processor from.
+ * @param injectionManager injection manager.
+ */
+ @Inject
+ public ThymeleafViewProcessor(Configuration config, InjectionManager injectionManager) {
+ super(config, injectionManager.getInstance(ServletContext.class), "thymeleaf", "html");
+ this.factory = getTemplateObjectFactory(injectionManager::createAndInitialize, ThymeleafConfigurationFactory.class,
+ () -> {
+ ThymeleafConfigurationFactory configuration =
+ getTemplateObjectFactory(
+ injectionManager::createAndInitialize,
+ ThymeleafConfigurationFactory.class, Values.empty());
+ if (configuration == null) {
+ return new ThymeleafDefaultConfigurationFactory(config);
+ } else {
+ return new ThymeleafSuppliedConfigurationFactory(configuration);
+ }
+ });
+ }
+
+ @Override
+ protected TemplateEngine resolve(final String templatePath, final Reader reader) throws Exception {
+ return factory.getTemplateEngine();
+ }
+
+ @Override
+ public void writeTo(final TemplateEngine templateEngine, final Viewable viewable, final MediaType mediaType,
+ final MultivaluedMap httpHeaders, final OutputStream out) throws IOException {
+ Context context = new Context();
+
+ Object model = viewable.getModel();
+ if (!(model instanceof Map)) {
+ context.setVariable("model", viewable.getModel());
+ } else {
+ context.setVariables((Map) viewable.getModel());
+ }
+
+ if (context.containsVariable("lang")) {
+ Object langValue = context.getVariable("lang");
+ if (langValue instanceof Locale) {
+ context.setLocale((Locale) langValue);
+ } else if (langValue instanceof String) {
+ Locale locale = Locale.forLanguageTag((String) langValue);
+ context.setLocale(locale);
+ }
+ }
+
+ Charset encoding = setContentType(mediaType, httpHeaders);
+
+ final Writer writer = new BufferedWriter(new OutputStreamWriter(out, encoding));
+ templateEngine.process(viewable.getTemplateName(), context, writer);
+ }
+}
diff --git a/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/package-info.java b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/package-info.java
new file mode 100644
index 0000000000..d8cd7ae4a9
--- /dev/null
+++ b/ext/mvc-thymeleaf/src/main/java/org/glassfish/jersey/server/mvc/thymeleaf/package-info.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.server.mvc.thymeleaf;
diff --git a/ext/pom.xml b/ext/pom.xml
index b9047ae1d0..a13a88e730 100644
--- a/ext/pom.xml
+++ b/ext/pom.xml
@@ -50,6 +50,7 @@
mvc-freemarker
mvc-jsp
mvc-mustache
+ mvc-thymeleaf
proxy-client
rx
spring6
diff --git a/pom.xml b/pom.xml
index 709a35edee..bc6c11a791 100644
--- a/pom.xml
+++ b/pom.xml
@@ -142,6 +142,9 @@
Stepan Vavra
+
+ Dmytro Dovnar
+
@@ -2292,6 +2295,7 @@
6.0.18
7.9.0
6.9.13.6
+ 3.1.2.RELEASE
4.0.3.Final
3.1.9.Final