self.philbrown.javaQuery.AjaxTask.java Source code

Java tutorial

Introduction

Here is the source code for self.philbrown.javaQuery.AjaxTask.java

Source

/*
 * Copyright 2013 Phil Brown
 *
 * 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 self.philbrown.javaQuery;

import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Semaphore;

import javax.imageio.ImageIO;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import self.philbrown.javaQuery.AjaxTask.TaskResponse;

/**
 * Asynchronously performs HTTP Requests
 * @author Phil Brown
 */
public class AjaxTask extends AsyncTask<Void, Void, TaskResponse> {
    /** Options used to configure this task */
    private AjaxOptions options;
    /** The HTTP Request to perform */
    private HttpUriRequest request = null;

    /** Used for synchronous operations. */
    private static Semaphore mutex = new Semaphore(1);
    /** Contains the current non-global tasks */
    private static volatile List<AjaxTask> localTasks = new ArrayList<AjaxTask>();
    /** Contains the current global tasks */
    private static volatile List<AjaxTask> globalTasks = new ArrayList<AjaxTask>();

    /** Represents a cached HTTP response */
    class CachedResponse {
        /** The response Object */
        public Object response;
        /** The Last-Requested timestamp */
        public Date timestamp;
        /** The Last-Modified timestamp */
        public Date lastModified;
    }

    /** 
     * Keeps track of the responses made by each URL. This cache will be used for caching and
     * modified headers. 
     */
    private static volatile Map<String, CachedResponse> URLresponses = new HashMap<String, CachedResponse>();

    /**
     * Constructor
     * @param options JSON representation of the Ajax Options
     * @throws Exception if the JSON is malformed
     */
    public AjaxTask(JSONObject options) throws Exception {
        this(new AjaxOptions(options));
    }

    /**
     * Can be used to restart an Ajax Task
     * @param request a request (to retry)
     * @param options options for request retry.
     */
    public AjaxTask(HttpUriRequest request, AjaxOptions options) {
        this(options);
        this.request = request;
    }

    /**
     * Constructor
     * @param options used to configure this task
     */
    public AjaxTask(AjaxOptions options) {
        this.options = options;
        if (options.url() == null) {
            throw new NullPointerException("Cannot call Ajax with null URL!");
        }

    }

    /**
     * Stops all currently running Ajax Tasks
     */
    public static void killTasks() {
        for (AjaxTask task : globalTasks) {
            task.cancel(true);
        }
        for (AjaxTask task : localTasks) {
            task.cancel(true);
        }
        globalTasks.clear();
        localTasks.clear();
        $.ajaxStop();

    }

    @Override
    protected void onPreExecute() {
        if (!options.async()) {
            try {
                mutex.acquire();
            } catch (InterruptedException e) {
                Log.w("AjaxTask", "Synchronization Error. Running Task Async");
            }
        }

        if (options.global()) {
            synchronized (globalTasks) {
                if (globalTasks.isEmpty()) {
                    $.ajaxStart();
                }
                globalTasks.add(this);
            }
        } else {
            synchronized (localTasks) {
                localTasks.add(this);
            }
        }

        if (options.beforeSend() != null) {
            if (options.context() != null)
                options.beforeSend().invoke(new $(options.context()), options);
            else
                options.beforeSend().invoke(null, options);
        }

        if (options.global())
            $.ajaxSend();

    }

