001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-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.template.Configuration;
022import freemarker.template.SimpleScalar;
023import freemarker.template.Template;
024import freemarker.template.TemplateException;
025import freemarker.template.TemplateExceptionHandler;
026import freemarker.template.TemplateMethodModelEx;
027import freemarker.template.TemplateModel;
028import freemarker.template.TemplateModelException;
029
030import java.io.IOException;
031import java.io.OutputStream;
032import java.io.OutputStreamWriter;
033import java.io.StringWriter;
034import java.time.Instant;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Locale;
039import java.util.Map;
040import java.util.MissingResourceException;
041import java.util.ResourceBundle;
042import java.util.concurrent.Future;
043import java.util.regex.Pattern;
044
045import org.jdrupes.httpcodec.protocols.http.HttpResponse;
046import org.jgrapes.core.Channel;
047import org.jgrapes.core.Component;
048import org.jgrapes.core.Components;
049import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
050import org.jgrapes.http.Session;
051import org.jgrapes.io.IOSubchannel;
052import org.jgrapes.portal.base.AbstractPortlet;
053import org.jgrapes.portal.base.AbstractPortlet.PortletBaseModel;
054import org.jgrapes.portal.base.PortalSession;
055import org.jgrapes.portal.base.RenderSupport;
056import org.jgrapes.portal.base.ResourceByGenerator;
057import org.jgrapes.portal.base.events.PortletResourceRequest;
058import org.jgrapes.portal.base.events.RenderPortlet;
059import org.jgrapes.portal.base.events.RenderPortletRequest;
060import org.jgrapes.portal.base.events.RenderPortletRequestBase;
061
062/**
063 * 
064 */
065public abstract class FreeMarkerPortlet extends AbstractPortlet {
066
067    @SuppressWarnings("PMD.VariableNamingConventions")
068    private static final Pattern templatePattern
069        = Pattern.compile(".*\\.ftl\\.[a-z]+$");
070
071    private Configuration fmConfig;
072    private Map<String, Object> fmModel;
073
074    /**
075     * Creates a new component that listens for new events
076     * on the given channel.
077     * 
078     * @param componentChannel
079     * @param trackPortalSessions if set, track the relationship between
080     * portal sessions and portelt ids
081     */
082    public FreeMarkerPortlet(Channel componentChannel,
083            boolean trackPortalSessions) {
084        super(componentChannel, trackPortalSessions);
085    }
086
087    /**
088     * Like {@link #FreeMarkerPortlet(Channel, boolean)}, but supports
089     * the specification of channel replacements.
090     * 
091     * @param componentChannel
092     * @param channelReplacements the channel replacements (see
093     * {@link Component})
094     * @param trackPortalSessions if set, track the relationship between
095     * portal sessions and portelt ids
096     */
097    public FreeMarkerPortlet(Channel componentChannel,
098            ChannelReplacements channelReplacements,
099            boolean trackPortalSessions) {
100        super(componentChannel, channelReplacements, trackPortalSessions);
101    }
102
103    /**
104     * Create the base freemarker configuration.
105     *
106     * @return the configuration
107     */
108    protected Configuration freemarkerConfig() {
109        if (fmConfig == null) {
110            fmConfig = new Configuration(Configuration.VERSION_2_3_26);
111            fmConfig.setClassLoaderForTemplateLoading(
112                getClass().getClassLoader(), getClass().getPackage()
113                    .getName().replace('.', '/'));
114            fmConfig.setDefaultEncoding("utf-8");
115            fmConfig.setTemplateExceptionHandler(
116                TemplateExceptionHandler.RETHROW_HANDLER);
117            fmConfig.setLogTemplateExceptions(false);
118        }
119        return fmConfig;
120    }
121
122    /**
123     * Creates the request independent part of the freemarker model. The
124     * result is cached as unmodifiable map as it can safely be assumed
125     * that the render support does not change for a given portal.
126     * 
127     * This model provides:
128     *  * The function `portletResource` that makes 
129     *    {@link RenderSupport#portletResource(String, java.net.URI)}
130     *    available in the template. The first argument is set to the name
131     *    of the portlet, only the second must be supplied when the function
132     *    is invoked in a template.
133     * 
134     * @param renderSupport the render support from the portal
135     * @return the result
136     */
137    protected Map<String, Object> fmTypeModel(
138            RenderSupport renderSupport) {
139        if (fmModel == null) {
140            fmModel = new HashMap<>();
141            fmModel.put("portletResource", new TemplateMethodModelEx() {
142                @Override
143                public Object exec(@SuppressWarnings("rawtypes") List arguments)
144                        throws TemplateModelException {
145                    @SuppressWarnings("unchecked")
146                    List<TemplateModel> args = (List<TemplateModel>) arguments;
147                    if (!(args.get(0) instanceof SimpleScalar)) {
148                        throw new TemplateModelException("Not a string.");
149                    }
150                    return renderSupport.portletResource(
151                        FreeMarkerPortlet.this.getClass().getName(),
152                        ((SimpleScalar) args.get(0)).getAsString())
153                        .getRawPath();
154                }
155            });
156            fmModel = Collections.unmodifiableMap(fmModel);
157        }
158        return fmModel;
159    }
160
161    /**
162     * Build a freemarker model holding the information associated 
163     * with the session.
164     * 
165     * This model provides:
166     *  * The `locale` (of type {@link Locale}).
167     *  * The `resourceBundle` (of type {@link ResourceBundle}).
168     *  * A function `_` that looks up the given key in the portlet's
169     *    resource bundle.
170     *    
171     * @param session the session
172     * @return the model
173     */
174    protected Map<String, Object> fmSessionModel(Session session) {
175        @SuppressWarnings("PMD.UseConcurrentHashMap")
176        final Map<String, Object> model = new HashMap<>();
177        Locale locale = session.locale();
178        model.put("locale", locale);
179        final ResourceBundle resourceBundle = resourceBundle(locale);
180        model.put("resourceBundle", resourceBundle);
181        model.put("_", new TemplateMethodModelEx() {
182            @Override
183            public Object exec(@SuppressWarnings("rawtypes") List arguments)
184                    throws TemplateModelException {
185                @SuppressWarnings("unchecked")
186                List<TemplateModel> args = (List<TemplateModel>) arguments;
187                if (!(args.get(0) instanceof SimpleScalar)) {
188                    throw new TemplateModelException("Not a string.");
189                }
190                String key = ((SimpleScalar) args.get(0)).getAsString();
191                try {
192                    return resourceBundle.getString(key);
193                } catch (MissingResourceException e) { // NOPMD
194                    // no luck
195                }
196                return key;
197            }
198        });
199        return model;
200    }
201
202    /**
203     * Build a freemarker model for the current request.
204     * 
205     * This model provides:
206     *  * The `event` property (of type {@link RenderPortletRequest}).
207     *  * The `portlet` property (of type {@link PortletBaseModel}).
208     *    
209     * @param event the event
210     * @param channel the channel
211     * @param portletModel the portlet model
212     * @return the model
213     */
214    protected Map<String, Object> fmPortletModel(
215            RenderPortletRequestBase<?> event,
216            IOSubchannel channel, PortletBaseModel portletModel) {
217        @SuppressWarnings("PMD.UseConcurrentHashMap")
218        final Map<String, Object> model = new HashMap<>();
219        model.put("event", event);
220        model.put("portlet", portletModel);
221        return model;
222    }
223
224    /**
225     * Build a freemarker model that combines {@link #fmTypeModel},
226     * {@link #fmSessionModel} and {@link #fmPortletModel}.
227     * 
228     * @param event the event
229     * @param channel the channel
230     * @param portletModel the portlet model
231     * @return the model
232     */
233    protected Map<String, Object> fmModel(RenderPortletRequestBase<?> event,
234            PortalSession channel, PortletBaseModel portletModel) {
235        final Map<String, Object> model
236            = fmSessionModel(channel.browserSession());
237        model.putAll(fmTypeModel(event.renderSupport()));
238        model.putAll(fmPortletModel(event, channel, portletModel));
239        return model;
240    }
241
242    /**
243     * Checks if the path of the requested resource ends with
244     * `*.ftl.*`. If so, processes the template with the
245     * {@link #fmTypeModel(RenderSupport)} and 
246     * {@link #fmSessionModel(Session)} and
247     * sends the result. Else, invoke the super class' method. 
248     * 
249     * @param event the event. The result will be set to
250     * `true` on success
251     * @param channel the channel
252     */
253    @Override
254    protected void doGetResource(PortletResourceRequest event,
255            IOSubchannel channel) {
256        if (!templatePattern.matcher(event.resourceUri().getPath()).matches()) {
257            super.doGetResource(event, channel);
258            return;
259        }
260        try {
261            // Prepare template
262            final Template tpl = freemarkerConfig().getTemplate(
263                event.resourceUri().getPath());
264            Map<String, Object> model = fmSessionModel(event.session());
265            model.putAll(fmTypeModel(event.renderSupport()));
266
267            // Everything successfully prepared
268            event.setResult(new ResourceByGenerator(event,
269                new ResourceByGenerator.Generator() {
270                    @Override
271                    public void write(OutputStream stream) throws IOException {
272                        try {
273                            tpl.process(model, new OutputStreamWriter(
274                                stream, "utf-8"));
275                        } catch (TemplateException e) {
276                            throw new IOException(e);
277                        }
278                    }
279                },
280                HttpResponse.contentType(event.resourceUri()),
281                Instant.now(), 0));
282            event.stop();
283        } catch (IOException e) { // NOPMD
284            throw new IllegalArgumentException(e);
285        }
286    }
287
288    /**
289     * Specifies how to render portlet content using a template.
290     */
291    public static class RenderPortletFromTemplate extends RenderPortlet {
292
293        private final Future<String> content;
294
295        /**
296         * Instantiates a new event.
297         *
298         * @param request the request
299         * @param portletClass the portlet class
300         * @param portletId the portlet id
301         * @param template the template
302         * @param dataModel the data model
303         */
304        public RenderPortletFromTemplate(RenderPortletRequestBase<?> request,
305                Class<?> portletClass, String portletId, Template template,
306                Object dataModel) {
307            super(portletClass, portletId);
308            setRenderMode(request.renderMode());
309            // Start to prepare the content immediately and concurrently.
310            content
311                = request.processedBy().map(procBy -> procBy.executorService())
312                    .orElse(Components.defaultExecutorService()).submit(() -> {
313                        StringWriter out = new StringWriter();
314                        try {
315                            template.process(dataModel, out);
316                        } catch (TemplateException | IOException e) {
317                            throw new IllegalArgumentException(e);
318                        }
319                        return out.toString();
320
321                    });
322        }
323
324        @Override
325        public Future<String> content() {
326            return content;
327        }
328    }
329}