How to cache traffic with littleproxy/netty? - java

I want to cache static files with littleproxy. So I created HashMap with key for uri and values for response body. Here's java code:
private static Map<String, FullHttpResponse> cache = new HashMap<>();
private static HttpFiltersSource getHttpFiltersSource() {
return new HttpFiltersSourceAdapter() {
#Override
public int getMaximumResponseBufferSizeInBytes() {
return 10 * 1024 * 1024;
}
public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
return new HttpFiltersAdapter(originalRequest) {
#Override
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
if (httpObject instanceof HttpRequest) {
HttpRequest request = (HttpRequest) httpObject;
String requestUri = request.getUri();
if(requestUri.matches(".*[./]png.*$") ||
requestUri.matches(".*[./]jpg.*$") ||
requestUri.matches(".*[./]jpeg.*$") ||
requestUri.matches(".*[./]woff2.*$") ||
requestUri.matches(".*[./]js.*$") ) {
if (cache.containsKey(requestUri)) {
System.out.println("GOT FROM CACHE " + requestUri);
return cache.get(requestUri);
}
}
}
return null;
}
#Override
public HttpObject serverToProxyResponse(HttpObject httpObject) {
if (httpObject instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) httpObject;
String requestUri = originalRequest.getUri();
if(requestUri.matches(".*[./]png.*$") ||
requestUri.matches(".*[./]jpg.*$") ||
requestUri.matches(".*[./]jpeg.*$") ||
requestUri.matches(".*[./]woff2.*$") ||
requestUri.matches(".*[./]js.*$") ) {
cache.put(requestUri, response.retain());
System.out.println("ADDED TO CACHE " + requestUri);
}
}
return httpObject;
}
};
}
};
}
But something wrong here with response in Map. When the browser reaches static files firstly there's a debug message in console: ADDED TO CACHE. When the browser reaches static files secondly there's a message: "GOT FROM CACHE", but browser spins forever waiting for a response.
What's the right way to save and store responses from server and return it to client when time comes?

I think you need to also duplicate the FullHttpResponse to ensure you have correct writer / reader indices.
cache.put(requestUri, response.duplicate().retain());
and:
return cache.get(requestUri).duplicate();
Also ensure that you call release() once you remove something from the cache.

Related

How to attach data to socket connection in order to get this data on `SessionDisconnectEvent`?

What I want to achieve
I want to get my string variable I am using as #DestinationVariable, called lobbyName, when socket disconnects using #EventListener on server side:
#Component
public class WebSocketEventListener {
private SimpMessageSendingOperations messagingTemplate;
public WebSocketEventListener(SimpMessageSendingOperations messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
#EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
//here I want to get my data
}
}
My problem
I have been trying to get lobbyName using SessionDisonnectEvent but I don't know how, when and where to put this lobbyName in order to have it in SessionDisconnectEvent.
What I have been trying
On Server Side:
#Controller
public class WebSocketController {
private final SimpMessagingTemplate template;
WebSocketController(SimpMessagingTemplate template) {
this.template = template;
}
public void pokeLobby(#DestinationVariable String lobbyName, SocketMessage message) {
// This didn't work
// Map<String, Object> headers = new HashMap<>();
// headers.put("lobbyName", lobbyName);
// this.template.convertAndSend("/lobby/"+lobbyName.toLowerCase(), message, headers);
this.template.convertAndSend("/lobby/"+lobbyName.toLowerCase(), message);
}
}
Is it possible to do on client side? :
connectToLobbyWebSocket(lobbyName: string): void {
const ws = new SockJS(this.addressStorage.apiAddress + '/socket');
this.stompClient = Stomp.over(ws);
// this.stompClient.debug = null;
const that = this;
this.stompClient.connect({}, function () {
that.stompClient.subscribe('/lobby/' + lobbyName, (message) => {
if (message.body) {
that.socketMessage.next(message.body); //client logic
}
});
});
}
EDIT (progress)
Since I can easily get sessionId on SessionDisconnectEvent I have decided to change sessionId (upon handshake) to something like playerId:lobbyName:uuid
I don't feel very comfortable with this solution so if you have any suggestions I am all ears.
const ws = new SockJS(this.addressStorage.apiAddress + '/socket', null, {
sessionId: function (): string {
return that.authManager.playerId + ':' + lobbyName + ':' + uuid();
}
});
You can send lobbyName in the body of the message as attribute and get it in the listner like this :
#EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String lobbyName = (String) headerAccessor.getSessionAttributes().get("lobbyName");
if(lobbyName != null) {
SocketMessage message = new SocketMessage();
messagingTemplate.convertAndSend("/topic/public/"+lobbyName, message);
}
}

