/*
* Copyright (C) 2012 Square, Inc.
*
* 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 retrofit;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import retrofit.client.Header;
import retrofit.client.Request;
import retrofit.converter.Converter;
import retrofit.mime.FormUrlEncodedTypedOutput;
import retrofit.mime.MultipartTypedOutput;
import retrofit.mime.TypedOutput;
import retrofit.mime.TypedString;
final class RequestBuilder implements RequestInterceptor.RequestFacade {
private final Converter converter;
private final List<Header> headers;
private final StringBuilder queryParams;
private final String[] paramNames;
private final RestMethodInfo.ParamUsage[] paramUsages;
private final String requestMethod;
private final boolean isSynchronous;
private final FormUrlEncodedTypedOutput formBody;
private final MultipartTypedOutput multipartBody;
private TypedOutput body;
private String relativeUrl;
private String apiUrl;
RequestBuilder(Converter converter, RestMethodInfo methodInfo) {
this.converter = converter;
paramNames = methodInfo.requestParamNames;
paramUsages = methodInfo.requestParamUsage;
requestMethod = methodInfo.requestMethod;
isSynchronous = methodInfo.isSynchronous;
headers = new ArrayList<Header>();
if (methodInfo.headers != null) {
headers.addAll(methodInfo.headers);
}
queryParams = new StringBuilder();
relativeUrl = methodInfo.requestUrl;
String requestQuery = methodInfo.requestQuery;
if (requestQuery != null) {
queryParams.append('?').append(requestQuery);
}
switch (methodInfo.requestType) {
case FORM_URL_ENCODED:
formBody = new FormUrlEncodedTypedOutput();
multipartBody = null;
body = formBody;
break;
case MULTIPART:
formBody = null;
multipartBody = new MultipartTypedOutput();
body = multipartBody;
break;
case SIMPLE:
formBody = null;
multipartBody = null;
// If present, 'body' will be set in 'setArguments' call.
break;
default:
throw new IllegalArgumentException("Unknown request type: " + methodInfo.requestType);
}
}
void setApiUrl(String apiUrl) {
this.apiUrl = apiUrl;
}
@Override
public void addHeader(String name, String value) {
if (name == null) {
throw new IllegalArgumentException("Header name must not be null.");
}
headers.add(new Header(name, value));
}
@Override
public void addPathParam(String name, String value) {
addPathParam(name, value, true);
}
@Override
public void addEncodedPathParam(String name, String value) {
addPathParam(name, value, false);
}
void addPathParam(String name, String value, boolean urlEncodeValue) {
if (name == null) {
throw new IllegalArgumentException("Path replacement name must not be null.");
}
if (value == null) {
throw new IllegalArgumentException("Path replacement \"" + name + "\" value must not be null.");
}
try {
if (urlEncodeValue) {
String encodedValue = URLEncoder.encode(String.valueOf(value), "UTF-8");
// URLEncoder encodes for use as a query parameter. Path encoding uses %20 to
// encode spaces rather than +. Query encoding difference specified in HTML spec.
// Any remaining plus signs represent spaces as already URLEncoded.
encodedValue = encodedValue.replace("+", "%20");
relativeUrl = relativeUrl.replace("{" + name + "}", encodedValue);
} else {
relativeUrl = relativeUrl.replace("{" + name + "}", String.valueOf(value));
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unable to convert path parameter \"" + name + "\" value to UTF-8:" + value, e);
}
}
@Override
public void addQueryParam(String name, String value) {
addQueryParam(name, value, true);
}
@Override
public void addEncodedQueryParam(String name, String value) {
addQueryParam(name, value, false);
}
void addQueryParam(String name, String value, boolean urlEncodeValue) {
if (name == null) {
throw new IllegalArgumentException("Query param name must not be null.");
}
if (value == null) {
throw new IllegalArgumentException("Query param \"" + name + "\" value must not be null.");
}
try {
if (urlEncodeValue) {
value = URLEncoder.encode(String.valueOf(value), "UTF-8");
}
StringBuilder queryParams = this.queryParams;
queryParams.append(queryParams.length() > 0 ? '&' : '?');
queryParams.append(name).append('=').append(value);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unable to convert query parameter \"" + name + "\" value to UTF-8: " + value, e);
}
}
void setArguments(Object[] args) {
if (args == null) {
return;
}
int count = args.length;
if (!isSynchronous) {
count -= 1;
}
for (int i = 0; i < count; i++) {
String name = paramNames[i];
Object value = args[i];
RestMethodInfo.ParamUsage paramUsage = paramUsages[i];
switch (paramUsage) {
case PATH:
if (value == null) {
throw new IllegalArgumentException("Path parameter \"" + name + "\" value must not be null.");
}
addPathParam(name, value.toString());
break;
case ENCODED_PATH:
if (value == null) {
throw new IllegalArgumentException("Path parameter \"" + name + "\" value must not be null.");
}
addEncodedPathParam(name, value.toString());
break;
case QUERY:
if (value != null) { // Skip null values.
addQueryParam(name, value.toString());
}
break;
case ENCODED_QUERY:
if (value != null) { // Skip null values.
addEncodedQueryParam(name, value.toString());
}
break;
case HEADER:
if (value != null) { // Skip null values.
addHeader(name, value.toString());
}
break;
case FIELD:
if (value != null) { // Skip null values.
formBody.addField(name, value.toString());
}
break;
case PART:
if (value != null) { // Skip null values.
if (value instanceof TypedOutput) {
multipartBody.addPart(name, (TypedOutput) value);
} else if (value instanceof String) {
multipartBody.addPart(name, new TypedString((String) value));
} else {
multipartBody.addPart(name, converter.toBody(value));
}
}
break;
case BODY:
if (value == null) {
throw new IllegalArgumentException("Body parameter value must not be null.");
}
if (value instanceof TypedOutput) {
body = (TypedOutput) value;
} else {
body = converter.toBody(value);
}
break;
default:
throw new IllegalArgumentException("Unknown parameter usage: " + paramUsage);
}
}
}
Request build() throws UnsupportedEncodingException {
String apiUrl = this.apiUrl;
StringBuilder url = new StringBuilder(apiUrl);
if (apiUrl.endsWith("/")) {
// We require relative paths to start with '/'. Prevent a double-slash.
url.deleteCharAt(url.length() - 1);
}
url.append(relativeUrl);
StringBuilder queryParams = this.queryParams;
if (queryParams.length() > 0) {
url.append(queryParams);
}
if (multipartBody != null && multipartBody.getPartCount() == 0) {
throw new IllegalStateException("Multipart requests must contain at least one part.");
}
return new Request(requestMethod, url.toString(), headers, body);
}
}