    @Override
    protected TaskResponse doInBackground(Void... arg0) {
        //handle cached responses
        CachedResponse cachedResponse = URLresponses
                .get(String.format(Locale.US, "%s_?=%s", options.url(), options.dataType()));
        //handle ajax caching option
        if (cachedResponse != null) {
            if (options.cache()) {
                if (new Date().getTime() - cachedResponse.timestamp.getTime() < options.cacheTimeout()) {
                    //return cached response
                    Success s = new Success();
                    s.obj = cachedResponse.response;
                    s.reason = "cached response";
                    s.headers = null;
                    return s;
                }
            }

        }

        if (request == null) {
            String type = options.type();
            if (type == null)
                type = "GET";
            if (type.equalsIgnoreCase("DELETE")) {
                request = new HttpDelete(options.url());
            } else if (type.equalsIgnoreCase("GET")) {
                request = new HttpGet(options.url());
            } else if (type.equalsIgnoreCase("HEAD")) {
                request = new HttpHead(options.url());
            } else if (type.equalsIgnoreCase("OPTIONS")) {
                request = new HttpOptions(options.url());
            } else if (type.equalsIgnoreCase("POST")) {
                request = new HttpPost(options.url());
            } else if (type.equalsIgnoreCase("PUT")) {
                request = new HttpPut(options.url());
            } else if (type.equalsIgnoreCase("TRACE")) {
                request = new HttpTrace(options.url());
            } else if (type.equalsIgnoreCase("CUSTOM")) {
                try {
                    request = options.customRequest();
                } catch (Exception e) {
                    request = null;
                }

                if (request == null) {
                    Log.w("javaQuery.ajax",
                            "CUSTOM type set, but AjaxOptions.customRequest is invalid. Defaulting to GET.");
                    request = new HttpGet();
                }

            } else {
                //default to GET
                request = new HttpGet();
            }
        }

        Map<String, Object> args = new HashMap<String, Object>();
        args.put("options", options);
        args.put("request", request);
        EventCenter.trigger("ajaxPrefilter", args, null);

        if (options.headers() != null) {
            if (options.headers().authorization() != null) {
                options.headers()
                        .authorization(options.headers().authorization() + " " + options.getEncodedCredentials());
            } else if (options.username() != null) {
                //guessing that authentication is basic
                options.headers().authorization("Basic " + options.getEncodedCredentials());
            }

            for (Entry<String, String> entry : options.headers().map().entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        }

        if (options.data() != null) {
            try {
                Method setEntity = request.getClass().getMethod("setEntity", new Class<?>[] { HttpEntity.class });
                if (options.processData() == null) {
                    setEntity.invoke(request, new StringEntity(options.data().toString()));
                } else {
                    Class<?> dataProcessor = Class.forName(options.processData());
                    Constructor<?> constructor = dataProcessor.getConstructor(new Class<?>[] { Object.class });
                    setEntity.invoke(request, constructor.newInstance(options.data()));
                }
            } catch (Throwable t) {
                Log.w("Ajax", "Could not post data");
            }
        }

        HttpParams params = new BasicHttpParams();

        if (options.timeout() != 0) {
            HttpConnectionParams.setConnectionTimeout(params, options.timeout());
            HttpConnectionParams.setSoTimeout(params, options.timeout());
        }

        HttpClient client = new DefaultHttpClient(params);

        HttpResponse response = null;
        try {

            if (options.cookies() != null) {
                CookieStore cookies = new BasicCookieStore();
                for (Entry<String, String> entry : options.cookies().entrySet()) {
                    cookies.addCookie(new BasicClientCookie(entry.getKey(), entry.getValue()));
                }
                HttpContext httpContext = new BasicHttpContext();
                httpContext.setAttribute(ClientContext.COOKIE_STORE, cookies);
                response = client.execute(request, httpContext);
            } else {
                response = client.execute(request);
            }

            if (options.dataFilter() != null) {
                if (options.context() != null)
                    options.dataFilter().invoke(new $(options.context()), response, options.dataType());
                else
                    options.dataFilter().invoke(null, response, options.dataType());
            }

            StatusLine statusLine = response.getStatusLine();

            Function function = options.statusCode().get(statusLine);
            if (function != null) {
                if (options.context() != null)
                    function.invoke(new $(options.context()));
                else
                    function.invoke(null);
            }

            if (statusLine.getStatusCode() >= 300) {
                //an error occurred
                Error e = new Error();
                AjaxError error = new AjaxError();
                error.request = request;
                error.options = options;
                e.status = statusLine.getStatusCode();
                e.reason = statusLine.getReasonPhrase();
                error.status = e.status;
                error.reason = e.reason;
                e.headers = response.getAllHeaders();
                e.error = error;
                return e;
            } else {
                //handle dataType
                String dataType = options.dataType();
                if (dataType == null)
                    dataType = "text";
                Object parsedResponse = null;
                boolean success = true;
                try {
                    if (dataType.equalsIgnoreCase("text") || dataType.equalsIgnoreCase("html")) {
                        parsedResponse = parseText(response);
                    } else if (dataType.equalsIgnoreCase("xml")) {
                        if (options.customXMLParser() != null) {
                            InputStream is = response.getEntity().getContent();
                            if (options.SAXContentHandler() != null)
                                options.customXMLParser().parse(is, options.SAXContentHandler());
                            else
                                options.customXMLParser().parse(is, new DefaultHandler());
                            parsedResponse = "Response handled by custom SAX parser";
                        } else if (options.SAXContentHandler() != null) {
                            InputStream is = response.getEntity().getContent();

                            SAXParserFactory factory = SAXParserFactory.newInstance();

                            factory.setFeature("http://xml.org/sax/features/namespaces", false);
                            factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);

                            SAXParser parser = factory.newSAXParser();

                            XMLReader reader = parser.getXMLReader();
                            reader.setContentHandler(options.SAXContentHandler());
                            reader.parse(new InputSource(is));
                            parsedResponse = "Response handled by custom SAX content handler";
                        } else {
                            parsedResponse = parseXML(response);
                        }
                    } else if (dataType.equalsIgnoreCase("json")) {
                        parsedResponse = parseJSON(response);
                    } else if (dataType.equalsIgnoreCase("script")) {
                        parsedResponse = parseScript(response);
                    } else if (dataType.equalsIgnoreCase("image")) {
                        parsedResponse = parseImage(response);
                    }
                } catch (ClientProtocolException cpe) {
                    if (options.debug())
                        cpe.printStackTrace();
                    success = false;
                    Error e = new Error();
                    AjaxError error = new AjaxError();
                    error.request = request;
                    error.options = options;
                    e.status = statusLine.getStatusCode();
                    e.reason = statusLine.getReasonPhrase();
                    error.status = e.status;
                    error.reason = e.reason;
                    e.headers = response.getAllHeaders();
                    e.error = error;
                    return e;
                } catch (Exception ioe) {
                    if (options.debug())
                        ioe.printStackTrace();
                    success = false;
                    Error e = new Error();
                    AjaxError error = new AjaxError();
                    error.request = request;
                    error.options = options;
                    e.status = statusLine.getStatusCode();
                    e.reason = statusLine.getReasonPhrase();
                    error.status = e.status;
                    error.reason = e.reason;
                    e.headers = response.getAllHeaders();
                    e.error = error;
                    return e;
                }
                if (success) {
                    //Handle cases where successful requests still return errors (these include
                    //configurations in AjaxOptions and HTTP Headers
                    String key = String.format(Locale.US, "%s_?=%s", options.url(), options.dataType());
                    CachedResponse cache = URLresponses.get(key);
                    Date now = new Date();
                    //handle ajax caching option
                    if (cache != null) {
                        if (options.cache()) {
                            if (now.getTime() - cache.timestamp.getTime() < options.cacheTimeout()) {
                                parsedResponse = cache;
                            } else {
                                cache.response = parsedResponse;
                                cache.timestamp = now;
                                synchronized (URLresponses) {
                                    URLresponses.put(key, cache);
                                }
                            }
                        }

                    }
                    //handle ajax ifModified option
                    Header[] lastModifiedHeaders = response.getHeaders("last-modified");
                    if (lastModifiedHeaders.length >= 1) {
                        try {
                            Header h = lastModifiedHeaders[0];
                            SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
                            Date lastModified = format.parse(h.getValue());
                            if (options.ifModified() && lastModified != null) {
                                if (cache.lastModified != null && cache.lastModified.compareTo(lastModified) == 0) {
                                    //request response has not been modified. 
                                    //Causes an error instead of a success.
                                    Error e = new Error();
                                    AjaxError error = new AjaxError();
                                    error.request = request;
                                    error.options = options;
                                    e.status = statusLine.getStatusCode();
                                    e.reason = statusLine.getReasonPhrase();
                                    error.status = e.status;
                                    error.reason = e.reason;
                                    e.headers = response.getAllHeaders();
                                    e.error = error;
                                    Function func = options.statusCode().get(304);
                                    if (func != null) {
                                        if (options.context() != null)
                                            func.invoke(new $(options.context()));
                                        else
                                            func.invoke(null);
                                    }
                                    return e;
                                } else {
                                    cache.lastModified = lastModified;
                                    synchronized (URLresponses) {
                                        URLresponses.put(key, cache);
                                    }
                                }
                            }
                        } catch (Throwable t) {
                            Log.e("Ajax", "Could not parse Last-Modified Header");
                        }

                    }

                    //Now handle a successful request

                    Success s = new Success();
                    s.obj = parsedResponse;
                    s.reason = statusLine.getReasonPhrase();
                    s.headers = response.getAllHeaders();
                    return s;
                }
                //success
                Success s = new Success();
                s.obj = parsedResponse;
                s.reason = statusLine.getReasonPhrase();
                s.headers = response.getAllHeaders();
                return s;
            }

        } catch (Throwable t) {
            if (options.debug())
                t.printStackTrace();
            if (t instanceof java.net.SocketTimeoutException) {
                Error e = new Error();
                AjaxError error = new AjaxError();
                error.request = request;
                error.options = options;
                e.status = 0;
                String reason = t.getMessage();
                if (reason == null)
                    reason = "Socket Timeout";
                e.reason = reason;
                error.status = e.status;
                error.reason = e.reason;
                if (response != null)
                    e.headers = response.getAllHeaders();
                else
                    e.headers = new Header[0];
                e.error = error;
                return e;
            }
            return null;
        }
    }