HttpServletRequestWrapper removed parameter still showing up

I'm trying to filter out a query parameter named 'reason' using a Filter in java/jsp.
Basically, the filter is in place to ensure that a user has entered a 'reason' for viewing a page. If they have not, it needs to redirect them to the 'enter reason' page. Once they have entered a valid reason, they can continue on to the page they requested.
So the basics of it work. However, the 'reason' is sent via a query paremter (i.e. GET parameter). Once the user selects a reason, the reason parameter is being forwarded on to the page they wanted to see. This is a problem, since checking if the reason paremeter exists is one of the main ways the filter determines if the user can move on.
I've tried extending HttpServletRequestWrapper, and overrode a bunch of methods (i.e. getPameter, etc) in an effort to remove the 'reason' parameter. However, I haven't been able to see the parameter get removed. Once the Filter forwards on to the requested page, the 'reason' parameter is always in the query string (i.e. the url in the browser url bar) as a GET parameter.
My filter class looks like:
public final class AccessRequestFilter implements Filter {
public class FilteredRequest extends HttpServletRequestWrapper {
public FilteredRequest(ServletRequest request) {
super((HttpServletRequest)request);
}
#Override
public String getParameter(String paramName) {
String value = super.getParameter(paramName);
if ("reason".equals(paramName)) {
value = null;
}
return value;
}
#Override
public String[] getParameterValues(String paramName) {
String[] values = super.getParameterValues(paramName);
if ("reason".equals(paramName)) {
values = null;
}
return values;
}
#Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
#Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> params = new HashMap<String, String[]>();
Map<String, String[]> originalParams = super.getParameterMap();
for(Object o : originalParams.entrySet()) {
Map.Entry<String, String[]> pairs = (Map.Entry<String, String[]>) o;
params.put(pairs.getKey(), pairs.getValue());
}
params.remove("reason");
return params;
}
#Override
public String getQueryString() {
String qs = super.getQueryString();
return qs.replaceAll("reason=", "old_reason=");
}
#Override
public StringBuffer getRequestURL() {
String qs = super.getRequestURL().toString();
return new StringBuffer( qs.replaceAll("reason=", "old_reason=") );
}
}
private FilterConfig filterConfig = null;
private static final Logger logger = MiscUtils.getLogger();
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
logger.debug("Entering AccessRequestFilter.doFilter()");
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpSession session = httpRequest.getSession();
boolean canView = false;
long echartAccessTime = 0L;
String demographicNo = "";
String reason = "";
Date current = new Date();
String user_no = (String) session.getAttribute("user");
ProgramProviderDAO programProviderDAO = (ProgramProviderDAO)SpringUtils.getBean("programProviderDAO");
ProgramQueueDao programQueueDao = (ProgramQueueDao)SpringUtils.getBean("programQueueDao");
// Check to see if user has submitted a reason
reason = request.getParameter("reason");
demographicNo = request.getParameter("demographicNo");
Long demographicNoAsLong = 0L;
try {
demographicNoAsLong = Long.parseLong( demographicNo );
} catch (Exception e) {
logger.error("Unable to parse demographic number.", e);
}
if (reason == null) {
// If no reason was submitted, see if user still has time remaining on previous submission (if there was one)
try {
echartAccessTime = (Long)session.getServletContext().getAttribute("echartAccessTime_" + demographicNo);
} catch (Exception e) {
logger.warn("No access time found");
}
if (current.getTime() - echartAccessTime < 30000) {
canView = true;
}
} else if (!reason.equals("")) {
// TODO: validate reason
canView = true;
session.getServletContext().setAttribute("echartAccessTime_" + demographicNo, current.getTime());
String ip = request.getRemoteAddr();
// Log the access request and the reason given for access
LogAction.addLog(user_no, "access", "eChart", demographicNo, ip, demographicNo, reason);
}
if (!canView) {
// Check if provider is part of circle of care
List<Long> programIds = new ArrayList<Long>();
List<ProgramQueue> programQueues = programQueueDao.getAdmittedProgramQueuesByDemographicId( demographicNoAsLong );
if (programQueues != null && programQueues.size() > 0) {
for (ProgramQueue pq : programQueues) {
programIds.add( pq.getProgramId() );
}
List<ProgramProvider> programProviders = programProviderDAO.getProgramProviderByProviderProgramId(user_no, programIds);
if (programProviders != null && programProviders.size() > 0) {
canView = true;
}
}
}
String useNewCaseMgmt;
if((useNewCaseMgmt = request.getParameter("newCaseManagement")) != null ) {
session.setAttribute("newCaseManagement", useNewCaseMgmt);
ArrayList<String> users = (ArrayList<String>)session.getServletContext().getAttribute("CaseMgmtUsers");
if( users != null ) {
users.add(request.getParameter("providerNo"));
session.getServletContext().setAttribute("CaseMgmtUsers", users);
}
}
else {
useNewCaseMgmt = (String)session.getAttribute("newCaseManagement");
}
String requestURI = httpRequest.getRequestURI();
String contextPath = httpRequest.getContextPath();
if (!canView && !requestURI.startsWith(contextPath + "/casemgmt/accessRequest.jsp")) {
httpResponse.sendRedirect(contextPath + "/casemgmt/accessRequest.jsp?" + httpRequest.getQueryString());
return;
}
logger.debug("AccessRequestFilter chainning");
chain.doFilter( new FilteredRequest(request), response);
}
}
The filter is setup to intercept all request and forwards coming into a subdirectory called casemgmt. The filter in web.xml is like:
<filter>
<filter-name>AccessRequestFilter</filter-name>
<filter-class>org.oscarehr.casemgmt.filter.AccessRequestFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>AccessRequestFilter</filter-name>
<url-pattern>/casemgmt/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
Anyone have any ideas how I can actually remove the 'reason' parameter?
Wrapping and manipulating the HttpServletRequest in the server side absolutely doesn't magically affect the URL as you see in browser's address bar. That URL stands as-is, as it's the one which the browser used to request the desired resource. The wrapped request would only affect the server side code which is running after the filter on the same request.
If you want to change the URL in browser's address bar, then you should be sending a redirect to exactly the desired URL.
Basically,
if (reasonParameterIsIn(queryString)) {
response.sendRedirect(requestURL + "?" + removeReasonParameterFrom(queryString));
return;
}

