001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.view.thymeleaf; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.util.*; 023 024import org.apache.juneau.rest.server.view.*; 025 026/** 027 * Immutable value class returned from {@code @RestOp}-annotated methods to ask the framework to 028 * render a Thymeleaf template. 029 * 030 * <p> 031 * Companion to {@link ThymeleafMixin} and {@link ThymeleafViewRenderer}: the mixin sets up 032 * the {@code /thymeleaf/*} mount and registers the renderer; the renderer detects 033 * {@code ThymeleafView} returns in the response-processor chain and asks the configured 034 * {@code org.thymeleaf.TemplateEngine} to {@code process(templateName, context, writer)} directly 035 * onto the response writer. 036 * 037 * <h5 class='figure'>Usage:</h5> 038 * 039 * <p class='bjava'> 040 * <ja>@RestGet</ja>(<js>"/hello/{name}"</js>) 041 * <jk>public</jk> View hello(<ja>@Path</ja> String <jv>name</jv>) { 042 * <jk>return</jk> ThymeleafView.<jsm>of</jsm>(<js>"hello"</js>) 043 * .attr(<js>"name"</js>, <jv>name</jv>) 044 * .attr(<js>"ts"</js>, Instant.<jsm>now</jsm>()); 045 * } 046 * </p> 047 * 048 * <p> 049 * The template name is engine-relative — the configured 050 * {@code ClassLoaderTemplateResolver} (or whatever resolver the active 051 * {@code org.thymeleaf.TemplateEngine} carries) prepends its own prefix and appends its own suffix 052 * (typically {@code .html}). With the bridge's default engine + {@code basePath("/templates/")}, 053 * {@code ThymeleafView.of("hello")} resolves to {@code /templates/hello.html} on the classpath. 054 * 055 * <h5 class='section'>Immutability:</h5> 056 * 057 * <p> 058 * Each {@code attr(...)} / {@code attrs(...)} / {@code header(...)} call returns a <b>new</b> 059 * {@code ThymeleafView} carrying the additional binding; the original instance is unchanged. This 060 * keeps {@code ThymeleafView} a value type safe to share across requests (e.g. as a static 061 * singleton built once and returned from many handlers). 062 * 063 * <h5 class='section'>See Also:</h5><ul> 064 * <li class='jc'>{@link ThymeleafMixin} 065 * <li class='jc'>{@link ThymeleafViewRenderer} 066 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ThymeleafViewSupport">Thymeleaf View Support</a> 067 * </ul> 068 * 069 * @since 10.0.0 070 */ 071public final class ThymeleafView implements View { 072 073 private final String templateName; 074 private final Map<String,Object> attributes; 075 private final Map<String,String> responseHeaders; 076 077 /** 078 * Creates a new {@code ThymeleafView} carrying the given template name and no attributes. 079 * 080 * <p> 081 * The template name is engine-relative; the configured 082 * {@code org.thymeleaf.TemplateEngine} resolver supplies the prefix and suffix. For the 083 * bridge's default engine: {@code "hello"} resolves to {@code <basePath>hello.html}. 084 * 085 * @param templateName The template name (engine-relative, no extension). Must not be 086 * {@code null} or blank. 087 * @return A new {@code ThymeleafView} instance. 088 * @throws IllegalArgumentException If {@code templateName} is {@code null} or blank. 089 */ 090 public static ThymeleafView of(String templateName) { 091 if (isBlank(templateName)) 092 throw iaex("templateName must not be null or blank"); 093 return new ThymeleafView(templateName, Map.of(), Map.of()); 094 } 095 096 private ThymeleafView(String templateName, Map<String,Object> attributes, Map<String,String> responseHeaders) { 097 this.templateName = templateName; 098 this.attributes = attributes; 099 this.responseHeaders = responseHeaders; 100 } 101 102 /** 103 * Returns a copy of this view with the given attribute added (or replaced if a binding for 104 * {@code key} already exists). 105 * 106 * <p> 107 * Each attribute is exposed to the template as a named variable (e.g. 108 * {@code th:text="${name}"} resolves the {@code "name"} key from the populated 109 * {@code Context}). {@code null} values are rejected at build-time because a 110 * Thymeleaf {@code Context} silently drops {@code null}-valued bindings, masking what is 111 * typically a caller bug; rejecting up-front surfaces the mistake at the call site. 112 * 113 * @param key The attribute key (exposed inside the template as {@code ${key}}). Must not be 114 * {@code null} or blank. 115 * @param value The attribute value. Must not be {@code null}. 116 * @return A new {@code ThymeleafView} carrying the additional attribute. 117 * @throws IllegalArgumentException If {@code key} is {@code null} or blank, or {@code value} 118 * is {@code null}. 119 */ 120 public ThymeleafView attr(String key, Object value) { 121 if (isBlank(key)) 122 throw iaex("attribute key must not be null or blank"); 123 if (value == null) 124 throw iaex("attribute value must not be null (attribute '%s')", key); 125 var copy = new LinkedHashMap<>(attributes); 126 copy.put(key, value); 127 return new ThymeleafView(templateName, Map.copyOf(copy), responseHeaders); 128 } 129 130 /** 131 * Returns a copy of this view with all entries from the given map added (or replaced if a 132 * binding for any key already exists). 133 * 134 * @param values The attributes to add. {@code null} is treated as an empty map (the result is 135 * the same instance with no entries added). 136 * @return A new {@code ThymeleafView} carrying the additional attributes. 137 * @throws IllegalArgumentException If any entry has a {@code null}/blank key or a {@code null} 138 * value. 139 */ 140 public ThymeleafView attrs(Map<String,?> values) { 141 if (ie(values)) 142 return this; 143 var copy = new LinkedHashMap<>(attributes); 144 values.forEach((k, v) -> { 145 if (isBlank(k)) 146 throw iaex("attribute key must not be null or blank"); 147 if (v == null) 148 throw iaex("attribute value must not be null (attribute '%s')", k); 149 copy.put(k, v); 150 }); 151 return new ThymeleafView(templateName, Map.copyOf(copy), responseHeaders); 152 } 153 154 /** 155 * Returns a copy of this view with the given response header set on the rendered response. 156 * 157 * <p> 158 * Typical usage: {@code .header("Content-Type", "text/html; charset=UTF-8")} or 159 * {@code .header("Cache-Control", "no-store")}. The renderer applies these headers via 160 * {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String) 161 * HttpServletResponse.setHeader(...)} before writing the rendered output. 162 * 163 * @param name The header name. Must not be {@code null} or blank. 164 * @param value The header value. Must not be {@code null}. 165 * @return A new {@code ThymeleafView} carrying the additional response header. 166 * @throws IllegalArgumentException If {@code name} is {@code null} or blank, or {@code value} 167 * is {@code null}. 168 */ 169 public ThymeleafView header(String name, String value) { 170 if (isBlank(name)) 171 throw iaex("header name must not be null or blank"); 172 if (value == null) 173 throw iaex("header value must not be null (header '%s')", name); 174 var copy = new LinkedHashMap<>(responseHeaders); 175 copy.put(name, value); 176 return new ThymeleafView(templateName, attributes, Map.copyOf(copy)); 177 } 178 179 @Override /* Overridden from View */ 180 public String getTemplateName() { 181 return templateName; 182 } 183 184 @Override /* Overridden from View */ 185 public Map<String,Object> getAttributes() { 186 return attributes; 187 } 188 189 @Override /* Overridden from View */ 190 public Map<String,String> getResponseHeaders() { 191 return responseHeaders; 192 } 193 194 @Override /* Overridden from Object */ 195 public String toString() { 196 return "ThymeleafView[template=" + templateName + ", attrs=" + attributes.keySet() 197 + ", headers=" + responseHeaders.keySet() + "]"; 198 } 199}