    @Override
    public void onPostExecute(TaskResponse response) {
        if (!options.async()) {
            mutex.release();
        }
        if (response == null) {
            if (options.error() != null) {
                AjaxError error = new AjaxError();
                error.request = request;
                error.status = 0;
                error.options = options;
                error.reason = "null response";
                //invoke error with Request, Status, and Error
                if (options.context() != null)
                    options.error().invoke(new $(options.context()), error, 0, "null response");
                else
                    options.error().invoke(null, error, 0, "null response");
            }

            if (options.global())
                $.ajaxError();
        } else if (response instanceof Error) {
            if (options.error() != null) {
                //invoke error with Request, Status, and Error
                Error e = (Error) response;
                if (options.context() != null)
                    options.error().invoke(new $(options.context()), e.error, e.status, e.reason, e.headers);
                else
                    options.error().invoke(null, e.error, e.status, e.reason, e.headers);
            }

            if (options.global())
                $.ajaxError();
        } else if (response instanceof Success) {
            Success s = (Success) response;
            if (options.success() != null) {
                //invoke success with parsed response and the status string
                if (options.context() != null)
                    options.success().invoke(new $(options.context()), s.obj, s.reason, s.headers);
                else
                    options.success().invoke(null, s.obj, s.reason, s.headers);
            }

            if (options.global())
                $.ajaxSuccess();
        }

        if (options.complete() != null) {
            if (response != null) {
                if (options.context() != null)
                    options.complete().invoke(new $(options.context()), response.reason, response.headers);
                else
                    options.complete().invoke(null, response.reason, response.headers);
            } else {
                if (options.context() != null)
                    options.complete().invoke(new $(options.context()), "null response");
                else
                    options.complete().invoke(null, "null response");
            }
        }
        if (options.global())
            $.ajaxComplete();

        if (options.global()) {
            synchronized (globalTasks) {
                globalTasks.remove(this);
                if (globalTasks.isEmpty()) {
                    $.ajaxStop();
                }
            }
        } else {
            synchronized (localTasks) {
                localTasks.remove(this);
            }
        }
    }

