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