Get the POST request body from HttpServletRequest

I am trying to get the whole body from the HttpServletRequest object.
The code I am following looks like this:
if ( request.getMethod().equals("POST") )
{
StringBuffer sb = new StringBuffer();
BufferedReader bufferedReader = null;
String content = "";
try {
//InputStream inputStream = request.getInputStream();
//inputStream.available();
//if (inputStream != null) {
bufferedReader = request.getReader() ; //new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead;
while ( (bytesRead = bufferedReader.read(charBuffer)) != -1 ) {
sb.append(charBuffer, 0, bytesRead);
}
//} else {
// sb.append("");
//}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
test = sb.toString();
}
and I am testing the functionality with curl and wget as follows:
curl --header "MD5: abcd" -F "fileupload=#filename.txt http://localhost:8080/abcd.html"
wget --header="MD5: abcd" --post-data='{"imei":"351553012623446","hni":"310150","wdp":false}' http://localhost:8080/abcd.html"
But the while ( (bytesRead = bufferedReader.read(charBuffer)) != -1 ) does not return anything, and so I get nothing appended on StringBuffer.
In Java 8, you can do it in a simpler and clean way :
if ("POST".equalsIgnoreCase(request.getMethod()))
{
test = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
}
Easy way with commons-io.
IOUtils.toString(request.getReader());
https://commons.apache.org/proper/commons-io/javadocs/api-2.5/org/apache/commons/io/IOUtils.html
Be aware, that your code is quite noisy.
I know the thread is old, but a lot of people will read it anyway.
You could do the same thing using the guava library with:
if ("POST".equalsIgnoreCase(request.getMethod())) {
test = CharStreams.toString(request.getReader());
}
If all you want is the POST request body, you could use a method like this:
static String extractPostRequestBody(HttpServletRequest request) throws IOException {
if ("POST".equalsIgnoreCase(request.getMethod())) {
Scanner s = new Scanner(request.getInputStream(), "UTF-8").useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
return "";
}
Credit to: https://stackoverflow.com/a/5445161/1389219
This works for both GET and POST:
#Context
private HttpServletRequest httpRequest;
private void printRequest(HttpServletRequest httpRequest) {
System.out.println(" \n\n Headers");
Enumeration headerNames = httpRequest.getHeaderNames();
while(headerNames.hasMoreElements()) {
String headerName = (String)headerNames.nextElement();
System.out.println(headerName + " = " + httpRequest.getHeader(headerName));
}
System.out.println("\n\nParameters");
Enumeration params = httpRequest.getParameterNames();
while(params.hasMoreElements()){
String paramName = (String)params.nextElement();
System.out.println(paramName + " = " + httpRequest.getParameter(paramName));
}
System.out.println("\n\n Row data");
System.out.println(extractPostRequestBody(httpRequest));
}
static String extractPostRequestBody(HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
Scanner s = null;
try {
s = new Scanner(request.getInputStream(), "UTF-8").useDelimiter("\\A");
} catch (IOException e) {
e.printStackTrace();
}
return s.hasNext() ? s.next() : "";
}
return "";
}
If the request body is empty, then it simply means that it's already been consumed beforehand. For example, by a request.getParameter(), getParameterValues() or getParameterMap() call. Just remove the lines doing those calls from your code.
This will work for all HTTP method.
public class HttpRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public HttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = IOUtils.toString(request.getReader());
}
#Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getBody().getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setReadListener(ReadListener listener) {
}
};
return servletInputStream;
}
public String getBody() {
return this.body;
}
}
Easiest way I could think of:
request.getReader().lines().reduce("",String::concat)
However, this will be one long string which you will have to parse. IF you send a username of tim and a password of 12345. The output of the code above would look like this:
{ "username":"tim", "password": "12345"}
Please be aware
Please be aware that with the reduce() method we are performing a Mutable Reduction which does a great deal of string copying and has a runtime of O(N^2) with N being the number of characters. Please check the Mutable Reduction documentation if you need a more performant result.
I resolved that situation in this way. I created a util method that return a object extracted from request body, using the readValue method of ObjectMapper that is capable of receiving a Reader.
public static <T> T getBody(ResourceRequest request, Class<T> class) {
T objectFromBody = null;
try {
ObjectMapper objectMapper = new ObjectMapper();
HttpServletRequest httpServletRequest = PortalUtil.getHttpServletRequest(request);
objectFromBody = objectMapper.readValue(httpServletRequest.getReader(), class);
} catch (IOException ex) {
log.error("Error message", ex);
}
return objectFromBody;
}
I personnally use this code (on a dev server, not in production). Seems to work. The main difficulty is that once you read the request body, it will be lost and not transferred to the app. So you have to "cache" it first.
/* Export this filter as a jar and place it under directory ".../tomcat/lib" on your Tomcat server/
In the lib directory, also place the dependencies you need
(ex. org.apache.commons.io => commons-io-2.8.0.jar)
Once this is done, in order to activate the filter, on the Tomcat server:
o in .../tomcat/conf/server.xml, add:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" [%{postdata}r] %s %b"/>
=> the server will log the "postdata" attribute we generate in the Java code.
o in .../tomcat/conf/web.xml, add:
<filter>
<filter-name>post-data-dumper-filter</filter-name>
<filter-class>filters.PostDataDumperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>post-data-dumper-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Once you've done this, restart your tomcat server. You will get extra infos in file "localhost_access_log.<date>.txt"
*/
package filters;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.stream.Collectors;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.io.IOUtils;
class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null)
cacheInputStream();
return new CachedServletInputStream();
}
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
/* Cache the inputstream in order to read it multiple times.
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/* An input stream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
//---------------------
#Override
public int read() throws IOException {
return input.read();
}
#Override
public boolean isFinished() {
return input.available() == 0;
}
#Override
public boolean isReady() {
return true;
}
//---------------------
#Override
public void setReadListener(ReadListener arg0) {
// TODO Auto-generated method stub
// Ex. : throw new RuntimeException("Not implemented");
}
}
}
public final class PostDataDumperFilter implements Filter {
private FilterConfig filterConfig = null;
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (filterConfig == null)
return;
StringBuffer output = new StringBuffer();
output.append("PostDataDumperFilter-");
/* Wrap the request in order to be able to read its body multiple times */
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
// TODO : test the method in order not to log the body when receiving GET/DELETE requests ?
// I finally leave it "as it", since I've seen GET requests containing bodies (hell...).
output.append("Content-type=" + multiReadRequest.getContentType());
output.append(" - HTTP Method=" + multiReadRequest.getMethod());
output.append(" - REQUEST BODY = " + multiReadRequest.getReader().lines().collect(Collectors.joining(System.lineSeparator())));
// Log the request parameters:
Enumeration names = multiReadRequest.getParameterNames();
if (names.hasMoreElements()) {
output.append("- REQUEST PARAMS = ");
}
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
output.append(name + "=");
String values[] = multiReadRequest.getParameterValues(name);
for (int i = 0; i < values.length; i++) {
if (i > 0) output.append("' ");
output.append(values[i]);
}
if (names.hasMoreElements()) output.append("&");
}
multiReadRequest.setAttribute("postdata", output);
chain.doFilter(multiReadRequest, response);
}
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
}

