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.util.*;
009import java.util.regex.*;
010
011import javax.json.*;
012import javax.servlet.*;
013import javax.servlet.http.*;
014
015import org.apache.commons.fileupload.*;
016import org.apache.commons.fileupload.servlet.*;
017
018import com.nuecho.rivr.core.servlet.*;
019import com.nuecho.rivr.core.util.*;
020import com.nuecho.rivr.voicexml.turn.first.*;
021import com.nuecho.rivr.voicexml.turn.input.*;
022import com.nuecho.rivr.voicexml.util.*;
023import com.nuecho.rivr.voicexml.util.json.*;
024
025/**
026 * VoiceXML specialization of {@link InputTurnFactory}.
027 * 
028 * @author Nu Echo Inc.
029 */
030public final class VoiceXmlInputTurnFactory implements InputTurnFactory<VoiceXmlInputTurn, VoiceXmlFirstTurn> {
031    public static final String INPUT_TURN_PARAMETER = "inputTurn";
032    public static final String ROOT_PATH = "/root/";
033
034    //general
035
036    //recording-related
037    public static final String RECORDING_PARAMETER = "recording";
038    public static final String RECORDING_META_DATA_PROPERTY = "recordingMetaData";
039    public static final String TERM_CHAR_PROPERTY = "termChar";
040    public static final String MAX_TIME_PROPERTY = "maxTime";
041    public static final String DURATION_PROPERTY = "duration";
042
043    //event-related
044    public static final String EVENTS_PROPERTY = "events";
045    public static final String EVENT_NAME_PROPERTY = "name";
046    public static final String EVENT_MESSAGE_PROPERTY = "message";
047
048    //recognition-related
049    public static final String DTMF_INPUTMODE_VALUE = "dtmf";
050    public static final String VOICE_INPUTMODE_VALUE = "voice";
051    public static final String INPUT_MODE_PROPERTY = "inputMode";
052    public static final String MARK_PROPERTY = "mark";
053    public static final String MARK_NAME_PROPERTY = "name";
054    public static final String MARK_TIME_PROPERTY = "time";
055    public static final String RECOGNITION_PROPERTY = "recognition";
056    public static final String RESULT_PROPERTY = "result";
057
058    //transfer-related
059    public static final String TRANSFER_PROPERTY = "transfer";
060    public static final String TRANSFER_DURATION_PROPERTY = "duration";
061    public static final String TRANSFER_STATUS_PROPERTY = "status";
062
063    //for subdialogue, script and object
064    public static final String VALUE_PROPERTY = "value";
065
066    private static final Pattern CHAR_SET_PATTERN = Pattern.compile("charset\\s*=\\s*([^ ;]+)");
067
068    @Override
069    public VoiceXmlFirstTurn createFirstTurn(HttpServletRequest request, HttpServletResponse response)
070            throws InputTurnFactoryException {
071
072        Map<String, String> parameters = new HashMap<String, String>();
073
074        @SuppressWarnings("unchecked")
075        Enumeration<String> parameterNames = request.getParameterNames();
076        while (parameterNames.hasMoreElements()) {
077            String parameterName = parameterNames.nextElement();
078            parameters.put(parameterName, request.getParameter(parameterName));
079        }
080
081        return new VoiceXmlFirstTurn(parameters);
082    }
083
084    @Override
085    public VoiceXmlInputTurn createInputTurn(HttpServletRequest request, HttpServletResponse response)
086            throws InputTurnFactoryException {
087
088        Map<String, String> parameters = new HashMap<String, String>();
089        Map<String, FileUpload> files = new HashMap<String, FileUpload>();
090        processRequestParametersAndFiles(request, parameters, files);
091
092        String result = parameters.get(INPUT_TURN_PARAMETER);
093
094        if (result == null)
095            throw new InputTurnFactoryException("Unable to process request. Missing '"
096                                                + INPUT_TURN_PARAMETER
097                                                + "' parameter.");
098
099        JsonReader jsonReader = JsonUtils.createReader(result);
100        JsonObject resultObject = jsonReader.readObject();
101        VoiceXmlInputTurn voiceXmlInputTurn = new VoiceXmlInputTurn();
102        voiceXmlInputTurn.setFiles(files);
103
104        addEvents(resultObject, voiceXmlInputTurn);
105        addObject(resultObject, voiceXmlInputTurn);
106        addTransferStatusInfo(resultObject, voiceXmlInputTurn);
107        addRecognitionInfo(resultObject, voiceXmlInputTurn);
108        addRecordingInfo(resultObject, voiceXmlInputTurn, files);
109        return voiceXmlInputTurn;
110    }
111
112    private static void addEvents(JsonObject resultObject, VoiceXmlInputTurn voiceXmlInputTurn) {
113        if (!resultObject.containsKey(EVENTS_PROPERTY)) return;
114
115        List<JsonValue> eventObjects = resultObject.getJsonArray(EVENTS_PROPERTY);
116        List<VoiceXmlEvent> events = new ArrayList<VoiceXmlEvent>();
117        for (JsonValue jsonValue : eventObjects) {
118            JsonObject eventObject = (JsonObject) jsonValue;
119
120            String name = eventObject.getString(EVENT_NAME_PROPERTY);
121            String message = eventObject.getString(EVENT_MESSAGE_PROPERTY, null);
122
123            events.add(new VoiceXmlEvent(name, message));
124        }
125
126        voiceXmlInputTurn.setEvents(events);
127    }
128
129    private static void addObject(JsonObject resultObject, VoiceXmlInputTurn voiceXmlInputTurn) {
130        if (!resultObject.containsKey(VALUE_PROPERTY)) return;
131
132        JsonValue subdialogueResultJsonValue = resultObject.get(VALUE_PROPERTY);
133        voiceXmlInputTurn.setJsonValue(subdialogueResultJsonValue);
134    }
135
136    private static void addTransferStatusInfo(JsonObject resultObject, VoiceXmlInputTurn voiceXmlInputTurn) {
137        if (!resultObject.containsKey(TRANSFER_PROPERTY)) return;
138
139        JsonObject transferResultObject = resultObject.getJsonObject(TRANSFER_PROPERTY);
140
141        TransferStatus transferStatus = new TransferStatus(transferResultObject.getString(TRANSFER_STATUS_PROPERTY));
142
143        long durationValue;
144
145        if (transferResultObject.containsKey(TRANSFER_DURATION_PROPERTY)) {
146            durationValue = JsonUtils.getLongProperty(transferResultObject, TRANSFER_DURATION_PROPERTY);
147        } else {
148            durationValue = 0;
149        }
150
151        Duration duration = Duration.milliseconds(durationValue);
152
153        voiceXmlInputTurn.setTransferResult(new TransferStatusInfo(transferStatus, duration));
154    }
155
156    private static void addRecordingInfo(JsonObject resultObject,
157                                         VoiceXmlInputTurn voiceXmlInputTurn,
158                                         Map<String, FileUpload> files) {
159        if (!resultObject.containsKey(RECORDING_META_DATA_PROPERTY)) return;
160
161        JsonObject recordingMetaData = resultObject.getJsonObject(RECORDING_META_DATA_PROPERTY);
162
163        Duration duration;
164        if (recordingMetaData.containsKey(DURATION_PROPERTY)) {
165            long durationInMilliseconds = JsonUtils.getLongProperty(recordingMetaData, DURATION_PROPERTY);
166            duration = Duration.milliseconds(durationInMilliseconds);
167        } else {
168            duration = null;
169        }
170
171        boolean maxTime = recordingMetaData.getBoolean(MAX_TIME_PROPERTY, false);
172
173        String dtmfTermChar = recordingMetaData.getString(TERM_CHAR_PROPERTY, null);
174
175        FileUpload file;
176        if (!files.containsKey(RECORDING_PARAMETER)) {
177            file = null;
178        } else {
179            file = files.get(RECORDING_PARAMETER);
180        }
181
182        voiceXmlInputTurn.setRecordingInfo(new RecordingInfo(file, duration, maxTime, dtmfTermChar));
183    }
184
185    private void addRecognitionInfo(JsonObject jsonObject, VoiceXmlInputTurn voiceXmlInputTurn) {
186
187        if (!jsonObject.containsKey(RECOGNITION_PROPERTY)) return;
188
189        JsonObject recognitionObject = jsonObject.getJsonObject(RECOGNITION_PROPERTY);
190
191        JsonArray recognitionResultArray = recognitionObject.getJsonArray(RESULT_PROPERTY);
192
193        MarkInfo markInfo = null;
194        if (recognitionObject.containsKey(MARK_PROPERTY)) {
195            JsonObject markObject = recognitionObject.getJsonObject(MARK_PROPERTY);
196            long timeInMilliseconds = JsonUtils.getLongProperty(markObject, MARK_TIME_PROPERTY);
197            markInfo = new MarkInfo(markObject.getString(MARK_NAME_PROPERTY), Duration.milliseconds(timeInMilliseconds));
198        }
199
200        voiceXmlInputTurn.setRecognitionInfo(new RecognitionInfo(recognitionResultArray, markInfo));
201    }
202
203    private void processRequestParametersAndFiles(HttpServletRequest request,
204                                                  Map<String, String> parameters,
205                                                  Map<String, FileUpload> files) throws InputTurnFactoryException {
206        if (ServletFileUpload.isMultipartContent(request)) {
207            ServletFileUpload servletFileUpload = new ServletFileUpload();
208            try {
209                FileItemIterator itemIterator = servletFileUpload.getItemIterator(request);
210                while (itemIterator.hasNext()) {
211                    FileItemStream fileItemStream = itemIterator.next();
212
213                    byte[] bytes;
214                    String parameterName = fileItemStream.getFieldName();
215                    try {
216                        InputStream stream = fileItemStream.openStream();
217                        bytes = IOUtils.toByteArray(stream);
218                    } catch (IOException exception) {
219                        throw new ServletException("Unable to read stream from " + parameterName, exception);
220                    }
221
222                    if (!fileItemStream.isFormField()) {
223                        FileUpload fileUpload = new FileUpload(fileItemStream.getName(),
224                                                               fileItemStream.getContentType(),
225                                                               bytes,
226                                                               getHeaders(fileItemStream));
227
228                        files.put(parameterName, fileUpload);
229                    } else {
230                        String encoding = findEncoding(request, fileItemStream);
231                        try {
232                            parameters.put(parameterName, new String(bytes, encoding));
233                        } catch (UnsupportedEncodingException exception) {
234                            throw new Exception("Unable to find encoding '"
235                                                + encoding
236                                                + "'. Cannot decode text form field '"
237                                                + parameterName
238                                                + "' of multipart request.", exception);
239                        }
240                    }
241
242                }
243            } catch (Exception exception) {
244                throw new InputTurnFactoryException("Unable to get recording.", exception);
245            }
246        } else {
247            @SuppressWarnings("unchecked")
248            Enumeration<String> parameterNames = request.getParameterNames();
249            while (parameterNames.hasMoreElements()) {
250                String parameterName = parameterNames.nextElement();
251                parameters.put(parameterName, request.getParameter(parameterName));
252            }
253        }
254
255    }
256
257    private String findEncoding(HttpServletRequest request, FileItemStream fileItemStream) {
258
259        String encoding = null;
260
261        String contentType = fileItemStream.getContentType();
262
263        if (contentType != null && contentType.startsWith("text/plain")) {
264            Matcher matcher = CHAR_SET_PATTERN.matcher(contentType);
265            if (matcher.find()) {
266                encoding = matcher.group(1);
267            } else {
268                encoding = Encoding.US_ASCII.getId();
269            }
270        }
271
272        if (encoding == null) {
273            encoding = request.getCharacterEncoding();
274        }
275
276        if (encoding == null) {
277            encoding = Encoding.US_ASCII.getId();
278        }
279
280        return encoding;
281    }
282
283    private Map<String, String> getHeaders(FileItemStream fileItemStream) {
284        Map<String, String> headers = new HashMap<String, String>();
285        FileItemHeaders sourceHeaders = fileItemStream.getHeaders();
286
287        if (sourceHeaders == null) return headers;
288
289        @SuppressWarnings("unchecked")
290        Iterator<String> headerNames = sourceHeaders.getHeaderNames();
291        while (headerNames.hasNext()) {
292            String headerName = headerNames.next();
293            headers.put(headerName, sourceHeaders.getHeader(headerName));
294        }
295        return headers;
296    }
297}