/** * */ package com.github.lpezet.antiope2.retrofitted; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.lpezet.antiope2.dao.ExecutionContext; import com.github.lpezet.antiope2.dao.http.BasicNameValuePair; import com.github.lpezet.antiope2.dao.http.HttpRequest; import com.github.lpezet.antiope2.dao.http.IHttpRequest; import com.github.lpezet.antiope2.dao.http.NameValuePair; import com.github.lpezet.antiope2.retrofitted.annotation.http.Body; import com.github.lpezet.antiope2.retrofitted.annotation.http.Field; import com.github.lpezet.antiope2.retrofitted.annotation.http.FieldMap; import com.github.lpezet.antiope2.retrofitted.annotation.http.Header; import com.github.lpezet.antiope2.retrofitted.annotation.http.Path; import com.github.lpezet.antiope2.retrofitted.annotation.http.Query; import com.github.lpezet.antiope2.retrofitted.annotation.http.QueryMap; import com.github.lpezet.antiope2.retrofitted.converter.Converter; import com.github.lpezet.antiope2.util.StringUtils; /** * @author Luc Pezet * */ public class RequestBuilder { private Logger mLogger = LoggerFactory.getLogger(this.getClass()); private MethodInfo mMethodInfo; private String mResourcePath; private List<NameValuePair> mParameters = new ArrayList<NameValuePair>(); private List<NameValuePair> mQueryParameters = new ArrayList<NameValuePair>(); private InputStream mRequestBody; private Converter mConverter; private String mEndpointUri; private ExecutionContext mExecutionContext; private com.github.lpezet.antiope2.dao.http.Headers mHeaders = new com.github.lpezet.antiope2.dao.http.Headers(); public RequestBuilder(String pEndpointUri, MethodInfo pMethodInfo, Converter pDefaultConverter) { this(pEndpointUri, pMethodInfo, pDefaultConverter, null); } public RequestBuilder(String pEndpointUri, MethodInfo pMethodInfo, Converter pDefaultConverter, ExecutionContext pExecutionContext) { mEndpointUri = pEndpointUri; mMethodInfo = pMethodInfo; mResourcePath= pMethodInfo.getResourcePath(); mConverter = pMethodInfo.getConverter() == null ? pDefaultConverter : pMethodInfo.getConverter(); mHeaders.addAll( pMethodInfo.getHeaders() ); mExecutionContext = pExecutionContext; } public void setArguments(Object[] pArgs) { if (pArgs == null) { return; } int oCount = pArgs.length; if (mMethodInfo.isAsync()) { // Last arg is Callback oCount -= 1; } for (int i = 0; i < oCount; i++) { Object oValue = pArgs[i]; Annotation oAnnotation = mMethodInfo.getParamAnnotations()[i]; Class<? extends Annotation> oAnnotationType = oAnnotation.annotationType(); if (oAnnotationType == Path.class) { Path oPath = (Path) oAnnotation; String oName = oPath.value(); if (oValue == null) { throw new IllegalArgumentException( "Path parameter \"" + oName + "\" value must not be null."); } addPathParam(oName, oValue.toString(), oPath.encode()); } else if (oAnnotationType == Query.class) { if (oValue != null) { // Skip null values. Query query = (Query) oAnnotation; addQueryParam(query.value(), oValue, query.template(), query.encodeName(), query.encodeValue()); } } else if (oAnnotationType == QueryMap.class) { if (oValue != null) { // Skip null values. QueryMap queryMap = (QueryMap) oAnnotation; addQueryParamMap(i, (Map<?, ?>) oValue, queryMap.encodeNames(), queryMap.encodeValues()); } } else if (oAnnotationType == Header.class) { if (oValue != null) { // Skip null values. String name = ((Header) oAnnotation).value(); if (oValue instanceof Iterable) { for (Object iterableValue : (Iterable<?>) oValue) { if (iterableValue != null) { // Skip null values. addHeader(name, iterableValue.toString()); } } } else if (oValue.getClass().isArray()) { for (int x = 0, arrayLength = Array.getLength(oValue); x < arrayLength; x++) { Object arrayValue = Array.get(oValue, x); if (arrayValue != null) { // Skip null values. addHeader(name, arrayValue.toString()); } } } else { addHeader(name, oValue.toString()); } } } else if (oAnnotationType == Field.class) { if (oValue != null) { // Skip null values. Field field = (Field) oAnnotation; String name = field.value(); boolean encode = field.encode(); if (oValue instanceof Iterable) { for (Object iterableValue : (Iterable<?>) oValue) { if (iterableValue != null) { // Skip null values. addFormField(name, iterableValue.toString(), encode); } } } else if (oValue.getClass().isArray()) { for (int x = 0, arrayLength = Array.getLength(oValue); x < arrayLength; x++) { Object arrayValue = Array.get(oValue, x); if (arrayValue != null) { // Skip null values. addFormField(name, arrayValue.toString(), encode); } } } else { addFormField(name, oValue.toString(), encode); } } } else if (oAnnotationType == FieldMap.class) { if (oValue != null) { // Skip null values. FieldMap fieldMap = (FieldMap) oAnnotation; boolean encode = fieldMap.encode(); for (Map.Entry<?, ?> entry : ((Map<?, ?>) oValue).entrySet()) { Object entryKey = entry.getKey(); if (entryKey == null) { throw new IllegalArgumentException( "Parameter #" + (i + 1) + " field map contained null key."); } Object entryValue = entry.getValue(); if (entryValue != null) { // Skip null values. addFormField(entryKey.toString(), entryValue.toString(), encode); } } } } // TODO: Support for multi part /* else if (annotationType == Part.class) { if (value != null) { // Skip null values. String name = ((Part) annotation).value(); String transferEncoding = ((Part) annotation).encoding(); Headers headers = Headers.of( "Content-Disposition", "name=\"" + name + "\"", "Content-Transfer-Encoding", transferEncoding); if (value instanceof RequestBody) { multipartBuilder.addPart(headers, (RequestBody) value); } else if (value instanceof String) { multipartBuilder.addPart(headers, RequestBody.create(MediaType.parse("text/plain"), (String) value)); } else { multipartBuilder.addPart(headers, converter.toBody(value, value.getClass())); } } } else if (annotationType == PartMap.class) { if (value != null) { // Skip null values. String transferEncoding = ((PartMap) annotation).encoding(); for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) { Object entryKey = entry.getKey(); if (entryKey == null) { throw new IllegalArgumentException( "Parameter #" + (i + 1) + " part map contained null key."); } String entryName = entryKey.toString(); Object entryValue = entry.getValue(); Headers headers = Headers.of( "Content-Disposition", "name=\"" + entryName + "\"", "Content-Transfer-Encoding", transferEncoding); if (entryValue != null) { // Skip null values. if (entryValue instanceof RequestBody) { multipartBuilder.addPart(headers, (RequestBody) entryValue); } else if (entryValue instanceof String) { multipartBuilder.addPart(headers, RequestBody.create(MediaType.parse("text/plain"), (String) entryValue)); } else { multipartBuilder.addPart(headers, converter.toBody(entryValue, entryValue.getClass())); } } } } } */ else if (oAnnotationType == Body.class) { if (oValue == null) { throw new IllegalArgumentException("Body parameter value must not be null."); } //Body oBody = (Body) oAnnotation; //Class<IWorker<?, InputStream>> oConverterClass = oBody.converter(); //mConverter = instantiate( oConverterClass ); if (oValue instanceof InputStream) { mRequestBody = (InputStream) oValue; } else { mRequestBody = mConverter.serialize(oValue, oValue.getClass()); } } else { throw new IllegalArgumentException( "Unknown annotation: " + oAnnotationType.getCanonicalName()); } } } private void addFormField(String name, String value, boolean encode) { /* if (encode) { formEncodingBuilder.add(name, value); } else { formEncodingBuilder.addEncoded(name, value); } */ try { //Here we're letting the Http Client to encode parameter values. // So if encode=true, then we leave the value as is, but if encode=false, we decode the value first. mParameters.add( new BasicNameValuePair(name, encode ? value : URLDecoder.decode( value, "UTF-8") )); } catch (UnsupportedEncodingException e) { mLogger.error("Error decoding value with UTF-8.", e); } } public void addHeader(String name, String value) { mLogger.info("addHeader(" + name + ", " + value + ")"); if (name == null) { throw new IllegalArgumentException("Header name must not be null."); } /* if ("Content-Type".equalsIgnoreCase(name)) { contentTypeHeader = value; return; } */ mHeaders.add(name, value); } /** * WARNING: Not sure here if we should put name/value in mParameters or create a new mQueryParameters Map. * * @param parameterNumber * @param pMap * @param pEncodeNames * @param pEncodeValues */ private void addQueryParamMap(int parameterNumber, Map<?, ?> pMap, boolean pEncodeNames, boolean pEncodeValues) { for (Map.Entry<?, ?> entry : pMap.entrySet()) { Object entryKey = entry.getKey(); if (entryKey == null) { throw new IllegalArgumentException( "Parameter #" + (parameterNumber + 1) + " query map contained null key."); } Object entryValue = entry.getValue(); if (entryValue != null) { // Skip null values. addQueryParam(entryKey.toString(), entryValue.toString(), null, pEncodeNames, pEncodeValues); } } } private void addQueryParam(String name, Object value, String pValueTemplate, boolean encodeName, boolean encodeValue) { if (value instanceof Iterable) { for (Object iterableValue : (Iterable<?>) value) { if (iterableValue != null) { // Skip null values addQueryParam(name, iterableValue.toString(), pValueTemplate, encodeName, encodeValue); } } } else if (value.getClass().isArray()) { for (int x = 0, arrayLength = Array.getLength(value); x < arrayLength; x++) { Object arrayValue = Array.get(value, x); if (arrayValue != null) { // Skip null values addQueryParam(name, arrayValue.toString(), pValueTemplate, encodeName, encodeValue); } } } else { addQueryParam(name, value.toString(), pValueTemplate, encodeName, encodeValue); } } private void addQueryParam(String pName, String pValue, String pValueTemplate, boolean pEncodeName, boolean pEncodeValue) { mLogger.info("addQueryParam(" + pName + ", " + pValue + ", " + pEncodeName + ", " + pEncodeValue + ")"); if (pName == null) { throw new IllegalArgumentException("Query param name must not be null."); } if (pValue == null) { throw new IllegalArgumentException("Query param \"" + pName + "\" value must not be null."); } String oValue = resolveQueryValue(pValueTemplate, pValue); String oName = pName; try { /* StringBuilder queryParams = this.queryParams; if (queryParams == null) { this.queryParams = queryParams = new StringBuilder(); } queryParams.append(queryParams.length() > 0 ? '&' : '?'); */ /* if (pEncodeName) { oName = URLEncoder.encode(oName, "UTF-8"); } if (pEncodeValue) { oValue = URLEncoder.encode(oValue, "UTF-8"); } */ if (!pEncodeName) { oName = URLDecoder.decode( oName, "UTF-8"); } if (!pEncodeValue) { oValue = URLDecoder.decode( oValue, "UTF-8" ); } mParameters.add( new BasicNameValuePair( oName, oValue) ); } catch (UnsupportedEncodingException e) { throw new RuntimeException( "Unable to convert query parameter \"" + pName + "\" value to UTF-8: " + pValue, e); } } private String resolveQueryValue(String pValueTemplate, String pValue) { if (StringUtils.isEmpty(pValueTemplate)) return pValue; return pValueTemplate.replace("{}", pValue); } private void addPathParam(String pName, String pValue, boolean pUrlEncodeValue) { mLogger.info("addPathParam(" + pName + ", " + pValue + ", " + pUrlEncodeValue + ")"); if (pName == null) { throw new IllegalArgumentException("Path replacement name must not be null."); } if (pValue == null) { throw new IllegalArgumentException( "Path replacement \"" + pName + "\" value must not be null."); } try { if (pUrlEncodeValue) { String encodedValue = URLEncoder.encode(String.valueOf(pValue), "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"); mResourcePath = mResourcePath.replace("{" + pName + "}", encodedValue); } else { mResourcePath = mResourcePath.replace("{" + pName + "}", String.valueOf(pValue)); } } catch (UnsupportedEncodingException e) { throw new RuntimeException( "Unable to convert path parameter \"" + pName + "\" value to UTF-8:" + pValue, e); } } public IHttpRequest build() throws Exception { HttpRequest oRequest = new HttpRequest(mEndpointUri); oRequest.setExecutionContext(mExecutionContext); oRequest.setContent( mRequestBody ); oRequest.setEndpoint(new URI( mEndpointUri )); for (com.github.lpezet.antiope2.dao.http.Header e : mHeaders.getAllHeaders()) { oRequest.addHeader( e.getName(), e.getValue() ); } oRequest.setHttpMethod( mMethodInfo.getRequestMethod() ); for (NameValuePair e : mParameters) { oRequest.addParameter( e.getName(), e.getValue() ); } String oPath = mResourcePath; if (mMethodInfo.getResourceQuery() != null) { //TODO: // Here 2 options: add it to the Parameters or append to ResourcePath // A 3rd option is to have IHttpRequest hold a Query member (like HttpServletRequest) String[] oParams = mMethodInfo.getResourceQuery().split("&"); for (String oParam : oParams) { String[] oNameValue = oParam.split("="); oRequest.addParameter(oNameValue[0], URLDecoder.decode( oNameValue[1], "UTF-8")); } } //WARNING: Here I'm not sure. For POST, best put everything in Parameters and put in body of request. // For so Retrofit-compatibility sake for now, doing it the Retrofit way. /* if (!mQueryParameters.isEmpty()) { if (oPath.indexOf("?") < 0) oPath += "?"; StringBuilder oBuilder = new StringBuilder(); for (Entry<String, V>) } */ oRequest.setResourcePath(oPath); //oRequest.setTimeOffset(...); return oRequest; } // private HttpMethodName getHttpMethod() { // return HttpMethodName.valueOf( mMethodInfo.getRequestMethod() ); // } }