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.portal.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;
032
033import java.io.IOException;
034import java.io.OutputStreamWriter;
035import java.io.Writer;
036import java.net.URI;
037import java.text.Collator;
038import java.util.ArrayList;
039import java.util.Comparator;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Locale;
043import java.util.Map;
044import java.util.MissingResourceException;
045import java.util.ResourceBundle;
046import java.util.UUID;
047
048import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
049import org.jdrupes.httpcodec.protocols.http.HttpField;
050import org.jdrupes.httpcodec.protocols.http.HttpResponse;
051import org.jdrupes.httpcodec.types.MediaType;
052import org.jgrapes.core.Channel;
053import org.jgrapes.http.LanguageSelector.Selection;
054import org.jgrapes.http.events.Request;
055import org.jgrapes.http.events.Response;
056import org.jgrapes.io.IOSubchannel;
057import org.jgrapes.io.util.ByteBufferOutputStream;
058import org.jgrapes.portal.base.PortalWeblet;
059
060/**
061 * 
062 */
063public abstract class FreeMarkerPortalWeblet extends PortalWeblet {
064
065    public static final String UTF_8 = "utf-8";
066    /**
067     * Initialized with a base FreeMarker configuration.
068     */
069    protected Configuration freeMarkerConfig;
070
071    /**
072     * Instantiates a new free marker portal weblet.
073     *
074     * @param webletChannel the weblet channel
075     * @param portalChannel the portal channel
076     * @param portalPrefix the portal prefix
077     */
078    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
079    public FreeMarkerPortalWeblet(Channel webletChannel, Channel portalChannel,
080            URI portalPrefix) {
081        super(webletChannel, portalChannel, portalPrefix);
082        freeMarkerConfig = new Configuration(Configuration.VERSION_2_3_26);
083        List<TemplateLoader> loaders = new ArrayList<>();
084        Class<?> clazz = getClass();
085        while (!clazz.equals(FreeMarkerPortalWeblet.class)) {
086            loaders.add(new ClassTemplateLoader(clazz.getClassLoader(),
087                clazz.getPackage().getName().replace('.', '/')));
088            clazz = clazz.getSuperclass();
089        }
090        freeMarkerConfig.setTemplateLoader(
091            new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
092        freeMarkerConfig.setDefaultEncoding(UTF_8);
093        freeMarkerConfig.setTemplateExceptionHandler(
094            TemplateExceptionHandler.RETHROW_HANDLER);
095        freeMarkerConfig.setLogTemplateExceptions(false);
096    }
097
098    /**
099     * Prepend a class template loader to the list of loaders 
100     * derived from the class hierarchy.
101     *
102     * @param classloader the class loader
103     * @param path the path
104     * @return the free marker portal weblet
105     */
106    public FreeMarkerPortalWeblet
107            prependClassTemplateLoader(ClassLoader classloader, String path) {
108        List<TemplateLoader> loaders = new ArrayList<>();
109        loaders.add(new ClassTemplateLoader(classloader, path));
110        MultiTemplateLoader oldLoader
111            = (MultiTemplateLoader) freeMarkerConfig.getTemplateLoader();
112        for (int i = 0; i < oldLoader.getTemplateLoaderCount(); i++) {
113            loaders.add(oldLoader.getTemplateLoader(i));
114        }
115        freeMarkerConfig.setTemplateLoader(
116            new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
117        return this;
118    }
119
120    /**
121     * Convenience version of 
122     * {@link #prependClassTemplateLoader(ClassLoader, String)} that derives
123     * the path from the class's package name.
124     *
125     * @param clazz the clazz
126     * @return the free marker portal weblet
127     */
128    public FreeMarkerPortalWeblet prependClassTemplateLoader(Class<?> clazz) {
129        return prependClassTemplateLoader(clazz.getClassLoader(),
130            clazz.getPackage().getName().replace('.', '/'));
131    }
132
133    /**
134     * Creates the portal base model.
135     *
136     * @return the base model
137     */
138    @SuppressWarnings("PMD.UseConcurrentHashMap")
139    protected Map<String, Object> createPortalBaseModel() {
140        // Create portal model
141        Map<String, Object> portalModel = new HashMap<>();
142        portalModel.put("renderSupport", renderSupport());
143        portalModel.put("useMinifiedResources", useMinifiedResources());
144        portalModel.put("minifiedExtension",
145            useMinifiedResources() ? ".min" : "");
146        portalModel.put(
147            "portalSessionRefreshInterval", portalSessionRefreshInterval());
148        portalModel.put(
149            "portalSessionInactivityTimeout", portalSessionInactivityTimeout());
150        return portalModel;
151    }
152
153    /**
154     * Expand the given portal model with data from the event 
155     * (portal session id, locale information).
156     *
157     * @param model the model
158     * @param event the event
159     * @param portalSessionId the portal session id
160     * @return the map
161     */
162    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
163    protected Map<String, Object> expandPortalModel(
164            Map<String, Object> model, Request.In.Get event,
165            UUID portalSessionId) {
166        // Portal Session UUID
167        model.put("portalSessionId", portalSessionId.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 locales
175        final Collator coll = Collator.getInstance(locale);
176        final Comparator<LanguageInfo> comp
177            = new Comparator<LanguageInfo>() {
178                @Override
179                public int compare(LanguageInfo o1, LanguageInfo o2) { // NOPMD
180                    return coll.compare(o1.getLabel(), o2.getLabel());
181                }
182            };
183        LanguageInfo[] languages = supportedLocales().stream()
184            .map(lang -> new LanguageInfo(lang))
185            .sorted(comp).toArray(size -> new LanguageInfo[size]);
186        model.put("supportedLanguages", languages);
187
188        final ResourceBundle baseResources = portalResourceBundle(locale);
189        model.put("_", new TemplateMethodModelEx() {
190            @Override
191            public Object exec(@SuppressWarnings("rawtypes") List arguments)
192                    throws TemplateModelException {
193                @SuppressWarnings("unchecked")
194                List<TemplateModel> args = (List<TemplateModel>) arguments;
195                if (!(args.get(0) instanceof SimpleScalar)) {
196                    throw new TemplateModelException("Not a string.");
197                }
198                String key = ((SimpleScalar) args.get(0)).getAsString();
199                try {
200                    return baseResources.getString(key);
201                } catch (MissingResourceException e) { // NOPMD
202                    // no luck
203                }
204                return key;
205            }
206        });
207        return model;
208    }
209
210    @Override
211    protected void renderPortal(Request.In.Get event, IOSubchannel channel,
212            UUID portalSessionId) throws IOException, InterruptedException {
213        event.setResult(true);
214        event.stop();
215
216        // Prepare response
217        HttpResponse response = event.httpRequest().response().get();
218        MediaType mediaType = MediaType.builder().setType("text", "html")
219            .setParameter("charset", UTF_8).build();
220        response.setField(HttpField.CONTENT_TYPE, mediaType);
221        response.setStatus(HttpStatus.OK);
222        response.setHasPayload(true);
223        channel.respond(new Response(response));
224        try (ByteBufferOutputStream bbos = new ByteBufferOutputStream(
225            channel, channel.responsePipeline());
226                Writer out = new OutputStreamWriter(bbos.suppressClose(),
227                    UTF_8)) {
228            @SuppressWarnings("PMD.UseConcurrentHashMap")
229
230            Template tpl = freeMarkerConfig.getTemplate("portal.ftl.html");
231            Map<String, Object> portalModel = expandPortalModel(
232                createPortalBaseModel(), event, portalSessionId);
233            tpl.process(portalModel, out);
234        } catch (TemplateException e) {
235            throw new IOException(e);
236        }
237    }
238
239    /**
240    * Holds the information about the selected language.
241    */
242    public static class LanguageInfo {
243        private final Locale locale;
244
245        /**
246         * Instantiates a new language info.
247         *
248         * @param locale the locale
249         */
250        public LanguageInfo(Locale locale) {
251            this.locale = locale;
252        }
253
254        /**
255         * Gets the locale.
256         *
257         * @return the locale
258         */
259        public Locale getLocale() {
260            return locale;
261        }
262
263        /**
264         * Gets the label.
265         *
266         * @return the label
267         */
268        public String getLabel() {
269            String str = locale.getDisplayName(locale);
270            return Character.toUpperCase(str.charAt(0)) + str.substring(1);
271        }
272    }
273
274}