posting data to a servlet using httpclient

I'm trying to post 2 fields, id and data, to a servlet using HttpClient.
The problem is that if the length of the data field is less than 1MB or so, the servlet will get what I posted. But if the length of the data field is larger than 1MB or so, the servlet will receive null for all fields. What am I missing here? Thanks.
Here's the sample data that I post to the servlet:
id=12312123123123
data=the content of a file that is base-64 encoded
Here's the method that I use to post data to the servlet.
private byte[] post(String aUrl,
Map<String,String> aParams,
String aCharsetEnc,
int aMaxWaitMs) throws Exception
{
PostMethod post = null;
try
{
HttpClient client = new HttpClient();
post = new PostMethod(aUrl);
post.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=" + aCharsetEnc);
for (String key : aParams.keySet())
{
post.addParameter(key, aParams.get(key));
}
final int code = client.executeMethod(post);
if (code == HttpStatus.SC_NO_CONTENT || code == HttpStatus.SC_NOT_FOUND)
{
return null;
}
else if (code != HttpStatus.SC_OK)
{
throw new HttpException("Error code " + code + " encountered.");
}
InputStream stream = post.getResponseBodyAsStream();
if (stream != null)
{
return BlobHelper.readBytes(stream);
}
return null;
}
finally
{
if (post != null)
{
post.releaseConnection();
}
}
}
Here's the method of the servlet.
public void doPost(HttpServletRequest aReq, HttpServletResponse aResp)
throws ServletException, IOException
{
setNoCache(aResp);
aResp.setContentType("text/plain");
try
{
final String id = aReq.getParameter(PARAM_ID);
final String dataStr = aReq.getParameter(PARAM_DATA);
if (log().isDebugEnabled())
{
log().debug("id=" + id);
log().debug("data=" + dataStr);
}
}
catch (Exception e)
{
}
}
Usually servlet containers have a maximum post size parameter.
For Tomcat you can follow the steps documented here(they should be similar for other appservers) -
Is there a max size for POST parameter content?

