/* GNU GENERAL LICENSE Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2017 Lobo Evolution This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General License for more details. You should have received a copy of the GNU General Public along with this program. If not, see <http://www.gnu.org/licenses/>. Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it */ package org.lobobrowser.http; import java.awt.Image; import java.awt.Toolkit; import java.beans.PropertyChangeListener; import java.io.ByteArrayOutputStream; import java.lang.ref.WeakReference; import java.net.URL; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.lobobrowser.xpath.XPathUtils; import org.w3c.dom.Document; /** * The Class HttpRequestImpl. */ public class HttpRequest extends AbstractBean { /** * The ReadyState of this HttpRequestImpl. */ private ReadyState readyState = ReadyState.UNINITIALIZED; /** * If the readyState attribute has a value other than RECEIVING or LOADED, * reponseText MUST be the empty string. Otherwise, it MUST be the fragment * of the entity body received so far (when readyState is RECEIVING) or the * complete entity body (when readyState is LOADED), interpreted as a stream * of characters. */ private String responseText; /** * If the status attribute is not available it MUST raise an exception. It * MUST be available when readyState is RECEIVING or LOADED. When available, * it MUST represent the HTTP status code. */ private int status; /** * If the statusText attribute is not available, it MUST raise an exception. * It MUST be available when readyState is RECEIVING or LOADED. When * available, it MUST represent the HTTP status text sent by the server */ private String statusText; /** * Worker class used to actually perform the background tasks. */ private AsyncWorker worker; /** * Flag used to indicate whether the task should be run asynchronously or * not. This is reset in the open() method. */ /** The image ref. */ private WeakReference<Image> imageRef; /** The buffer. */ private ByteArrayOutputStream buffer; /** The response xml. */ private Document responseXML; /** The async flag. */ private boolean asyncFlag; /** The s. */ private Session s; /** The exception. */ private Exception exception; /** The on ready state change. */ private PropertyChangeListener onReadyStateChange; /** The req. */ private Request req = new Request(); // the request. Reset in reset(). Never // null. /* private EventListenerList listenerList = new EventListenerList(); */ // -------------------------------------------------------- Constructors /** * Creates a new instance of HttpRequestImpl. */ public HttpRequest() { } // -------------------------------------------------------- Bean methods /** Sets the session. * * @param s * the new session */ public void setSession(Session s) { Session old = getSession(); this.s = s; firePropertyChange("session", old, getSession()); } /** Gets the session. * * @return the session */ public Session getSession() { return s; } // ------------------------------------ Methods as per the specification /** Sets the on ready state change. * * @param listener * the new on ready state change */ public void setOnReadyStateChange(PropertyChangeListener listener) { PropertyChangeListener old = getOnReadyStateChange(); removeReadyStateChangeListener(old); addReadyStateChangeListener(listener); onReadyStateChange = listener; firePropertyChange("onReadyStateChange", old, listener); } /** Gets the on ready state change. * * @return the on ready state change */ public final PropertyChangeListener getOnReadyStateChange() { return onReadyStateChange; } /** Gets the ReadyState of this HttpRequestImpl. * * @return the ReadyState of this HttpRequestImpl */ public final ReadyState getReadyState() { return readyState; } /** * Initializes the HttpRequestImpl prior to sending a request. */ public void open(String method, URL url) { open(method, url, true); } /** * Initializes the HttpRequestImpl prior to sending a request. */ public void open(String method, URL url, boolean asyncFlag) { open(method, url, asyncFlag, null, null); } /** * Initializes the HttpRequestImpl prior to sending a request. Calling this * method initializes HttpRequestImpl by storing the method, uri, asyncFlag, * userName, and password values. This also sets the readyState to OPEN, * resets the response state to their initial values, and resets the request * headers. * * If open() is called when readyState is LOADED, then the entire object is * reset. (huh?) * * NOTE: Private because authentication is not yet implemented (and indeed, * I don't know yet how to implement it.) * * @param method * @param uri * @param asyncFlag * Defaults to "true" * @param userName * Defaults to null * @param password * Defaults to null */ public void open(String method, URL url, boolean asyncFlag, String username, String password) { if (worker != null && (readyState == ReadyState.SENT || readyState == ReadyState.RECEIVING)) { throw new IllegalStateException( "You must abort() the current task, or wait for it to " + "finish completing before starting a new one!"); } reset(); this.asyncFlag = asyncFlag; this.req.setUrl(url.toString()); this.req.setMethod(method); req.setUsername(username); req.setPassword(password); setReadyState(ReadyState.OPEN); } /** * Specifies a request header for the HTTP request. * * @param header * @param value */ public void setRequestHeader(String header, String value) { if (getReadyState() != ReadyState.OPEN) { throw new IllegalStateException( "The HttpRequestImpl must be opened prior to " + "setting a request header"); } // TODO // if the header argument doesn't match the "field-name production", // throw an illegal argument exception // if the value argument doesn't match the "field-value production", // throw an illegal argument exception if (header == null || value == null) { throw new IllegalArgumentException( "Neither the header, nor value, may be null"); } // NOTE: The spec says, nothing should be done if the header argument // matches: // Accept-Charset, Accept-Encoding, Content-Length, Expect, Date, Host, // Keep-Alive, // Referer, TE, Trailer, Transfer-Encoding, Upgrade // The spec says this for security reasons, but I don't understand why? // I'll follow // the spec's suggestion until I know more (can always allow more // headers, but // restricting them is more painful). Note that Session doesn't impose // any such // restrictions, so you can always set "Accept-Encoding" etc on the // Session... // except that Session has no way to set these at the moment, except via // a Request. if (header.equalsIgnoreCase("Accept-Charset") || header.equalsIgnoreCase("Accept-Encoding") || header.equalsIgnoreCase("Content-Length") || header.equalsIgnoreCase("Expect") || header.equalsIgnoreCase("Date") || header.equalsIgnoreCase("Host") || header.equalsIgnoreCase("Keep-Alive") || header.equalsIgnoreCase("Referer") || header.equalsIgnoreCase("TE") || header.equalsIgnoreCase("Trailer") || header.equalsIgnoreCase("Transfer-Encoding") || header.equalsIgnoreCase("Upgrade")) { // ignore the header } if (header.equalsIgnoreCase("Authorization") || header.equalsIgnoreCase("Content-Base") || header.equalsIgnoreCase("Content-Location") || header.equalsIgnoreCase("Content-MD5") || header.equalsIgnoreCase("Content-Range") || header.equalsIgnoreCase("Content-Type") || header.equalsIgnoreCase("Content-Version") || header.equalsIgnoreCase("Delta-Base") || header.equalsIgnoreCase("Depth") || header.equalsIgnoreCase("Destination") || header.equalsIgnoreCase("ETag") || header.equalsIgnoreCase("Expect") || header.equalsIgnoreCase("From") || header.equalsIgnoreCase("If-Modified-Since") || header.equalsIgnoreCase("If-Range") || header.equalsIgnoreCase("If-Unmodified-Since") || header.equalsIgnoreCase("Max-Forwards") || header.equalsIgnoreCase("MIME-Version") || header.equalsIgnoreCase("Overwrite") || header.equalsIgnoreCase("Proxy-Authorization") || header.equalsIgnoreCase("SOAPAction") || header.equalsIgnoreCase("Timeout")) { // replace the current header, if any for (Header h : req.getHeaders()) { if (h.getName().equalsIgnoreCase(header)) { req.removeHeader(h); req.setHeader(new Header(header, value)); break; } } } else { // append the value to the header, if one is already specified. // Else, // just add it as a new header boolean appended = false; for (Header h : req.getHeaders()) { if (h.getName().equalsIgnoreCase(header)) { req.removeHeader(h); req.setHeader( new Header(header, h.getValue() + ", " + value)); appended = true; break; } } if (!appended) { req.setHeader(new Header(header, value)); } } } /** * Sends the request to the server. If the readyState property has a value * other than OPEN, then an IllegalStateException will be thrown. At the * beginning of this method the readyState will be set to SENT. If the async * flag is set to false, then the method will not return until the request * has completed (ie: the method will block). Otherwise, a background task * is used and this method will return immediately. * * Note: Authors should specify the Content-Type header via setRequestHeader * before invoking send() with an argument. * * Redirects must be followed. * * Proxies should be supported * * Authentication should be supported. * * State management (cookies)? * * caching(?) * * et etc * * Immediately before receiving the message body (if any), the readyState * attribute will be changed to RECEIVING. When the request has completed * loading, the readyState attribute will be set to LOADED. In case of a * HEAD request, readyState will be set to LOADED immediately after having * been set to RECEIVING. */ public void send() { send((String) null); } /** * @param content */ public void send(String content) { if (readyState != ReadyState.OPEN) { throw new IllegalStateException( "HttpRequestImpl must be in an OPEN state before " + "invokation of the send() method"); } worker = createAsyncWorker(content); worker.sendRequest(getSession(), content); } /** * @param dom */ public void send(Document dom) { // convert the dom to a String, and send that if (dom == null) { send((String) null); } else { send(XPathUtils.toXML(dom)); } } /** * Cancels any network activity and resets the object. */ public void abort() { if (worker != null) { worker.cancel(true); worker = null; } reset(); }; /** Gets the all response headers. * * @return the all response headers */ public String getAllResponseHeaders() { if (readyState == ReadyState.RECEIVING || readyState == ReadyState.LOADED) { StringBuffer buffer = new StringBuffer(); for (Header header : worker.response.getHeaders()) { buffer.append(header.toString()); buffer.append("\r\n"); } return buffer.toString(); } else { return null; } } /** * <p> * Gets a single response header as a string. * </p> * * <p> * If the readyState property has a value other than RECEIVING or LOADED, * this method will return null. Otherwise, it will represent the value of * the given HTTP header in the data received so far from the last request * sent, as a single string. If more than one header of the given name was * received, then the values will be concatenated, separated from each other * by a comma followed by a single space. If no headers of that name were * received, then it will return the empty String. * </p> * * @param headerLabel * the label of the response header to retreive. * @return the response header corrosponding to the provided label */ public String getResponseHeader(String headerLabel) { if (readyState == ReadyState.RECEIVING || readyState == ReadyState.LOADED) { Header header = worker.response.getHeader(headerLabel); return header == null ? null : header.getValue(); } else { return null; } } /** Gets the if the readyState attribute has a value other than RECEIVING * or LOADED, reponseText MUST be the empty string. * * @return the if the readyState attribute has a value other than RECEIVING * or LOADED, reponseText MUST be the empty string */ public String getResponseText() { if (readyState == ReadyState.RECEIVING) { return responseText == null ? "" : responseText; } else if (readyState == ReadyState.LOADED) { return responseText; } else { return ""; } } /** Gets the if the status attribute is not available it MUST raise an * exception. * * @return the if the status attribute is not available it MUST raise an * exception */ public int getStatus() { if (readyState == ReadyState.RECEIVING || readyState == ReadyState.LOADED) { return status; } else { throw new IllegalStateException("You cannot call getStatus() unless" + " readyState == RECEIVING || LOADING"); } } /** Gets the if the statusText attribute is not available, it MUST raise * an exception. * * @return the if the statusText attribute is not available, it MUST raise * an exception */ public String getStatusText() { if (readyState == ReadyState.RECEIVING || readyState == ReadyState.LOADED) { return statusText; } else { throw new IllegalStateException( "You cannot call getStatusText() unless" + " readyState == RECEIVING || LOADING"); } } // --------------------------- Optional methods as per the specification /** Sets the follows redirects. * * @param flag * the new follows redirects */ public void setFollowsRedirects(boolean flag) { if (readyState != ReadyState.OPEN) { throw new IllegalStateException( "The request must be OPEN before setting the follows redirects flag"); } req.setFollowRedirects(flag); } /** Gets the follow redirects. * * @return the follow redirects */ public final boolean getFollowRedirects() { return req.getFollowRedirects(); } /* * public void setTimeout(long timeout) { if (readyState != ReadyState.OPEN) * { throw new IllegalStateException( * "The request must be OPEN before setting the timeout"); } timeout = * timeout < 0 ? -1 : timeout; } public final long getTimeout() { return * timeout; } */ // ------------------------------------------------- Convenience methods /** Gets the exception. * * @return the exception */ public Exception getException() { if (readyState == ReadyState.LOADED) { return exception; } else { return null; } } /** * Returns the Parameter with the given name, or null if there is no such * Parameter. These are reset whenever this HttpRequestImpl is reset. * * @param name * the name to look for. This must not be null. * @return the Parameter with the given name. */ public Parameter getParameter(String name) { return req.getParameter(name); } /** Sets the parameter. * * @param param * the new parameter */ public void setParameter(Parameter param) { req.setParameter(param); } /** * Adds the given parameter to the set of parameters. These are reset * whenever this HttpRequestImpl is reset. This is a convenience method. * * @param name * the name of the parameter * @param value * the value of the parameter */ public void setParameter(String name, String value) { setParameter(new Parameter(name, value)); } /** Gets the parameters. * * @return the parameters */ public Parameter[] getParameters() { return req.getParameters(); } /** Sets the parameters. * * @param params * the new parameters */ public void setParameters(Parameter... params) { req.setParameters(params); } // -------------- Event Listener public void addReadyStateChangeListener(PropertyChangeListener listener) { super.addPropertyChangeListener("readyState", listener); } public void removeReadyStateChangeListener( PropertyChangeListener listener) { super.removePropertyChangeListener("readyState", listener); } /** Gets the ready state change listeners. * * @return the ready state change listeners */ public PropertyChangeListener[] getReadyStateChangeListeners() { return super.getPropertyChangeListeners("readyState"); } /* * public void addAsyncRequestListener(AsyncRequestListener listener) { * listenerList.add(AsyncRequestListener.class, listener); } public void * removeAsyncRequestListener(AsyncRequestListener listener) { * listenerList.remove(AsyncRequestListener.class, listener); } public * AsyncRequestListener[] getAsyncRequestListeners() { return * listenerList.getListeners(AsyncRequestListener.class); } TODO */ // --------------------------------------------------- Protected Methods protected AsyncWorker createAsyncWorker(String content) { return new AsyncWorker(); } /** * Clears any response state and resets the readyState to UNINITIALIZED. Any * overriding implementations MUST call super.reset() at the end of the * implementation. */ protected void reset() { /* * userName = null; password = null; timeout = -1; */ exception = null; req = new Request(); req.setFollowRedirects(false); String old = responseText; responseText = null; firePropertyChange("responseText", old, null); setStatus(-1); setStatusText(null); setResponseXML(null); setReadyState(ReadyState.UNINITIALIZED); } /** * Method that provides a hook for subclasses to create concrete types (such * as DOM, JSONObject, etc) when the response has been fully read. There is * no need to call super.handleResponse(txt). */ protected void handleResponse(String responseText) throws Exception { } // ---------------------------------------------- Private helper methods /** Sets the ReadyState of this HttpRequestImpl. * * @param state * the new ReadyState of this HttpRequestImpl */ private void setReadyState(ReadyState state) { ReadyState old = this.readyState; this.readyState = state; firePropertyChange("readyState", old, this.readyState); } /** Sets the if the status attribute is not available it MUST raise an * exception. * * @param status * the new if the status attribute is not available it MUST raise * an exception */ private void setStatus(int status) { int old = this.status; this.status = status; firePropertyChange("status", old, this.status); } /** Sets the if the statusText attribute is not available, it MUST raise * an exception. * * @param text * the new if the statusText attribute is not available, it MUST * raise an exception */ private void setStatusText(String text) { String old = this.statusText; this.statusText = text; firePropertyChange("statusText", old, this.statusText); } /** Sets the response xml. * * @param dom * the new response xml */ private void setResponseXML(Document dom) { Document old = this.responseXML; this.responseXML = dom; firePropertyChange("responseXML", old, this.responseXML); } /* * private void fireOnLoadEvent() { for (AsyncRequestListener l : * getAsyncRequestListeners()) { l.onLoad(); } } private void fireOnError() * { for (AsyncRequestListener l : getAsyncRequestListeners()) { * l.onError(); } } private void fireOnError() { for (AsyncRequestListener l * : getAsyncRequestListeners()) { l.onError(); } } private void * fireOnProgress() { for (AsyncRequestListener l : * getAsyncRequestListeners()) { l.onProgress(); } } private void * fireOnAbort() { for (AsyncRequestListener l : getAsyncRequestListeners()) * { l.onAbort(); } } private void fireOnTimeout() { for * (AsyncRequestListener l : getAsyncRequestListeners()) { l.onTimeout(); } * TODO } */ /** The Class AsyncWorker. */ // -------------- Private impl details protected class AsyncWorker extends SwingWorker<Object, Object> { /** The data. */ private String data; /** The s. */ private Session s; /** The response. */ private Response response; private void sendRequest(Session s, String data) { this.s = s == null ? new Session() : s; safeSetReadyState(ReadyState.SENT); this.data = data; if (asyncFlag) { execute(); // puts on queue, async } else { run(); // blocks done(); } } @Override protected Object doInBackground() throws Exception { try { // TODO!!! // connection timeout // another thread is used (java.util.Timer) to keep track of a // timeout. // if the timeout occurs, then this thread is cancelled. // k. Bundle any data that needs to be sent response = null; req.setBody(data); response = s.execute(req); // TODO!!! Need to see if there is a way to set to RECEIVING // when the first bit of data // comes down, instead of waiting until the whole thing is read // (which is what I THINK // is happening here). safeSetReadyState(ReadyState.RECEIVING); // grab the resulting data responseText = response.getBody(); handleResponse(responseText); // causes the cached version of // the string to be created return responseText; } catch (Exception e) { exception = e; return null; } } /** * Helper method which allows me to set the ready state on the EDT */ protected void safeSetReadyState(final ReadyState state) { if (SwingUtilities.isEventDispatchThread()) { HttpRequest.this.setReadyState(state); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { HttpRequest.this.setReadyState(state); } }); } } @Override protected void done() { setReadyState(ReadyState.LOADED); } } /** Gets the response xml. * * @return the response xml */ public Document getResponseXML() { if (getReadyState() == ReadyState.LOADED) { return responseXML; } else { return null; } } /** Gets the response image. * * @return the response image */ public Image getResponseImage() { // A hard reference to the image is not a good idea here. // Images will retain their observers, and it's also // hard to estimate their actual size. WeakReference<Image> imageRef = this.imageRef; Image img = imageRef == null ? null : imageRef.get(); if (img == null) { byte[] bytes = this.getResponseBytes(); if (bytes != null) { img = Toolkit.getDefaultToolkit().createImage(bytes); this.imageRef = new WeakReference<Image>(img); } } return img; } /** Gets the response bytes. * * @return the response bytes */ public byte[] getResponseBytes() { ByteArrayOutputStream out = this.buffer; return out == null ? null : out.toByteArray(); } }