/*
 * Copyright (C) 2013 SpotHero
 *
 * 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 com.spothero.volley;

import com.android.volley.AuthFailureError;
import com.android.volley.DefaultRetryPolicy;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.HttpHeaderParser;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.http.HttpStatus;

import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JacksonRequest<T> extends Request<T> {

	private static final int DEFAULT_TIMEOUT = 30000; // 30 seconds
	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

	private Map<String, String> mParams;
	private List<Integer> mAcceptedStatusCodes;
	private final JacksonRequestListener<T> mListener;

	public JacksonRequest(int method, String url, JacksonRequestListener<T> listener) {
		this(DEFAULT_TIMEOUT, method, url, null, listener);
	}

	public JacksonRequest(int timeout, int method, String url, JacksonRequestListener<T> listener) {
		this(timeout, method, url, null, listener);
	}

	public JacksonRequest(int method, String baseUrl, String endpoint, Map<String, String> params, JacksonRequestListener<T> listener) {
		this(DEFAULT_TIMEOUT, method, baseUrl, endpoint, params, listener);
	}

	public JacksonRequest(int timeout, int method, String baseUrl, String endpoint, Map<String, String> params, JacksonRequestListener<T> listener) {
		this(timeout, method, getUrl(method, baseUrl, endpoint, params), params, listener);
	}

	public JacksonRequest(int method, String url, Map<String, String> params, JacksonRequestListener<T> listener) {
		this(DEFAULT_TIMEOUT, method, url, params, listener);
	}

	public JacksonRequest(int timeout, int method, String url, Map<String, String> params, JacksonRequestListener<T> listener) {
		super(method, url, null);

		setShouldCache(false);

		mListener = listener;

		mAcceptedStatusCodes = new ArrayList<Integer>();
		mAcceptedStatusCodes.add(HttpStatus.SC_OK);
		mAcceptedStatusCodes.add(HttpStatus.SC_NO_CONTENT);

		setRetryPolicy(new DefaultRetryPolicy(timeout, 1, 1));

		if (method == Method.POST || method == Method.PUT) {
			mParams = params;
		}
	}

	/**
	 * Gets the ObjectMapper instance used when mapping network responses. Use this to customize the mapper in any way you see fit.
	 *
	 * @return The ObjectMapper instance used when mapping network responses
	 */
	public static ObjectMapper getObjectMapper() {
		return OBJECT_MAPPER;
	}

	/**
	 * Allows you to add additional status codes (besides 200 and 204) that will be parsed.
	 *
	 * @param statusCodes An array of additional status codes to parse network responses for
	 */
	public void addAcceptedStatusCodes(int[] statusCodes) {
		for (int statusCode : statusCodes) {
			mAcceptedStatusCodes.add(statusCode);
		}
	}

	/**
	 * Gets all status codes that will be parsed as successful (Note: some {@link com.android.volley.toolbox.HttpStack}
	 * implementations, including the default, may not allow certain status codes to be parsed. To get around this
	 * limitation, use a custom HttpStack, such as the one provided with the excellent OkHttp library
	 *
	 * @return A list of all status codes that will be counted as successful
	 */
	public List<Integer> getAcceptedStatusCodes() {
		return mAcceptedStatusCodes;
	}

	/**
	 * Converts a base URL, endpoint, and parameters into a full URL
	 *
	 * @param method The {@link com.android.volley.Request.Method} of the URL
	 * @param baseUrl The base URL
	 * @param endpoint The endpoint being hit
	 * @param params The parameters to be appended to the URL if a GET method is used
	 *
	 * @return The full URL
	 */
	private static String getUrl(int method, String baseUrl, String endpoint, Map<String, String> params) {
		if (params != null) {
			for (Map.Entry<String, String> entry : params.entrySet()) {
				if (entry.getValue() == null || entry.getValue().equals("null")) {
					entry.setValue("");
				}
			}
		}

		if (method == Method.GET && params != null && !params.isEmpty()) {
			final StringBuilder result = new StringBuilder(baseUrl + endpoint);
			final int startLength = result.length();
			for (String key : params.keySet()) {
				try {
					final String encodedKey = URLEncoder.encode(key, "UTF-8");
					final String encodedValue = URLEncoder.encode(params.get(key), "UTF-8");
					if (result.length() > startLength) {
						result.append("&");
					} else {
						result.append("?");
					}
					result.append(encodedKey);
					result.append("=");
					result.append(encodedValue);
				} catch (Exception e) { }
			}
			return result.toString();
		} else {
			return baseUrl + endpoint;
		}
	}

	@Override
	protected void deliverResponse(T response) {
		mListener.onResponse(response, HttpStatus.SC_OK, null);
	}

	@Override
	public void deliverError(VolleyError error) {
		int statusCode;
		if (error != null && error.networkResponse != null) {
			statusCode = error.networkResponse.statusCode;
		} else {
			statusCode = 0;
		}

		mListener.onResponse(null, statusCode, error);
	}

	@Override
	protected Response<T> parseNetworkResponse(NetworkResponse response) {
		JavaType returnType = mListener.getReturnType();
		T returnData = null;
		if (returnType != null) {
			try {
				if (response.data != null) {
					returnData = OBJECT_MAPPER.readValue(response.data, returnType);
				} else if (response instanceof JacksonNetworkResponse) {
					returnData = OBJECT_MAPPER.readValue(((JacksonNetworkResponse)response).inputStream, returnType);
				}
			} catch (Exception e) {
				VolleyLog.e(e, "An error occurred while parsing network response:");
				return Response.error(new ParseError(response));
			}
		}
		return mListener.onParseResponseComplete(Response.success(returnData, HttpHeaderParser.parseCacheHeaders(response)));
	}

	@Override
	public Map<String, String> getHeaders() throws AuthFailureError {
		Map<String, String> headers = new HashMap<String, String>();
		headers.put("Accept", "application/json");

		if (getMethod() == Method.POST || getMethod() == Method.PUT) {
			headers.put("Content-Type", "application/x-www-form-urlencoded; charset=utf8");
		}

		return headers;
	}

	@Override
	public Map<String, String> getParams() {
		return mParams;
	}
}

@@ -3216,23 +3216,16 @@
 %09public 
-static 
 ObjectMa
@@ -6086,37 +6086,41 @@
 eturnData = 
-OBJECT_MAPPER
+getObjectMapper()
 .readValue(r
@@ -6225,29 +6225,33 @@
 nData = 
-OBJECT_MAPPER
+getObjectMapper()
 .readVal

Change getObjectMapper from static public to public method, allowing override in subclass. Also change internal references from OBJECT_MAPPER static class var to getObjectMapper() instance method.