-
Notifications
You must be signed in to change notification settings - Fork 25
/
Request.java
696 lines (627 loc) · 22.2 KB
/
Request.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
/*******************************************************************************
* Copyright 2012 The Regents of the University of California
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package org.ohmage.request;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.ohmage.annotator.Annotator;
import org.ohmage.annotator.Annotator.ErrorCode;
import org.ohmage.exception.InvalidRequestException;
import org.ohmage.exception.ValidationException;
import org.ohmage.jee.filter.GzipFilter;
import org.ohmage.jee.servlet.RequestServlet;
import org.springframework.util.CollectionUtils;
/**
* Superclass for all requests. Defines the basic requirements for a request.
*
* @author John Jenkins
* @author Joshua Selsky
*/
public abstract class Request {
private static final Logger LOGGER = Logger.getLogger(Request.class);
/**
* The key to use when responding with a JSONObject about whether the
* request was a success or failure.
*/
public static final String JSON_KEY_RESULT = "result";
/**
* The value to use for the {@link #JSON_KEY_RESULT} when the request is
* successful.
*/
public static final String RESULT_SUCCESS = "success";
/**
* The value to use for the {@link #JSON_KEY_RESULT} when the request has
* failed.
*/
public static final String RESULT_FAILURE = "failure";
/**
* The key to use when responding with a JSONObject where the request is
* successful. The value associated with this key is the requested data.
*/
public static final String JSON_KEY_DATA = "data";
/**
* The metadata associated with the response.
*/
public static final String JSON_KEY_METADATA = "metadata";
/**
* The key to use when responding with a JSONOBject where the request has
* failed. The value associated with this key is the error code and error
* text describing why this request failed.
*/
public static final String JSON_KEY_ERRORS = "errors";
/**
* The JSON key for the metadata that describes how many total results
* exist that match the criteria as opposed to the number being
* returned, which may be different due to paging or aggregation.
*/
public static final String JSON_KEY_TOTAL_NUM_RESULTS =
"total_num_results";
/**
* A hard-coded JSONObject which represents a successful result.
*/
public static final String RESPONSE_SUCCESS_JSON_TEXT =
"{\"" + JSON_KEY_RESULT + "\":\"" + RESULT_SUCCESS + "\"}";
/**
* A hard-coded JSONObject with which to respond in the event that the
* JSONObject that is the response cannot be built.
*/
public static final String RESPONSE_ERROR_JSON_TEXT =
"{\"" + JSON_KEY_RESULT + "\":\"" + RESULT_FAILURE + "\"," +
"\"" + JSON_KEY_ERRORS + "\":[" +
"{\"" + Annotator.JSON_KEY_CODE + "\":\"0103\"," +
"\"" + Annotator.JSON_KEY_TEXT + "\":\"An error occurred while building the JSON response.\"}" +
"]}";
private static final String KEY_AUDIT_REQUESTER_INTERNET_ADDRESS =
"requester_inet_addr";
/**
* The value our Android app uses when setting the client parameter for
* each request.
*/
public static final String ANDROID_CLIENT_NAME = "ohmage-android";
private final Annotator annotator;
private boolean failed;
private final Map<String, String[]> parameters;
private final String requesterInetAddr;
/**
* Initializes this request.
*
* @param httpRequest An HttpServletRequest that was used to create this
* request. This may be null if no such request exists.
*
* @param parameters The parameters for this request. If this is null, the
* parameters are decoded from the HTTP request.
* Otherwise, the parameters in this map are used.
*
* @throws InvalidRequestException Thrown if the parameters cannot be
* parsed. This is only applicable in the
* event of the HTTP parameters being
* parsed.
*
* @throws IOException There was an error reading from the request.
*/
@SuppressWarnings("unchecked")
protected Request(
final HttpServletRequest httpRequest,
final Map<String, String[]> parameters)
throws IOException, InvalidRequestException {
annotator = new Annotator();
failed = false;
Map<String, String[]> tParameters = new HashMap<String, String[]>();
String tRequesterInetAddr = null;
try {
if(httpRequest != null) {
// Get the requester's IP address.
tRequesterInetAddr = httpRequest.getRemoteAddr();
// Get the parameters.
if (parameters == null) {
// LOGGER.debug("HT: parameters is null");
Object parametersObject =
httpRequest
.getAttribute(GzipFilter.ATTRIBUTE_KEY_PARAMETERS);
if(parametersObject instanceof Map) {
// We make the assumption that we are the only one
// setting this value, so it must be a map.
tParameters = (Map<String, String[]>) parametersObject;
// LOGGER.debug("HT: parametersObject is an instance of a map");
if (CollectionUtils.isEmpty(tParameters.entrySet())) {
try { // check whether the request/file is too large
httpRequest.getParts();
} catch (IllegalStateException e) {
LOGGER.info("The request body is larger than maxRequestSize:" +
RequestServlet.MAX_REQUEST_SIZE +
", or a part is larger than the maxFileSize:" +
RequestServlet.MAX_FILE_SIZE, e);
throw new InvalidRequestException(ErrorCode.SYSTEM_REQUEST_TOO_LARGE ,
"The file/request is too large. "
+ "Maximum file size:" + RequestServlet.MAX_FILE_SIZE
+ ". Maximum request size: " + RequestServlet.MAX_REQUEST_SIZE + ".");
} catch (ServletException e) {}
}
}
else if(parametersObject == null) {
// LOGGER.debug("HT: parametersObject is null");
throw new ValidationException(
"The parameter map was never set which should have been done in the GZIP filter.");
}
else {
throw new ValidationException(
"The parameters object was not a map.");
}
}
else {
tParameters = parameters;
}
// HT iterates through the param map
//LOGGER.debug("HT: About to iterate through the param map");
for (Map.Entry<String,String[]> entry : tParameters.entrySet()) {
String key = entry.getKey();
String[] value = entry.getValue();
LOGGER.debug("HT:" + key + " : " + Arrays.toString(value));
}
}
}
catch(ValidationException e) {
e.failRequest(this);
e.logException(LOGGER);
throw new InvalidRequestException(ErrorCode.SYSTEM_GENERAL_ERROR, "Can't extract parameters from the request");
}
this.parameters = tParameters;
this.requesterInetAddr = tRequesterInetAddr;
}
/**
* @return Returns the parameters from the HTTP request.
*/
protected Map<String, String[]> getParameters() {
return parameters;
}
/**
* Returns whether or not this request has failed.
*
* @return Whether or not this request has failed.
*/
public boolean isFailed() {
return failed;
}
/**
* The annotator describing why this request has failed, if it has. If it
* hasn't failed or it has and a reason wasn't given, then a generic
* annotator will be returned.
*
* @return The annotator describing why this request failed or the generic
* annotator if this request hasn't failed or a reason was not
* given.
*/
public Annotator getAnnotator() {
return annotator;
}
/**
* Simply sets the request as failed without updating the error message.
*/
public void setFailed() {
failed = true;
}
/**
* Marks that the request has failed and updates its response with the
* given error code and error text.
*
* @param errorCode A four-character error code related to the error text.
*
* @param errorText The text to be returned to the user.
*/
public void setFailed(final ErrorCode errorCode, final String errorText) {
annotator.update(errorCode, errorText);
failed = true;
}
/**
* Returns a String representation of the failure message that would be
* returned to a user if this request has failed. All requests have a
* default failure message, so this will always return some error message;
* however, if the request has not yet failed, this result is meaningless.
*
* @return A String representation of the current failure message for this
* request.
*/
public String getFailureMessage() {
String result;
try {
// Use the annotator's message to build the response.
JSONObject resultJson = new JSONObject();
resultJson.put(JSON_KEY_RESULT, RESULT_FAILURE);
// FIXME: We no longer have multiple error messages per failed
// response, so we need to get rid of this unnecessary array.
JSONArray jsonArray = new JSONArray();
jsonArray.put(annotator.toJsonObject());
resultJson.put(JSON_KEY_ERRORS, jsonArray);
result = resultJson.toString();
}
catch(JSONException e) {
// If we can't even build the failure message, write a hand-
// written message as the response.
LOGGER.error("An error occurred while building the failure JSON response.", e);
result = RESPONSE_ERROR_JSON_TEXT;
}
return result;
}
/**
* Returns an unmodifiable version of the parameter map.
*
* @return An unmodifiable version of the parameter map.
*/
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(parameters);
}
/**
* Returns an array of all of the values from a parameter in the request.
*
* @param parameterKey The key to use to lookup the parameter value.
*
* @return An array of all values given for the parameter. The array may be
* empty, but will never be null.
*/
protected String[] getParameterValues(String parameterKey) {
if(parameterKey == null) {
return new String[0];
}
String[] result = parameters.get(parameterKey);
if(result == null) {
result = new String[0];
}
return result;
}
/**
* Returns the first value for some key from the parameter list. If there
* are no values for a key, null is returned.
*
* @param parameterKey The key to use to lookup a list of values, the first
* of which will be returned.
*
* @return Returns the first of a list of values for some key or null if no
* values exist for the key.
*/
protected String getParameter(String parameterKey) {
String[] values = getParameterValues(parameterKey);
if(values.length == 0) {
return null;
}
else {
return values[0];
}
}
/**
* Performs the operations for which this Request is responsible and
* aggregates any resulting data. This should be container agnostic. The
* specific constructors should gather the required information to perform
* this service and any results set by this function should be not be
* specific to any type of response generated by the container.
*/
public abstract void service();
/**
* Gathers an request-specific data that should be logged in the audit.
*/
public Map<String, String[]> getAuditInformation() {
Map<String, String[]> auditInfo = new HashMap<String, String[]>();
if(requesterInetAddr != null) {
auditInfo.put(
KEY_AUDIT_REQUESTER_INTERNET_ADDRESS,
new String[] { requesterInetAddr });
}
return auditInfo;
}
/**************************************************************************
* Begin JEE Requirements
*************************************************************************/
/**
* Writes a response to the request.
*
* @param httpRequest The initial HTTP request.
*
* @param httpResponse The HTTP response to this request.
*/
public abstract void respond(HttpServletRequest httpRequest, HttpServletResponse httpResponse);
/**
* Writes the response that is a JSONObject. This is a helper function for
* when {@link #respond(HttpServletRequest, HttpServletResponse)} is called
* given that most responses are in some sort of JSON format. The response
* is modified only add a "success" message and then is sent, unless the
* request fails in which case a failure message is sent instead of the
* response. This means that the key {@link #JSON_KEY_RESULT} is added to
* this JSONObject and any previous value associated with that key is
* removed.
*
* @param httpRequest The initial HTTP request that we are processing.
*
* @param httpResponse The response for this HTTP request.
*
* @param jsonResponse An already-constructed JSONObject that contains the
* 'data' portion of the object.
*/
protected void respond(
final HttpServletRequest httpRequest,
final HttpServletResponse httpResponse,
final JSONObject response) {
// Create a writer for the HTTP response object.
Writer writer = null;
String responseText = "";
try {
writer =
new BufferedWriter(
new OutputStreamWriter(
getOutputStream(
httpRequest,
httpResponse)));
// Sets the HTTP headers to disable caching.
expireResponse(httpResponse);
httpResponse.setContentType("application/json");
// If the response hasn't failed yet, attempt to create and write the
// JSON response.
if(! failed) {
try {
response.put(JSON_KEY_RESULT, RESULT_SUCCESS);
responseText = response.toString();
}
catch(JSONException e) {
// If anything fails, echo it in the logs and set the request
// as failed.
LOGGER.error("An error occurred while building the success JSON response.", e);
failed = true;
}
}
// If the request failed, either during the build or while the response
// was being built, write a failure message.
if(failed) {
responseText = getFailureMessage();
}
writer.write(responseText);
}
// SN: commenting as this exception is a subclass of IOException
// and the exception is tomcat-specific.
//catch(ClientAbortException e) {
// LOGGER.info("The client hung up unexpectedly.", e);
//}
catch(IOException e) {
LOGGER.error("Unable to write response message. Aborting.", e);
}
finally {
if(writer != null) {
try {
writer.close();
}
catch(IOException e) {
LOGGER.warn("Unable to close the writer.", e);
}
}
}
}
/**
* <p>
* Retrieves a parameter from either parts or the servlet container's
* deserialization.
* </p>
*
* <p>
* This supersedes {@link #getMultipartValue(HttpServletRequest, String)}.
* </p>
*
* @param httpRequest
* The HTTP request.
*
* @param key
* The parameter key.
*
* @return The parameter if given otherwise null.
*
* @throws ValidationException
* There was a problem reading from the request.
*/
protected byte[] getParameter(
final HttpServletRequest httpRequest,
final String key)
throws ValidationException {
// First, attempt to decode it as a multipart/form-data post.
try {
// Get the part. If it isn't a multipart/form-data post, an
// exception will be thrown. If it is and such a part does not
// exist, return null.
Part part = httpRequest.getPart(key);
if(part == null) {
return null;
}
// If the part exists, attempt to retrieve it.
InputStream partInputStream = part.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int amountRead;
while((amountRead = partInputStream.read(chunk)) != -1) {
outputStream.write(chunk, 0, amountRead);
}
// If the buffer is empty, return null. Otherwise, return the byte
// array.
if(outputStream.size() == 0) {
return null;
}
else {
return outputStream.toByteArray();
}
}
// This will be thrown if it isn't a multipart/form-post, at which
// point we can attempt to use the servlet container's deserialization
// of the parameters.
catch(ServletException e) {
// Get the parameter.
String result = httpRequest.getParameter(key);
// If it doesn't exist, return null.
if(result == null) {
return null;
}
// Otherwise, return its bytes.
else {
return result.getBytes();
}
}
// If we could not read a parameter, something more severe happened,
// and we need to fail the request and throw an exception.
catch(IOException e) {
LOGGER
.info(
"There was an error reading the message from the input " +
"stream.",
e);
setFailed();
throw new ValidationException(e);
}
// check for large request/file
catch(IllegalStateException e) {
LOGGER.info("The request body is larger than maxRequestSize:" +
RequestServlet.MAX_REQUEST_SIZE +
", or a part is larger than the maxFileSize:" +
RequestServlet.MAX_FILE_SIZE, e);
setFailed(ErrorCode.SERVER_REQUEST_TOO_LARGE,
"The request body is larger than maxRequestSize:" +
RequestServlet.MAX_REQUEST_SIZE +
", or a part is larger than the maxFileSize:" + RequestServlet.MAX_FILE_SIZE);
throw new ValidationException(e);
}
}
/**
* Reads the HttpServletRequest for a key-value pair and returns the value
* where the key is equal to the given key.
*
* @param httpRequest A "multipart/form-data" request that contains the
* parameter that has a key value 'key'.
*
* @param key The key for the value we are after in the 'httpRequest'.
*
* @return Returns null if there is no such key in the request or if,
* after reading the object, it has a length of 0. Otherwise, it
* returns the value associated with the key as a byte array.
*
* @throws ServletException Thrown if the 'httpRequest' is not a
* "multipart/form-data" request.
*
* @throws IOException Thrown if there is an error reading the value from
* the request's input stream.
*
* @throws IllegalStateException Thrown if the entire request is larger
* than the maximum allowed size for a
* request or if the value of the requested
* key is larger than the maximum allowed
* size for a single value.
*/
protected byte[] getMultipartValue(HttpServletRequest httpRequest, String key) throws ValidationException {
try {
Part part = httpRequest.getPart(key);
if(part == null) {
return null;
}
// Get the input stream.
InputStream partInputStream = part.getInputStream();
// Wrap the input stream in a GZIP de-compressor if it is GZIP'd.
String contentType = part.getContentType();
if((contentType != null) && contentType.contains("gzip")) {
LOGGER.info("Part was GZIP'd: " + key);
partInputStream = new GZIPInputStream(partInputStream);
}
// Parse the data.
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int amountRead;
while((amountRead = partInputStream.read(chunk)) != -1) {
outputStream.write(chunk, 0, amountRead);
}
if(outputStream.size() == 0) {
return null;
}
else {
return outputStream.toByteArray();
}
}
catch(ServletException e) {
LOGGER.error("This is not a multipart/form-data POST.", e);
setFailed(ErrorCode.SYSTEM_GENERAL_ERROR, "This is not a multipart/form-data POST which is what we expect for the current API call.");
throw new ValidationException(e);
}
catch(IOException e) {
LOGGER
.info("There was a problem with the zipping of the data.", e);
throw
new ValidationException(
ErrorCode.SERVER_INVALID_GZIP_DATA,
"The zipped data was not valid zip data.",
e);
}
}
/**
* Sets the response headers to disallow client caching.
*/
protected void expireResponse(HttpServletResponse response) {
response.setHeader("Expires", "Fri, 5 May 1995 12:00:00 GMT");
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
// This is done to allow client content to be served up from from
// different domains than the server data e.g., when you want to run a
// client in a local sandbox, but retrieve data from a remote server
//response.setHeader("Access-Control-Allow-Origin","*");
}
/**
* There is functionality in Tomcat 6 to perform this action, but it is
* also nice to have it controlled programmatically.
*
* @return an OutputStream appropriate for the headers found in the
* request.
*/
protected OutputStream getOutputStream(HttpServletRequest request, HttpServletResponse response)
throws IOException {
OutputStream os = null;
// Determine if the response can be gzipped
String encoding = request.getHeader("Accept-Encoding");
if (encoding != null && encoding.indexOf("gzip") >= 0) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("Returning a GZIPOutputStream");
}
response.setHeader("Content-Encoding","gzip");
response.setHeader("Vary", "Accept-Encoding");
os = new GZIPOutputStream(response.getOutputStream());
} else {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("Returning the default OutputStream");
}
os = response.getOutputStream();
}
return os;
}
/**************************************************************************
* End JEE Requirements
*************************************************************************/
}