    /**
     * Parses the HTTP response as JSON representation
     * @param response the response to parse
     * @return a JSONObject response
     */
    private Object parseJSON(HttpResponse response) throws ClientProtocolException, IOException {
        JSONResponseHandler handler = new JSONResponseHandler();
        return handler.handleResponse(response);
    }

    /**
     * Parses the HTTP response as XML representation
     * @param response the response to parse
     * @return an XML Document response
     */
    private Document parseXML(HttpResponse response) throws ClientProtocolException, IOException {
        XMLResponseHandler handler = new XMLResponseHandler();
        return handler.handleResponse(response);
    }

    /**
     * Parses the HTTP response as Text
     * @param response the response to parse
     * @return a String response
     */
    private String parseText(HttpResponse response) throws ClientProtocolException, IOException {
        BasicResponseHandler handler = new BasicResponseHandler();
        return handler.handleResponse(response);
    }

    /**
     * Parses the HTTP response as a Script, then runs it.
     * @param response the response to parse
     * @return a ScriptResponse Object containing the output String, if any, as well as the original
     * Script
     */
    private ScriptResponse parseScript(HttpResponse response) throws ClientProtocolException, IOException {
        ScriptResponseHandler handler = new ScriptResponseHandler();
        return handler.handleResponse(response);
    }

    /**
     * Parses the HTTP response as an Image Object
     * @param response the response to parse
     * @return an Image Object containing the retrieved Image
     * @throws ClientProtocolException
     * @throws IOException
     */
    private Image parseImage(HttpResponse response) throws ClientProtocolException, IOException {
        return ImageIO.read(response.getEntity().getContent());
    }

    /**
     * Defines a response to a Task
     * @see Error
     * @see Success
     */
    class TaskResponse {
        /** The reason text */
        public String reason;
        /** The status ID */
        public int status;
        /** The response Headers. If a cached response is returned, {@code headers} will be {@code null}. */
        public Header[] headers;
    }

    /**
     * Response for tasks that run into an error or exception
     */
    class Error extends TaskResponse {
        /** The response Object */
        public AjaxError error;
    }

    /**
     * Response for tasks that complete successfully
     */
    class Success extends TaskResponse {
        /** The response Object */
        public Object obj;
    }

    /**
     * This is the first object that is returned when an Error occurs for an Ajax Request
     * @see AjaxTask#AjaxTask(HttpUriRequest, AjaxOptions)
     */
    public static class AjaxError {
        /** The original request */
        public HttpUriRequest request;
        /** The original options */
        public AjaxOptions options;
        /** The error status code */
        public int status;
        /** The error string */
        public String reason;
    }
}