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.mustache; 018 019import java.io.*; 020 021import org.apache.juneau.commons.utils.*; 022import org.apache.juneau.http.response.*; 023import org.apache.juneau.rest.server.*; 024import org.apache.juneau.rest.server.processor.*; 025import org.apache.juneau.rest.server.view.*; 026 027import com.github.mustachejava.*; 028 029/** 030 * {@link ResponseProcessor} that detects {@link MustacheView}-typed return values and asks the 031 * configured {@link MustacheFactory} to render them directly onto the response writer. 032 * 033 * <p> 034 * Auto-registered by {@link MustacheMixin} via 035 * {@link Rest#responseProcessors() @Rest(responseProcessors=...)} 036 * — callers who add the mixin don't need to wire up this class explicitly. Callers who want 037 * to handle {@code MustacheView} returns <i>without</i> adopting the mixin can add this class to 038 * their own 039 * {@link Rest#responseProcessors() responseProcessors} list. 040 * 041 * <h5 class='section'>Behavior:</h5> 042 * 043 * <ol class='spaced-list'> 044 * <li>Inspect the response content. If the value is not a {@link MustacheView}, return 045 * {@link ResponseProcessor#NEXT NEXT} so the rest of the chain runs. 046 * <li>Read the active {@link MustacheMixin} from the {@code RestContext} bean store to 047 * discover the {@link MustacheFactory} (lazy default if no factory bean is registered) and 048 * the optional template-suffix knob. 049 * <li>Apply every entry from {@link MustacheView#getResponseHeaders()} via 050 * {@link jakarta.servlet.http.HttpServletResponse#setHeader(String, String) 051 * res.setHeader(...)}. 052 * <li>Default {@code Content-Type} to {@code text/html;charset=UTF-8} if the caller did not set 053 * one explicitly (Mustache is content-neutral, but HTML is overwhelmingly the most common 054 * output target; an explicit caller header wins). 055 * <li>Call {@code factory.compile(templateName).execute(res.getWriter(), view.getAttributes())} 056 * to stream the rendered output. 057 * <li>When no Mustache engine is on the classpath, the {@link MustacheFactory}-typed import 058 * here fails to load at first use and surfaces {@link #NO_ENGINE_DIAGNOSTIC} naming the 059 * missing dependency. 060 * </ol> 061 * 062 * <h5 class='section'>See Also:</h5><ul> 063 * <li class='jc'>{@link MustacheMixin} 064 * <li class='jc'>{@link MustacheView} 065 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ResponseProcessors">Response Processors</a> 066 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MustacheViewSupport">Mustache View Support</a> 067 * </ul> 068 * 069 * @since 10.0.0 070 */ 071@SuppressWarnings({ 072 "resource" // Closeables here are framework-managed and not owned/closed by this class; not a real leak. 073}) 074public class MustacheViewRenderer implements ViewRenderer { 075 076 /** Default {@code Content-Type} applied when the view does not specify one explicitly. */ 077 public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=UTF-8"; 078 079 /** 080 * Diagnostic message emitted when no Mustache engine can be found on the classpath at 081 * render-time. 082 * 083 * <p> 084 * Public so tests and consumer apps can pattern-match against the message text without 085 * depending on internal string literals. 086 */ 087 public static final String NO_ENGINE_DIAGNOSTIC = 088 """ 089 No Mustache engine is available on the classpath. Add: 090 - com.github.spullara.mustache.java:compiler (mustache.java — the engine the bridge targets) 091 Or register a custom @Bean MustacheFactory that picks up your preferred resolvers. 092 Note: Spring Boot's spring-boot-starter-mustache ships com.samskivert:jmustache, NOT 093 mustache.java; the bridge is mustache.java-specific. To use jmustache, supply your own 094 ResponseProcessor instead of MustacheViewRenderer. 095 See https://juneau.apache.org/docs/topics/MustacheViewSupport for the full matrix."""; 096 097 // Lazily-created fallback used when no MustacheMixin bean is registered. Cached (double-checked) so 098 // the bridge-default MustacheFactory is built once instead of rebuilt per render. Created lazily (not 099 // eagerly) so a missing Mustache engine still surfaces at render time as NO_ENGINE_DIAGNOSTIC rather 100 // than during RestContext construction. 101 @SuppressWarnings({ 102 "java:S3077" // Publish-once cache: assigned once under double-checked locking in fallbackMixin(); MustacheMixin is fully built (effectively immutable — its only field is final) before assignment, so volatile safe-publication is sufficient. 103 }) 104 private volatile MustacheMixin fallbackMixin; 105 106 @Override /* Overridden from ResponseProcessor */ 107 public int process(RestOpSession opSession) throws IOException, BasicHttpException { 108 var req = opSession.getRequest(); 109 var res = opSession.getResponse(); 110 111 var content = res.getContent(Object.class); 112 if (! (content instanceof MustacheView content2)) 113 return NEXT; 114 115 // Resolve the bridge resource (carries MustacheFactory + cached default + templateSuffix). 116 // Fall back to a cached MustacheMixin when the renderer is used standalone without 117 // the mixin. 118 var bridge = req.getContext().getBeanStore() 119 .getBean(MustacheMixin.class) 120 .orElseGet(this::fallbackMixin); 121 122 // Apply caller-supplied response headers first so a caller-provided Content-Type wins 123 // over the bridge's default below. 124 content2.getResponseHeaders().forEach(res::setHeader); 125 if (! res.containsHeader("Content-Type")) 126 res.setHeader("Content-Type", DEFAULT_CONTENT_TYPE); 127 128 // Apply the same traversal gate the raw /mustache/* mount uses so a typed-View template name 129 // assembled from user input (e.g. MustacheView.of(userInput)) cannot escape the configured base 130 // path via '../' segments. Mirrors the JSP bridge's typed-View gate. 131 String safeName; 132 try { 133 safeName = gateTemplateName(bridge.getBasePath(), content2.getTemplateName()); 134 } catch (IllegalArgumentException ex) { 135 throw new InternalServerError(ex, "Mustache template name escapes configured base path: '%s'", content2.getTemplateName()); 136 } 137 138 var templateName = bridge.applyTemplateSuffix(safeName); 139 try { 140 var factory = bridge.resolveMustacheFactory(req); 141 var mustache = factory.compile(templateName); 142 mustache.execute(res.getWriter(), content2.getAttributes()).flush(); 143 return FINISHED; 144 } catch (LinkageError ex) { 145 throw new InternalServerError(ex, NO_ENGINE_DIAGNOSTIC); 146 } catch (IOException ex) { 147 throw ex; 148 } catch (Exception ex) { 149 throw new InternalServerError(ex, "Mustache render failed for '%s'", templateName); 150 } 151 } 152 153 // Rejects '../'-style traversal in a typed-View template name using the same virtual-path gate the raw 154 // mount applies, returning the configuration-relative template name. Delegates to the shared 155 // FileUtils.resolveVirtualPathSafely + MustacheDispatcher.stripBasePath used by the raw render path. 156 static String gateTemplateName(String basePath, String templateName) { 157 var resolved = FileUtils.resolveVirtualPathSafely(basePath, templateName); 158 return MustacheDispatcher.stripBasePath(basePath, resolved); 159 } 160 161 // Returns the cached fallback mixin, creating it once (double-checked) on first standalone render. 162 private MustacheMixin fallbackMixin() { 163 var local = fallbackMixin; 164 if (local == null) { 165 synchronized (this) { 166 local = fallbackMixin; 167 if (local == null) { 168 local = new MustacheMixin(); 169 fallbackMixin = local; 170 } 171 } 172 } 173 return local; 174 } 175}