How to cache in a Blackberry BrowserField

I am creating a Blackberry application to display a full screen web view of a certain site. I have a working browserfield that displays properly but navigation from page to page is slower than that of the native browser. The browserfield does not seem to have a built in cache causing the load time to be slow. When I add the following code to manage the cache the site no longer displays properly.
BrowserFieldScreen.java:
import net.rim.device.api.browser.field2.*;
import net.rim.device.api.script.ScriptEngine;
import net.rim.device.api.system.*;
import net.rim.device.api.ui.*;
import net.rim.device.api.ui.component.*;
import net.rim.device.api.ui.container.*;
import org.w3c.dom.Document;
class BrowserFieldScreen extends MainScreen
{
BrowserField browserField;
LoadingScreen load = new LoadingScreen();;
public BrowserFieldScreen()
{
browserField = new BrowserField();
browserField.getConfig().setProperty(
BrowserFieldConfig.JAVASCRIPT_ENABLED,
Boolean.TRUE);
browserField.getConfig().setProperty(
BrowserFieldConfig.NAVIGATION_MODE,
BrowserFieldConfig.NAVIGATION_MODE_POINTER);
browserField.getConfig().setProperty(
BrowserFieldConfig.CONTROLLER,
new CacheProtocolController(browserField));
browserField.requestContent("http://www.stackoverflow.com");
add(browserField);
}
}
CacheProtocolController.java:
import javax.microedition.io.HttpConnection;
import javax.microedition.io.InputConnection;
import net.rim.device.api.browser.field2.BrowserField;
import net.rim.device.api.browser.field2.BrowserFieldRequest;
import net.rim.device.api.browser.field2.ProtocolController;
public class CacheProtocolController extends ProtocolController{
// The BrowserField instance
private BrowserField browserField;
// CacheManager will take care of cached resources
private CacheManager cacheManager;
public CacheProtocolController(BrowserField browserField) {
super(browserField);
this.browserField = browserField;
}
private CacheManager getCacheManager() {
if ( cacheManager == null ) {
cacheManager = new CacheManagerImpl();
}
return cacheManager;
}
/**
* Handle navigation requests (e.g., link clicks)
*/
public void handleNavigationRequest(BrowserFieldRequest request)
throws Exception
{
InputConnection ic = handleResourceRequest(request);
browserField.displayContent(ic, request.getURL());
}
/**
* Handle resource request
* (e.g., images, external css/javascript resources)
*/
public InputConnection handleResourceRequest(BrowserFieldRequest request)
throws Exception
{
// if requested resource is cacheable (e.g., an "http" resource),
// use the cache
if (getCacheManager() != null
&& getCacheManager().isRequestCacheable(request))
{
InputConnection ic = null;
// if requested resource is cached, retrieve it from cache
if (getCacheManager().hasCache(request.getURL())
&& !getCacheManager().hasCacheExpired(request.getURL()))
{
ic = getCacheManager().getCache(request.getURL());
}
// if requested resource is not cached yet, cache it
else
{
ic = super.handleResourceRequest(request);
if (ic instanceof HttpConnection)
{
HttpConnection response = (HttpConnection) ic;
if (getCacheManager().isResponseCacheable(response))
{
ic = getCacheManager().createCache(request.getURL(),
response);
}
}
}
return ic;
}
// if requested resource is not cacheable, load it as usual
return super.handleResourceRequest(request);
}
}
CacheManager.java:
import javax.microedition.io.HttpConnection;
import javax.microedition.io.InputConnection;
import net.rim.device.api.browser.field2.BrowserFieldRequest;
public interface CacheManager {
public boolean isRequestCacheable(BrowserFieldRequest request);
public boolean isResponseCacheable(HttpConnection response);
public boolean hasCache(String url);
public boolean hasCacheExpired(String url);
public InputConnection getCache(String url);
public InputConnection createCache(String url, HttpConnection response);
public void clearCache(String url);
}
CacheManagerImpl.java:
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Hashtable;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.InputConnection;
import net.rim.device.api.browser.field2.BrowserFieldRequest;
import net.rim.device.api.browser.field2.BrowserFieldResponse;
import net.rim.device.api.io.http.HttpHeaders;
public class CacheManagerImpl implements CacheManager {
private static final int MAX_STANDARD_CACHE_AGE = 2592000;
private Hashtable cacheTable;
public CacheManagerImpl() {
cacheTable = new Hashtable();
}
public boolean isRequestCacheable(BrowserFieldRequest request) {
// Only HTTP requests are cacheable
if (!request.getProtocol().equals("http")) {
return false;
}
// Don't cache the request whose method is not "GET".
if (request instanceof HttpConnection) {
if (!((HttpConnection) request).getRequestMethod().equals("GET"))
{
return false;
}
}
// Don't cache the request with post data.
if (request.getPostData() != null) {
return false;
}
// Don't cache authentication request.
if (request.getHeaders().getPropertyValue("Authorization") != null) {
return false;
}
return true;
}
public boolean isResponseCacheable(HttpConnection response) {
try {
if (response.getResponseCode() != 200) {
return false;
}
} catch (IOException ioe) {
return false;
}
if (!response.getRequestMethod().equals("GET")) {
return false;
}
if (containsPragmaNoCache(response)) {
return false;
}
if (isExpired(response)) {
return false;
}
if (containsCacheControlNoCache(response)) {
return false;
}
if ( response.getLength() <= 0 ) {
return false;
}
// additional checks can be implemented here to inspect
// the HTTP cache-related headers of the response object
return true;
}
private boolean isExpired(HttpConnection response) {
try
{
// getExpiration() returns 0 if not known
long expires = response.getExpiration();
if (expires > 0 && expires <= (new Date()).getTime()) {
return true;
}
return false;
} catch (IOException ioe) {
return true;
}
}
private boolean containsPragmaNoCache(HttpConnection response) {
try
{
if (response.getHeaderField("pragma") != null
&& response.getHeaderField("pragma")
.toLowerCase()
.indexOf("no-cache") >= 0)
{
return true;
}
return false;
} catch (IOException ioe) {
return true;
}
}
private boolean containsCacheControlNoCache(HttpConnection response) {
try {
String cacheControl = response.getHeaderField("cache-control");
if (cacheControl != null) {
cacheControl = removeSpace(cacheControl.toLowerCase());
if (cacheControl.indexOf("no-cache") >= 0
|| cacheControl.indexOf("no-store") >= 0
|| cacheControl.indexOf("private") >= 0
|| cacheControl.indexOf("max-age=0") >= 0) {
return true;
}
long maxAge = parseMaxAge(cacheControl);
if (maxAge > 0 && response.getDate() > 0) {
long date = response.getDate();
long now = (new Date()).getTime();
if (now > date + maxAge) {
// Already expired
return true;
}
}
}
return false;
} catch (IOException ioe) {
return true;
}
}
public InputConnection createCache(String url, HttpConnection response) {
byte[] data = null;
InputStream is = null;
try {
// Read data
int len = (int) response.getLength();
if (len > 0) {
is = response.openInputStream();
int actual = 0;
int bytesread = 0 ;
data = new byte[len];
while ((bytesread != len) && (actual != -1)) {
actual = is.read(data, bytesread, len - bytesread);
bytesread += actual;
}
}
} catch (IOException ioe) {
data = null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ioe) {
}
}
if (response != null) {
try {
response.close();
} catch (IOException ioe) {
}
}
}
if (data == null) {
return null;
}
// Calculate expires
long expires = calculateCacheExpires(response);
// Copy headers
HttpHeaders headers = copyResponseHeaders(response);
// add item to cache
cacheTable.put(url, new CacheItem(url, expires, data, headers));
return new BrowserFieldResponse(url, data, headers);
}
private long calculateCacheExpires(HttpConnection response) {
long date = 0;
try {
date = response.getDate();
} catch (IOException ioe) {
}
if (date == 0) {
date = (new Date()).getTime();
}
long expires = getResponseExpires(response);
// If an expire date has not been specified assumes the maximum time
if ( expires == 0 ) {
return date + (MAX_STANDARD_CACHE_AGE * 1000L);
}
return expires;
}
private long getResponseExpires(HttpConnection response) {
try {
// Calculate expires from "expires"
long expires = response.getExpiration();
if (expires > 0) {
return expires;
}
// Calculate expires from "max-age" and "date"
if (response.getHeaderField("cache-control") != null) {
String cacheControl = removeSpace(response
.getHeaderField("cache-control")
.toLowerCase());
long maxAge = parseMaxAge(cacheControl);
long date = response.getDate();
if (maxAge > 0 && date > 0) {
return (date + maxAge);
}
}
} catch (IOException ioe) {
}
return 0;
}
private long parseMaxAge(String cacheControl) {
if (cacheControl == null) {
return 0;
}
long maxAge = 0;
if (cacheControl.indexOf("max-age=") >= 0) {
int maxAgeStart = cacheControl.indexOf("max-age=") + 8;
int maxAgeEnd = cacheControl.indexOf(',', maxAgeStart);
if (maxAgeEnd < 0) {
maxAgeEnd = cacheControl.length();
}
try {
maxAge = Long.parseLong(cacheControl.substring(maxAgeStart,
maxAgeEnd));
} catch (NumberFormatException nfe) {
}
}
// Multiply maxAge by 1000 to convert seconds to milliseconds
maxAge *= 1000L;
return maxAge;
}
private static String removeSpace(String s) {
StringBuffer result= new StringBuffer();
int count = s.length();
for (int i = 0; i < count; i++) {
char c = s.charAt(i);
if (c != ' ') {
result.append(c);
}
}
return result.toString();
}
private HttpHeaders copyResponseHeaders(HttpConnection response) {
HttpHeaders headers = new HttpHeaders();
try {
int index = 0;
while (response.getHeaderFieldKey(index) != null) {
headers.addProperty(response.getHeaderFieldKey(index),
response.getHeaderField(index));
index++;
}
} catch (IOException ioe) {
}
return headers;
}
public boolean hasCache(String url) {
return cacheTable.containsKey(url);
}
public boolean hasCacheExpired(String url) {
Object o = cacheTable.get(url);
if (o instanceof CacheItem) {
CacheItem ci = (CacheItem) o;
long date = (new Date()).getTime();
if (ci.getExpires() > date) {
return false;
} else {
// Remove the expired cache item
clearCache(url);
}
}
return true;
}
public void clearCache(String url) {
cacheTable.remove(url);
}
public InputConnection getCache(String url) {
Object o = cacheTable.get(url);
if (o instanceof CacheItem) {
CacheItem ci = (CacheItem) o;
return new BrowserFieldResponse(url,
ci.getData(),
ci.getHttpHeaders());
}
return null;
}
}
CacheItem.java:
import net.rim.device.api.io.http.HttpHeaders;
public class CacheItem {
private String url;
private long expires;
private byte[] data;
private HttpHeaders httpHeaders;
public CacheItem(String url,
long expires,
byte[] data,
HttpHeaders httpHeaders)
{
this.url = url;
this.expires = expires;
this.data = data;
this.httpHeaders = httpHeaders;
}
public String getUrl() {
return url;
}
public long getExpires() {
return expires;
}
public byte[] getData() {
return data;
}
public HttpHeaders getHttpHeaders() {
return httpHeaders;
}
}
Any help that can be giving towards this will be greatly appreciated. This really has me stumped. Thanks.
UPDATE: It looks like the caching only works at a certain level of the Blackberry libraries. I have added logic to check the current Software level and turn on the caching if it is supported by the device's current software level. This provides me with a good work around, but i would still like to know if there is a better way for the caching to work with all devices.
UPDATE 2 Based on comments: The site no longer displaying properly pertains to site not displaying the proper layout, images and text. It basically give a white background with links and text displaying as a bulleted list, all formatting removed.
I've been looking at your code, and the only thing I've found there's wrong with it, is you are completely ignoring the possibility of response.getLength(); returning less than zero (in CacheManagerImpl.createCache()). Although this didn't happen to me at the stackoverflow.com page, some pages use Transfer-Encoding: chunked, which means Content-Length is not present. This is, however, well handled, and should not cause the cache to fail (it would only be less effective).
I suggest testing your code on smaller problems, one step at a time. First, create cacheable page that only contains some text (like "hello") without any HTML tags. That should work pretty well, and in case it does not, it shouldn't be hard to determine where the data are getting lost. Or try to manually create cache item that does not expire and contains a webpage with no (external) stylesheet nor images, and see if it's even possible to pass it to BrowserField the way you do it. Then build on, add an image, add a style sheet so you can corner the problem.
The code is written very nicely, but at this point, it is not possible to help you because there are no evident flaws in the code and you are not explaining yourself very well, it is not clear how the error manifests itself, if it is every time or random, ... If I had a Blackberry device, I could probably try running the code for myself, but i don't.

Categories