001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.protocol.http.sampler;
020:
021: import java.io.BufferedInputStream;
022: import java.io.ByteArrayOutputStream;
023: import java.io.File;
024: import java.io.FileInputStream;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.OutputStream;
028: import java.io.UnsupportedEncodingException;
029: import java.net.URLConnection;
030:
031: import org.apache.jmeter.protocol.http.util.HTTPArgument;
032: import org.apache.jmeter.protocol.http.util.HTTPConstants;
033: import org.apache.jmeter.testelement.property.PropertyIterator;
034:
035: /**
036: * Class for setting the necessary headers for a POST request, and sending the
037: * body of the POST.
038: */
039: public class PostWriter {
040:
041: private static final String DASH_DASH = "--"; // $NON-NLS-1$
042: private static final byte[] DASH_DASH_BYTES = DASH_DASH.getBytes();
043:
044: /** The bounday string between multiparts */
045: protected final static String BOUNDARY = "---------------------------7d159c1302d0y0"; // $NON-NLS-1$
046:
047: private final static byte[] CRLF = { 0x0d, 0x0A };
048:
049: public static final String ENCODING = "ISO-8859-1"; // $NON-NLS-1$
050:
051: /** The form data that is going to be sent as url encoded */
052: protected byte[] formDataUrlEncoded;
053: /** The form data that is going to be sent in post body */
054: protected byte[] formDataPostBody;
055: /** The start of the file multipart to be sent */
056: private byte[] formDataFileStartMultipart;
057: /** The boundary string for multipart */
058: private final String boundary;
059:
060: /**
061: * Constructor for PostWriter.
062: * Uses the PostWriter.BOUNDARY as the boundary string
063: *
064: */
065: public PostWriter() {
066: this (BOUNDARY);
067: }
068:
069: /**
070: * Constructor for PostWriter
071: *
072: * @param boundary the boundary string to use as marker between multipart parts
073: */
074: public PostWriter(String boundary) {
075: this .boundary = boundary;
076: }
077:
078: /**
079: * Send POST data from Entry to the open connection.
080: *
081: * @return the post body sent. Actual file content is not returned, it
082: * is just shown as a placeholder text "actual file content"
083: */
084: public String sendPostData(URLConnection connection,
085: HTTPSampler sampler) throws IOException {
086: // Buffer to hold the post body, except file content
087: StringBuffer postedBody = new StringBuffer(1000);
088:
089: // Check if we should do a multipart/form-data or an
090: // application/x-www-form-urlencoded post request
091: if (sampler.getUseMultipartForPost()) {
092: OutputStream out = connection.getOutputStream();
093:
094: // Write the form data post body, which we have constructed
095: // in the setHeaders. This contains the multipart start divider
096: // and any form data, i.e. arguments
097: out.write(formDataPostBody);
098: // We get the posted bytes as UTF-8, since java is using UTF-8
099: postedBody.append(new String(formDataPostBody, "UTF-8")); // $NON-NLS-1$
100:
101: // Add any files
102: if (sampler.hasUploadableFiles()) {
103: // First write the start multipart file
104: out.write(formDataFileStartMultipart);
105: // We get the posted bytes as UTF-8, since java is using UTF-8
106: postedBody.append(new String(
107: formDataFileStartMultipart, "UTF-8")); // $NON-NLS-1$
108:
109: // Write the actual file content
110: writeFileToStream(sampler.getFilename(), out);
111: // We just add placeholder text for file content
112: postedBody
113: .append("<actual file content, not shown here>"); // $NON-NLS-1$
114:
115: // Write the end of multipart file
116: byte[] fileMultipartEndDivider = getFileMultipartEndDivider();
117: out.write(fileMultipartEndDivider);
118: // We get the posted bytes as UTF-8, since java is using UTF-8
119: postedBody.append(new String(fileMultipartEndDivider,
120: "UTF-8")); // $NON-NLS-1$
121: }
122:
123: // Write end of multipart
124: byte[] multipartEndDivider = getMultipartEndDivider();
125: out.write(multipartEndDivider);
126: // We get the posted bytes as UTF-8, since java is using UTF-8
127: postedBody.append(new String(multipartEndDivider, "UTF-8")); // $NON-NLS-1$
128:
129: out.flush();
130: out.close();
131: } else {
132: // If there are no arguments, we can send a file as the body of the request
133: if (sampler.getArguments() != null
134: && !sampler.hasArguments()
135: && sampler.getSendFileAsPostBody()) {
136: OutputStream out = connection.getOutputStream();
137: writeFileToStream(sampler.getFilename(), out);
138: out.flush();
139: out.close();
140:
141: // We just add placeholder text for file content
142: postedBody
143: .append("<actual file content, not shown here>"); // $NON-NLS-1$
144: } else if (formDataUrlEncoded != null) { // may be null for PUT
145: // In an application/x-www-form-urlencoded request, we only support
146: // parameters, no file upload is allowed
147: OutputStream out = connection.getOutputStream();
148: out.write(formDataUrlEncoded);
149: out.flush();
150: out.close();
151:
152: // We get the posted bytes as UTF-8, since java is using UTF-8
153: postedBody.append(new String(formDataUrlEncoded,
154: "UTF-8")); // $NON-NLS-1$
155: }
156: }
157: return postedBody.toString();
158: }
159:
160: public void setHeaders(URLConnection connection, HTTPSampler sampler)
161: throws IOException {
162: // Get the encoding to use for the request
163: String contentEncoding = sampler.getContentEncoding();
164: if (contentEncoding == null || contentEncoding.length() == 0) {
165: contentEncoding = ENCODING;
166: }
167: long contentLength = 0L;
168:
169: // Check if we should do a multipart/form-data or an
170: // application/x-www-form-urlencoded post request
171: if (sampler.getUseMultipartForPost()) {
172: // Set the content type
173: connection.setRequestProperty(
174: HTTPConstants.HEADER_CONTENT_TYPE,
175: HTTPConstants.MULTIPART_FORM_DATA + "; boundary="
176: + getBoundary()); // $NON-NLS-1$
177:
178: // Write the form section
179: ByteArrayOutputStream bos = new ByteArrayOutputStream();
180:
181: // First the multipart start divider
182: bos.write(getMultipartDivider());
183: // Add any parameters
184: PropertyIterator args = sampler.getArguments().iterator();
185: while (args.hasNext()) {
186: HTTPArgument arg = (HTTPArgument) args.next()
187: .getObjectValue();
188: String parameterName = arg.getName();
189: if (parameterName.length() == 0) {
190: continue; // Skip parameters with a blank name (allows use of optional variables in parameter lists)
191: }
192: // End the previous multipart
193: bos.write(CRLF);
194: // Write multipart for parameter
195: writeFormMultipart(bos, parameterName, arg.getValue(),
196: contentEncoding);
197: }
198: // If there are any files, we need to end the previous multipart
199: if (sampler.hasUploadableFiles()) {
200: // End the previous multipart
201: bos.write(CRLF);
202: }
203: bos.flush();
204: // Keep the content, will be sent later
205: formDataPostBody = bos.toByteArray();
206: bos.close();
207: contentLength = formDataPostBody.length;
208:
209: // Now we just construct any multipart for the files
210: // We only construct the file multipart start, we do not write
211: // the actual file content
212: if (sampler.hasUploadableFiles()) {
213: bos = new ByteArrayOutputStream();
214: // Write multipart for file
215: writeStartFileMultipart(bos, sampler.getFilename(),
216: sampler.getFileField(), sampler.getMimetype());
217: bos.flush();
218: formDataFileStartMultipart = bos.toByteArray();
219: bos.close();
220: contentLength += formDataFileStartMultipart.length;
221: // Add also the length of the file content
222: File uploadFile = new File(sampler.getFilename());
223: contentLength += uploadFile.length();
224: // And the end of the file multipart
225: contentLength += getFileMultipartEndDivider().length;
226: }
227:
228: // Add the end of multipart
229: contentLength += getMultipartEndDivider().length;
230:
231: // Set the content length
232: connection.setRequestProperty(
233: HTTPConstants.HEADER_CONTENT_LENGTH, Long
234: .toString(contentLength));
235:
236: // Make the connection ready for sending post data
237: connection.setDoOutput(true);
238: connection.setDoInput(true);
239: } else {
240: // Check if the header manager had a content type header
241: // This allows the user to specify his own content-type for a POST request
242: String contentTypeHeader = connection
243: .getRequestProperty(HTTPConstants.HEADER_CONTENT_TYPE);
244: boolean hasContentTypeHeader = contentTypeHeader != null
245: && contentTypeHeader.length() > 0;
246:
247: // If there are no arguments, we can send a file as the body of the request
248: if (sampler.getArguments() != null
249: && sampler.getArguments().getArgumentCount() == 0
250: && sampler.getSendFileAsPostBody()) {
251: if (!hasContentTypeHeader) {
252: // Allow the mimetype of the file to control the content type
253: if (sampler.getMimetype() != null
254: && sampler.getMimetype().length() > 0) {
255: connection.setRequestProperty(
256: HTTPConstants.HEADER_CONTENT_TYPE,
257: sampler.getMimetype());
258: } else {
259: connection
260: .setRequestProperty(
261: HTTPConstants.HEADER_CONTENT_TYPE,
262: HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
263: }
264: }
265:
266: // Create the content length we are going to write
267: File inputFile = new File(sampler.getFilename());
268: contentLength = inputFile.length();
269: } else {
270: // We create the post body content now, so we know the size
271: ByteArrayOutputStream bos = new ByteArrayOutputStream();
272:
273: // If none of the arguments have a name specified, we
274: // just send all the values as the post body
275: String postBody = null;
276: if (!sampler.getSendParameterValuesAsPostBody()) {
277: // Set the content type
278: if (!hasContentTypeHeader) {
279: connection
280: .setRequestProperty(
281: HTTPConstants.HEADER_CONTENT_TYPE,
282: HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
283: }
284:
285: // It is a normal post request, with parameter names and values
286: postBody = sampler.getQueryString(contentEncoding);
287: } else {
288: // Allow the mimetype of the file to control the content type
289: // This is not obvious in GUI if you are not uploading any files,
290: // but just sending the content of nameless parameters
291: if (!hasContentTypeHeader) {
292: if (sampler.getMimetype() != null
293: && sampler.getMimetype().length() > 0) {
294: connection.setRequestProperty(
295: HTTPConstants.HEADER_CONTENT_TYPE,
296: sampler.getMimetype());
297: } else {
298: // TODO: is this the correct default?
299: connection
300: .setRequestProperty(
301: HTTPConstants.HEADER_CONTENT_TYPE,
302: HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
303: }
304: }
305:
306: // Just append all the parameter values, and use that as the post body
307: StringBuffer postBodyBuffer = new StringBuffer();
308: PropertyIterator args = sampler.getArguments()
309: .iterator();
310: while (args.hasNext()) {
311: HTTPArgument arg = (HTTPArgument) args.next()
312: .getObjectValue();
313: postBodyBuffer.append(arg
314: .getEncodedValue(contentEncoding));
315: }
316: postBody = postBodyBuffer.toString();
317: }
318:
319: // Query string should be encoded in UTF-8
320: bos.write(postBody.getBytes("UTF-8")); // $NON-NLS-1$
321: bos.flush();
322: bos.close();
323:
324: // Keep the content, will be sent later
325: formDataUrlEncoded = bos.toByteArray();
326: contentLength = bos.toByteArray().length;
327: }
328:
329: // Set the content length
330: connection.setRequestProperty(
331: HTTPConstants.HEADER_CONTENT_LENGTH, Long
332: .toString(contentLength));
333:
334: // Make the connection ready for sending post data
335: connection.setDoOutput(true);
336: }
337: }
338:
339: /**
340: * Get the boundary string, used to separate multiparts
341: *
342: * @return the boundary string
343: */
344: protected String getBoundary() {
345: return boundary;
346: }
347:
348: /**
349: * Get the bytes used to separate multiparts
350: *
351: * @return the bytes used to separate multiparts
352: * @throws IOException
353: */
354: private byte[] getMultipartDivider() throws IOException {
355: return (DASH_DASH + getBoundary()).getBytes(ENCODING);
356: }
357:
358: /**
359: * Get the bytes used to end a file multipat
360: *
361: * @return the bytes used to end a file multipart
362: * @throws IOException
363: */
364: private byte[] getFileMultipartEndDivider() throws IOException {
365: byte[] ending = getMultipartDivider();
366: byte[] completeEnding = new byte[ending.length + CRLF.length];
367: System.arraycopy(CRLF, 0, completeEnding, 0, CRLF.length);
368: System.arraycopy(ending, 0, completeEnding, CRLF.length,
369: ending.length);
370: return completeEnding;
371: }
372:
373: /**
374: * Get the bytes used to end the multipart request
375: *
376: * @return the bytes used to end the multipart request
377: * @throws IOException
378: */
379: private byte[] getMultipartEndDivider() throws IOException {
380: byte[] ending = DASH_DASH_BYTES;
381: byte[] completeEnding = new byte[ending.length + CRLF.length];
382: System.arraycopy(ending, 0, completeEnding, 0, ending.length);
383: System.arraycopy(CRLF, 0, completeEnding, ending.length,
384: CRLF.length);
385: return completeEnding;
386: }
387:
388: /**
389: * Write the start of a file multipart, up to the point where the
390: * actual file content should be written
391: */
392: private void writeStartFileMultipart(OutputStream out,
393: String filename, String nameField, String mimetype)
394: throws IOException {
395: write(out, "Content-Disposition: form-data; name=\""); // $NON-NLS-1$
396: write(out, nameField);
397: write(out, "\"; filename=\"");// $NON-NLS-1$
398: write(out, (new File(filename).getName()));
399: writeln(out, "\""); // $NON-NLS-1$
400: writeln(out, "Content-Type: " + mimetype); // $NON-NLS-1$
401: writeln(out, "Content-Transfer-Encoding: binary"); // $NON-NLS-1$
402: out.write(CRLF);
403: }
404:
405: /**
406: * Write the content of a file to the output stream
407: *
408: * @param filename the filename of the file to write to the stream
409: * @param out the stream to write to
410: * @throws IOException
411: */
412: private void writeFileToStream(String filename, OutputStream out)
413: throws IOException {
414: byte[] buf = new byte[1024];
415: // 1k - the previous 100k made no sense (there's tons of buffers
416: // elsewhere in the chain) and it caused OOM when many concurrent
417: // uploads were being done. Could be fixed by increasing the evacuation
418: // ratio in bin/jmeter[.bat], but this is better.
419: InputStream in = new BufferedInputStream(new FileInputStream(
420: filename));
421: int read;
422: try {
423: while ((read = in.read(buf)) > 0) {
424: out.write(buf, 0, read);
425: }
426: } finally {
427: in.close();
428: }
429: }
430:
431: /**
432: * Writes form data in multipart format.
433: */
434: private void writeFormMultipart(OutputStream out, String name,
435: String value, String charSet) throws IOException {
436: writeln(out, "Content-Disposition: form-data; name=\"" + name
437: + "\""); // $NON-NLS-1$ // $NON-NLS-2$
438: writeln(out, "Content-Type: text/plain; charset=" + charSet); // $NON-NLS-1$
439: writeln(out, "Content-Transfer-Encoding: 8bit"); // $NON-NLS-1$
440:
441: out.write(CRLF);
442: out.write(value.getBytes(charSet));
443: out.write(CRLF);
444: // Write boundary end marker
445: out.write(getMultipartDivider());
446: }
447:
448: private void write(OutputStream out, String value)
449: throws UnsupportedEncodingException, IOException {
450: out.write(value.getBytes(ENCODING));
451: }
452:
453: private void writeln(OutputStream out, String value)
454: throws UnsupportedEncodingException, IOException {
455: out.write(value.getBytes(ENCODING));
456: out.write(CRLF);
457: }
458: }
|