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}