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 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 077 public FreeMarkerConsoleWeblet(Channel webletChannel, 078 Channel consoleChannel, URI consolePrefix) { 079 super(webletChannel, consoleChannel, consolePrefix); 080 freeMarkerConfig = new Configuration(Configuration.VERSION_2_3_26); 081 List<TemplateLoader> loaders = new ArrayList<>(); 082 Class<?> clazz = getClass(); 083 while (!clazz.equals(FreeMarkerConsoleWeblet.class)) { 084 loaders.add(new ClassTemplateLoader(clazz.getClassLoader(), 085 clazz.getPackage().getName().replace('.', '/'))); 086 clazz = clazz.getSuperclass(); 087 } 088 freeMarkerConfig.setTemplateLoader( 089 new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0]))); 090 freeMarkerConfig.setDefaultEncoding(UTF_8); 091 freeMarkerConfig.setTemplateExceptionHandler( 092 TemplateExceptionHandler.RETHROW_HANDLER); 093 freeMarkerConfig.setLogTemplateExceptions(false); 094 } 095 096 /** 097 * Prepend a class template loader to the list of loaders 098 * derived from the class hierarchy. 099 * 100 * @param classloader the class loader 101 * @param path the path 102 * @return the free marker console weblet 103 */ 104 public FreeMarkerConsoleWeblet 105 prependClassTemplateLoader(ClassLoader classloader, String path) { 106 List<TemplateLoader> loaders = new ArrayList<>(); 107 loaders.add(new ClassTemplateLoader(classloader, path)); 108 MultiTemplateLoader oldLoader 109 = (MultiTemplateLoader) freeMarkerConfig.getTemplateLoader(); 110 for (int i = 0; i < oldLoader.getTemplateLoaderCount(); i++) { 111 loaders.add(oldLoader.getTemplateLoader(i)); 112 } 113 freeMarkerConfig.setTemplateLoader( 114 new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0]))); 115 return this; 116 } 117 118 /** 119 * Convenience version of 120 * {@link #prependClassTemplateLoader(ClassLoader, String)} that derives 121 * the path from the class's package name. 122 * 123 * @param clazz the clazz 124 * @return the free marker console weblet 125 */ 126 public FreeMarkerConsoleWeblet prependClassTemplateLoader(Class<?> clazz) { 127 return prependClassTemplateLoader(clazz.getClassLoader(), 128 clazz.getPackage().getName().replace('.', '/')); 129 } 130 131 /** 132 * Creates the console base model. 133 * 134 * @return the base model 135 */ 136 @SuppressWarnings("PMD.UseConcurrentHashMap") 137 protected Map<String, Object> createConsoleBaseModel() { 138 // Create console model 139 Map<String, Object> consoleModel = new HashMap<>(); 140 consoleModel.put("consoleType", getClass().getName()); 141 consoleModel.put("renderSupport", renderSupport()); 142 consoleModel.put("useMinifiedResources", useMinifiedResources()); 143 consoleModel.put("minifiedExtension", 144 useMinifiedResources() ? ".min" : ""); 145 consoleModel.put("connectionRefreshInterval", 146 connectionRefreshInterval()); 147 consoleModel.put("connectionInactivityTimeout", 148 connectionInactivityTimeout()); 149 return consoleModel; 150 } 151 152 /** 153 * Invoked by {@link #renderConsole renderConsole} 154 * to expand the {@link #createConsoleBaseModel() 155 * base model} with information from the current event. 156 * 157 * @param model the model 158 * @param event the event 159 * @param consoleConnectionId the console connection id 160 * @return the map 161 */ 162 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 163 protected Map<String, Object> expandConsoleModel( 164 Map<String, Object> model, Request.In.Get event, 165 UUID consoleConnectionId) { 166 // WebConsole Connection UUID 167 model.put("consoleConnectionId", consoleConnectionId.toString()); 168 169 // Add locale 170 final Locale locale = event.associated(Selection.class).map( 171 sel -> sel.get()[0]).orElse(Locale.getDefault()); 172 model.put("locale", locale); 173 174 // Add supported languages 175 model.put("supportedLanguages", new TemplateMethodModelEx() { 176 private Object cachedResult; 177 178 @Override 179 public Object exec(@SuppressWarnings("rawtypes") List arguments) 180 throws TemplateModelException { 181 if (cachedResult != null) { 182 return cachedResult; 183 } 184 final Collator coll = Collator.getInstance(locale); 185 final Comparator<LanguageInfo> comp = new Comparator<>() { 186 @Override 187 public int compare(LanguageInfo o1, LanguageInfo o2) { // NOPMD 188 return coll.compare(o1.getLabel(), o2.getLabel()); 189 } 190 }; 191 return cachedResult = supportedLocales().entrySet().stream() 192 .map(entry -> new LanguageInfo(entry.getKey(), 193 entry.getValue())) 194 .sorted(comp).toArray(size -> new LanguageInfo[size]); 195 } 196 }); 197 198 final ResourceBundle baseResources = consoleResourceBundle(locale); 199 model.put("_", new TemplateMethodModelEx() { 200 @Override 201 public Object exec(@SuppressWarnings("rawtypes") List arguments) 202 throws TemplateModelException { 203 @SuppressWarnings("unchecked") 204 List<TemplateModel> args = (List<TemplateModel>) arguments; 205 if (!(args.get(0) instanceof SimpleScalar)) { 206 throw new TemplateModelException("Not a string."); 207 } 208 String key = ((SimpleScalar) args.get(0)).getAsString(); 209 try { 210 return baseResources.getString(key); 211 } catch (MissingResourceException e) { // NOPMD 212 // no luck 213 } 214 return key; 215 } 216 }); 217 return model; 218 } 219 220 /** 221 * Render the console page using the freemarker template 222 * "console.ftl.html". The template for the console page 223 * (and all included templates) are loaded using the 224 * a list of template class loaders created as follows: 225 * 226 * 1. Start with the actual type of the console conlet. 227 * 228 * 2. Using the current type, add a freemarker class template loader 229 * {@link ClassTemplateLoader#ClassTemplateLoader(ClassLoader, String)} 230 * that uses the package name as path (all dots replaced with 231 * slashes). 232 * 233 * 3. Repeat step 2 with the super class of the current type 234 * until type {@link FreeMarkerConsoleWeblet} is reached. 235 * 236 * This approach allows a (base) console weblet to provide a 237 * console page template that includes another template 238 * e.g. "footer.ftl.html" to provide a specific part of the 239 * console page. A derived console weblet can then provide its 240 * own "footer.ftl.html" and thus override the version from the 241 * base class(es). 242 * 243 * @param event the event 244 * @param channel the channel 245 * @throws IOException Signals that an I/O exception has occurred. 246 * @throws InterruptedException the interrupted exception 247 */ 248 @Override 249 protected void renderConsole(Request.In.Get event, IOSubchannel channel, 250 UUID consoleConnectionId) throws IOException, InterruptedException { 251 event.setResult(true); 252 event.stop(); 253 254 // Prepare response 255 HttpResponse response = event.httpRequest().response().get(); 256 MediaType mediaType = MediaType.builder().setType("text", "html") 257 .setParameter("charset", UTF_8).build(); 258 response.setField(HttpField.CONTENT_TYPE, mediaType); 259 response.setStatus(HttpStatus.OK); 260 response.setHasPayload(true); 261 channel.respond(new Response(response)); 262 try (@SuppressWarnings("resource") 263 Writer out = new ByteBufferWriter(channel).suppressClose()) { 264 Template tpl = freeMarkerConfig.getTemplate("console.ftl.html"); 265 Map<String, Object> consoleModel = expandConsoleModel( 266 createConsoleBaseModel(), event, consoleConnectionId); 267 tpl.process(consoleModel, out); 268 } catch (TemplateException e) { 269 throw new IOException(e); 270 } 271 } 272 273}