/*
GNU GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
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 2 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 Public License for more details.
You should have received a copy of the GNU General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: lobochief@users.sourceforge.net
*/
/*
* Created on Feb 4, 2006
*/
package org.lobobrowser.context;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.ImageObserver;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jdt.annotation.NonNull;
import org.lobobrowser.clientlet.ClientletAccess;
import org.lobobrowser.clientlet.ClientletContext;
import org.lobobrowser.clientlet.ClientletException;
import org.lobobrowser.clientlet.ClientletResponse;
import org.lobobrowser.request.RequestEngine;
import org.lobobrowser.request.RequestHandler;
import org.lobobrowser.request.SimpleRequestHandler;
import org.lobobrowser.ua.ImageResponse;
import org.lobobrowser.ua.ImageResponse.State;
import org.lobobrowser.ua.NavigatorProgressEvent;
import org.lobobrowser.ua.NetworkRequest;
import org.lobobrowser.ua.NetworkRequestEvent;
import org.lobobrowser.ua.NetworkRequestListener;
import org.lobobrowser.ua.ProgressType;
import org.lobobrowser.ua.RequestType;
import org.lobobrowser.ua.UserAgentContext;
import org.lobobrowser.ua.UserAgentContext.Request;
import org.lobobrowser.util.EventDispatch;
import org.lobobrowser.util.GenericEventListener;
import org.lobobrowser.util.Threads;
import org.lobobrowser.util.Urls;
import org.w3c.dom.Document;
public class NetworkRequestImpl implements NetworkRequest {
// TODO: Class not thread safe?
private static final Logger logger = Logger.getLogger(NetworkRequestImpl.class.getName());
private final EventDispatch READY_STATE_CHANGE = new EventDispatch();
private volatile int readyState = NetworkRequest.STATE_UNINITIALIZED;
private volatile LocalResponse localResponse;
final private UserAgentContext uaContext;
public NetworkRequestImpl(final UserAgentContext uaContext) {
this.uaContext = uaContext;
}
public Optional<URL> getURL() {
return Optional.of(requestURL);
}
public int getReadyState() {
return this.readyState;
}
public String getResponseText() {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getResponseText();
}
public Document getResponseXML() {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getResponseXML();
}
public @NonNull ImageResponse getResponseImage() {
final LocalResponse lr = this.localResponse;
if (lr == null) {
return new ImageResponse();
} else {
return lr.getResponseImage();
}
}
// public java.util.jar.JarFile getResponseJarFile() throws java.io.IOException {
// LocalResponse lr = this.localResponse;
// return lr == null ? null : lr.getResponseJarFile();
// }
public byte[] getResponseBytes() {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getResponseBytes();
}
public int getStatus() {
try {
final LocalResponse lr = this.localResponse;
return lr == null ? NetworkRequest.STATE_UNINITIALIZED : lr.getStatus();
} catch (final java.io.IOException ioe) {
return 0;
}
}
public String getStatusText() {
try {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getStatusText();
} catch (final java.io.IOException ioe) {
return null;
}
}
private volatile RequestHandler currentRequestHandler;
public void abort() {
this.readyState = NetworkRequest.STATE_ABORTED;
this.READY_STATE_CHANGE.fireEvent(new NetworkRequestEvent(this, this.readyState));
final RequestHandler rhToDelete = this.currentRequestHandler;
if (rhToDelete != null) {
RequestEngine.getInstance().cancelRequest(rhToDelete);
}
}
public String getAllResponseHeaders(final List<String> excludedHeadersLowerCase) {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getAllResponseHeaders(excludedHeadersLowerCase);
}
public String getResponseHeader(final String headerName) {
final LocalResponse lr = this.localResponse;
return lr == null ? null : lr.getResponseHeader(headerName);
}
public void open(final String method, final String url) throws IOException {
this.open(method, url, true);
}
public void open(final String method, final @NonNull URL url) {
this.open(method, url, true, null, null);
}
public void open(final String method, final @NonNull URL url, final boolean asyncFlag) {
this.open(method, url, asyncFlag, null, null);
}
public void open(final String method, final String url, final boolean asyncFlag) throws IOException {
final URL urlObj = Urls.createURL(null, url);
this.open(method, urlObj, asyncFlag, null, null);
}
public void open(final String method, final @NonNull URL url, final boolean asyncFlag, final String userName) {
this.open(method, url, asyncFlag, userName, null);
}
private boolean isAsynchronous = false;
private String requestMethod;
private URL requestURL;
// private String requestUserName;
// private String requestPassword;
public void open(final String method, final @NonNull URL url, final boolean asyncFlag, final String userName, final String password) {
this.isAsynchronous = asyncFlag;
this.requestMethod = method;
this.requestURL = url;
// this.requestUserName = userName;
// this.requestPassword = password;
this.changeReadyState(NetworkRequest.STATE_LOADING);
}
public void send(final String content, final Request requestType) throws IOException {
final URL requestURLLocal = this.requestURL;
if (requestURLLocal != null && uaContext.isRequestPermitted(requestType)) {
try {
final Map<String, String> requestedHeadersCopy = new HashMap<>(requestedHeaders);
final RequestHandler rhandler = new LocalRequestHandler(requestURLLocal, this.requestMethod, content, uaContext,
requestedHeadersCopy);
this.currentRequestHandler = rhandler;
try {
// TODO: Username and password support
if (this.isAsynchronous) {
RequestEngine.getInstance().scheduleRequest(rhandler);
} else {
RequestEngine.getInstance().inlineRequest(rhandler);
}
} finally {
this.currentRequestHandler = null;
}
} catch (final Exception err) {
logger.log(Level.SEVERE, "open()", err);
}
} else {
abort();
}
}
public void addNetworkRequestListener(final NetworkRequestListener listener) {
this.READY_STATE_CHANGE.addListener(new GenericEventListener() {
public void processEvent(final EventObject event) {
listener.readyStateChanged((NetworkRequestEvent) event);
}
});
}
public boolean isAsnyc() {
return isAsynchronous;
}
private void changeReadyState(final int newState) {
this.readyState = newState;
this.READY_STATE_CHANGE.fireEvent(new NetworkRequestEvent(this, newState));
}
private void setResponse(final ClientletResponse response) {
final Runnable runnable = () -> {
if (response.isFromCache()) {
final Object cachedResponse = response.getTransientCachedObject();
if (cachedResponse instanceof CacheableResponse) {
// It can be of a different type.
final CacheableResponse cr = (CacheableResponse) cachedResponse;
this.changeReadyState(NetworkRequest.STATE_LOADING);
this.localResponse = cr.newLocalResponse(response);
this.changeReadyState(NetworkRequest.STATE_LOADED);
this.changeReadyState(NetworkRequest.STATE_INTERACTIVE);
this.changeReadyState(NetworkRequest.STATE_COMPLETE);
return;
}
}
try {
this.changeReadyState(NetworkRequest.STATE_LOADING);
final LocalResponse newResponse = new LocalResponse(response);
this.localResponse = newResponse;
this.changeReadyState(NetworkRequest.STATE_LOADED);
final int cl = response.getContentLength();
final InputStream in = response.getInputStream();
final int bufferSize = cl == -1 ? 8192 : Math.min(cl, 8192);
final byte[] buffer = new byte[bufferSize];
int numRead;
int readSoFar = 0;
boolean firstTime = true;
final ClientletContext threadContext = ClientletAccess.getCurrentClientletContext();
NavigatorProgressEvent prevProgress = null;
if (threadContext != null) {
prevProgress = threadContext.getProgressEvent();
}
try {
long lastProgress = 0;
while ((numRead = in.read(buffer)) != -1) {
if (numRead == 0) {
if (logger.isLoggable(Level.INFO)) {
logger.info("setResponse(): Read zero bytes from " + response.getResponseURL());
}
break;
}
readSoFar += numRead;
if (threadContext != null) {
final long currentTime = System.currentTimeMillis();
if ((currentTime - lastProgress) > 500) {
lastProgress = currentTime;
threadContext.setProgressEvent(ProgressType.CONTENT_LOADING, readSoFar, cl, response.getResponseURL());
}
}
newResponse.writeBytes(buffer, 0, numRead);
if (firstTime) {
firstTime = false;
this.changeReadyState(NetworkRequest.STATE_INTERACTIVE);
}
}
} finally {
if (threadContext != null) {
threadContext.setProgressEvent(prevProgress);
}
}
newResponse.setComplete(true);
// The following should return non-null if the response is complete.
final CacheableResponse cacheable = newResponse.getCacheableResponse();
if (cacheable != null) {
response.setNewTransientCachedObject(cacheable, cacheable.getEstimatedSize());
}
this.changeReadyState(NetworkRequest.STATE_COMPLETE);
} catch (final IOException ioe) {
logger.log(Level.WARNING, "setResponse()", ioe);
this.localResponse = null;
this.changeReadyState(NetworkRequest.STATE_COMPLETE);
}
};
if (isAsynchronous) {
// TODO: Use the JS queue to schedule this
runnable.run();
} else {
runnable.run();
}
}
private class LocalRequestHandler extends SimpleRequestHandler {
private final String method;
private final Map<String, String> requestedHeadersCopy;
public LocalRequestHandler(final @NonNull URL url, final String method, final String altPostData, final UserAgentContext uaContext,
final Map<String, String> requestedHeaders) {
super(url, method, altPostData, RequestType.ELEMENT, uaContext);
this.method = method;
this.requestedHeadersCopy = requestedHeaders;
}
@Override
public String getLatestRequestMethod() {
return this.method;
}
/*
* (non-Javadoc)
*
* @see
* net.sourceforge.xamj.http.BaseRequestHandler#handleException(java.net
* .URL, java.lang.Exception)
*/
@Override
public boolean handleException(final ClientletResponse response, final Throwable exception, final RequestType requestType)
throws ClientletException {
logger.log(Level.WARNING, "handleException(): url=" + this.getLatestRequestURL() + ",response=[" + response + "]", exception);
NetworkRequestImpl.this.abort();
return true;
}
/*
* (non-Javadoc)
*
* @see
* net.sourceforge.xamj.http.BaseRequestHandler#processResponse(org.xamjwg
* .clientlet.ClientletResponse)
*/
public void processResponse(final ClientletResponse response) throws ClientletException, IOException {
NetworkRequestImpl.this.setResponse(response);
}
/*
* (non-Javadoc)
*
* @see net.sourceforge.xamj.http.RequestHandler#handleProgress(int,
* java.net.URL, int, int)
*/
// public void handleProgress(final org.lobobrowser.ua.ProgressType progressType, final URL url, final int value, final int max) {
// }
@Override
public Optional<Map<String, String>> getRequestedHeaders() {
return Optional.of(requestedHeadersCopy);
}
}
private static class CacheableResponse {
private WeakReference<Image> imageRef;
private java.io.ByteArrayOutputStream buffer;
private Document document;
private String textContent;
private boolean complete;
public int getEstimatedSize() {
final ByteArrayOutputStream out = this.buffer;
final int factor = 3;
// Note that when this is called, no one has
// necessarily called getResponseText().
return ((out == null ? 0 : out.size()) * factor) + 512;
}
public LocalResponse newLocalResponse(final ClientletResponse response) {
return new LocalResponse(response, this);
}
public @NonNull ImageResponse 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.
final WeakReference<Image> imageRef = this.imageRef;
Image img = imageRef == null ? null : imageRef.get();
if (this.complete) {
if (img == null) {
final byte[] bytes = this.getResponseBytes();
img = Toolkit.getDefaultToolkit().createImage(bytes);
Toolkit.getDefaultToolkit().prepareImage(img, -1, -1, null);
int checkedFlags = Toolkit.getDefaultToolkit().checkImage(img, -1, -1, null);
while (!isImgDone(checkedFlags)) {
checkedFlags = Toolkit.getDefaultToolkit().checkImage(img, -1, -1, null);
Threads.sleep(33);
}
if ((checkedFlags & ImageObserver.ERROR) != 0) {
return new ImageResponse(State.error, null);
} else {
this.imageRef = new WeakReference<>(img);
return new ImageResponse(State.loaded, img);
}
} else {
return new ImageResponse(State.loaded, img);
}
} else {
return new ImageResponse();
}
}
private static boolean isImgDone(final int checkedFlags) {
return ((checkedFlags & ImageObserver.ERROR) != 0) || ((checkedFlags & (ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0);
}
public String getResponseText(final String charset) {
String responseText = this.textContent;
if (responseText != null) {
return responseText;
}
final byte[] bytes = this.getResponseBytes();
if (bytes == null) {
return null;
}
try {
responseText = new String(bytes, charset);
} catch (final UnsupportedEncodingException uee) {
logger.log(Level.WARNING, "getResponseText()", uee);
try {
responseText = new String(bytes, "ISO-8859-1");
} catch (final UnsupportedEncodingException uee2) {
// ignore
}
}
this.textContent = responseText;
return responseText;
}
/**
* @return Returns the responseBytes.
*/
public byte[] getResponseBytes() {
final ByteArrayOutputStream out = this.buffer;
return out == null ? null : out.toByteArray();
}
public Document getResponseXML() {
Document doc = this.document;
// TODO: GH #138
// Although the following works, it has two issues
// 1. It returns an internal class (com.sun.*) after parsing, and security policy has not given permission yet
// 2. Even if permission is given, need to check if it will work
/*
if ((doc == null) && this.complete) {
final byte[] bytes = this.getResponseBytes();
if (bytes != null) {
final InputStream in = new ByteArrayInputStream(bytes);
try {
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in);
} catch (final Exception err) {
logger.log(Level.SEVERE, "getResponseXML()", err);
}
this.document = doc;
}
}*/
return doc;
}
}
private static class LocalResponse {
private final ClientletResponse cresponse;
private final CacheableResponse cacheable;
// Caching fields:
private Map<String, String> headers;
/**
* @param status
* @param text
* @param bytes
* @param headers
*/
public LocalResponse(final ClientletResponse response) {
this.cresponse = response;
this.cacheable = new CacheableResponse();
}
public LocalResponse(final ClientletResponse response, final CacheableResponse cacheable) {
this.cresponse = response;
this.cacheable = cacheable;
}
public CacheableResponse getCacheableResponse() {
final CacheableResponse c = this.cacheable;
if (!c.complete) {
return null;
}
return c;
}
public void writeBytes(final byte[] bytes, final int offset, final int length) throws java.io.IOException {
ByteArrayOutputStream out = this.cacheable.buffer;
if (out == null) {
out = new ByteArrayOutputStream();
this.cacheable.buffer = out;
}
out.write(bytes, offset, length);
}
public void setComplete(final boolean complete) {
this.cacheable.complete = complete;
}
public Map<String, String> getHeaders() {
Map<String, String> h = this.headers;
if (h == null) {
h = this.getHeadersImpl();
this.headers = h;
}
return h;
}
private Map<String, String> getHeadersImpl() {
final Map<String, String> headers = new HashMap<>();
final ClientletResponse cresponse = this.cresponse;
final Iterator<String> headerNames = cresponse.getHeaderNames();
while (headerNames.hasNext()) {
final String headerName = headerNames.next();
if (headerName != null) {
final String[] values = cresponse.getHeaders(headerName);
if ((values != null) && (values.length > 0)) {
headers.put(headerName.toLowerCase(), values[0]);
}
}
}
return headers;
}
// public int getLength() {
// final ByteArrayOutputStream out = this.cacheable.buffer;
// return out == null ? 0 : out.size();
// }
/**
* @return Returns the status.
*/
public int getStatus() throws IOException {
return this.cresponse.getResponseCode();
}
/**
* @return Returns the statusText.
*/
public String getStatusText() throws IOException {
return this.cresponse.getResponseMessage();
}
public String getResponseHeader(final String headerName) {
return this.getHeaders().get(headerName.toLowerCase());
}
public String getAllResponseHeaders(final List<String> excludedHeadersLowerCase) {
final ClientletResponse cresponse = this.cresponse;
final Iterator<String> headerNames = cresponse.getHeaderNames();
final StringBuffer allHeadersBuf = new StringBuffer();
while (headerNames.hasNext()) {
final String headerName = headerNames.next();
if (headerName != null) {
if (!excludedHeadersLowerCase.contains(headerName.toLowerCase())) {
final String[] values = cresponse.getHeaders(headerName);
for (final String value : values) {
allHeadersBuf.append(headerName);
allHeadersBuf.append(": ");
allHeadersBuf.append(value);
allHeadersBuf.append("\r\n");
}
}
}
}
return allHeadersBuf.toString();
}
public String getResponseText() {
return this.cacheable.getResponseText(this.cresponse.getCharset());
}
public Document getResponseXML() {
return this.cacheable.getResponseXML();
}
public @NonNull ImageResponse getResponseImage() {
return this.cacheable.getResponseImage();
}
public byte[] getResponseBytes() {
// TODO: OPTIMIZATION: When the response comes from the RAM cache,
// there's no need to build a custom buffer here.
return this.cacheable.getResponseBytes();
}
}
private final Map<String, String> requestedHeaders = new HashMap<>();
public void addRequestedHeader(final String key, final String value) {
if ((key != null) && (value != null)) {
if (requestedHeaders.containsKey(key)) {
// Need to merge values as per https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#setRequestHeader()
final String oldValue = requestedHeaders.get(key);
requestedHeaders.put(key, oldValue + "," + key);
}
requestedHeaders.put(key, value);
}
}
}