001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2018,2022 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.webconsole.base.freemarker; 020 021import freemarker.cache.ClassTemplateLoader; 022import freemarker.cache.MultiTemplateLoader; 023import freemarker.cache.TemplateLoader; 024import freemarker.template.Configuration; 025import freemarker.template.SimpleScalar; 026import freemarker.template.Template; 027import freemarker.template.TemplateException; 028import freemarker.template.TemplateExceptionHandler; 029import freemarker.template.TemplateMethodModelEx; 030import freemarker.template.TemplateModel; 031import freemarker.template.TemplateModelException; 032import java.io.IOException; 033import java.io.Writer; 034import java.net.URI; 035import java.text.Collator; 036import java.util.ArrayList; 037import java.util.Comparator; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Locale; 041import java.util.Map; 042import java.util.MissingResourceException; 043import java.util.ResourceBundle; 044import java.util.UUID; 045import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus; 046import org.jdrupes.httpcodec.protocols.http.HttpField; 047import org.jdrupes.httpcodec.protocols.http.HttpResponse; 048import org.jdrupes.httpcodec.types.MediaType; 049import org.jgrapes.core.Channel; 050import org.jgrapes.http.LanguageSelector.Selection; 051import org.jgrapes.http.events.Request; 052import org.jgrapes.http.events.Response; 053import org.jgrapes.io.IOSubchannel; 054import org.jgrapes.io.util.ByteBufferWriter; 055import org.jgrapes.webconsole.base.ConsoleWeblet; 056 057/** 058 * A console weblet that uses a freemarker template to generate 059 * the HTML source for the console page. 060 */ 061public abstract class FreeMarkerConsoleWeblet extends ConsoleWeblet { 062 063 public static final String UTF_8 = "utf-8"; 064 /** 065 * Initialized with a base FreeMarker configuration. 066 */ 067 protected Configuration freeMarkerConfig; 068 069 /** 070 * Instantiates a new free marker console weblet. 071 * 072 * @param webletChannel the weblet channel 073 * @param consoleChannel the console channel 074 * @param consolePrefix the console prefix 075 */ 076 public FreeMarkerConsoleWeblet(Channel webletChannel, 077 Channel consoleChannel, URI consolePrefix) { 078 super(webletChannel, consoleChannel, consolePrefix); 079 freeMarkerConfig = new Configuration(Configuration.VERSION_2_3_26); 080 List<TemplateLoader> loaders = new ArrayList<>(); 081 Class<?> clazz = getClass(); 082 while (!clazz.equals(FreeMarkerConsoleWeblet.class)) { 083 loaders.add(new ClassTemplateLoader(clazz.getClassLoader(), 084 clazz.getPackage().getName().replace('.', '/'))); 085 clazz = clazz.getSuperclass(); 086 } 087 freeMarkerConfig.setTemplateLoader( 088 new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0]))); 089 freeMarkerConfig.setDefaultEncoding(UTF_8); 090 freeMarkerConfig.setTemplateExceptionHandler( 091 TemplateExceptionHandler.RETHROW_HANDLER); 092 freeMarkerConfig.setLogTemplateExceptions(false); 093 } 094 095 /** 096 * Prepend a class template loader to the list of loaders 097 * derived from the class hierarchy. 098 * 099 * @param classloader the class loader 100 * @param path the path 101 * @return the free marker console weblet 102 */ 103 public FreeMarkerConsoleWeblet 104 prependClassTemplateLoader(ClassLoader classloader, String path) { 105 List<TemplateLoader> loaders = new ArrayList<>(); 106 loaders.add(new ClassTemplateLoader(classloader, path)); 107 MultiTemplateLoader oldLoader 108 = (MultiTemplateLoader) freeMarkerConfig.getTemplateLoader(); 109 for (int i = 0; i < oldLoader.getTemplateLoaderCount(); i++) { 110 loaders.add(oldLoader.getTemplateLoader(i)); 111 } 112 freeMarkerConfig.setTemplateLoader( 113 new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0]))); 114 return this; 115 } 116 117 /** 118 * Convenience version of 119 * {@link #prependClassTemplateLoader(ClassLoader, String)} that derives 120 * the path from the class's package name. 121 * 122 * @param clazz the clazz 123 * @return the free marker console weblet 124 */ 125 public FreeMarkerConsoleWeblet prependClassTemplateLoader(Class<?> clazz) { 126 return prependClassTemplateLoader(clazz.getClassLoader(), 127 clazz.getPackage().getName().replace('.', '/')); 128 } 129 130 /** 131 * Creates the console base model. 132 * 133 * @return the base model 134 */ 135 @SuppressWarnings("PMD.UseConcurrentHashMap") 136 protected Map<String, Object> createConsoleBaseModel() { 137 // Create console model 138 Map<String, Object> consoleModel = new HashMap<>(); 139 consoleModel.put("consoleType", getClass().getName()); 140 consoleModel.put("renderSupport", renderSupport()); 141 consoleModel.put("useMinifiedResources", useMinifiedResources()); 142 consoleModel.put("minifiedExtension", 143 useMinifiedResources() ? ".min" : ""); 144 consoleModel.put("connectionRefreshInterval", 145 connectionRefreshInterval()); 146 consoleModel.put("connectionInactivityTimeout", 147 connectionInactivityTimeout()); 148 return consoleModel; 149 } 150 151 /** 152 * Invoked by {@link #renderConsole renderConsole} 153 * to expand the {@link #createConsoleBaseModel() 154 * base model} with information from the current event. 155 * 156 * @param model the model 157 * @param event the event 158 * @param consoleConnectionId the console connection id 159 * @return the map 160 */ 161 protected Map<String, Object> expandConsoleModel( 162 Map<String, Object> model, Request.In.Get event, 163 UUID consoleConnectionId) { 164 // WebConsole Connection UUID 165 model.put("consoleConnectionId", consoleConnectionId.toString()); 166 167 // Add locale 168 final Locale locale = event.associated(Selection.class).map( 169 sel -> sel.get()[0]).orElse(Locale.getDefault()); 170 model.put("locale", locale); 171 172 // Add supported languages 173 model.put("supportedLanguages", new TemplateMethodModelEx() { 174 private Object cachedResult; 175 176 @Override 177 @SuppressWarnings("PMD.AssignmentInOperand") 178 public Object exec(@SuppressWarnings("rawtypes") List arguments) 179 throws TemplateModelException { 180 if (cachedResult != null) { 181 return cachedResult; 182 } 183 final Collator coll = Collator.getInstance(locale); 184 final Comparator<LanguageInfo> comp = new Comparator<>() { 185 @Override 186 public int compare(LanguageInfo o1, LanguageInfo o2) { // NOPMD 187 return coll.compare(o1.getLabel(), o2.getLabel()); 188 } 189 }; 190 return cachedResult = supportedLocales().entrySet().stream() 191 .map(entry -> new LanguageInfo(entry.getKey(), 192 entry.getValue())) 193 .sorted(comp).toArray(size -> new LanguageInfo[size]); 194 } 195 }); 196 197 final ResourceBundle baseResources = consoleResourceBundle(locale); 198 model.put("_", new TemplateMethodModelEx() { 199 @Override 200 public Object exec(@SuppressWarnings("rawtypes") List arguments) 201 throws TemplateModelException { 202 @SuppressWarnings("unchecked") 203 List<TemplateModel> args = (List<TemplateModel>) arguments; 204 if (!(args.get(0) instanceof SimpleScalar)) { 205 throw new TemplateModelException("Not a string."); 206 } 207 String key = ((SimpleScalar) args.get(0)).getAsString(); 208 try { 209 return baseResources.getString(key); 210 } catch (MissingResourceException e) { // NOPMD 211 // no luck 212 } 213 return key; 214 } 215 }); 216 return model; 217 } 218 219 /** 220 * Render the console page using the freemarker template 221 * "console.ftl.html". The template for the console page 222 * (and all included templates) are loaded using the 223 * a list of template class loaders created as follows: 224 * 225 * 1. Start with the actual type of the console conlet. 226 * 227 * 2. Using the current type, add a freemarker class template loader 228 * {@link ClassTemplateLoader#ClassTemplateLoader(ClassLoader, String)} 229 * that uses the package name as path (all dots replaced with 230 * slashes). 231 * 232 * 3. Repeat step 2 with the super class of the current type 233 * until type {@link FreeMarkerConsoleWeblet} is reached. 234 * 235 * This approach allows a (base) console weblet to provide a 236 * console page template that includes another template 237 * e.g. "footer.ftl.html" to provide a specific part of the 238 * console page. A derived console weblet can then provide its 239 * own "footer.ftl.html" and thus override the version from the 240 * base class(es). 241 * 242 * @param event the event 243 * @param channel the channel 244 * @throws IOException Signals that an I/O exception has occurred. 245 * @throws InterruptedException the interrupted exception 246 */ 247 @Override 248 protected void renderConsole(Request.In.Get event, IOSubchannel channel, 249 UUID consoleConnectionId) throws IOException, InterruptedException { 250 event.setResult(true); 251 event.stop(); 252 253 // Prepare response 254 HttpResponse response = event.httpRequest().response().get(); 255 MediaType mediaType = MediaType.builder().setType("text", "html") 256 .setParameter("charset", UTF_8).build(); 257 response.setField(HttpField.CONTENT_TYPE, mediaType); 258 response.setStatus(HttpStatus.OK); 259 response.setHasPayload(true); 260 channel.respond(new Response(response)); 261 try (@SuppressWarnings("resource") 262 Writer out = new ByteBufferWriter(channel).suppressClose()) { 263 Template tpl = freeMarkerConfig.getTemplate("console.ftl.html"); 264 Map<String, Object> consoleModel = expandConsoleModel( 265 createConsoleBaseModel(), event, consoleConnectionId); 266 tpl.process(consoleModel, out); 267 } catch (TemplateException e) { 268 throw new IOException(e); 269 } 270 } 271 272}