001/*
002 * Copyright (c) 2013 Nu Echo Inc. All rights reserved.
003 */
004
005package com.nuecho.rivr.voicexml.servlet;
006
007import java.io.*;
008import java.security.*;
009import java.util.*;
010
011import javax.servlet.*;
012import javax.servlet.http.*;
013
014import org.slf4j.*;
015import org.w3c.dom.*;
016
017import com.nuecho.rivr.core.dialogue.*;
018import com.nuecho.rivr.core.servlet.*;
019import com.nuecho.rivr.core.servlet.session.*;
020import com.nuecho.rivr.core.util.*;
021import com.nuecho.rivr.voicexml.dialogue.*;
022import com.nuecho.rivr.voicexml.rendering.json.*;
023import com.nuecho.rivr.voicexml.rendering.voicexml.*;
024import com.nuecho.rivr.voicexml.turn.*;
025import com.nuecho.rivr.voicexml.turn.first.*;
026import com.nuecho.rivr.voicexml.turn.input.*;
027import com.nuecho.rivr.voicexml.turn.last.*;
028import com.nuecho.rivr.voicexml.turn.output.*;
029import com.nuecho.rivr.voicexml.util.*;
030
031/**
032 * Implementation of the {@link DialogueServlet} specialized for VoiceXML. This
033 * servlet handles requests from the VoiceXML platform and responds with
034 * VoiceXML documents. It also intercepts special resources (
035 * <code>/script</code> and <code>/root</code>).
036 * <h3>init args</h3>
037 * <p> The following servlet initial arguments are supported:
038 * <dl>
039 * <dt>com.nuecho.rivr.voicexml.errorHandler.class</dt>
040 * <dd>Class name of the error handlder. This class must implements
041 * {@link VoiceXmlErrorHandler}, be public and non-abstract and have a public
042 * no-argument constructor. Default:
043 * <code>com.nuecho.rivr.voicexml.servlet.DefaultErrorHandler</code></dd>
044 * <dt>com.nuecho.rivr.voicexml.errorHandler.key</dt>
045 * <dd>As an alternative to
046 * <code>com.nuecho.rivr.voicexml.errorHandler.class</code>, this indicates the
047 * servlet context attribute name under which the {@link VoiceXmlErrorHandler}
048 * can be found. Default: (none: an instance of
049 * <code>com.nuecho.rivr.voicexml.servlet.DefaultErrorHandler</code> is used)</dd>
050 * <dt>com.nuecho.rivr.voicexml.dialogueFactory.class</dt>
051 * <dd>Class name of the dialogue factory. This class must implements
052 * {@link VoiceXmlDialogueFactory}, be public and non-abstract and have a public
053 * no-argument constructor. Default: (none).</dd>
054 * <dt>com.nuecho.rivr.voicexml.dialogueFactory.key</dt>
055 * <dd>As an alternative to
056 * <code>com.nuecho.rivr.voicexml.dialogueFactory.class</code>, this indicates
057 * the servlet context attribute name under which the
058 * {@link VoiceXmlDialogueFactory} can be found. Default: (none)</dd>
059 * <dt>com.nuecho.rivr.voicexml.dialogue.class</dt>
060 * <dd>If neither <code>com.nuecho.rivr.voicexml.dialogueFactory.class</code>
061 * nor <code>com.nuecho.rivr.voicexml.dialogueFactory.key</code> is specified,
062 * this specifies the class name of the dialogue. This class must implements
063 * {@link VoiceXmlDialogue}, be public and non-abstract and have a public
064 * no-argument constructor. Default: (none).</dd>
065 * <dt>com.nuecho.rivr.voicexml.dialogue.key</dt>
066 * <dd>If neither <code>com.nuecho.rivr.voicexml.dialogueFactory.class</code>,
067 * <code>com.nuecho.rivr.voicexml.dialogueFactory.key</code> nor
068 * <code>com.nuecho.rivr.voicexml.dialogue.class</code> is specified, this
069 * specifies the servlet context attribute name under which the
070 * {@link VoiceXmlDialogue} can be found. Default: (none)</dd>
071 * <dt>com.nuecho.rivr.voicexml.loggerFactory.class</dt>
072 * <dd>Class name of the dialogue factory. This class must implements
073 * {@link LoggerFactory org.slf4j.LoggerFactory}, be public and non-abstract and
074 * have a public no-argument constructor. Default: (none:
075 * {@link org.slf4j.LoggerFactory#getILoggerFactory()} is used as the logger
076 * factory).</dd>
077 * <dt>com.nuecho.rivr.voicexml.loggerFactory.key</dt>
078 * <dd>As an alternative to
079 * <code>com.nuecho.rivr.voicexml.loggerFactory.class</code>, this indicates the
080 * servlet context attribute name under which the {@link LoggerFactory
081 * org.slf4j.LoggerFactory} can be found. Default: (none:
082 * {@link org.slf4j.LoggerFactory#getILoggerFactory()} is used as the logger
083 * factory).</dd>
084 * </dl>
085 * <p>
086 * <b>Important:</b> one of the following must be specified, they are mutually
087 * exclusive:
088 * <ul>
089 * <li>com.nuecho.rivr.voicexml.dialogueFactory.class</li>
090 * <li>com.nuecho.rivr.voicexml.dialogueFactory.key</li>
091 * <li>com.nuecho.rivr.voicexml.dialogue.class</li>
092 * <li>com.nuecho.rivr.voicexml.dialogue.key</li>
093 * </ul>
094 * 
095 * @author Nu Echo Inc.
096 */
097public class VoiceXmlDialogueServlet
098        extends
099        DialogueServlet<VoiceXmlInputTurn, VoiceXmlOutputTurn, VoiceXmlFirstTurn, VoiceXmlLastTurn, VoiceXmlDialogueContext> {
100
101    private static final long serialVersionUID = 1L;
102
103    private static final String INITIAL_ARGUMENT_PREFIX = "com.nuecho.rivr.voicexml.";
104    private static final String INITIAL_ARGUMENT_ERROR_HANDLER = INITIAL_ARGUMENT_PREFIX + "errorHandler";
105    private static final String INITIAL_ARGUMENT_DIALOGUE_FACTORY = INITIAL_ARGUMENT_PREFIX + "dialogueFactory";
106    private static final String INITIAL_ARGUMENT_DIALOGUE = INITIAL_ARGUMENT_PREFIX + "dialogue";
107    private static final String INITIAL_ARGUMENT_LOGGER_FACTORY = INITIAL_ARGUMENT_PREFIX + "loggerFactory";
108
109    public static final String ROOT_PATH = "/root/";
110    public static final String RIVR_SCRIPT = "/scripts/rivr.js";
111
112    private static final String IF_NONE_MATCH = "If-None-Match";
113    private static final String ETAG = "ETag";
114
115    private VoiceXmlStepRenderer mVoiceXmlStepRenderer;
116    private JsonStepRenderer mJsonStepRenderer;
117
118    public static final String VOICE_XML_CONTENT_TYPE = "application/voicexml+xml";
119    public static final String JAVASCRIPT_CONTENT_TYPE = "application/javascript";
120
121    private static final String ACCEPT_HEADER = "Accept";
122
123    private VoiceXmlRootDocumentFactory mRootDocumentFactory = new DefaultVoiceXmlRootDocumentFactory();
124
125    private List<? extends VoiceXmlDocumentAdapter> mVoiceXmlDocumentAdapters;
126
127    protected void initializeVoiceXmlDialogueServlet() {}
128
129    @Override
130    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
131        String pathInfo = request.getPathInfo();
132
133        if (pathInfo != null) {
134            if (pathInfo.startsWith(ROOT_PATH)) {
135                processRootDocument(request, response);
136                return;
137            }
138
139            if (RIVR_SCRIPT.equals(pathInfo)) {
140                processRessource(request, response, pathInfo);
141                return;
142            }
143        }
144
145        super.doGet(request, response);
146    }
147
148    public void setRootDocumentFactory(VoiceXmlRootDocumentFactory rootDocumentFactory) {
149        Assert.notNull(rootDocumentFactory, "rootDocumentFactory");
150        mRootDocumentFactory = rootDocumentFactory;
151    }
152
153    @Override
154    protected final void initDialogueServlet() throws DialogueServletInitializationException {
155        setInputTurnFactory(new VoiceXmlInputTurnFactory());
156        setDialogueContextFactory(new VoiceXmlDialogueContextFactory());
157        setErrorHandler(new DefaultErrorHandler());
158        initializeProperties();
159        initializeVoiceXmlDialogueServlet();
160
161        mVoiceXmlStepRenderer = new VoiceXmlStepRenderer(mVoiceXmlDocumentAdapters);
162        mJsonStepRenderer = new JsonStepRenderer(mVoiceXmlStepRenderer);
163    }
164
165    @Override
166    protected void destroyDialogueServlet() {}
167
168    public void setVoiceXmlDocumentAdapters(List<VoiceXmlDocumentAdapter> voiceXmlDocumentAdapters) {
169        mVoiceXmlDocumentAdapters = voiceXmlDocumentAdapters;
170    }
171
172    private void initializeProperties() throws DialogueServletInitializationException {
173
174        ILoggerFactory loggerFactory = find(INITIAL_ARGUMENT_LOGGER_FACTORY, ILoggerFactory.class);
175        if (loggerFactory != null) {
176            setLoggerFactory(loggerFactory);
177        }
178
179        VoiceXmlDialogueFactory dialogueFactory = getDialogueFactory();
180        if (dialogueFactory != null) {
181            setDialogueFactory(dialogueFactory);
182        } else {
183            setImplicitDialogueFactory();
184        }
185
186        VoiceXmlErrorHandler errorHandler = find(INITIAL_ARGUMENT_ERROR_HANDLER, VoiceXmlErrorHandler.class);
187        if (errorHandler != null) {
188            setErrorHandler(errorHandler);
189        }
190
191    }
192
193    private void setImplicitDialogueFactory() throws DialogueServletInitializationException {
194        ServletConfig servletConfig = getServletConfig();
195        String className = servletConfig.getInitParameter(INITIAL_ARGUMENT_DIALOGUE + ".class");
196        String key = servletConfig.getInitParameter(INITIAL_ARGUMENT_DIALOGUE + ".key");
197
198        if (className != null) {
199            try {
200                Class<?> rawDialogueClass = Class.forName(className);
201                if (!VoiceXmlDialogue.class.isAssignableFrom(rawDialogueClass)) {
202                    String message = "Dialogue class "
203                                     + rawDialogueClass
204                                     + " does not implements "
205                                     + VoiceXmlDialogue.class.getName()
206                                     + ".";
207                    throw new DialogueServletInitializationException(message);
208                }
209
210                @SuppressWarnings("unchecked")
211                Class<? extends VoiceXmlDialogue> dialogueClass = (Class<? extends VoiceXmlDialogue>) rawDialogueClass;
212                setDialogueFactory(new SimpleVoiceXmlDialogueFactory(dialogueClass));
213            } catch (ClassNotFoundException exception) {
214                throw new DialogueServletInitializationException("Cannot find dialogue class.", exception);
215            } catch (DialogueFactoryException exception) {
216                throw new DialogueServletInitializationException("Cannot initialize dialogue factory.", exception);
217            }
218        } else if (key != null) {
219            VoiceXmlDialogue dialogue = findInServletContext(key, VoiceXmlDialogue.class, key);
220            setDialogueFactory(new SimpleVoiceXmlDialogueFactory(dialogue));
221        }
222    }
223
224    private <T> T find(String prefix, Class<T> type) throws DialogueServletInitializationException {
225        T object = null;
226        ServletConfig servletConfig = getServletConfig();
227        String className = servletConfig.getInitParameter(prefix + ".class");
228        if (className != null) {
229            object = instantiate(className, type, prefix);
230        }
231
232        String key = servletConfig.getInitParameter(prefix + ".key");
233        if (key != null) {
234            object = findInServletContext(key, type, prefix);
235        }
236        return object;
237    }
238
239    protected VoiceXmlDialogueFactory getDialogueFactory() throws DialogueServletInitializationException {
240        return find(INITIAL_ARGUMENT_DIALOGUE_FACTORY, VoiceXmlDialogueFactory.class);
241    }
242
243    private <T> T findInServletContext(String servletContextKey, Class<T> type, String item)
244            throws DialogueServletInitializationException {
245        Object object = getServletContext().getAttribute(servletContextKey);
246
247        if (object == null)
248            throw new DialogueServletInitializationException("Cannot find "
249                                                             + item
250                                                             + " with name ["
251                                                             + servletContextKey
252                                                             + "] in servlet context.");
253
254        if (!(type.isInstance(object)))
255            throw new DialogueServletInitializationException("Servlet context object ["
256                                                             + servletContextKey
257                                                             + "] does not implements "
258                                                             + type.getName()
259                                                             + ". Actual class is "
260                                                             + object.getClass().getName());
261        return type.cast(object);
262    }
263
264    private <T> T instantiate(String className, Class<T> type, String item)
265            throws DialogueServletInitializationException {
266        try {
267            Class<?> classObject = Class.forName(className);
268            if (!type.isAssignableFrom(classObject))
269                throw new DialogueServletInitializationException("Incompatible class type.");
270            return type.cast(classObject.newInstance());
271        } catch (ClassNotFoundException exception) {
272            throw new DialogueServletInitializationException("Cannot find " + item + " class '" + className + "'",
273                                                             exception);
274        } catch (InstantiationException exception) {
275            throw new DialogueServletInitializationException("Cannot instantiate "
276                                                             + item
277                                                             + " of class '"
278                                                             + className
279                                                             + "'", exception);
280        } catch (IllegalAccessException exception) {
281            throw new DialogueServletInitializationException("Cannot access " + item + " class '" + className + "'",
282                                                             exception);
283        }
284    }
285
286    @Override
287    protected StepRenderer<VoiceXmlInputTurn, VoiceXmlOutputTurn, VoiceXmlLastTurn, VoiceXmlDialogueContext> getStepRenderer(HttpServletRequest request,
288                                                                                                                             Session<VoiceXmlInputTurn, VoiceXmlOutputTurn, VoiceXmlFirstTurn, VoiceXmlLastTurn, VoiceXmlDialogueContext> session) {
289        String acceptHeader = request.getHeader(ACCEPT_HEADER);
290        if (acceptHeader == null) return mVoiceXmlStepRenderer;
291        if (acceptHeader.indexOf("application/json") != -1) return mJsonStepRenderer;
292        if (acceptHeader.indexOf("application/javascript") != -1) return mJsonStepRenderer;
293        if (acceptHeader.indexOf("application/voicexml+xml") != -1) return mVoiceXmlStepRenderer;
294        return mVoiceXmlStepRenderer;
295    }
296
297    private void processRessource(HttpServletRequest request, HttpServletResponse response, String pathInfo)
298            throws IOException, ServletException {
299        InputStream inputStream = VoiceXmlDialogueServlet.class.getResourceAsStream(pathInfo.substring(1));
300        byte[] bytes = IOUtils.toByteArray(inputStream);
301        String eTag = getETag(bytes);
302        String ifNoneMatch = request.getHeader(IF_NONE_MATCH);
303
304        if (eTag.equals(ifNoneMatch)) {
305            response.sendError(HttpServletResponse.SC_NOT_MODIFIED, "Not Modified");
306        } else {
307            response.setContentType(JAVASCRIPT_CONTENT_TYPE);
308            response.addHeader(ETAG, eTag);
309            response.getOutputStream().write(bytes);
310        }
311    }
312
313    private void processRootDocument(HttpServletRequest request, HttpServletResponse response) throws ServletException,
314            IOException {
315
316        try {
317            Document rootDocument = mRootDocumentFactory.getDocument(request);
318            response.setContentType(VOICE_XML_CONTENT_TYPE);
319            DomUtils.writeToOutputStream(rootDocument, response.getOutputStream(), Encoding.UTF_8);
320        } catch (VoiceXmlDocumentRenderingException exception) {
321            throw new ServletException("Error while rendering root document.", exception);
322        }
323    }
324
325    private String getETag(byte[] bytes) throws ServletException {
326        try {
327            byte[] digest = MessageDigest.getInstance("MD5").digest(bytes);
328            return StringUtils.bytesToHex(digest);
329        } catch (NoSuchAlgorithmException exception) {
330            throw new ServletException("Could not create message digest.", exception);
331        }
332    }
333
334}