001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2018 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.OutputStreamWriter;
034import java.io.Writer;
035import java.net.URI;
036import java.text.Collator;
037import java.util.ArrayList;
038import java.util.Comparator;
039import java.util.HashMap;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043import java.util.MissingResourceException;
044import java.util.ResourceBundle;
045import java.util.UUID;
046import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
047import org.jdrupes.httpcodec.protocols.http.HttpField;
048import org.jdrupes.httpcodec.protocols.http.HttpResponse;
049import org.jdrupes.httpcodec.types.MediaType;
050import org.jgrapes.core.Channel;
051import org.jgrapes.http.LanguageSelector.Selection;
052import org.jgrapes.http.events.Request;
053import org.jgrapes.http.events.Response;
054import org.jgrapes.io.IOSubchannel;
055import org.jgrapes.io.util.ByteBufferOutputStream;
056import org.jgrapes.webconsole.base.ConsoleWeblet;
057
058/**
059 * 
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,
079            URI consolePrefix) {
080        super(webletChannel, consoleChannel, consolePrefix);
081        freeMarkerConfig = new Configuration(Configuration.VERSION_2_3_26);
082        List<TemplateLoader> loaders = new ArrayList<>();
083        Class<?> clazz = getClass();
084        while (!clazz.equals(FreeMarkerConsoleWeblet.class)) {
085            loaders.add(new ClassTemplateLoader(clazz.getClassLoader(),
086                clazz.getPackage().getName().replace('.', '/')));
087            clazz = clazz.getSuperclass();
088        }
089        freeMarkerConfig.setTemplateLoader(
090            new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
091        freeMarkerConfig.setDefaultEncoding(UTF_8);
092        freeMarkerConfig.setTemplateExceptionHandler(
093            TemplateExceptionHandler.RETHROW_HANDLER);
094        freeMarkerConfig.setLogTemplateExceptions(false);
095    }
096
097    /**
098     * Prepend a class template loader to the list of loaders 
099     * derived from the class hierarchy.
100     *
101     * @param classloader the class loader
102     * @param path the path
103     * @return the free marker console weblet
104     */
105    public FreeMarkerConsoleWeblet
106            prependClassTemplateLoader(ClassLoader classloader, String path) {
107        List<TemplateLoader> loaders = new ArrayList<>();
108        loaders.add(new ClassTemplateLoader(classloader, path));
109        MultiTemplateLoader oldLoader
110            = (MultiTemplateLoader) freeMarkerConfig.getTemplateLoader();
111        for (int i = 0; i < oldLoader.getTemplateLoaderCount(); i++) {
112            loaders.add(oldLoader.getTemplateLoader(i));
113        }
114        freeMarkerConfig.setTemplateLoader(
115            new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
116        return this;
117    }
118
119    /**
120     * Convenience version of 
121     * {@link #prependClassTemplateLoader(ClassLoader, String)} that derives
122     * the path from the class's package name.
123     *
124     * @param clazz the clazz
125     * @return the free marker console weblet
126     */
127    public FreeMarkerConsoleWeblet prependClassTemplateLoader(Class<?> clazz) {
128        return prependClassTemplateLoader(clazz.getClassLoader(),
129            clazz.getPackage().getName().replace('.', '/'));
130    }
131
132    /**
133     * Creates the console base model.
134     *
135     * @return the base model
136     */
137    @SuppressWarnings("PMD.UseConcurrentHashMap")
138    protected Map<String, Object> createConsoleBaseModel() {
139        // Create console model
140        Map<String, Object> consoleModel = new HashMap<>();
141        consoleModel.put("renderSupport", renderSupport());
142        consoleModel.put("useMinifiedResources", useMinifiedResources());
143        consoleModel.put("minifiedExtension",
144            useMinifiedResources() ? ".min" : "");
145        consoleModel.put(
146            "consoleSessionRefreshInterval", consoleSessionRefreshInterval());
147        consoleModel.put(
148            "consoleSessionInactivityTimeout",
149            consoleSessionInactivityTimeout());
150        return consoleModel;
151    }
152
153    /**
154     * Expand the given console model with data from the event 
155     * (console session id, locale information).
156     *
157     * @param model the model
158     * @param event the event
159     * @param consoleSessionId the console session 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 consoleSessionId) {
166        // WebConsole Session UUID
167        model.put("consoleSessionId", consoleSessionId.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            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
186                    = new Comparator<LanguageInfo>() {
187                        @Override
188                        public int compare(LanguageInfo o1, LanguageInfo o2) { // NOPMD
189                            return coll.compare(o1.getLabel(), o2.getLabel());
190                        }
191                    };
192                return cachedResult = supportedLocales().entrySet().stream()
193                    .map(entry -> new LanguageInfo(entry.getKey(),
194                        entry.getValue()))
195                    .sorted(comp).toArray(size -> new LanguageInfo[size]);
196            }
197        });
198
199        final ResourceBundle baseResources = consoleResourceBundle(locale);
200        model.put("_", new TemplateMethodModelEx() {
201            @Override
202            public Object exec(@SuppressWarnings("rawtypes") List arguments)
203                    throws TemplateModelException {
204                @SuppressWarnings("unchecked")
205                List<TemplateModel> args = (List<TemplateModel>) arguments;
206                if (!(args.get(0) instanceof SimpleScalar)) {
207                    throw new TemplateModelException("Not a string.");
208                }
209                String key = ((SimpleScalar) args.get(0)).getAsString();
210                try {
211                    return baseResources.getString(key);
212                } catch (MissingResourceException e) { // NOPMD
213                    // no luck
214                }
215                return key;
216            }
217        });
218        return model;
219    }
220
221    @Override
222    protected void renderConsole(Request.In.Get event, IOSubchannel channel,
223            UUID consoleSessionId) throws IOException, InterruptedException {
224        event.setResult(true);
225        event.stop();
226
227        // Prepare response
228        HttpResponse response = event.httpRequest().response().get();
229        MediaType mediaType = MediaType.builder().setType("text", "html")
230            .setParameter("charset", UTF_8).build();
231        response.setField(HttpField.CONTENT_TYPE, mediaType);
232        response.setStatus(HttpStatus.OK);
233        response.setHasPayload(true);
234        channel.respond(new Response(response));
235        try (ByteBufferOutputStream bbos = new ByteBufferOutputStream(
236            channel, channel.responsePipeline());
237                Writer out = new OutputStreamWriter(bbos.suppressClose(),
238                    UTF_8)) {
239            @SuppressWarnings("PMD.UseConcurrentHashMap")
240
241            Template tpl = freeMarkerConfig.getTemplate("console.ftl.html");
242            Map<String, Object> consoleModel = expandConsoleModel(
243                createConsoleBaseModel(), event, consoleSessionId);
244            tpl.process(consoleModel, out);
245        } catch (TemplateException e) {
246            throw new IOException(e);
247        }
248    }
249
250    /**
251    * Holds the information about the selected language.
252    */
253    public static class LanguageInfo {
254        private final Locale locale;
255        private final ResourceBundle bundle;
256
257        /**
258         * Instantiates a new language info.
259         *
260         * @param locale the locale
261         */
262        public LanguageInfo(Locale locale, ResourceBundle bundle) {
263            this.locale = locale;
264            this.bundle = bundle;
265        }
266
267        /**
268         * Gets the locale.
269         *
270         * @return the locale
271         */
272        public Locale getLocale() {
273            return locale;
274        }
275
276        /**
277         * Gets the label.
278         *
279         * @return the label
280         */
281        public String getLabel() {
282            String str = locale.getDisplayName(locale);
283            return Character.toUpperCase(str.charAt(0)) + str.substring(1);
284        }
285
286        /**
287         * Gets the bundle.
288         *
289         * @return the bundle
290         */
291        public ResourceBundle getL10nBundle() {
292            return bundle;
293        }
294    }
295
296}