1 module hunt.http.client.FormBody; 2 3 import hunt.http.HttpBody; 4 import hunt.http.HttpOutputStream; 5 6 import hunt.io.ByteBuffer; 7 import hunt.io.HeapByteBuffer; 8 import hunt.io.BufferUtils; 9 import hunt.Exceptions; 10 import hunt.logging; 11 import hunt.net.util.UrlEncoded; 12 import hunt.util.MimeType; 13 14 import std.array; 15 import std.conv; 16 import std.container; 17 18 19 /** 20 * 21 */ 22 final class FormBody : HttpBody { 23 enum CONTENT_TYPE = MimeType.APPLICATION_X_WWW_FORM_VALUE; 24 private ByteBuffer _buffer; 25 26 private string[] _encodedNames; 27 private string[] _encodedValues; 28 29 this(string[] encodedNames, string[] encodedValues) { 30 this._encodedNames = encodedNames; 31 this._encodedValues = encodedValues; 32 } 33 34 /** The number of key-value pairs in this form-encoded body. */ 35 int size() { 36 return cast(int) _encodedNames.length; 37 } 38 39 string encodedName(int index) { 40 return _encodedNames[index]; 41 } 42 43 string name(int index) { 44 return UrlEncoded.decodeString(_encodedNames[index]); 45 } 46 47 string encodedValue(int index) { 48 return _encodedValues[index]; 49 } 50 51 string value(int index) { 52 return UrlEncoded.decodeString(_encodedValues[index]); 53 } 54 55 override string contentType() { 56 return CONTENT_TYPE; 57 } 58 59 override long contentLength() { 60 if(_buffer is null) { 61 _buffer = encode(); 62 } 63 64 return _buffer.remaining(); 65 } 66 67 override void writeTo(HttpOutputStream sink) { 68 if(_buffer is null) { 69 _buffer = encode(); 70 } 71 72 sink.write(_buffer); 73 } 74 75 /** 76 * Either writes this request to {@code sink} or measures its content length. We have one method 77 * do double-duty to make sure the counting and content are consistent, particularly when it comes 78 * to awkward operations like measuring the encoded length of header strings, or the 79 * length-in-digits of an encoded integer. 80 */ 81 private ByteBuffer encode() { 82 ByteBuffer buffer = BufferUtils.allocate(2 * 1024); 83 for (size_t i = 0; i < _encodedNames.length; i++) { 84 if (i > 0) 85 buffer.put('&'); 86 buffer.put(cast(byte[])_encodedNames[i]); 87 buffer.put('='); 88 buffer.put(cast(byte[])_encodedValues[i]); 89 } 90 91 buffer.flip(); 92 93 version(HUNT_HTTP_DEBUG) trace(cast(string)buffer.peekRemaining()); 94 95 return buffer; 96 } 97 98 static final class Builder { 99 private Array!string names; 100 private Array!string values; 101 // private final Charset charset; 102 103 this() { 104 // this(null); 105 } 106 107 // this(Charset charset) { 108 // this.charset = charset; 109 // } 110 111 Builder add(string name, string value) { 112 if (name.empty) 113 throw new NullPointerException("name is empty"); 114 if (value.empty) 115 throw new NullPointerException("value is empty"); 116 117 names.insertBack(UrlEncoded.encodeString(name)); 118 values.insertBack(UrlEncoded.encodeString(value)); 119 120 return this; 121 } 122 123 Builder add(T)(string name, T value) if(!is(T == string)) { 124 return add(name, to!string(value)); 125 } 126 127 Builder addEncoded(string name, string value) { 128 if (name.empty) 129 throw new NullPointerException("name is empty"); 130 if (value.empty) 131 throw new NullPointerException("value is empty"); 132 133 names.insertBack(name); 134 values.insertBack(value); 135 return this; 136 } 137 138 FormBody build() { 139 return new FormBody(names.array, values.array); 140 } 141 } 142 } 143 144 alias FormBodyBuilder = FormBody.Builder;